Skip to content

Commit 4dbefbd

Browse files
authored
feat: Extend tag sets and add environment tags (#548)
* feat: Extend tag sets and add environment tags * feat: Add tag set scope validation and feedback for single tag/tenant prompt * fix: tags in tests were incorrect
1 parent 03ec83b commit 4dbefbd

File tree

12 files changed

+862
-92
lines changed

12 files changed

+862
-92
lines changed

pkg/cmd/environment/create/create.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import (
99
"github.com/OctopusDeploy/cli/pkg/factory"
1010
"github.com/OctopusDeploy/cli/pkg/output"
1111
"github.com/OctopusDeploy/cli/pkg/question"
12+
"github.com/OctopusDeploy/cli/pkg/question/selectors"
1213
"github.com/OctopusDeploy/cli/pkg/util/flag"
14+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
1315
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments"
16+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets"
1417
"github.com/spf13/cobra"
1518
)
1619

@@ -19,13 +22,15 @@ const (
1922
FlagDescription = "description"
2023
FlagUseGuidedFailure = "use-guided-failure"
2124
FlagDynamicInfrastructure = "allow-dynamic-infrastructure"
25+
FlagTag = "tag"
2226
)
2327

2428
type CreateFlags struct {
2529
Name *flag.Flag[string]
2630
Description *flag.Flag[string]
2731
GuidedFailureMode *flag.Flag[bool]
2832
DynamicInfrastructure *flag.Flag[bool]
33+
Tag *flag.Flag[[]string]
2934
}
3035

3136
func NewCreateFlags() *CreateFlags {
@@ -34,18 +39,23 @@ func NewCreateFlags() *CreateFlags {
3439
Description: flag.New[string](FlagDescription, false),
3540
GuidedFailureMode: flag.New[bool](FlagUseGuidedFailure, false),
3641
DynamicInfrastructure: flag.New[bool](FlagDynamicInfrastructure, false),
42+
Tag: flag.New[[]string](FlagTag, false),
3743
}
3844
}
3945

46+
type GetAllTagSetsCallback func() ([]*tagsets.TagSet, error)
47+
4048
type CreateOptions struct {
4149
*CreateFlags
4250
*cmd.Dependencies
51+
GetAllTagsCallback GetAllTagSetsCallback
4352
}
4453

4554
func NewCreateOptions(createFlags *CreateFlags, dependencies *cmd.Dependencies) *CreateOptions {
4655
return &CreateOptions{
47-
CreateFlags: createFlags,
48-
Dependencies: dependencies,
56+
CreateFlags: createFlags,
57+
Dependencies: dependencies,
58+
GetAllTagsCallback: getAllTagSetsCallback(dependencies.Client),
4959
}
5060
}
5161

@@ -70,6 +80,7 @@ func NewCmdCreate(f factory.Factory) *cobra.Command {
7080
flags.StringVarP(&createFlags.Description.Value, createFlags.Description.Name, "d", "", "Description of the environment")
7181
flags.BoolVar(&createFlags.GuidedFailureMode.Value, createFlags.GuidedFailureMode.Name, false, "Use guided failure mode by default")
7282
flags.BoolVar(&createFlags.DynamicInfrastructure.Value, createFlags.DynamicInfrastructure.Name, false, "Allow dynamic infrastructure")
83+
flags.StringArrayVarP(&createFlags.Tag.Value, createFlags.Tag.Name, "t", []string{}, "Tag to apply to environment, must use canonical name: <tag_set>/<tag_name>")
7384

7485
return cmd
7586
}
@@ -80,12 +91,24 @@ func createRun(opts *CreateOptions) error {
8091
if err != nil {
8192
return err
8293
}
94+
} else {
95+
// Validate tags when running with --no-prompt
96+
if len(opts.Tag.Value) > 0 {
97+
tagSets, err := opts.GetAllTagsCallback()
98+
if err != nil {
99+
return err
100+
}
101+
if err := selectors.ValidateTags(opts.Tag.Value, tagSets); err != nil {
102+
return err
103+
}
104+
}
83105
}
84106

85107
env := environments.NewEnvironment(opts.Name.Value)
86108
env.Description = opts.Description.Value
87109
env.AllowDynamicInfrastructure = opts.DynamicInfrastructure.Value
88110
env.UseGuidedFailure = opts.GuidedFailureMode.Value
111+
env.EnvironmentTags = opts.Tag.Value
89112

90113
createEnv, err := opts.Client.Environments.Add(env)
91114
if err != nil {
@@ -101,7 +124,7 @@ func createRun(opts *CreateOptions) error {
101124
fmt.Fprintf(opts.Out, "View this environment on Octopus Deploy: %s\n", link)
102125

103126
if !opts.NoPrompt {
104-
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.Description, opts.GuidedFailureMode, opts.DynamicInfrastructure)
127+
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.Description, opts.GuidedFailureMode, opts.DynamicInfrastructure, opts.Tag)
105128
fmt.Fprintf(opts.Out, "%s\n", autoCmd)
106129
}
107130

@@ -122,6 +145,17 @@ func PromptMissing(opts *CreateOptions) error {
122145
_, err = promptBool(opts, &opts.GuidedFailureMode.Value, false, "Use guided failure", "If guided failure is enabled for an environment, Octopus Deploy will prompt for user intervention if a deployment fails in the environment.")
123146
_, err = promptBool(opts, &opts.DynamicInfrastructure.Value, false, "Allow dynamic infrastructure", "If dynamic infrastructure is enabled for an environment, deployments to this environment are allowed to create infrastructure, such as targets and accounts.")
124147

148+
tagSets, err := opts.GetAllTagsCallback()
149+
if err != nil {
150+
return err
151+
}
152+
153+
tags, err := selectors.Tags(opts.Ask, []string{}, opts.Tag.Value, tagSets)
154+
if err != nil {
155+
return err
156+
}
157+
opts.Tag.Value = tags
158+
125159
return nil
126160
}
127161

@@ -136,3 +170,16 @@ func promptBool(opts *CreateOptions, value *bool, defaultValue bool, message str
136170
}, value)
137171
return *value, err
138172
}
173+
174+
func getAllTagSetsCallback(client *client.Client) GetAllTagSetsCallback {
175+
return func() ([]*tagsets.TagSet, error) {
176+
query := tagsets.TagSetsQuery{
177+
Scopes: []string{string(tagsets.TagSetScopeEnvironment)},
178+
}
179+
result, err := tagsets.Get(client, client.GetSpaceID(), query)
180+
if err != nil {
181+
return nil, err
182+
}
183+
return result.Items, nil
184+
}
185+
}

