Skip to content

Commit eaeffcd

Browse files
committed
refactor(cli): migrate from kingpin to cobra
BREAKING CHANGE: Migrates the CLI framework from kingpin to cobra for improved maintainability and better shell completion support. This change brings several improvements: - Enhanced shell completion with cobra's native completion engine - More intuitive command structure and flag handling - Better error messages and help text formatting - Improved maintainability with cobra's cleaner command organization All command functionality remains the same, but the underlying CLI framework has been completely replaced. Users should experience no breaking changes in command usage, though some internal behaviors may differ slightly. Commands migrated: - add, remove, list, rotate - exec, export, clear - login, proxy The migration maintains backward compatibility for all command-line arguments and flags while providing a more robust foundation for future enhancements.
1 parent e5b4deb commit eaeffcd

File tree

18 files changed

+708
-486
lines changed

18 files changed

+708
-486
lines changed

.github/workflows/go.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,24 @@ jobs:
3030
go-version: 'stable'
3131
#go-version-file: 'go.mod'
3232
check-latest: true
33+
- name: Setup Test Keychain
34+
if: runner.os == 'macOS'
35+
run: |
36+
security create-keychain -p test-password aws-vault-test.keychain
37+
38+
security default-keychain -s aws-vault-test.keychain
39+
40+
security unlock-keychain -p test-password aws-vault-test.keychain
41+
42+
security set-keychain-settings -t 3600 -u aws-vault-test.keychain
43+
44+
security list-keychains -d user -s aws-vault-test.keychain $(security list-keychains -d user | sed s/\"//g)
3345
- name: Run tests
3446
run: make test #go test -race ./...
47+
- name: Cleanup Test Keychain
48+
if: runner.os == 'macOS' && always()
49+
run: |
50+
security delete-keychain aws-vault-test.keychain || true
3551
lint:
3652
permissions:
3753
contents: read # for actions/checkout to fetch code

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 ConfigureAddCommand(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: 6 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,11 @@ 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+
rootCmd := &cobra.Command{Use: "aws-vault"}
29+
a := ConfigureGlobals(rootCmd)
30+
rootCmd.AddCommand(ConfigureAddCommand(a))
31+
rootCmd.SetArgs([]string{"add", "--debug", "--env", "foo"})
32+
_ = rootCmd.Execute()
3133

3234
// Output:
3335
// 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 ConfigureClearCommand(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 {

0 commit comments

Comments
 (0)