Skip to content

Commit 87745a9

Browse files
authored
feat: Add support for generic oidc accounts (#455)
* feat: Add support for generic oidc accounts * fix: missing audience from automation command * chore: update go client version
1 parent 20c2f9f commit 87745a9

File tree

7 files changed

+378
-3
lines changed

7 files changed

+378
-3
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/AlecAivazis/survey/v2 v2.3.7
77
github.com/MakeNowJust/heredoc/v2 v2.0.1
88
github.com/OctopusDeploy/go-octodiff v1.0.0
9-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.62.0
9+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.0
1010
github.com/bmatcuk/doublestar/v4 v4.4.0
1111
github.com/briandowns/spinner v1.19.0
1212
github.com/google/uuid v1.3.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
4848
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
4949
github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0=
5050
github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU=
51-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.62.0 h1:p2qapGMs+BGZfRmKa+1K6j7J9g+n6bxTWPZFeHZ/W0o=
52-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.62.0/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw=
51+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.0 h1:TshwN+IqKt21uY9aXzj0ou0Ew92uIi3+ZGTccVd9Z8g=
52+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.0/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw=
5353
github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic=
5454
github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
5555
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=

pkg/cmd/account/account.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
cmdCreate "github.com/OctopusDeploy/cli/pkg/cmd/account/create"
99
cmdDelete "github.com/OctopusDeploy/cli/pkg/cmd/account/delete"
1010
cmdGCP "github.com/OctopusDeploy/cli/pkg/cmd/account/gcp"
11+
cmdGenericOidc "github.com/OctopusDeploy/cli/pkg/cmd/account/generic-oidc"
1112
cmdList "github.com/OctopusDeploy/cli/pkg/cmd/account/list"
1213
cmdSSH "github.com/OctopusDeploy/cli/pkg/cmd/account/ssh"
1314
cmdToken "github.com/OctopusDeploy/cli/pkg/cmd/account/token"
@@ -35,6 +36,7 @@ func NewCmdAccount(f factory.Factory) *cobra.Command {
3536
cmd.AddCommand(cmdAWS.NewCmdAws(f))
3637
cmd.AddCommand(cmdAzure.NewCmdAzure(f))
3738
cmd.AddCommand(cmdAzureOidc.NewCmdAzureOidc(f))
39+
cmd.AddCommand(cmdGenericOidc.NewCmdGenericOidc(f))
3840
cmd.AddCommand(cmdGCP.NewCmdGcp(f))
3941
cmd.AddCommand(cmdSSH.NewCmdSsh(f))
4042
cmd.AddCommand(cmdUsr.NewCmdUsername(f))
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package create
2+
3+
import (
4+
"fmt"
5+
"github.com/AlecAivazis/survey/v2"
6+
"github.com/MakeNowJust/heredoc/v2"
7+
"github.com/OctopusDeploy/cli/pkg/cmd"
8+
"github.com/OctopusDeploy/cli/pkg/cmd/account/helper"
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/surveyext"
15+
"github.com/OctopusDeploy/cli/pkg/util"
16+
"github.com/OctopusDeploy/cli/pkg/util/flag"
17+
"github.com/OctopusDeploy/cli/pkg/validation"
18+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/accounts"
19+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments"
20+
"github.com/spf13/cobra"
21+
"os"
22+
)
23+
24+
type CreateFlags struct {
25+
Name *flag.Flag[string]
26+
Description *flag.Flag[string]
27+
Environments *flag.Flag[[]string]
28+
ExecutionSubjectKeys *flag.Flag[[]string]
29+
Audience *flag.Flag[string]
30+
}
31+
32+
type CreateOptions struct {
33+
*CreateFlags
34+
*cmd.Dependencies
35+
selectors.GetAllEnvironmentsCallback
36+
}
37+
38+
func NewCreateFlags() *CreateFlags {
39+
return &CreateFlags{
40+
Name: flag.New[string]("name", false),
41+
Description: flag.New[string]("description", false),
42+
Environments: flag.New[[]string]("environment", false),
43+
ExecutionSubjectKeys: flag.New[[]string]("execution-subject-keys", false),
44+
Audience: flag.New[string]("audience", false),
45+
}
46+
}
47+
48+
func NewCreateOptions(flags *CreateFlags, dependencies *cmd.Dependencies) *CreateOptions {
49+
return &CreateOptions{
50+
CreateFlags: flags,
51+
Dependencies: dependencies,
52+
GetAllEnvironmentsCallback: func() ([]*environments.Environment, error) {
53+
return selectors.GetAllEnvironments(dependencies.Client)
54+
},
55+
}
56+
}
57+
58+
func NewCmdCreate(f factory.Factory) *cobra.Command {
59+
createFlags := NewCreateFlags()
60+
descriptionFilePath := ""
61+
62+
cmd := &cobra.Command{
63+
Use: "create",
64+
Short: "Create an Generic OpenID Connect account",
65+
Long: "Create an Generic OpenID Connect account in Octopus Deploy",
66+
Example: heredoc.Docf("$ %s account generic-oidc create", constants.ExecutableName),
67+
Aliases: []string{"new"},
68+
RunE: func(c *cobra.Command, _ []string) error {
69+
opts := NewCreateOptions(createFlags, cmd.NewDependencies(f, c))
70+
if descriptionFilePath != "" {
71+
if err := validation.IsExistingFile(descriptionFilePath); err != nil {
72+
return err
73+
}
74+
data, err := os.ReadFile(descriptionFilePath)
75+
if err != nil {
76+
return err
77+
}
78+
opts.Description.Value = string(data)
79+
}
80+
opts.NoPrompt = !f.IsPromptEnabled()
81+
82+
if opts.Environments.Value != nil {
83+
env, err := helper.ResolveEnvironmentNames(opts.Environments.Value, opts.Client)
84+
if err != nil {
85+
return err
86+
}
87+
opts.Environments.Value = env
88+
}
89+
return CreateRun(opts)
90+
},
91+
}
92+
93+
flags := cmd.Flags()
94+
flags.StringVarP(&createFlags.Name.Value, createFlags.Name.Name, "n", "", "A short, memorable, unique name for this account.")
95+
flags.StringVarP(&createFlags.Description.Value, createFlags.Description.Value, "d", "", "A summary explaining the use of the account to other users.")
96+
flags.StringArrayVarP(&createFlags.Environments.Value, createFlags.Environments.Name, "e", nil, "The environments that are allowed to use this account")
97+
flags.StringArrayVarP(&createFlags.ExecutionSubjectKeys.Value, createFlags.ExecutionSubjectKeys.Name, "E", nil, "The subject keys used for a deployment or runbook")
98+
flags.StringVar(&createFlags.Audience.Value, createFlags.Audience.Name, "", "The audience claim for the federated credentials. Defaults to api://default")
99+
flags.StringVarP(&descriptionFilePath, "description-file", "D", "", "Read the description from `file`")
100+
101+
return cmd
102+
}
103+
104+
func CreateRun(opts *CreateOptions) error {
105+
if !opts.NoPrompt {
106+
if err := PromptMissing(opts); err != nil {
107+
return err
108+
}
109+
}
110+
var createdAccount accounts.IAccount
111+
oidcAccount, err := accounts.NewGenericOIDCAccount(
112+
opts.Name.Value,
113+
)
114+
if err != nil {
115+
return err
116+
}
117+
oidcAccount.DeploymentSubjectKeys = opts.ExecutionSubjectKeys.Value
118+
oidcAccount.Audience = opts.Audience.Value
119+
oidcAccount.Description = opts.Description.Value
120+
121+
createdAccount, err = opts.Client.Accounts.Add(oidcAccount)
122+
if err != nil {
123+
return err
124+
}
125+
126+
_, err = fmt.Fprintf(opts.Out, "Successfully created Generic account %s %s.\n", createdAccount.GetName(), output.Dimf("(%s)", createdAccount.GetSlug()))
127+
if err != nil {
128+
return err
129+
}
130+
link := output.Bluef("%s/app#/%s/infrastructure/accounts/%s", opts.Host, opts.Space.GetID(), createdAccount.GetID())
131+
fmt.Fprintf(opts.Out, "\nView this account on Octopus Deploy: %s\n", link)
132+
if !opts.NoPrompt {
133+
autoCmd := flag.GenerateAutomationCmd(
134+
opts.CmdPath,
135+
opts.Name,
136+
opts.Description,
137+
opts.Environments,
138+
opts.ExecutionSubjectKeys,
139+
opts.Audience,
140+
)
141+
fmt.Fprintf(opts.Out, "\nAutomation Command: %s\n", autoCmd)
142+
}
143+
return nil
144+
}
145+
146+
func PromptMissing(opts *CreateOptions) error {
147+
if opts.Name.Value == "" {
148+
if err := opts.Ask(&survey.Input{
149+
Message: "Name",
150+
Help: "A short, memorable, unique name for this account.",
151+
}, &opts.Name.Value, survey.WithValidator(survey.ComposeValidators(
152+
survey.MaxLength(200),
153+
survey.MinLength(1),
154+
survey.Required,
155+
))); err != nil {
156+
return err
157+
}
158+
}
159+
160+
if opts.Description.Value == "" {
161+
if err := opts.Ask(&surveyext.OctoEditor{
162+
Editor: &survey.Editor{
163+
Message: "Description",
164+
Help: "A summary explaining the use of the account to other users.",
165+
FileName: "*.md",
166+
},
167+
Optional: true,
168+
}, &opts.Description.Value); err != nil {
169+
return err
170+
}
171+
}
172+
173+
var err error
174+
if len(opts.ExecutionSubjectKeys.Value) == 0 {
175+
opts.ExecutionSubjectKeys.Value, err = promptSubjectKeys(opts.Ask, "Deployment and Runbook subject keys", []string{"space", "environment", "project", "tenant", "runbook", "account", "type"})
176+
if err != nil {
177+
return err
178+
}
179+
}
180+
181+
if opts.Audience.Value == "" {
182+
if err := opts.Ask(&survey.Input{
183+
Message: "Audience",
184+
Default: "api://default",
185+
Help: "Set this if you need to override the default Audience value.",
186+
}, &opts.Audience.Value); err != nil {
187+
return err
188+
}
189+
}
190+
191+
if opts.Environments.Value == nil {
192+
envs, err := selectors.EnvironmentsMultiSelect(opts.Ask, opts.GetAllEnvironmentsCallback,
193+
"Choose the environments that are allowed to use this account.\n"+
194+
output.Dim("If nothing is selected, the account can be used for deployments to any environment."), false)
195+
if err != nil {
196+
return err
197+
}
198+
opts.Environments.Value = util.SliceTransform(envs, func(e *environments.Environment) string { return e.ID })
199+
}
200+
return nil
201+
}
202+
203+
func promptSubjectKeys(ask question.Asker, message string, opts []string) ([]string, error) {
204+
keys, err := question.MultiSelectMap(ask, message, opts, func(item string) string { return item }, false)
205+
if err != nil {
206+
return nil, err
207+
}
208+
if len(keys) > 0 {
209+
return keys, nil
210+
}
211+
212+
return nil, nil
213+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package create_test
2+
3+
import (
4+
"github.com/OctopusDeploy/cli/pkg/cmd"
5+
"github.com/OctopusDeploy/cli/test/fixtures"
6+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments"
7+
"github.com/stretchr/testify/assert"
8+
"testing"
9+
10+
"github.com/OctopusDeploy/cli/pkg/cmd/account/generic-oidc/create"
11+
"github.com/OctopusDeploy/cli/test/testutil"
12+
)
13+
14+
func TestPromptMissing_AllOptionsSupplied(t *testing.T) {
15+
pa := []*testutil.PA{}
16+
17+
asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa)
18+
flags := create.NewCreateFlags()
19+
flags.Name.Value = "The Final Frontier"
20+
flags.Description.Value = "Where no person has gone before"
21+
flags.ExecutionSubjectKeys.Value = []string{"space"}
22+
flags.Audience.Value = "custom audience"
23+
flags.Environments.Value = []string{"dev"}
24+
25+
opts := &create.CreateOptions{
26+
CreateFlags: flags,
27+
Dependencies: &cmd.Dependencies{Ask: asker},
28+
}
29+
_ = create.PromptMissing(opts)
30+
checkRemainingPrompts()
31+
}
32+
33+
func TestPromptMissing_NoOptionsSupplied(t *testing.T) {
34+
pa := []*testutil.PA{
35+
testutil.NewInputPrompt("Name", "A short, memorable, unique name for this account.", "oidc account"),
36+
testutil.NewMultiSelectPrompt("Deployment and Runbook subject keys", "", []string{"space", "environment", "project", "tenant", "runbook", "account", "type"}, []string{"space", "type"}),
37+
testutil.NewInputPromptWithDefault("Audience", "Set this if you need to override the default Audience value.", "api://default", "custom audience"),
38+
testutil.NewMultiSelectPrompt("Choose the environments that are allowed to use this account.\nIf nothing is selected, the account can be used for deployments to any environment.", "", []string{"testenv"}, []string{"testenv"}),
39+
}
40+
41+
asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa)
42+
flags := create.NewCreateFlags()
43+
flags.Description.Value = "the description" // this is due the input mocking not support OctoEditor
44+
45+
opts := &create.CreateOptions{
46+
CreateFlags: flags,
47+
Dependencies: &cmd.Dependencies{Ask: asker},
48+
GetAllEnvironmentsCallback: func() ([]*environments.Environment, error) {
49+
return []*environments.Environment{fixtures.NewEnvironment("Spaces-1", "Environments-1", "testenv")}, nil
50+
},
51+
}
52+
_ = create.PromptMissing(opts)
53+
54+
assert.Equal(t, "oidc account", flags.Name.Value)
55+
assert.Equal(t, "custom audience", flags.Audience.Value)
56+
assert.Equal(t, []string{"Environments-1"}, flags.Environments.Value)
57+
checkRemainingPrompts()
58+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package generic_oidc
2+
3+
import (
4+
"github.com/MakeNowJust/heredoc/v2"
5+
cmdCreate "github.com/OctopusDeploy/cli/pkg/cmd/account/generic-oidc/create"
6+
cmdList "github.com/OctopusDeploy/cli/pkg/cmd/account/generic-oidc/list"
7+
"github.com/OctopusDeploy/cli/pkg/constants"
8+
"github.com/OctopusDeploy/cli/pkg/factory"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func NewCmdGenericOidc(f factory.Factory) *cobra.Command {
13+
cmd := &cobra.Command{
14+
Use: "generic-oidc <command>",
15+
Short: "Manage Generic OpenID Connect accounts",
16+
Long: "Manage Generic OpenID Connect accounts in Octopus Deploy",
17+
Example: heredoc.Docf("$ %s account generic-oidc list", constants.ExecutableName),
18+
}
19+
20+
cmd.AddCommand(cmdList.NewCmdList(f))
21+
cmd.AddCommand(cmdCreate.NewCmdCreate(f))
22+
23+
return cmd
24+
}

0 commit comments

Comments
 (0)