Skip to content

Commit 2a2bb98

Browse files
committed
refactor(cli): migrate from kingpin to cobra
1 parent e5b4deb commit 2a2bb98

File tree

17 files changed

+698
-487
lines changed

17 files changed

+698
-487
lines changed

cli/add.go

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66
"os"
77

88
"github.com/byteness/keyring"
9-
"github.com/alecthomas/kingpin/v2"
109
"github.com/aws/aws-sdk-go-v2/aws"
1110
"github.com/byteness/aws-vault/v7/prompt"
1211
"github.com/byteness/aws-vault/v7/vault"
12+
"github.com/spf13/cobra"
1313
)
1414

1515
type AddCommandInput struct {
@@ -18,35 +18,41 @@ type AddCommandInput struct {
1818
AddConfig bool
1919
}
2020

21-
func ConfigureAddCommand(app *kingpin.Application, a *AwsVault) {
21+
func NewAddCommand(a *AwsVault) *cobra.Command {
2222
input := AddCommandInput{}
2323

24-
cmd := app.Command("add", "Add credentials to the secure keystore.")
25-
26-
cmd.Arg("profile", "Name of the profile").
27-
Required().
28-
StringVar(&input.ProfileName)
24+
cmd := &cobra.Command{
25+
Use: "add [profile]",
26+
Short: "Add credentials to the secure keystore",
27+
Long: "Add credentials to the secure keystore",
28+
Args: cobra.ExactArgs(1),
29+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
30+
if len(args) == 0 {
31+
return a.CompleteProfileNames()(cmd, args, toComplete)
32+
}
33+
return nil, cobra.ShellCompDirectiveNoFileComp
34+
},
35+
RunE: func(cmd *cobra.Command, args []string) error {
36+
input.ProfileName = args[0]
37+
keyring, err := a.Keyring()
38+
if err != nil {
39+
return err
40+
}
41+
awsConfigFile, err := a.AwsConfigFile()
42+
if err != nil {
43+
return err
44+
}
45+
return AddCommand(input, keyring, awsConfigFile)
46+
},
47+
}
2948

30-
cmd.Flag("env", "Read the credentials from the environment (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)").
31-
BoolVar(&input.FromEnv)
49+
// --env flag to read credentials from environment variables
50+
cmd.Flags().BoolVar(&input.FromEnv, "env", false, "Read the credentials from the environment (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)")
3251

33-
cmd.Flag("add-config", "Add a profile to ~/.aws/config if one doesn't exist").
34-
Default("true").
35-
BoolVar(&input.AddConfig)
52+
// --add-config flag (default is true)
53+
cmd.Flags().BoolVar(&input.AddConfig, "add-config", true, "Add a profile to ~/.aws/config if one doesn't exist")
3654

37-
cmd.Action(func(c *kingpin.ParseContext) error {
38-
keyring, err := a.Keyring()
39-
if err != nil {
40-
return err
41-
}
42-
awsConfigFile, err := a.AwsConfigFile()
43-
if err != nil {
44-
return err
45-
}
46-
err = AddCommand(input, keyring, awsConfigFile)
47-
app.FatalIfError(err, "add")
48-
return nil
49-
})
55+
return cmd
5056
}
5157

5258
func AddCommand(input AddCommandInput, keyring keyring.Keyring, awsConfigFile *vault.ConfigFile) error {
@@ -73,6 +79,7 @@ func AddCommand(input AddCommandInput, keyring keyring.Keyring, awsConfigFile *v
7379
if secretKey, err = prompt.TerminalSecretPrompt("Enter Secret Access Key: "); err != nil {
7480
return err
7581
}
82+
// PRESERVED: MFA serial prompt functionality
7683
if mfaSerial, err = prompt.TerminalPrompt("Enter MFA Device ARN (If MFA is not enabled, leave this blank): "); err != nil {
7784
return err
7885
}
@@ -96,7 +103,7 @@ func AddCommand(input AddCommandInput, keyring keyring.Keyring, awsConfigFile *v
96103
if input.AddConfig {
97104
newProfileSection := vault.ProfileSection{
98105
Name: input.ProfileName,
99-
MfaSerial: mfaSerial,
106+
MfaSerial: mfaSerial, // PRESERVED: MFA serial saved to config
100107
}
101108
log.Printf("Adding profile %s to config at %s", input.ProfileName, awsConfigFile.Path)
102109
if err := awsConfigFile.Add(newProfileSection); err != nil {

cli/add_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"log"
55
"os"
66

7-
"github.com/alecthomas/kingpin/v2"
7+
"github.com/spf13/cobra"
88
)
99

1010
func ExampleAddCommand() {
@@ -25,9 +25,12 @@ func ExampleAddCommand() {
2525
defer os.Unsetenv("AWS_VAULT_BACKEND")
2626
defer os.Unsetenv("AWS_VAULT_FILE_PASSPHRASE")
2727

28-
app := kingpin.New(`aws-vault`, ``)
29-
ConfigureAddCommand(app, ConfigureGlobals(app))
30-
kingpin.MustParse(app.Parse([]string{"add", "--debug", "--env", "foo"}))
28+
a := NewAwsVault()
29+
rootCmd := &cobra.Command{Use: "aws-vault"}
30+
AddGlobalFlags(rootCmd, a)
31+
rootCmd.AddCommand(NewAddCommand(a))
32+
rootCmd.SetArgs([]string{"add", "--debug", "--env", "foo"})
33+
_ = rootCmd.Execute()
3134

3235
// Output:
3336
// Added credentials to profile "foo" in vault

cli/clear.go

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,47 @@ import (
44
"fmt"
55

66
"github.com/byteness/keyring"
7-
"github.com/alecthomas/kingpin/v2"
87
"github.com/byteness/aws-vault/v7/vault"
8+
"github.com/spf13/cobra"
99
)
1010

1111
type ClearCommandInput struct {
1212
ProfileName string
1313
}
1414

15-
func ConfigureClearCommand(app *kingpin.Application, a *AwsVault) {
15+
func NewClearCommand(a *AwsVault) *cobra.Command {
1616
input := ClearCommandInput{}
1717

18-
cmd := app.Command("clear", "Clear temporary credentials from the secure keystore.")
18+
cmd := &cobra.Command{
19+
Use: "clear [profile]",
20+
Short: "Clear temporary credentials from the secure keystore",
21+
Long: "Clear temporary credentials from the secure keystore",
22+
Args: cobra.MaximumNArgs(1),
23+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
24+
if len(args) == 0 {
25+
return a.CompleteProfileNames()(cmd, args, toComplete)
26+
}
27+
return nil, cobra.ShellCompDirectiveNoFileComp
28+
},
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
if len(args) > 0 {
31+
input.ProfileName = args[0]
32+
}
1933

20-
cmd.Arg("profile", "Name of the profile").
21-
HintAction(a.MustGetProfileNames).
22-
StringVar(&input.ProfileName)
34+
keyring, err := a.Keyring()
35+
if err != nil {
36+
return err
37+
}
38+
awsConfigFile, err := a.AwsConfigFile()
39+
if err != nil {
40+
return err
41+
}
2342

24-
cmd.Action(func(c *kingpin.ParseContext) (err error) {
25-
keyring, err := a.Keyring()
26-
if err != nil {
27-
return err
28-
}
29-
awsConfigFile, err := a.AwsConfigFile()
30-
if err != nil {
31-
return err
32-
}
43+
return ClearCommand(input, awsConfigFile, keyring)
44+
},
45+
}
3346

34-
err = ClearCommand(input, awsConfigFile, keyring)
35-
app.FatalIfError(err, "clear")
36-
return nil
37-
})
47+
return cmd
3848
}
3949

4050
func ClearCommand(input ClearCommandInput, awsConfigFile *vault.ConfigFile, keyring keyring.Keyring) error {

cli/exec.go

Lines changed: 90 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import (
1313
"syscall"
1414
"time"
1515

16-
"github.com/alecthomas/kingpin/v2"
1716
"github.com/aws/aws-sdk-go-v2/aws"
1817
"github.com/byteness/aws-vault/v7/iso8601"
1918
"github.com/byteness/aws-vault/v7/server"
2019
"github.com/byteness/aws-vault/v7/vault"
2120
"github.com/byteness/keyring"
21+
"github.com/spf13/cobra"
2222
)
2323

2424
type ExecCommandInput struct {
@@ -66,108 +66,111 @@ func hasBackgroundServer(input ExecCommandInput) bool {
6666
return input.StartEcsServer || input.StartEc2Server
6767
}
6868

69-
func ConfigureExecCommand(app *kingpin.Application, a *AwsVault) {
69+
func NewExecCommand(a *AwsVault) *cobra.Command {
7070
input := ExecCommandInput{}
7171

72-
cmd := app.Command("exec", "Execute a command with AWS credentials.")
73-
74-
cmd.Flag("duration", "Duration of the temporary or assume-role session. Defaults to 1h").
75-
Short('d').
76-
DurationVar(&input.SessionDuration)
77-
78-
cmd.Flag("no-session", "Skip creating STS session with GetSessionToken").
79-
Short('n').
80-
BoolVar(&input.NoSession)
81-
82-
cmd.Flag("region", "The AWS region").
83-
StringVar(&input.Config.Region)
84-
85-
cmd.Flag("mfa-token", "The MFA token to use").
86-
Short('t').
87-
StringVar(&input.Config.MfaToken)
88-
89-
cmd.Flag("json", "Output credentials in JSON that can be used by credential_process").
90-
Short('j').
91-
Hidden().
92-
BoolVar(&input.JSONDeprecated)
93-
94-
cmd.Flag("server", "Alias for --ecs-server").
95-
Short('s').
96-
BoolVar(&input.StartEcsServer)
97-
98-
cmd.Flag("ec2-server", "Run a EC2 metadata server in the background for credentials").
99-
BoolVar(&input.StartEc2Server)
100-
101-
cmd.Flag("ecs-server", "Run a ECS credential server in the background for credentials (the SDK or app must support AWS_CONTAINER_CREDENTIALS_FULL_URI)").
102-
BoolVar(&input.StartEcsServer)
103-
104-
cmd.Flag("lazy", "When using --ecs-server, lazily fetch credentials").
105-
BoolVar(&input.Lazy)
106-
107-
cmd.Flag("stdout", "Print the SSO link to the terminal without automatically opening the browser").
108-
BoolVar(&input.UseStdout)
109-
110-
cmd.Arg("profile", "Name of the profile").
111-
//Required().
112-
Default(os.Getenv("AWS_PROFILE")).
113-
HintAction(a.MustGetProfileNames).
114-
StringVar(&input.ProfileName)
72+
cmd := &cobra.Command{
73+
Use: "exec [profile] [cmd] [args...]",
74+
Short: "Execute a command with AWS credentials",
75+
Long: "Execute a command with AWS credentials",
76+
Args: cobra.MinimumNArgs(0),
77+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
78+
if len(args) == 0 {
79+
return a.CompleteProfileNames()(cmd, args, toComplete)
80+
}
81+
// Disable file completion for command arguments
82+
return nil, cobra.ShellCompDirectiveNoFileComp
83+
},
84+
RunE: func(cmd *cobra.Command, args []string) error {
85+
// Parse args: [profile] [command] [command args...]
86+
if len(args) > 0 {
87+
input.ProfileName = args[0]
88+
}
89+
if len(args) > 1 {
90+
input.Command = args[1]
91+
input.Args = args[2:]
92+
}
11593

116-
cmd.Arg("cmd", "Command to execute, defaults to $SHELL").
117-
StringVar(&input.Command)
94+
// Apply defaults if profile not provided
95+
if input.ProfileName == "" {
96+
input.ProfileName = os.Getenv("AWS_PROFILE")
97+
}
11898

119-
cmd.Arg("args", "Command arguments").
120-
StringsVar(&input.Args)
99+
input.Config.MfaPromptMethod = a.PromptDriver(hasBackgroundServer(input))
100+
input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration
101+
input.Config.AssumeRoleDuration = input.SessionDuration
102+
input.Config.SSOUseStdout = input.UseStdout
103+
input.ShowHelpMessages = !a.Debug && input.Command == "" && isATerminal() && os.Getenv("AWS_VAULT_DISABLE_HELP_MESSAGE") != "1"
121104

122-
cmd.Action(func(c *kingpin.ParseContext) (err error) {
123-
input.Config.MfaPromptMethod = a.PromptDriver(hasBackgroundServer(input))
124-
input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration
125-
input.Config.AssumeRoleDuration = input.SessionDuration
126-
input.Config.SSOUseStdout = input.UseStdout
127-
input.ShowHelpMessages = !a.Debug && input.Command == "" && isATerminal() && os.Getenv("AWS_VAULT_DISABLE_HELP_MESSAGE") != "1"
105+
f, err := a.AwsConfigFile()
106+
if err != nil {
107+
return err
108+
}
109+
keyring, err := a.Keyring()
110+
if err != nil {
111+
return err
112+
}
128113

129-
f, err := a.AwsConfigFile()
130-
if err != nil {
131-
return err
132-
}
133-
keyring, err := a.Keyring()
134-
if err != nil {
135-
return err
136-
}
114+
if input.ProfileName == "" {
115+
// If no profile provided select from configured AWS profiles
116+
ProfileName, err := pickAwsProfile(f.ProfileNames())
137117

138-
if input.ProfileName == "" {
139-
// If no profile provided select from configured AWS profiles
140-
ProfileName, err := pickAwsProfile(f.ProfileNames())
118+
if err != nil {
119+
return fmt.Errorf("unable to select a 'profile'. Try --help: %w", err)
120+
}
141121

142-
if err != nil {
143-
return fmt.Errorf("unable to select a 'profile'. Try --help: %w", err)
122+
input.ProfileName = ProfileName
144123
}
145124

146-
input.ProfileName = ProfileName
147-
}
148-
149-
exitcode := 0
150-
if input.JSONDeprecated {
151-
exportCommandInput := ExportCommandInput{
152-
ProfileName: input.ProfileName,
153-
Format: "json",
154-
Config: input.Config,
155-
SessionDuration: input.SessionDuration,
156-
NoSession: input.NoSession,
125+
exitcode := 0
126+
if input.JSONDeprecated {
127+
exportCommandInput := ExportCommandInput{
128+
ProfileName: input.ProfileName,
129+
Format: "json",
130+
Config: input.Config,
131+
SessionDuration: input.SessionDuration,
132+
NoSession: input.NoSession,
133+
}
134+
135+
err = ExportCommand(exportCommandInput, f, keyring)
136+
} else {
137+
exitcode, err = ExecCommand(input, f, keyring)
157138
}
158139

159-
err = ExportCommand(exportCommandInput, f, keyring)
160-
} else {
161-
exitcode, err = ExecCommand(input, f, keyring)
162-
}
140+
if err != nil {
141+
return err
142+
}
163143

164-
app.FatalIfError(err, "exec")
144+
// override exit code if not err
145+
os.Exit(exitcode)
165146

166-
// override exit code if not err
167-
os.Exit(exitcode)
147+
return nil
148+
},
149+
}
168150

169-
return nil
151+
cmd.Flags().DurationVarP(&input.SessionDuration, "duration", "d", time.Hour, "Duration of the temporary or assume-role session. Defaults to 1h")
152+
cmd.Flags().BoolVarP(&input.NoSession, "no-session", "n", false, "Skip creating STS session with GetSessionToken")
153+
cmd.Flags().StringVar(&input.Config.Region, "region", "", "The AWS region")
154+
cmd.Flags().StringVarP(&input.Config.MfaToken, "mfa-token", "t", "", "The MFA token to use")
155+
cmd.Flags().BoolVarP(&input.JSONDeprecated, "json", "j", false, "Output credentials in JSON that can be used by credential_process")
156+
_ = cmd.Flags().MarkHidden("json")
157+
cmd.Flags().BoolVarP(&input.StartEcsServer, "server", "s", false, "Alias for --ecs-server")
158+
cmd.Flags().BoolVar(&input.StartEc2Server, "ec2-server", false, "Run a EC2 metadata server in the background for credentials")
159+
cmd.Flags().BoolVar(&input.StartEcsServer, "ecs-server", false, "Run a ECS credential server in the background for credentials (the SDK or app must support AWS_CONTAINER_CREDENTIALS_FULL_URI)")
160+
cmd.Flags().BoolVar(&input.Lazy, "lazy", false, "When using --ecs-server, lazily fetch credentials")
161+
cmd.Flags().BoolVar(&input.UseStdout, "stdout", false, "Print the SSO link to the terminal without automatically opening the browser")
162+
163+
cmd.RegisterFlagCompletionFunc("duration", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
164+
return []string{"1h", "2h", "4h", "8h", "12h"}, cobra.ShellCompDirectiveNoFileComp
165+
})
166+
cmd.RegisterFlagCompletionFunc("region", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
167+
return AwsRegions(), cobra.ShellCompDirectiveNoFileComp
170168
})
169+
cmd.RegisterFlagCompletionFunc("mfa-token", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
170+
return []string{}, cobra.ShellCompDirectiveNoFileComp
171+
})
172+
173+
return cmd
171174
}
172175

173176
func ExecCommand(input ExecCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) (exitcode int, err error) {

0 commit comments

Comments
 (0)