pkg/cmd/environment/environment.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
cmdCreate "github.com/OctopusDeploy/cli/pkg/cmd/environment/create"
66
cmdDelete "github.com/OctopusDeploy/cli/pkg/cmd/environment/delete"
77
cmdList "github.com/OctopusDeploy/cli/pkg/cmd/environment/list"
8+
cmdTag "github.com/OctopusDeploy/cli/pkg/cmd/environment/tag"
89
"github.com/OctopusDeploy/cli/pkg/constants"
910
"github.com/OctopusDeploy/cli/pkg/constants/annotations"
1011
"github.com/OctopusDeploy/cli/pkg/factory"
@@ -28,5 +29,6 @@ func NewCmdEnvironment(f factory.Factory) *cobra.Command {
2829
cmd.AddCommand(cmdList.NewCmdList(f))
2930
cmd.AddCommand(cmdDelete.NewCmdDelete(f))
3031
cmd.AddCommand(cmdCreate.NewCmdCreate(f))
32+
cmd.AddCommand(cmdTag.NewCmdTag(f))
3133
return cmd
3234
}

pkg/cmd/environment/list/list.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,21 @@ func NewCmdList(f factory.Factory) *cobra.Command {
3939

4040
return output.PrintArray(allEnvs, cmd, output.Mappers[*environments.Environment]{
4141
Json: func(item *environments.Environment) any {
42-
return output.IdAndName{Id: item.GetID(), Name: item.Name}
42+
return struct {
43+
Id string `json:"Id"`
44+
Name string `json:"Name"`
45+
EnvironmentTags []string `json:"EnvironmentTags,omitempty"`
46+
}{
47+
Id: item.GetID(),
48+
Name: item.Name,
49+
EnvironmentTags: item.EnvironmentTags,
50+
}
4351
},
4452
Table: output.TableDefinition[*environments.Environment]{
45-
Header: []string{"NAME", "GUIDED FAILURE"},
53+
Header: []string{"NAME", "GUIDED FAILURE", "TAGS"},
4654
Row: func(item *environments.Environment) []string {
4755

48-
return []string{output.Bold(item.Name), strconv.FormatBool(item.UseGuidedFailure)}
56+
return []string{output.Bold(item.Name), strconv.FormatBool(item.UseGuidedFailure), output.FormatAsList(item.EnvironmentTags)}
4957
},
5058
},
5159
Basic: func(item *environments.Environment) string {

pkg/cmd/environment/tag/tag.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package tag
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/MakeNowJust/heredoc/v2"
8+
"github.com/OctopusDeploy/cli/pkg/cmd"
9+
"github.com/OctopusDeploy/cli/pkg/constants"
10+
"github.com/OctopusDeploy/cli/pkg/factory"
11+
"github.com/OctopusDeploy/cli/pkg/output"
12+
"github.com/OctopusDeploy/cli/pkg/question"
13+
"github.com/OctopusDeploy/cli/pkg/question/selectors"
14+
"github.com/OctopusDeploy/cli/pkg/util/flag"
15+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
const (
20+
FlagTag = "tag"
21+
FlagEnvironment = "environment"
22+
)
23+
24+
type TagFlags struct {
25+
Tag *flag.Flag[[]string]
26+
Environment *flag.Flag[string]
27+
}
28+
29+
func NewTagFlags() *TagFlags {
30+
return &TagFlags{
31+
Tag: flag.New[[]string](FlagTag, false),
32+
Environment: flag.New[string](FlagEnvironment, false),
33+
}
34+
}
35+
36+
func NewCmdTag(f factory.Factory) *cobra.Command {
37+
createFlags := NewTagFlags()
38+
39+
cmd := &cobra.Command{
40+
Use: "tag",
41+
Short: "Override tags for an environment",
42+
Long: "Override tags for an environment in Octopus Deploy",
43+
Example: heredoc.Docf("$ %s environment tag Environment-1", constants.ExecutableName),
44+
RunE: func(c *cobra.Command, _ []string) error {
45+
opts := NewTagOptions(createFlags, cmd.NewDependencies(f, c))
46+
47+
return createRun(opts)
48+
},
49+
}
50+
51+
flags := cmd.Flags()
52+
flags.StringArrayVarP(&createFlags.Tag.Value, createFlags.Tag.Name, "t", []string{}, "Tag to apply to environment, must use canonical name: <tag_set>/<tag_name>")
53+
flags.StringVar(&createFlags.Environment.Value, createFlags.Environment.Name, "", "Name or ID of the environment you wish to update")
54+
55+
return cmd
56+
}
57+
58+
func createRun(opts *TagOptions) error {
59+
var optsArray []cmd.Dependable
60+
var err error
61+
if !opts.NoPrompt {
62+
optsArray, err = PromptMissing(opts)
63+
if err != nil {
64+
return err
65+
}
66+
} else {
67+
// Validate tags when running with --no-prompt
68+
if len(opts.Tag.Value) > 0 {
69+
tagSets, err := opts.GetAllTagsCallback()
70+
if err != nil {
71+
return err
72+
}
73+
if err := selectors.ValidateTags(opts.Tag.Value, tagSets); err != nil {
74+
return err
75+
}
76+
}
77+
optsArray = append(optsArray, opts)
78+
}
79+
80+
for _, o := range optsArray {
81+
if err := o.Commit(); err != nil {
82+
return err
83+
}
84+
}
85+
86+
if !opts.NoPrompt {
87+
fmt.Fprintln(opts.Out, "\nAutomation Commands:")
88+
for _, o := range optsArray {
89+
o.GenerateAutomationCmd()
90+
}
91+
}
92+
93+
return nil
94+
}
95+
96+
func PromptMissing(opts *TagOptions) ([]cmd.Dependable, error) {
97+
nestedOpts := []cmd.Dependable{}
98+
99+
environment, err := AskEnvironments(opts.Ask, opts.Out, opts.Environment.Value, opts.GetEnvironmentsCallback, opts.GetEnvironmentCallback)
100+
if err != nil {
101+
return nil, err
102+
}
103+
opts.environment = environment
104+
opts.Environment.Value = environment.Name
105+
106+
tagSets, err := opts.GetAllTagsCallback()
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
tags, err := selectors.Tags(opts.Ask, opts.environment.EnvironmentTags, opts.Tag.Value, tagSets)
112+
if err != nil {
113+
return nil, err
114+
}
115+
opts.Tag.Value = tags
116+
117+
nestedOpts = append(nestedOpts, opts)
118+
return nestedOpts, nil
119+
}
120+
121+
func AskEnvironments(ask question.Asker, out io.Writer, value string, getEnvironmentsCallback GetEnvironmentsCallback, getEnvironmentCallback GetEnvironmentCallback) (*environments.Environment, error) {
122+
if value != "" {
123+
environment, err := getEnvironmentCallback(value)
124+
if err != nil {
125+
return nil, err
126+
}
127+
return environment, nil
128+
}
129+
130+
// Check if there's only one environment
131+
envs, err := getEnvironmentsCallback()
132+
if err != nil {
133+
return nil, err
134+
}
135+
if len(envs) == 1 {
136+
fmt.Fprintf(out, "Selecting only available environment '%s'.\n", output.Cyan(envs[0].Name))
137+
return envs[0], nil
138+
}
139+
140+
environment, err := selectors.Select(ask, "Select the Environment you would like to update", getEnvironmentsCallback, func(item *environments.Environment) string {
141+
return item.Name
142+
})
143+
if err != nil {
144+
return nil, err
145+
}
146+
147+
return environment, nil
148+
}
149+

0 commit comments

Comments
 (0)