Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,31 @@ fingerprint=$(ssh-keygen -E md5 -lf /etc/ssh/ssh_host_rsa_key.pub | cut -d' ' -f
monoExists=$(command -v mono)
if [ $monoExists ]
then
octopus deployment-target ssh create --account "TheAccount" --name "MySshTargetName" --host $localIp --fingerprint $fingerprint--role linux --runtime mono --no-prompt
octopus deployment-target ssh create --account "TheAccount" --name "MySshTargetName" --host $localIp --fingerprint $fingerprint --role linux --runtime mono --no-prompt
else
octopus deployment-target ssh create --account "TheAccount" --name "MySshTargetName" --host $localIp --fingerprint $fingerprint--role linux --runtime self-contained --platform linux-x64 --no-prompt
octopus deployment-target ssh create --account "TheAccount" --name "MySshTargetName" --host $localIp --fingerprint $fingerprint --role linux --runtime self-contained --platform linux-x64 --no-prompt
fi
```

# Create deployment target with target tags

Target tag sets provide organized tagging with validation. Use `--tag` with canonical format (TagSetName/TagName):

```
octopus deployment-target ssh create \
--account "TheAccount" \
--name "MySshTargetName" \
--host $localIp \
--fingerprint $fingerprint \
--environment Production \
--tag "[System] Target Tags/web-server" \
--tag Region/US-East \
--runtime mono \
--no-prompt
```

Note: The `--role` flag continues to work for backwards compatibility but will be deprecated in favor of `--tag` once target tag sets are widely adopted.

# Bulk deleting releases by created date

This example will delete all releases created before 2AM 6 Dec 2022 UTC
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/MakeNowJust/heredoc/v2 v2.0.1
github.com/OctopusDeploy/go-octodiff v1.0.0
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.89.1
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.101.0
github.com/bmatcuk/doublestar/v4 v4.4.0
github.com/briandowns/spinner v1.19.0
github.com/google/uuid v1.3.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4
github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.89.1 h1:EDo4CdA7jYZBHiaKu8nZRf9ndonJXC70OlU29+6KXKc=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.89.1/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.101.0 h1:AhFBHMVWvt4++0BaL3dUtMVIqikVmg3hAaor7tVquJQ=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.101.0/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus=
github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic=
github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=
Expand Down
9 changes: 7 additions & 2 deletions pkg/cmd/target/azure-web-app/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ func createRun(opts *CreateOptions) error {
}
environmentIds := util.SliceTransform(envs, func(e *environments.Environment) string { return e.ID })

combinedRoles, err := shared.CombineRolesAndTags(opts.Client, opts.Roles.Value, opts.Tags.Value)
if err != nil {
return err
}

account, err := getAzureAccount(opts)
if err != nil {
return err
Expand All @@ -154,7 +159,7 @@ func createRun(opts *CreateOptions) error {
endpoint.WebAppName = opts.WebApp.Value
endpoint.ResourceGroupName = opts.ResourceGroup.Value
endpoint.WebAppSlotName = opts.Slot.Value
deploymentTarget := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(opts.Roles.Value))
deploymentTarget := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(combinedRoles))

err = shared.ConfigureTenant(deploymentTarget, opts.CreateTargetTenantFlags, opts.CreateTargetTenantOptions)
if err != nil {
Expand All @@ -168,7 +173,7 @@ func createRun(opts *CreateOptions) error {

fmt.Fprintf(opts.Out, "Successfully created Azure web app '%s'.\n", deploymentTarget.Name)
if !opts.NoPrompt {
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.Account, opts.WebApp, opts.ResourceGroup, opts.Slot, opts.Environments, opts.Roles, opts.TenantedDeploymentMode, opts.Tenants, opts.TenantTags)
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.Account, opts.WebApp, opts.ResourceGroup, opts.Slot, opts.Environments, opts.Roles, opts.Tags, opts.TenantedDeploymentMode, opts.Tenants, opts.TenantTags)
fmt.Fprintf(opts.Out, "\nAutomation Command: %s\n", autoCmd)
}

Expand Down
9 changes: 7 additions & 2 deletions pkg/cmd/target/cloud-region/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ func createRun(opts *CreateOptions) error {
}
environmentIds := util.SliceTransform(envs, func(e *environments.Environment) string { return e.ID })

combinedRoles, err := shared.CombineRolesAndTags(opts.Client, opts.Roles.Value, opts.Tags.Value)
if err != nil {
return err
}

endpoint := machines.NewCloudRegionEndpoint()
if opts.WorkerPool.Value != "" {
workerPoolId, err := shared.FindWorkerPoolId(opts.GetAllWorkerPoolsCallback, opts.WorkerPool.Value)
Expand All @@ -111,7 +116,7 @@ func createRun(opts *CreateOptions) error {
endpoint.DefaultWorkerPoolID = workerPoolId
}

target := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(opts.Roles.Value))
target := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(combinedRoles))
err = shared.ConfigureTenant(target, opts.CreateTargetTenantFlags, opts.CreateTargetTenantOptions)
if err != nil {
return err
Expand All @@ -123,7 +128,7 @@ func createRun(opts *CreateOptions) error {
}
fmt.Fprintf(opts.Out, "Successfully created cloud region '%s'.\n", target.Name)
if !opts.NoPrompt {
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.WorkerPool, opts.Environments, opts.Roles, opts.TenantedDeploymentMode, opts.Tenants, opts.TenantTags)
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.WorkerPool, opts.Environments, opts.Roles, opts.Tags, opts.TenantedDeploymentMode, opts.Tenants, opts.TenantTags)
fmt.Fprintf(opts.Out, "\nAutomation Command: %s\n", autoCmd)
}

Expand Down
8 changes: 7 additions & 1 deletion pkg/cmd/target/kubernetes/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,11 @@ func (opts *CreateOptions) Commit() error {
}
environmentIds := util.SliceTransform(envs, func(e *environments.Environment) string { return e.ID })

combinedRoles, err := shared.CombineRolesAndTags(opts.Client, opts.Roles.Value, opts.Tags.Value)
if err != nil {
return err
}

kubernetesUrl, err := url.Parse(opts.KubernetesClusterURL.Value)
if err != nil {
return err
Expand Down Expand Up @@ -446,7 +451,7 @@ func (opts *CreateOptions) Commit() error {
endpoint.Authentication = auth
}

deploymentTarget := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(opts.Roles.Value))
deploymentTarget := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(combinedRoles))

machinePolicy, err := machinescommon.FindDefaultMachinePolicy(opts.GetAllMachinePoliciesCallback)
if err != nil {
Expand Down Expand Up @@ -511,6 +516,7 @@ func (opts *CreateOptions) Commit() error {

opts.Environments,
opts.Roles,
opts.Tags,
opts.TenantedDeploymentMode,
opts.Tenants,
opts.TenantTags,
Expand Down
9 changes: 7 additions & 2 deletions pkg/cmd/target/listening-tentacle/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ func createRun(opts *CreateOptions) error {
}
environmentIds := util.SliceTransform(envs, func(e *environments.Environment) string { return e.ID })

combinedRoles, err := shared.CombineRolesAndTags(opts.Client, opts.Roles.Value, opts.Tags.Value)
if err != nil {
return err
}

endpoint := machines.NewListeningTentacleEndpoint(url, opts.Thumbprint.Value)
if opts.Proxy.Value != "" {
proxy, err := machinescommon.FindProxy(opts.CreateTargetProxyOptions, opts.CreateTargetProxyFlags)
Expand All @@ -131,7 +136,7 @@ func createRun(opts *CreateOptions) error {
endpoint.ProxyID = proxy.GetID()
}

deploymentTarget := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(opts.Roles.Value))
deploymentTarget := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(combinedRoles))
machinePolicy, err := machinescommon.FindMachinePolicy(opts.GetAllMachinePoliciesCallback, opts.MachinePolicy.Value)
if err != nil {
return err
Expand All @@ -149,7 +154,7 @@ func createRun(opts *CreateOptions) error {

fmt.Fprintf(opts.Out, "Successfully created listening tenatcle '%s'.\n", deploymentTarget.Name)
if !opts.NoPrompt {
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.URL, opts.Thumbprint, opts.Environments, opts.Roles, opts.Proxy, opts.MachinePolicy, opts.TenantedDeploymentMode, opts.Tenants, opts.TenantTags)
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.URL, opts.Thumbprint, opts.Environments, opts.Roles, opts.Tags, opts.Proxy, opts.MachinePolicy, opts.TenantedDeploymentMode, opts.Tenants, opts.TenantTags)
fmt.Fprintf(opts.Out, "\nAutomation Command: %s\n", autoCmd)
}

Expand Down
88 changes: 80 additions & 8 deletions pkg/cmd/target/shared/role.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
package shared

import (
"strings"

"github.com/OctopusDeploy/cli/pkg/cmd"
"github.com/OctopusDeploy/cli/pkg/question"
"github.com/OctopusDeploy/cli/pkg/question/selectors"
"github.com/OctopusDeploy/cli/pkg/util"
"github.com/OctopusDeploy/cli/pkg/util/flag"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets"
"github.com/spf13/cobra"
)

const (
FlagRole = "role"
FlagTag = "tag"
)

type GetAllRolesCallback func() ([]string, error)

type CreateTargetRoleFlags struct {
Roles *flag.Flag[[]string]
Tags *flag.Flag[[]string]
}

type CreateTargetRoleOptions struct {
Expand All @@ -37,26 +43,39 @@ func NewCreateTargetRoleOptions(dependencies *cmd.Dependencies) *CreateTargetRol
func NewCreateTargetRoleFlags() *CreateTargetRoleFlags {
return &CreateTargetRoleFlags{
Roles: flag.New[[]string](FlagRole, false),
Tags: flag.New[[]string](FlagTag, false),
}
}

func RegisterCreateTargetRoleFlags(cmd *cobra.Command, commonFlags *CreateTargetRoleFlags) {
cmd.Flags().StringSliceVar(&commonFlags.Roles.Value, FlagRole, []string{}, "Choose at least one role that this deployment target will provide.")
cmd.Flags().StringSliceVar(&commonFlags.Roles.Value, FlagRole, []string{}, "Choose at least one role that this deployment target will provide (use --tag for tag sets with validation).")
cmd.Flags().StringSliceVar(&commonFlags.Tags.Value, FlagTag, []string{}, "Target tags in canonical format (TagSetName/TagName).")
}

func PromptForRoles(opts *CreateTargetRoleOptions, flags *CreateTargetRoleFlags) error {

if util.Empty(flags.Roles.Value) {
availableRoles, err := opts.GetAllRolesCallback()
if util.Empty(flags.Roles.Value) && util.Empty(flags.Tags.Value) {
tagSets, err := getTargetTagSets(opts.Client)
if err != nil {
return err
}
roles, err := question.MultiSelectWithAddMap(opts.Ask, "Choose at least one role for the deployment target.\n", availableRoles, true)

if err != nil {
return err
if len(tagSets) > 0 {
tags, err := selectors.Tags(opts.Ask, []string{}, []string{}, tagSets)
if err != nil {
return err
}
flags.Tags.Value = tags
} else {
availableRoles, err := opts.GetAllRolesCallback()
if err != nil {
return err
}
roles, err := question.MultiSelectWithAddMap(opts.Ask, "Choose at least one role for the deployment target.\n", availableRoles, true)
if err != nil {
return err
}
flags.Roles.Value = roles
}
flags.Roles.Value = roles
}
return nil
}
Expand All @@ -73,3 +92,56 @@ func getAllMachineRoles(client client.Client) ([]string, error) {
}
return roles, nil
}

func getTargetTagSets(client *client.Client) ([]*tagsets.TagSet, error) {
if client == nil {
return []*tagsets.TagSet{}, nil
}

query := tagsets.TagSetsQuery{
Scopes: []string{string(tagsets.TagSetScopeTarget)},
}
result, err := tagsets.Get(client, client.GetSpaceID(), query)
if err != nil {
return nil, err
}
return result.Items, nil
}

// ValidateTags validates tags in canonical format (TagSetName/TagName) against target scoped tag sets
// Returns plain tag names to send to the API
func ValidateTags(client *client.Client, tags []string) ([]string, error) {
if len(tags) == 0 {
return []string{}, nil
}

tagSets, err := getTargetTagSets(client)
if err != nil {
return nil, err
}

if err := selectors.ValidateTags(tags, tagSets); err != nil {
return nil, err
}

plainNames := make([]string, 0, len(tags))
for _, tag := range tags {
parts := strings.SplitN(tag, "/", 2)
plainNames = append(plainNames, parts[1])
}

return plainNames, nil
}

func CombineRolesAndTags(client *client.Client, roles []string, tags []string) ([]string, error) {
combined := make([]string, 0, len(roles)+len(tags))
combined = append(combined, roles...)

validatedTags, err := ValidateTags(client, tags)
if err != nil {
return nil, err
}
combined = append(combined, validatedTags...)

return combined, nil
}
36 changes: 36 additions & 0 deletions pkg/cmd/target/shared/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,39 @@ func TestPromptRolesAndEnvironments_ShouldPrompt(t *testing.T) {

assert.Equal(t, []string{"Ninja #3"}, flags.Roles.Value)
}

func TestValidateTags_EmptyList(t *testing.T) {
result, err := shared.ValidateTags(nil, []string{})
assert.NoError(t, err)
assert.Empty(t, result)
}

func TestValidateTags_InvalidFormat_NoSlash(t *testing.T) {
_, err := shared.ValidateTags(nil, []string{"PlainName"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "is not in the correct format")
}

func TestValidateTags_InvalidFormat_MultipleSlashes(t *testing.T) {
_, err := shared.ValidateTags(nil, []string{"Set/Tag/Extra"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "does not belong to any tag set")
}

func TestCombineRolesAndTags_RolesOnly(t *testing.T) {
result, err := shared.CombineRolesAndTags(nil, []string{"web-server", "db-server"}, []string{})
assert.NoError(t, err)
assert.Equal(t, []string{"web-server", "db-server"}, result)
}

func TestCombineRolesAndTags_EmptyBoth(t *testing.T) {
result, err := shared.CombineRolesAndTags(nil, []string{}, []string{})
assert.NoError(t, err)
assert.Empty(t, result)
}

func TestCombineRolesAndTags_TagValidationError(t *testing.T) {
_, err := shared.CombineRolesAndTags(nil, []string{"web-server"}, []string{"InvalidFormat"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "is not in the correct format")
}
9 changes: 7 additions & 2 deletions pkg/cmd/target/ssh/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ func createRun(opts *CreateOptions) error {
}
environmentIds := util.SliceTransform(envs, func(e *environments.Environment) string { return e.ID })

combinedRoles, err := shared.CombineRolesAndTags(opts.Client, opts.Roles.Value, opts.Tags.Value)
if err != nil {
return err
}

account, err := machinescommon.GetSshAccount(opts.SshCommonOptions, opts.SshCommonFlags)
if err != nil {
return err
Expand All @@ -141,7 +146,7 @@ func createRun(opts *CreateOptions) error {
endpoint.ProxyID = proxy.GetID()
}

deploymentTarget := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(opts.Roles.Value))
deploymentTarget := machines.NewDeploymentTarget(opts.Name.Value, endpoint, environmentIds, util.SliceDistinct(combinedRoles))
machinePolicy, err := machinescommon.FindMachinePolicy(opts.GetAllMachinePoliciesCallback, opts.MachinePolicy.Value)
if err != nil {
return err
Expand All @@ -160,7 +165,7 @@ func createRun(opts *CreateOptions) error {

fmt.Fprintf(opts.Out, "Successfully created SSH deployment target '%s'.\n", deploymentTarget.Name)
if !opts.NoPrompt {
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.HostName, opts.Port, opts.Fingerprint, opts.Runtime, opts.Platform, opts.Environments, opts.Roles, opts.Account, opts.Proxy, opts.MachinePolicy, opts.TenantedDeploymentMode, opts.Tenants, opts.TenantTags)
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.HostName, opts.Port, opts.Fingerprint, opts.Runtime, opts.Platform, opts.Environments, opts.Roles, opts.Tags, opts.Account, opts.Proxy, opts.MachinePolicy, opts.TenantedDeploymentMode, opts.Tenants, opts.TenantTags)
fmt.Fprintf(opts.Out, "\nAutomation Command: %s\n", autoCmd)
}

Expand Down
Loading