From a881457b20c47c04197b90979cd484e84f5f5e33 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Fri, 18 Nov 2022 16:07:00 -0700 Subject: [PATCH 01/12] Add Secret Store support Adds the following commands: fastly secret-store create Create secret store get Get secret store delete Delete secret store list List secret stores fastly secret-store-entry create Create secret get Get secret delete Delete secret list List secrets --- pkg/api/interface.go | 9 + pkg/app/commands.go | 19 + pkg/app/run.go | 2 +- pkg/app/run_test.go | 2 + pkg/commands/secretstore/createsecret.go | 117 +++++++ pkg/commands/secretstore/createstore.go | 52 +++ pkg/commands/secretstore/deletesecret.go | 63 ++++ pkg/commands/secretstore/deletestore.go | 60 ++++ pkg/commands/secretstore/doc.go | 5 + pkg/commands/secretstore/flags.go | 73 ++++ pkg/commands/secretstore/getsecret.go | 53 +++ pkg/commands/secretstore/getstore.go | 52 +++ pkg/commands/secretstore/helper_test.go | 58 +++ pkg/commands/secretstore/json.go | 36 ++ pkg/commands/secretstore/listsecrets.go | 71 ++++ pkg/commands/secretstore/liststores.go | 70 ++++ pkg/commands/secretstore/root_secret.go | 31 ++ pkg/commands/secretstore/root_store.go | 31 ++ pkg/commands/secretstore/secret_test.go | 426 +++++++++++++++++++++++ pkg/commands/secretstore/store_test.go | 358 +++++++++++++++++++ pkg/mock/api.go | 49 +++ pkg/sync/sync.go | 7 +- pkg/testutil/assert.go | 2 +- pkg/text/secretstore.go | 66 ++++ pkg/text/text.go | 32 +- 25 files changed, 1734 insertions(+), 10 deletions(-) create mode 100644 pkg/commands/secretstore/createsecret.go create mode 100644 pkg/commands/secretstore/createstore.go create mode 100644 pkg/commands/secretstore/deletesecret.go create mode 100644 pkg/commands/secretstore/deletestore.go create mode 100644 pkg/commands/secretstore/doc.go create mode 100644 pkg/commands/secretstore/flags.go create mode 100644 pkg/commands/secretstore/getsecret.go create mode 100644 pkg/commands/secretstore/getstore.go create mode 100644 pkg/commands/secretstore/helper_test.go create mode 100644 pkg/commands/secretstore/json.go create mode 100644 pkg/commands/secretstore/listsecrets.go create mode 100644 pkg/commands/secretstore/liststores.go create mode 100644 pkg/commands/secretstore/root_secret.go create mode 100644 pkg/commands/secretstore/root_store.go create mode 100644 pkg/commands/secretstore/secret_test.go create mode 100644 pkg/commands/secretstore/store_test.go create mode 100644 pkg/text/secretstore.go diff --git a/pkg/api/interface.go b/pkg/api/interface.go index 7ad9c605c..3e5315dff 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -335,6 +335,15 @@ type Interface interface { GetObjectStoreKey(i *fastly.GetObjectStoreKeyInput) (string, error) InsertObjectStoreKey(i *fastly.InsertObjectStoreKeyInput) error + CreateSecretStore(i *fastly.CreateSecretStoreInput) (*fastly.SecretStore, error) + GetSecretStore(i *fastly.GetSecretStoreInput) (*fastly.SecretStore, error) + DeleteSecretStore(i *fastly.DeleteSecretStoreInput) error + ListSecretStores(i *fastly.ListSecretStoresInput) (*fastly.SecretStores, error) + CreateSecret(i *fastly.CreateSecretInput) (*fastly.Secret, error) + GetSecret(i *fastly.GetSecretInput) (*fastly.Secret, error) + DeleteSecret(i *fastly.DeleteSecretInput) error + ListSecrets(i *fastly.ListSecretsInput) (*fastly.Secrets, error) + CreateResource(i *fastly.CreateResourceInput) (*fastly.Resource, error) } diff --git a/pkg/app/commands.go b/pkg/app/commands.go index 3a2d94296..5c885bfb8 100644 --- a/pkg/app/commands.go +++ b/pkg/app/commands.go @@ -45,6 +45,7 @@ import ( "github.com/fastly/cli/pkg/commands/pop" "github.com/fastly/cli/pkg/commands/profile" "github.com/fastly/cli/pkg/commands/purge" + "github.com/fastly/cli/pkg/commands/secretstore" "github.com/fastly/cli/pkg/commands/service" "github.com/fastly/cli/pkg/commands/serviceauth" "github.com/fastly/cli/pkg/commands/serviceversion" @@ -312,6 +313,16 @@ func defineCommands( profileToken := profile.NewTokenCommand(profileCmdRoot.CmdClause, globals) profileUpdate := profile.NewUpdateCommand(profileCmdRoot.CmdClause, profile.APIClientFactory(opts.APIClient), globals) purgeCmdRoot := purge.NewRootCommand(app, globals, data) + secretstoreCmdRoot := secretstore.NewStoreRootCommand(app, globals) + secretstoreCreateStore := secretstore.NewCreateStoreCommand(secretstoreCmdRoot.CmdClause, globals, data) + secretstoreGetStore := secretstore.NewGetStoreCommand(secretstoreCmdRoot.CmdClause, globals, data) + secretstoreDeleteStore := secretstore.NewDeleteStoreCommand(secretstoreCmdRoot.CmdClause, globals, data) + secretstoreListStores := secretstore.NewListStoresCommand(secretstoreCmdRoot.CmdClause, globals, data) + secretstoreSecretCmdRoot := secretstore.NewSecretRootCommand(app, globals) + secretstoreCreateSecret := secretstore.NewCreateSecretCommand(secretstoreSecretCmdRoot.CmdClause, globals, data) + secretstoreGetSecret := secretstore.NewGetSecretCommand(secretstoreSecretCmdRoot.CmdClause, globals, data) + secretstoreDeleteSecret := secretstore.NewDeleteSecretCommand(secretstoreSecretCmdRoot.CmdClause, globals, data) + secretstoreListSecrets := secretstore.NewListSecretsCommand(secretstoreSecretCmdRoot.CmdClause, globals, data) serviceCmdRoot := service.NewRootCommand(app, globals) serviceCreate := service.NewCreateCommand(serviceCmdRoot.CmdClause, globals) serviceDelete := service.NewDeleteCommand(serviceCmdRoot.CmdClause, globals, data) @@ -630,6 +641,14 @@ func defineCommands( profileToken, profileUpdate, purgeCmdRoot, + secretstoreCreateStore, + secretstoreGetStore, + secretstoreDeleteStore, + secretstoreListStores, + secretstoreCreateSecret, + secretstoreGetSecret, + secretstoreDeleteSecret, + secretstoreListSecrets, serviceCmdRoot, serviceCreate, serviceDelete, diff --git a/pkg/app/run.go b/pkg/app/run.go index 19d4cfc28..06689fd10 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -131,7 +131,7 @@ func Run(opts RunOpts) error { return err } - // If we are using the token from config file, check the files permissions + // If we are using the token from config file, check the file's permissions // to assert if they are not too open or have been altered outside of the // application and warn if so. segs := strings.Split(name, " ") diff --git a/pkg/app/run_test.go b/pkg/app/run_test.go index ec3cea5a9..cb536549b 100644 --- a/pkg/app/run_test.go +++ b/pkg/app/run_test.go @@ -74,6 +74,8 @@ objectstore pops profile purge +secret-store +secret-store-entry service service-auth service-version diff --git a/pkg/commands/secretstore/createsecret.go b/pkg/commands/secretstore/createsecret.go new file mode 100644 index 000000000..c9f52b41b --- /dev/null +++ b/pkg/commands/secretstore/createsecret.go @@ -0,0 +1,117 @@ +package secretstore + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "os" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v7/fastly" +) + +const ( + // Maximum secret length, as defined at https://developer.fastly.com/reference/api/secret-store. + maxSecretKiB = 64 + maxSecretLen = maxSecretKiB * 1024 +) + +// NewCreateSecretCommand returns a usable command registered under the parent. +func NewCreateSecretCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *CreateSecretCommand { + var c CreateSecretCommand + c.Globals = globals + c.manifest = data + c.CmdClause = parent.Command("create", "Create secret") + c.RegisterFlag(storeIDFlag(&c.Input.ID)) + c.RegisterFlag(secretNameFlag(&c.Input.Name)) + c.RegisterFlag(secretFileFlag(&c.secretFile)) + c.RegisterFlagBool(secretStdinFlag(&c.secretSTDIN)) + c.RegisterFlagBool(c.jsonFlag()) + return &c +} + +// CreateSecretCommand calls the Fastly API to create a secret. +type CreateSecretCommand struct { + cmd.Base + manifest manifest.Data + Input fastly.CreateSecretInput + secretFile string + secretSTDIN bool + jsonOutput +} + +var errMultipleSecretValue = fsterr.RemediationError{ + Inner: fmt.Errorf("invalid flag combination, --secret-file and --secret-stdin"), + Remediation: "Use one of --file or --stdin flag", +} + +var errNoSTDINData = fsterr.RemediationError{ + Inner: fmt.Errorf("unable to read from STDIN"), + Remediation: "Provide data to STDIN, or use --file to read from a file", +} + +var errMaxSecretLength = fsterr.RemediationError{ + Inner: fmt.Errorf("max secret size exceeded"), + Remediation: fmt.Sprintf("Maximum secret size is %dKiB", maxSecretKiB), +} + +// Exec invokes the application logic for the command. +func (cmd *CreateSecretCommand) Exec(in io.Reader, out io.Writer) error { + if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + if cmd.secretFile != "" && cmd.secretSTDIN { + return errMultipleSecretValue + } + + // Read secret's value: either from STDIN, a file, or prompt. + switch { + case cmd.secretSTDIN: + // Determine if 'in' has data available. + if in != nil && !text.IsTTY(in) { + var buf bytes.Buffer + if _, err := buf.ReadFrom(in); err != nil { + return err + } + cmd.Input.Secret = buf.Bytes() + } else { + return errNoSTDINData + } + + case cmd.secretFile != "": + var err error + if cmd.Input.Secret, err = os.ReadFile(cmd.secretFile); err != nil { + return err + } + + default: + secret, err := text.InputSecure(out, "Secret: ", in) + if err != nil { + return err + } + cmd.Input.Secret = []byte(secret) + } + + if len(cmd.Input.Secret) > maxSecretLen { + return errMaxSecretLength + } + + o, err := cmd.Globals.APIClient.CreateSecret(&cmd.Input) + if err != nil { + cmd.Globals.ErrLog.Add(err) + return err + } + + if ok, err := cmd.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created secret %s in store %s (digest %s)", o.Name, cmd.Input.ID, hex.EncodeToString(o.Digest)) + + return nil +} diff --git a/pkg/commands/secretstore/createstore.go b/pkg/commands/secretstore/createstore.go new file mode 100644 index 000000000..19b781e31 --- /dev/null +++ b/pkg/commands/secretstore/createstore.go @@ -0,0 +1,52 @@ +package secretstore + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v7/fastly" +) + +// NewCreateStoreCommand returns a usable command registered under the parent. +func NewCreateStoreCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *CreateStoreCommand { + var c CreateStoreCommand + c.Globals = globals + c.manifest = data + c.CmdClause = parent.Command("create", "Create secret store") + c.RegisterFlag(storeNameFlag(&c.Input.Name)) + c.RegisterFlagBool(c.jsonFlag()) + return &c +} + +// CreateStoreCommand calls the Fastly API to create a secret store. +type CreateStoreCommand struct { + cmd.Base + manifest manifest.Data + Input fastly.CreateSecretStoreInput + jsonOutput +} + +// Exec invokes the application logic for the command. +func (cmd *CreateStoreCommand) Exec(_ io.Reader, out io.Writer) error { + if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + o, err := cmd.Globals.APIClient.CreateSecretStore(&cmd.Input) + if err != nil { + cmd.Globals.ErrLog.Add(err) + return err + } + + if ok, err := cmd.WriteJSON(out, o); ok { + return err + } + + text.Success(out, "Created secret store %s (name %s)", o.ID, o.Name) + + return nil +} diff --git a/pkg/commands/secretstore/deletesecret.go b/pkg/commands/secretstore/deletesecret.go new file mode 100644 index 000000000..704f90803 --- /dev/null +++ b/pkg/commands/secretstore/deletesecret.go @@ -0,0 +1,63 @@ +package secretstore + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v7/fastly" +) + +// NewDeleteSecretCommand returns a usable command registered under the parent. +func NewDeleteSecretCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *DeleteSecretCommand { + var c DeleteSecretCommand + c.Globals = globals + c.manifest = data + c.CmdClause = parent.Command("delete", "Delete secret") + c.RegisterFlag(storeIDFlag(&c.Input.ID)) + c.RegisterFlag(secretNameFlag(&c.Input.Name)) + c.RegisterFlagBool(c.jsonFlag()) + return &c +} + +// DeleteSecretCommand calls the Fastly API to delete a secret. +type DeleteSecretCommand struct { + cmd.Base + manifest manifest.Data + Input fastly.DeleteSecretInput + jsonOutput +} + +// Exec invokes the application logic for the command. +func (cmd *DeleteSecretCommand) Exec(_ io.Reader, out io.Writer) error { + if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + err := cmd.Globals.APIClient.DeleteSecret(&cmd.Input) + if err != nil { + cmd.Globals.ErrLog.Add(err) + return err + } + + if cmd.jsonOutput.enabled { + o := struct { + Name string `json:"name"` + ID string `json:"store_id"` + Deleted bool `json:"deleted"` + }{ + cmd.Input.Name, + cmd.Input.ID, + true, + } + _, err := cmd.WriteJSON(out, o) + return err + } + + text.Success(out, "Deleted secret %s from store %s", cmd.Input.Name, cmd.Input.ID) + + return nil +} diff --git a/pkg/commands/secretstore/deletestore.go b/pkg/commands/secretstore/deletestore.go new file mode 100644 index 000000000..1131e4f65 --- /dev/null +++ b/pkg/commands/secretstore/deletestore.go @@ -0,0 +1,60 @@ +package secretstore + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v7/fastly" +) + +// NewDeleteStoreCommand returns a usable command registered under the parent. +func NewDeleteStoreCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *DeleteStoreCommand { + var c DeleteStoreCommand + c.Globals = globals + c.manifest = data + c.CmdClause = parent.Command("delete", "Delete secret store") + c.RegisterFlag(storeIDFlag(&c.Input.ID)) + c.RegisterFlagBool(c.jsonFlag()) + return &c +} + +// DeleteStoreCommand calls the Fastly API to delete a secret store. +type DeleteStoreCommand struct { + cmd.Base + manifest manifest.Data + Input fastly.DeleteSecretStoreInput + jsonOutput +} + +// Exec invokes the application logic for the command. +func (cmd *DeleteStoreCommand) Exec(_ io.Reader, out io.Writer) error { + if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + err := cmd.Globals.APIClient.DeleteSecretStore(&cmd.Input) + if err != nil { + cmd.Globals.ErrLog.Add(err) + return err + } + + if cmd.jsonOutput.enabled { + o := struct { + ID string `json:"id"` + Deleted bool `json:"deleted"` + }{ + cmd.Input.ID, + true, + } + _, err := cmd.WriteJSON(out, o) + return err + } + + text.Success(out, "Deleted secret store %s", cmd.Input.ID) + + return nil +} diff --git a/pkg/commands/secretstore/doc.go b/pkg/commands/secretstore/doc.go new file mode 100644 index 000000000..7db5573cc --- /dev/null +++ b/pkg/commands/secretstore/doc.go @@ -0,0 +1,5 @@ +// Package secretstore contains commands to inspect and manipulate Fastly edge +// secret stores. +// +// https://developer.fastly.com/reference/api/secret-store/ +package secretstore diff --git a/pkg/commands/secretstore/flags.go b/pkg/commands/secretstore/flags.go new file mode 100644 index 000000000..e8562c486 --- /dev/null +++ b/pkg/commands/secretstore/flags.go @@ -0,0 +1,73 @@ +package secretstore + +import ( + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/kingpin" +) + +// Secret Store flags. + +func storeIDFlag(dst *string) cmd.StringFlagOpts { + return cmd.StringFlagOpts{ + Name: "store-id", + Short: 's', + Description: "Store ID", + Dst: dst, + Required: true, + } +} + +func storeNameFlag(dst *string) cmd.StringFlagOpts { + return cmd.StringFlagOpts{ + Name: "name", + Short: 'n', + Description: "Store name", + Dst: dst, + Required: true, + } +} + +func secretNameFlag(dst *string) cmd.StringFlagOpts { + return cmd.StringFlagOpts{ + Name: "name", + Short: 'n', + Description: "Secret name", + Dst: dst, + Required: true, + } +} + +func secretFileFlag(dst *string) cmd.StringFlagOpts { + return cmd.StringFlagOpts{ + Name: "file", + Short: 'f', + Description: "Read secret value from file instead of prompt", + Dst: dst, + Required: false, + } +} + +func secretStdinFlag(dst *bool) cmd.BoolFlagOpts { + return cmd.BoolFlagOpts{ + Name: "stdin", + Description: "Read secret value from STDIN instead of prompt", + Dst: dst, + Required: false, + } +} + +func cursorFlag(dst *string) cmd.StringFlagOpts { + return cmd.StringFlagOpts{ + Name: "cursor", + Short: 'c', + Description: "Pagination cursor (Use 'next_cursor' value from list output)", + Dst: dst, + } +} + +func limitFlag(cmd *kingpin.CmdClause, dst *int) { + limit := cmd.Flag("limit", "Maxiumum number of items to list") + limit = limit.Default("50") + limit = limit.Short('l') + limit.IntVar(dst) +} diff --git a/pkg/commands/secretstore/getsecret.go b/pkg/commands/secretstore/getsecret.go new file mode 100644 index 000000000..15fdc10a7 --- /dev/null +++ b/pkg/commands/secretstore/getsecret.go @@ -0,0 +1,53 @@ +package secretstore + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v7/fastly" +) + +// NewGetSecretCommand returns a usable command registered under the parent. +func NewGetSecretCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *GetSecretCommand { + var c GetSecretCommand + c.Globals = globals + c.manifest = data + c.CmdClause = parent.Command("get", "Get secret") + c.RegisterFlag(storeIDFlag(&c.Input.ID)) + c.RegisterFlag(secretNameFlag(&c.Input.Name)) + c.RegisterFlagBool(c.jsonFlag()) + return &c +} + +// GetSecretCommand calls the Fastly API to list the available secret stores. +type GetSecretCommand struct { + cmd.Base + manifest manifest.Data + Input fastly.GetSecretInput + jsonOutput +} + +// Exec invokes the application logic for the command. +func (cmd *GetSecretCommand) Exec(_ io.Reader, out io.Writer) error { + if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + o, err := cmd.Globals.APIClient.GetSecret(&cmd.Input) + if err != nil { + cmd.Globals.ErrLog.Add(err) + return err + } + + if ok, err := cmd.WriteJSON(out, o); ok { + return err + } + + text.PrintSecret(out, "", o) + + return nil +} diff --git a/pkg/commands/secretstore/getstore.go b/pkg/commands/secretstore/getstore.go new file mode 100644 index 000000000..4e83843b0 --- /dev/null +++ b/pkg/commands/secretstore/getstore.go @@ -0,0 +1,52 @@ +package secretstore + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v7/fastly" +) + +// NewGetStoreCommand returns a usable command registered under the parent. +func NewGetStoreCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *GetStoreCommand { + var c GetStoreCommand + c.Globals = globals + c.manifest = data + c.CmdClause = parent.Command("get", "Get secret store") + c.RegisterFlag(storeIDFlag(&c.Input.ID)) + c.RegisterFlagBool(c.jsonFlag()) + return &c +} + +// GetStoreCommand calls the Fastly API to list the available secret stores. +type GetStoreCommand struct { + cmd.Base + manifest manifest.Data + Input fastly.GetSecretStoreInput + jsonOutput +} + +// Exec invokes the application logic for the command. +func (cmd *GetStoreCommand) Exec(_ io.Reader, out io.Writer) error { + if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + o, err := cmd.Globals.APIClient.GetSecretStore(&cmd.Input) + if err != nil { + cmd.Globals.ErrLog.Add(err) + return err + } + + if ok, err := cmd.WriteJSON(out, o); ok { + return err + } + + text.PrintSecretStore(out, "", o) + + return nil +} diff --git a/pkg/commands/secretstore/helper_test.go b/pkg/commands/secretstore/helper_test.go new file mode 100644 index 000000000..fe4b05eb1 --- /dev/null +++ b/pkg/commands/secretstore/helper_test.go @@ -0,0 +1,58 @@ +package secretstore_test + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v7/fastly" +) + +func fmtSuccess(format string, args ...any) string { + var b bytes.Buffer + text.Success(&b, format, args...) + return b.String() +} + +func fmtStore(s *fastly.SecretStore) string { + var b bytes.Buffer + text.PrintSecretStore(&b, "", s) + return b.String() +} + +func fmtStores(s *fastly.SecretStores) string { + var b bytes.Buffer + text.PrintSecretStoresTbl(&b, s) + return b.String() +} + +func fmtSecret(s *fastly.Secret) string { + var b bytes.Buffer + text.PrintSecret(&b, "", s) + return b.String() +} + +func fmtSecrets(s *fastly.Secrets) string { + var b bytes.Buffer + text.PrintSecretsTbl(&b, s) + return b.String() +} + +// fmtJSON decodes then re-encodes back to JSON, with indentation matching +// that of jsonOutput.WriteJSON. +func fmtJSON(format string, args ...any) string { + var r json.RawMessage + if err := json.Unmarshal([]byte(fmt.Sprintf(format, args...)), &r); err != nil { + panic(err) + } + return encodeJSON(r) +} + +func encodeJSON(value any) string { + var b bytes.Buffer + enc := json.NewEncoder(&b) + enc.SetIndent("", " ") + enc.Encode(value) + return b.String() +} diff --git a/pkg/commands/secretstore/json.go b/pkg/commands/secretstore/json.go new file mode 100644 index 000000000..fd5ac5e77 --- /dev/null +++ b/pkg/commands/secretstore/json.go @@ -0,0 +1,36 @@ +package secretstore + +import ( + "encoding/json" + "io" + + "github.com/fastly/cli/pkg/cmd" +) + +// jsonOutput is a helper for adding a `--json` flag and encoding +// values to JSON. It can be embedded into command structs. +type jsonOutput struct { + enabled bool // Set via flag. +} + +// jsonFlag creates a flag for enabling JSON output. +func (j *jsonOutput) jsonFlag() cmd.BoolFlagOpts { + return cmd.BoolFlagOpts{ + Name: cmd.FlagJSONName, + Description: cmd.FlagJSONDesc, + Dst: &j.enabled, + Short: 'j', + } +} + +// WriteJSON checks whether the enabled flag is set or not. If set, +// then the given value is written as JSON to out. Otherwise, false is returned. +func (j *jsonOutput) WriteJSON(out io.Writer, value any) (bool, error) { + if !j.enabled { + return false, nil + } + + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + return true, enc.Encode(value) +} diff --git a/pkg/commands/secretstore/listsecrets.go b/pkg/commands/secretstore/listsecrets.go new file mode 100644 index 000000000..3507c7d48 --- /dev/null +++ b/pkg/commands/secretstore/listsecrets.go @@ -0,0 +1,71 @@ +package secretstore + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v7/fastly" +) + +// NewListSecretsCommand returns a usable command registered under the parent. +func NewListSecretsCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *ListSecretsCommand { + var c ListSecretsCommand + c.Globals = globals + c.manifest = data + c.CmdClause = parent.Command("list", "List secrets") + c.RegisterFlag(storeIDFlag(&c.Input.ID)) + c.RegisterFlag(cursorFlag(&c.Input.Cursor)) + limitFlag(c.CmdClause, &c.Input.Limit) + c.RegisterFlagBool(c.jsonFlag()) + return &c +} + +// ListSecretsCommand calls the Fastly API to list the available secret stores. +type ListSecretsCommand struct { + cmd.Base + manifest manifest.Data + Input fastly.ListSecretsInput + jsonOutput +} + +// Exec invokes the application logic for the command. +func (cmd *ListSecretsCommand) Exec(in io.Reader, out io.Writer) error { + if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + for { + o, err := cmd.Globals.APIClient.ListSecrets(&cmd.Input) + if err != nil { + cmd.Globals.ErrLog.Add(err) + return err + } + + if ok, err := cmd.WriteJSON(out, o); ok { + // No pagination prompt w/ JSON output. + return err + } + + text.PrintSecretsTbl(out, o) + + if o != nil && o.Meta.NextCursor != "" { + // Check if 'out' is interactive before prompting. + if !cmd.Globals.Flag.NonInteractive && text.IsTTY(out) { + printNext, err := text.AskYesNo(out, "Print next page [yes/no]: ", in) + if err != nil { + return err + } + if printNext { + cmd.Input.Cursor = o.Meta.NextCursor + continue + } + } + } + + return nil + } +} diff --git a/pkg/commands/secretstore/liststores.go b/pkg/commands/secretstore/liststores.go new file mode 100644 index 000000000..8154c2e73 --- /dev/null +++ b/pkg/commands/secretstore/liststores.go @@ -0,0 +1,70 @@ +package secretstore + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/manifest" + "github.com/fastly/cli/pkg/text" + "github.com/fastly/go-fastly/v7/fastly" +) + +// NewListStoresCommand returns a usable command registered under the parent. +func NewListStoresCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *ListStoresCommand { + var c ListStoresCommand + c.Globals = globals + c.manifest = data + c.CmdClause = parent.Command("list", "List secret stores") + c.RegisterFlag(cursorFlag(&c.Input.Cursor)) + limitFlag(c.CmdClause, &c.Input.Limit) + c.RegisterFlagBool(c.jsonFlag()) + return &c +} + +// ListStoresCommand calls the Fastly API to list the available secret stores. +type ListStoresCommand struct { + cmd.Base + manifest manifest.Data + Input fastly.ListSecretStoresInput + jsonOutput +} + +// Exec invokes the application logic for the command. +func (cmd *ListStoresCommand) Exec(in io.Reader, out io.Writer) error { + if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + for { + o, err := cmd.Globals.APIClient.ListSecretStores(&cmd.Input) + if err != nil { + cmd.Globals.ErrLog.Add(err) + return err + } + + if ok, err := cmd.WriteJSON(out, o); ok { + // No pagination prompt w/ JSON output. + return err + } + + text.PrintSecretStoresTbl(out, o) + + if o != nil && o.Meta.NextCursor != "" { + // Check if 'out' is interactive before prompting. + if !cmd.Globals.Flag.NonInteractive && text.IsTTY(out) { + printNext, err := text.AskYesNo(out, "Print next page [yes/no]: ", in) + if err != nil { + return err + } + if printNext { + cmd.Input.Cursor = o.Meta.NextCursor + continue + } + } + } + + return nil + } +} diff --git a/pkg/commands/secretstore/root_secret.go b/pkg/commands/secretstore/root_secret.go new file mode 100644 index 000000000..1ea9e2fb1 --- /dev/null +++ b/pkg/commands/secretstore/root_secret.go @@ -0,0 +1,31 @@ +package secretstore + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" +) + +// RootNameSecret is the base command name for secret operations. +const RootNameSecret = "secret-store-entry" + +// NewSecretRootCommand returns a new command registered in the parent. +func NewSecretRootCommand(parent cmd.Registerer, globals *config.Data) *SecretRootCommand { + var c SecretRootCommand + c.Globals = globals + c.CmdClause = parent.Command(RootNameSecret, "Manipulate Fastly secret store secrets") + return &c +} + +// SecretRootCommand is the parent command for all 'secret' subcommands. +// It should be installed under the primary root command. +type SecretRootCommand struct { + cmd.Base + // no flags +} + +// Exec implements the command interface. +func (c *SecretRootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/secretstore/root_store.go b/pkg/commands/secretstore/root_store.go new file mode 100644 index 000000000..ed8a41fe3 --- /dev/null +++ b/pkg/commands/secretstore/root_store.go @@ -0,0 +1,31 @@ +package secretstore + +import ( + "io" + + "github.com/fastly/cli/pkg/cmd" + "github.com/fastly/cli/pkg/config" +) + +// RootNameStore is the base command name for secret store operations. +const RootNameStore = "secret-store" + +// NewStoreRootCommand returns a new command registered in the parent. +func NewStoreRootCommand(parent cmd.Registerer, globals *config.Data) *StoreRootCommand { + var c StoreRootCommand + c.Globals = globals + c.CmdClause = parent.Command(RootNameStore, "Manipulate Fastly secret stores") + return &c +} + +// StoreRootCommand is the parent command for all 'store' subcommands. +// It should be installed under the primary root command. +type StoreRootCommand struct { + cmd.Base + // no flags +} + +// Exec implements the command interface. +func (c *StoreRootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/secretstore/secret_test.go b/pkg/commands/secretstore/secret_test.go new file mode 100644 index 000000000..17085e6f4 --- /dev/null +++ b/pkg/commands/secretstore/secret_test.go @@ -0,0 +1,426 @@ +package secretstore_test + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "os" + "path" + "runtime" + "testing" + + "github.com/fastly/cli/pkg/app" + "github.com/fastly/cli/pkg/commands/secretstore" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/go-fastly/v7/fastly" +) + +func TestCreateSecretCommand(t *testing.T) { + const ( + storeID = "store123" + secretName = "testsecret" + secretDigest = "digest" + secretValue = "the secret" + ) + + tmpDir := t.TempDir() + secretFile := path.Join(tmpDir, "secret-file") + if err := os.WriteFile(secretFile, []byte(secretValue), 0x777); err != nil { + t.Fatal(err) + } + doesNotExistFile := path.Join(tmpDir, "DOES-NOT-EXIST") + + scenarios := []struct { + args string + stdin string + api mock.API + wantAPIInvoked bool + wantError string + wantOutput string + }{ + { + args: "create --name test", + wantError: "error parsing arguments: required flag --store-id not provided", + }, + { + args: "create --store-id abc123", + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: fmt.Sprintf("create --store-id %s --name %s --file %s", storeID, secretName, doesNotExistFile), + wantError: func() string { + if runtime.GOOS == "windows" { + return "The system cannot find the file specified" + } + return "no such file or directory" + }(), + }, + { + args: fmt.Sprintf("create --store-id %s --name %s --stdin", storeID, secretName), + wantError: "unable to read from STDIN", + }, + // Read from STDIN. + { + args: fmt.Sprintf("create --store-id %s --name %s --stdin", storeID, secretName), + stdin: secretValue, + api: mock.API{ + CreateSecretFn: func(i *fastly.CreateSecretInput) (*fastly.Secret, error) { + if secret := string(i.Secret); secret != secretValue { + return nil, fmt.Errorf("invalid secret: %s", secret) + } + return &fastly.Secret{ + Name: i.Name, + Digest: []byte(secretDigest), + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtSuccess("Created secret %s in store %s (digest %s)", secretName, storeID, hex.EncodeToString([]byte(secretDigest))), + }, + // Read from file. + { + args: fmt.Sprintf("create --store-id %s --name %s --file %s", storeID, secretName, secretFile), + api: mock.API{ + CreateSecretFn: func(i *fastly.CreateSecretInput) (*fastly.Secret, error) { + if secret := string(i.Secret); secret != secretValue { + return nil, fmt.Errorf("invalid secret: %s", secret) + } + return &fastly.Secret{ + Name: i.Name, + Digest: []byte(secretDigest), + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtSuccess("Created secret %s in store %s (digest %s)", secretName, storeID, hex.EncodeToString([]byte(secretDigest))), + }, + { + args: fmt.Sprintf("create --store-id %s --name %s --file %s --json", storeID, secretName, secretFile), + api: mock.API{ + CreateSecretFn: func(i *fastly.CreateSecretInput) (*fastly.Secret, error) { + if secret := string(i.Secret); secret != secretValue { + return nil, fmt.Errorf("invalid secret: %s", secret) + } + return &fastly.Secret{ + Name: i.Name, + Digest: []byte(secretDigest), + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: encodeJSON(&fastly.Secret{ + Name: secretName, + Digest: []byte(secretDigest), + }), + }, + } + + for _, testcase := range scenarios { + testcase := testcase + t.Run(testcase.args, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testutil.Args(secretstore.RootNameSecret+" "+testcase.args), &stdout) + if testcase.stdin != "" { + var stdin bytes.Buffer + stdin.WriteString(testcase.stdin) + opts.Stdin = &stdin + } + + f := testcase.api.CreateSecretFn + var apiInvoked bool + testcase.api.CreateSecretFn = func(i *fastly.CreateSecretInput) (*fastly.Secret, error) { + apiInvoked = true + return f(i) + } + + opts.APIClient = mock.APIClient(testcase.api) + + err := app.Run(opts) + + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, stdout.String()) + if apiInvoked != testcase.wantAPIInvoked { + t.Fatalf("API CreateSecret invoked = %v, want %v", apiInvoked, testcase.wantAPIInvoked) + } + }) + } +} + +func TestGetSecretCommand(t *testing.T) { + const ( + storeID = "testid" + storeName = "testname" + storeDigest = "testdigest" + ) + + scenarios := []struct { + args string + api mock.API + wantAPIInvoked bool + wantError string + wantOutput string + }{ + { + args: "get --store-id abc", + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: "get --name abc", + wantError: "error parsing arguments: required flag --store-id not provided", + }, + { + args: fmt.Sprintf("get --store-id %s --name %s", "DOES-NOT-EXIST", storeName), + api: mock.API{ + GetSecretFn: func(i *fastly.GetSecretInput) (*fastly.Secret, error) { + if i.ID != storeID || i.Name != storeName { + return nil, errors.New("invalid request") + } + return &fastly.Secret{ + Name: storeName, + Digest: []byte(storeDigest), + }, nil + }, + }, + wantAPIInvoked: true, + wantError: "invalid request", + }, + { + args: fmt.Sprintf("get --store-id %s --name %s", storeID, storeName), + api: mock.API{ + GetSecretFn: func(i *fastly.GetSecretInput) (*fastly.Secret, error) { + if i.ID != storeID || i.Name != storeName { + return nil, errors.New("invalid request") + } + return &fastly.Secret{ + Name: storeName, + Digest: []byte(storeDigest), + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtSecret(&fastly.Secret{ + Name: storeName, + Digest: []byte(storeDigest), + }), + }, + { + args: fmt.Sprintf("get --store-id %s --name %s --json", storeID, storeName), + api: mock.API{ + GetSecretFn: func(i *fastly.GetSecretInput) (*fastly.Secret, error) { + if i.ID != storeID || i.Name != storeName { + return nil, errors.New("invalid request") + } + return &fastly.Secret{ + Name: storeName, + Digest: []byte(storeDigest), + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: encodeJSON(&fastly.Secret{ + Name: storeName, + Digest: []byte(storeDigest), + }), + }, + } + + for _, testcase := range scenarios { + testcase := testcase + t.Run(testcase.args, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testutil.Args(secretstore.RootNameSecret+" "+testcase.args), &stdout) + + f := testcase.api.GetSecretFn + var apiInvoked bool + testcase.api.GetSecretFn = func(i *fastly.GetSecretInput) (*fastly.Secret, error) { + apiInvoked = true + return f(i) + } + + opts.APIClient = mock.APIClient(testcase.api) + + err := app.Run(opts) + + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, stdout.String()) + if apiInvoked != testcase.wantAPIInvoked { + t.Fatalf("API GetSecret invoked = %v, want %v", apiInvoked, testcase.wantAPIInvoked) + } + }) + } +} + +func TestDeleteSecretCommand(t *testing.T) { + const ( + storeID = "test123" + secretName = "testName" + ) + + scenarios := []struct { + args string + api mock.API + wantAPIInvoked bool + wantError string + wantOutput string + }{ + { + args: "delete --name test", + wantError: "error parsing arguments: required flag --store-id not provided", + }, + { + args: "delete --store-id test", + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: fmt.Sprintf("delete --store-id %s --name DOES-NOT-EXIST", storeID), + api: mock.API{ + DeleteSecretFn: func(i *fastly.DeleteSecretInput) error { + if i.ID != storeID || i.Name != secretName { + return errors.New("not found") + } + return nil + }, + }, + wantAPIInvoked: true, + wantError: "not found", + }, + { + args: fmt.Sprintf("delete --store-id %s --name %s", storeID, secretName), + api: mock.API{ + DeleteSecretFn: func(i *fastly.DeleteSecretInput) error { + if i.ID != storeID || i.Name != secretName { + return errors.New("not found") + } + return nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtSuccess("Deleted secret %s from store %s\n", secretName, storeID), + }, + { + args: fmt.Sprintf("delete --store-id %s --name %s --json", storeID, secretName), + api: mock.API{ + DeleteSecretFn: func(i *fastly.DeleteSecretInput) error { + if i.ID != storeID || i.Name != secretName { + return errors.New("not found") + } + return nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtJSON(`{"name": %q, "store_id": %q, "deleted": true}`, secretName, storeID), + }, + } + + for _, testcase := range scenarios { + testcase := testcase + t.Run(testcase.args, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testutil.Args(secretstore.RootNameSecret+" "+testcase.args), &stdout) + + f := testcase.api.DeleteSecretFn + var apiInvoked bool + testcase.api.DeleteSecretFn = func(i *fastly.DeleteSecretInput) error { + apiInvoked = true + return f(i) + } + + opts.APIClient = mock.APIClient(testcase.api) + + err := app.Run(opts) + + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, stdout.String()) + if apiInvoked != testcase.wantAPIInvoked { + t.Fatalf("API DeleteSecret invoked = %v, want %v", apiInvoked, testcase.wantAPIInvoked) + } + }) + } +} + +func TestListSecretsCommand(t *testing.T) { + const ( + secretName = "test123" + storeID = "store-id-123" + ) + + secrets := &fastly.Secrets{ + Meta: fastly.SecretStoreMeta{ + Limit: 123, + NextCursor: "abc", + }, + Data: []fastly.Secret{ + {Name: secretName, Digest: []byte(secretName)}, + }, + } + + scenarios := []struct { + args string + api mock.API + wantAPIInvoked bool + wantError string + wantOutput string + }{ + { + args: "list", + wantError: "required flag --store-id not provided", + }, + { + args: fmt.Sprintf("list --store-id %s", storeID), + api: mock.API{ + ListSecretsFn: func(i *fastly.ListSecretsInput) (*fastly.Secrets, error) { + return secrets, errors.New("unknown error") + }, + }, + wantAPIInvoked: true, + wantError: "unknown error", + }, + { + args: fmt.Sprintf("list --store-id %s", storeID), + api: mock.API{ + ListSecretsFn: func(i *fastly.ListSecretsInput) (*fastly.Secrets, error) { + return secrets, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtSecrets(secrets), + }, + { + args: fmt.Sprintf("list --store-id %s --json", storeID), + api: mock.API{ + ListSecretsFn: func(i *fastly.ListSecretsInput) (*fastly.Secrets, error) { + return secrets, nil + }, + }, + wantAPIInvoked: true, + wantOutput: encodeJSON(secrets), + }, + } + + for _, testcase := range scenarios { + testcase := testcase + t.Run(testcase.args, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testutil.Args(secretstore.RootNameSecret+" "+testcase.args), &stdout) + + f := testcase.api.ListSecretsFn + var apiInvoked bool + testcase.api.ListSecretsFn = func(i *fastly.ListSecretsInput) (*fastly.Secrets, error) { + apiInvoked = true + return f(i) + } + + opts.APIClient = mock.APIClient(testcase.api) + + err := app.Run(opts) + + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, stdout.String()) + if apiInvoked != testcase.wantAPIInvoked { + t.Fatalf("API ListSecrets invoked = %v, want %v", apiInvoked, testcase.wantAPIInvoked) + } + }) + } +} diff --git a/pkg/commands/secretstore/store_test.go b/pkg/commands/secretstore/store_test.go new file mode 100644 index 000000000..a97e9aaa3 --- /dev/null +++ b/pkg/commands/secretstore/store_test.go @@ -0,0 +1,358 @@ +package secretstore_test + +import ( + "bytes" + "errors" + "fmt" + "testing" + "time" + + "github.com/fastly/cli/pkg/app" + "github.com/fastly/cli/pkg/commands/secretstore" + "github.com/fastly/cli/pkg/mock" + "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/go-fastly/v7/fastly" +) + +func TestCreateStoreCommand(t *testing.T) { + const ( + storeName = "test123" + storeID = "store-id-123" + ) + now := time.Now() + + scenarios := []struct { + args string + api mock.API + wantAPIInvoked bool + wantError string + wantOutput string + }{ + { + args: "create", + wantError: "error parsing arguments: required flag --name not provided", + }, + { + args: fmt.Sprintf("create --name %s", storeName), + api: mock.API{ + CreateSecretStoreFn: func(i *fastly.CreateSecretStoreInput) (*fastly.SecretStore, error) { + return nil, errors.New("invalid request") + }, + }, + wantAPIInvoked: true, + wantError: "invalid request", + }, + { + args: fmt.Sprintf("create --name %s", storeName), + api: mock.API{ + CreateSecretStoreFn: func(i *fastly.CreateSecretStoreInput) (*fastly.SecretStore, error) { + return &fastly.SecretStore{ + ID: storeID, + Name: i.Name, + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtSuccess("Created secret store %s (name %s)", storeID, storeName), + }, + { + args: fmt.Sprintf("create --name %s --json", storeName), + api: mock.API{ + CreateSecretStoreFn: func(i *fastly.CreateSecretStoreInput) (*fastly.SecretStore, error) { + return &fastly.SecretStore{ + ID: storeID, + Name: i.Name, + CreatedAt: now, + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtJSON(`{"id": %q, "name": %q, "created_at": %q}`, storeID, storeName, now.Format(time.RFC3339Nano)), + }, + } + + for _, testcase := range scenarios { + testcase := testcase + t.Run(testcase.args, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testutil.Args(secretstore.RootNameStore+" "+testcase.args), &stdout) + + f := testcase.api.CreateSecretStoreFn + var apiInvoked bool + testcase.api.CreateSecretStoreFn = func(i *fastly.CreateSecretStoreInput) (*fastly.SecretStore, error) { + apiInvoked = true + return f(i) + } + + opts.APIClient = mock.APIClient(testcase.api) + + err := app.Run(opts) + + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, stdout.String()) + if apiInvoked != testcase.wantAPIInvoked { + t.Fatalf("API CreateSecretStore invoked = %v, want %v", apiInvoked, testcase.wantAPIInvoked) + } + }) + } +} + +func TestGetStoreCommand(t *testing.T) { + const ( + storeName = "test123" + storeID = "store-id-123" + ) + + scenarios := []struct { + args string + api mock.API + wantAPIInvoked bool + wantError string + wantOutput string + }{ + { + args: "get", + wantError: "error parsing arguments: required flag --store-id not provided", + }, + { + args: fmt.Sprintf("get --store-id %s", storeID), + api: mock.API{ + GetSecretStoreFn: func(i *fastly.GetSecretStoreInput) (*fastly.SecretStore, error) { + return nil, errors.New("invalid request") + }, + }, + wantAPIInvoked: true, + wantError: "invalid request", + }, + { + args: fmt.Sprintf("get --store-id %s", storeID), + api: mock.API{ + GetSecretStoreFn: func(i *fastly.GetSecretStoreInput) (*fastly.SecretStore, error) { + return &fastly.SecretStore{ + ID: i.ID, + Name: storeName, + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtStore(&fastly.SecretStore{ + ID: storeID, + Name: storeName, + }), + }, + { + args: fmt.Sprintf("get --store-id %s --json", storeID), + api: mock.API{ + GetSecretStoreFn: func(i *fastly.GetSecretStoreInput) (*fastly.SecretStore, error) { + return &fastly.SecretStore{ + ID: i.ID, + Name: storeName, + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: encodeJSON(&fastly.SecretStore{ + ID: storeID, + Name: storeName, + }), + }, + } + + for _, testcase := range scenarios { + testcase := testcase + t.Run(testcase.args, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testutil.Args(secretstore.RootNameStore+" "+testcase.args), &stdout) + + f := testcase.api.GetSecretStoreFn + var apiInvoked bool + testcase.api.GetSecretStoreFn = func(i *fastly.GetSecretStoreInput) (*fastly.SecretStore, error) { + apiInvoked = true + return f(i) + } + + opts.APIClient = mock.APIClient(testcase.api) + + err := app.Run(opts) + + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, stdout.String()) + if apiInvoked != testcase.wantAPIInvoked { + t.Fatalf("API GetSecretStore invoked = %v, want %v", apiInvoked, testcase.wantAPIInvoked) + } + }) + } +} + +func TestDeleteStoreCommand(t *testing.T) { + const storeID = "test123" + errStoreNotFound := errors.New("store not found") + + scenarios := []struct { + args string + api mock.API + wantAPIInvoked bool + wantError string + wantOutput string + }{ + { + args: "delete", + wantError: "error parsing arguments: required flag --store-id not provided", + }, + { + args: "delete --store-id DOES-NOT-EXIST", + api: mock.API{ + DeleteSecretStoreFn: func(i *fastly.DeleteSecretStoreInput) error { + if i.ID != storeID { + return errStoreNotFound + } + return nil + }, + }, + wantAPIInvoked: true, + wantError: errStoreNotFound.Error(), + }, + { + args: fmt.Sprintf("delete --store-id %s", storeID), + api: mock.API{ + DeleteSecretStoreFn: func(i *fastly.DeleteSecretStoreInput) error { + if i.ID != storeID { + return errStoreNotFound + } + return nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtSuccess("Deleted secret store %s\n", storeID), + }, + { + args: fmt.Sprintf("delete --store-id %s --json", storeID), + api: mock.API{ + DeleteSecretStoreFn: func(i *fastly.DeleteSecretStoreInput) error { + if i.ID != storeID { + return errStoreNotFound + } + return nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtJSON(`{"id": %q, "deleted": true}`, storeID), + }, + } + + for _, testcase := range scenarios { + testcase := testcase + t.Run(testcase.args, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testutil.Args(secretstore.RootNameStore+" "+testcase.args), &stdout) + + f := testcase.api.DeleteSecretStoreFn + var apiInvoked bool + testcase.api.DeleteSecretStoreFn = func(i *fastly.DeleteSecretStoreInput) error { + apiInvoked = true + return f(i) + } + + opts.APIClient = mock.APIClient(testcase.api) + + err := app.Run(opts) + + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, stdout.String()) + if apiInvoked != testcase.wantAPIInvoked { + t.Fatalf("API DeleteSecretStore invoked = %v, want %v", apiInvoked, testcase.wantAPIInvoked) + } + }) + } +} + +func TestListStoresCommand(t *testing.T) { + const ( + storeName = "test123" + storeID = "store-id-123" + ) + + stores := &fastly.SecretStores{ + Meta: fastly.SecretStoreMeta{ + Limit: 123, + NextCursor: "abc", + }, + Data: []fastly.SecretStore{ + {ID: storeID, Name: storeName}, + }, + } + + scenarios := []struct { + args string + api mock.API + wantAPIInvoked bool + wantError string + wantOutput string + }{ + { + args: "list", + api: mock.API{ + ListSecretStoresFn: func(i *fastly.ListSecretStoresInput) (*fastly.SecretStores, error) { + return nil, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtStores(&fastly.SecretStores{}), + }, + { + args: "list", + api: mock.API{ + ListSecretStoresFn: func(i *fastly.ListSecretStoresInput) (*fastly.SecretStores, error) { + return nil, errors.New("unknown error") + }, + }, + wantAPIInvoked: true, + wantError: "unknown error", + }, + { + args: "list", + api: mock.API{ + ListSecretStoresFn: func(i *fastly.ListSecretStoresInput) (*fastly.SecretStores, error) { + return stores, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fmtStores(stores), + }, + { + args: "list --json", + api: mock.API{ + ListSecretStoresFn: func(i *fastly.ListSecretStoresInput) (*fastly.SecretStores, error) { + return stores, nil + }, + }, + wantAPIInvoked: true, + wantOutput: encodeJSON(stores), + }, + } + + for _, testcase := range scenarios { + testcase := testcase + t.Run(testcase.args, func(t *testing.T) { + var stdout bytes.Buffer + opts := testutil.NewRunOpts(testutil.Args(secretstore.RootNameStore+" "+testcase.args), &stdout) + + f := testcase.api.ListSecretStoresFn + var apiInvoked bool + testcase.api.ListSecretStoresFn = func(i *fastly.ListSecretStoresInput) (*fastly.SecretStores, error) { + apiInvoked = true + return f(i) + } + + opts.APIClient = mock.APIClient(testcase.api) + + err := app.Run(opts) + + testutil.AssertErrorContains(t, err, testcase.wantError) + testutil.AssertString(t, testcase.wantOutput, stdout.String()) + if apiInvoked != testcase.wantAPIInvoked { + t.Fatalf("API ListSecretStores invoked = %v, want %v", apiInvoked, testcase.wantAPIInvoked) + } + }) + } +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 781e5973a..0e3e4aef8 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -326,6 +326,15 @@ type API struct { GetObjectStoreKeyFn func(i *fastly.GetObjectStoreKeyInput) (string, error) InsertObjectStoreKeyFn func(i *fastly.InsertObjectStoreKeyInput) error + CreateSecretStoreFn func(i *fastly.CreateSecretStoreInput) (*fastly.SecretStore, error) + GetSecretStoreFn func(i *fastly.GetSecretStoreInput) (*fastly.SecretStore, error) + DeleteSecretStoreFn func(i *fastly.DeleteSecretStoreInput) error + ListSecretStoresFn func(i *fastly.ListSecretStoresInput) (*fastly.SecretStores, error) + CreateSecretFn func(i *fastly.CreateSecretInput) (*fastly.Secret, error) + GetSecretFn func(i *fastly.GetSecretInput) (*fastly.Secret, error) + DeleteSecretFn func(i *fastly.DeleteSecretInput) error + ListSecretsFn func(i *fastly.ListSecretsInput) (*fastly.Secrets, error) + CreateResourceFn func(i *fastly.CreateResourceInput) (*fastly.Resource, error) } @@ -1649,6 +1658,46 @@ func (m API) InsertObjectStoreKey(i *fastly.InsertObjectStoreKeyInput) error { return m.InsertObjectStoreKeyFn(i) } +// CreateSecretStore implements Interface. +func (m API) CreateSecretStore(i *fastly.CreateSecretStoreInput) (*fastly.SecretStore, error) { + return m.CreateSecretStoreFn(i) +} + +// GetSecretStore implements Interface. +func (m API) GetSecretStore(i *fastly.GetSecretStoreInput) (*fastly.SecretStore, error) { + return m.GetSecretStoreFn(i) +} + +// DeleteSecretStore implements Interface. +func (m API) DeleteSecretStore(i *fastly.DeleteSecretStoreInput) error { + return m.DeleteSecretStoreFn(i) +} + +// ListSecretStores implements Interface. +func (m API) ListSecretStores(i *fastly.ListSecretStoresInput) (*fastly.SecretStores, error) { + return m.ListSecretStoresFn(i) +} + +// CreateSecret implements Interface. +func (m API) CreateSecret(i *fastly.CreateSecretInput) (*fastly.Secret, error) { + return m.CreateSecretFn(i) +} + +// GetSecret implements Interface. +func (m API) GetSecret(i *fastly.GetSecretInput) (*fastly.Secret, error) { + return m.GetSecretFn(i) +} + +// DeleteSecret implements Interface. +func (m API) DeleteSecret(i *fastly.DeleteSecretInput) error { + return m.DeleteSecretFn(i) +} + +// ListSecrets implements Interface. +func (m API) ListSecrets(i *fastly.ListSecretsInput) (*fastly.Secrets, error) { + return m.ListSecretsFn(i) +} + // CreateResourceFn implements Interface. func (m API) CreateResource(i *fastly.CreateResourceInput) (*fastly.Resource, error) { return m.CreateResourceFn(i) diff --git a/pkg/sync/sync.go b/pkg/sync/sync.go index e25536819..482118cad 100644 --- a/pkg/sync/sync.go +++ b/pkg/sync/sync.go @@ -8,13 +8,14 @@ import ( // Writer protects any io.Writer with a mutex. type Writer struct { mtx sync.Mutex - w io.Writer + // W is public to allow for type checking, but should otherwise not be accessed directly. + W io.Writer } // NewWriter wraps an io.Writer with a mutex. func NewWriter(w io.Writer) *Writer { return &Writer{ - w: w, + W: w, } } @@ -22,5 +23,5 @@ func NewWriter(w io.Writer) *Writer { func (w *Writer) Write(p []byte) (int, error) { w.mtx.Lock() defer w.mtx.Unlock() - return w.w.Write(p) + return w.W.Write(p) } diff --git a/pkg/testutil/assert.go b/pkg/testutil/assert.go index 96ae41255..c121ece9d 100644 --- a/pkg/testutil/assert.go +++ b/pkg/testutil/assert.go @@ -69,7 +69,7 @@ func AssertErrorContains(t *testing.T, err error, target string) { case err == nil && target != "": t.Fatalf("want %q, have no error", target) case err != nil && target == "": - t.Fatalf("want no error, have %v", err) + t.Fatalf("want no error, have %q", err) case err != nil && target != "": if want, have := target, err.Error(); !strings.Contains(have, want) { t.Fatalf("want %q, have %q", want, have) diff --git a/pkg/text/secretstore.go b/pkg/text/secretstore.go new file mode 100644 index 000000000..9e307a19f --- /dev/null +++ b/pkg/text/secretstore.go @@ -0,0 +1,66 @@ +package text + +import ( + "encoding/hex" + "fmt" + "io" + + "github.com/fastly/go-fastly/v7/fastly" + "github.com/segmentio/textio" +) + +func PrintSecretStoresTbl(out io.Writer, stores *fastly.SecretStores) { + tbl := NewTable(out) + tbl.AddHeader("Name", "ID") + + if stores == nil { + tbl.Print() + return + } + + for _, s := range stores.Data { + // avoid gosec loop aliasing check :/ + s := s + tbl.AddLine(s.Name, s.ID) + } + tbl.Print() + + if stores.Meta.NextCursor != "" { + fmt.Fprintf(out, "\nNext cursor: %s\n", stores.Meta.NextCursor) + } +} + +func PrintSecretsTbl(out io.Writer, secrets *fastly.Secrets) { + tbl := NewTable(out) + tbl.AddHeader("Name", "Digest") + + if secrets == nil { + tbl.Print() + return + } + + for _, s := range secrets.Data { + // avoid gosec loop aliasing check :/ + s := s + tbl.AddLine(s.Name, hex.EncodeToString(s.Digest)) + } + tbl.Print() + + if secrets.Meta.NextCursor != "" { + fmt.Fprintf(out, "\nNext cursor: %s\n", secrets.Meta.NextCursor) + } +} + +func PrintSecretStore(out io.Writer, prefix string, s *fastly.SecretStore) { + out = textio.NewPrefixWriter(out, prefix) + + fmt.Fprintf(out, "Name: %s\n", s.Name) + fmt.Fprintf(out, "ID: %s\n", s.ID) +} + +func PrintSecret(out io.Writer, prefix string, s *fastly.Secret) { + out = textio.NewPrefixWriter(out, prefix) + + fmt.Fprintf(out, "Name: %s\n", s.Name) + fmt.Fprintf(out, "Digest: %s\n", hex.EncodeToString(s.Digest)) +} diff --git a/pkg/text/text.go b/pkg/text/text.go index 9a9702627..602b8096a 100644 --- a/pkg/text/text.go +++ b/pkg/text/text.go @@ -8,6 +8,7 @@ import ( "strings" "syscall" + "github.com/fastly/cli/pkg/sync" "github.com/mitchellh/go-wordwrap" "golang.org/x/term" ) @@ -99,14 +100,35 @@ outer: } } +// IsStdin returns true if r is standard input. +func IsStdin(r io.Reader) bool { + if f, ok := r.(*os.File); ok { + return f.Fd() == uintptr(syscall.Stdin) + } + return false +} + +// IsTTY returns true if fd is a terminal. When used in combination +// with IsStdin, it can be used to determine whether standard input +// is being piped data (i.e. IsStdin == true && IsTTY == false). +// Provide STDOUT as a way to determine whether formating and/or +// prompting is acceptable output. +func IsTTY(fd any) bool { + if s, ok := fd.(*sync.Writer); ok { + // STDOUT is commonly wrapped in a sync.Writer, so here + // we unwrap it to gain access to the underlying Writer/STDOUT. + fd = s.W + } + if f, ok := fd.(*os.File); ok { + return term.IsTerminal(int(f.Fd())) + } + return false +} + // InputSecure is like Input but doesn't echo input back to the terminal, // if and only if r is os.Stdin. func InputSecure(w io.Writer, prefix string, r io.Reader, validators ...func(string) error) (string, error) { - var ( - f, ok = r.(*os.File) - isStdin = ok && uintptr(f.Fd()) == uintptr(syscall.Stdin) - ) - if !isStdin { + if !IsStdin(r) { return Input(w, prefix, r, validators...) } From 714ec6c8a2e9d78f69c1c5125bcf85ab448b8251 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Tue, 29 Nov 2022 17:35:35 +0100 Subject: [PATCH 02/12] Remove extraneous comment. Co-authored-by: Mark McDonnell --- pkg/commands/secretstore/flags.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/commands/secretstore/flags.go b/pkg/commands/secretstore/flags.go index e8562c486..c99e8f2c6 100644 --- a/pkg/commands/secretstore/flags.go +++ b/pkg/commands/secretstore/flags.go @@ -5,8 +5,6 @@ import ( "github.com/fastly/kingpin" ) -// Secret Store flags. - func storeIDFlag(dst *string) cmd.StringFlagOpts { return cmd.StringFlagOpts{ Name: "store-id", From 8a61c42114c56cb0836765b98d2e5d5d1f2bf6b5 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Tue, 29 Nov 2022 17:36:39 +0100 Subject: [PATCH 03/12] Re-order fields for consistency and readability Co-authored-by: Mark McDonnell --- pkg/commands/secretstore/createsecret.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/commands/secretstore/createsecret.go b/pkg/commands/secretstore/createsecret.go index c9f52b41b..1c947e644 100644 --- a/pkg/commands/secretstore/createsecret.go +++ b/pkg/commands/secretstore/createsecret.go @@ -38,11 +38,12 @@ func NewCreateSecretCommand(parent cmd.Registerer, globals *config.Data, data ma // CreateSecretCommand calls the Fastly API to create a secret. type CreateSecretCommand struct { cmd.Base - manifest manifest.Data + jsonOutput + Input fastly.CreateSecretInput + manifest manifest.Data secretFile string secretSTDIN bool - jsonOutput } var errMultipleSecretValue = fsterr.RemediationError{ From cddf27a741604ac590e472ff7f54d4aaf7369903 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Tue, 29 Nov 2022 17:36:59 +0100 Subject: [PATCH 04/12] Consistent formatting. Co-authored-by: Mark McDonnell --- pkg/commands/secretstore/createsecret.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/commands/secretstore/createsecret.go b/pkg/commands/secretstore/createsecret.go index 1c947e644..faeac5218 100644 --- a/pkg/commands/secretstore/createsecret.go +++ b/pkg/commands/secretstore/createsecret.go @@ -23,9 +23,13 @@ const ( // NewCreateSecretCommand returns a usable command registered under the parent. func NewCreateSecretCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *CreateSecretCommand { - var c CreateSecretCommand - c.Globals = globals - c.manifest = data + c := CreateSecretCommand{ + Base: cmd.Base{ + Globals: globals, + }, + manifest: data, + } + c.CmdClause = parent.Command("create", "Create secret") c.RegisterFlag(storeIDFlag(&c.Input.ID)) c.RegisterFlag(secretNameFlag(&c.Input.Name)) From 0c85bf45094d45875600f4164afcd88418b602f1 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Tue, 29 Nov 2022 17:37:31 +0100 Subject: [PATCH 05/12] Remove else block Co-authored-by: Mark McDonnell --- pkg/commands/secretstore/createsecret.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/commands/secretstore/createsecret.go b/pkg/commands/secretstore/createsecret.go index faeac5218..5304f667c 100644 --- a/pkg/commands/secretstore/createsecret.go +++ b/pkg/commands/secretstore/createsecret.go @@ -78,15 +78,14 @@ func (cmd *CreateSecretCommand) Exec(in io.Reader, out io.Writer) error { switch { case cmd.secretSTDIN: // Determine if 'in' has data available. - if in != nil && !text.IsTTY(in) { - var buf bytes.Buffer - if _, err := buf.ReadFrom(in); err != nil { - return err - } - cmd.Input.Secret = buf.Bytes() - } else { + if in == nil || text.IsTTY(in) { return errNoSTDINData } + var buf bytes.Buffer + if _, err := buf.ReadFrom(in); err != nil { + return err + } + cmd.Input.Secret = buf.Bytes() case cmd.secretFile != "": var err error From 1883923737f45e3a3685bac176976d14c9b00b4e Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Tue, 29 Nov 2022 17:38:11 +0100 Subject: [PATCH 06/12] Add TODO Co-authored-by: Mark McDonnell --- pkg/commands/secretstore/createsecret.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/commands/secretstore/createsecret.go b/pkg/commands/secretstore/createsecret.go index 5304f667c..b0d3d549a 100644 --- a/pkg/commands/secretstore/createsecret.go +++ b/pkg/commands/secretstore/createsecret.go @@ -111,6 +111,7 @@ func (cmd *CreateSecretCommand) Exec(in io.Reader, out io.Writer) error { return err } + // TODO: Use this approach across the code base. if ok, err := cmd.WriteJSON(out, o); ok { return err } From cc4e9ec86fd9a4cf48742a8555a0d61aeb168c69 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Tue, 29 Nov 2022 17:39:03 +0100 Subject: [PATCH 07/12] Comment exported function. Co-authored-by: Mark McDonnell --- pkg/text/secretstore.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/text/secretstore.go b/pkg/text/secretstore.go index 9e307a19f..5d508d88e 100644 --- a/pkg/text/secretstore.go +++ b/pkg/text/secretstore.go @@ -9,6 +9,7 @@ import ( "github.com/segmentio/textio" ) +// PrintSecretStoresTbl displays store data in a table format. func PrintSecretStoresTbl(out io.Writer, stores *fastly.SecretStores) { tbl := NewTable(out) tbl.AddHeader("Name", "ID") @@ -30,6 +31,7 @@ func PrintSecretStoresTbl(out io.Writer, stores *fastly.SecretStores) { } } +// PrintSecretsTbl displays secrets data in a table format. func PrintSecretsTbl(out io.Writer, secrets *fastly.Secrets) { tbl := NewTable(out) tbl.AddHeader("Name", "Digest") @@ -51,6 +53,7 @@ func PrintSecretsTbl(out io.Writer, secrets *fastly.Secrets) { } } +// PrintSecretStore displays store data. func PrintSecretStore(out io.Writer, prefix string, s *fastly.SecretStore) { out = textio.NewPrefixWriter(out, prefix) @@ -58,6 +61,7 @@ func PrintSecretStore(out io.Writer, prefix string, s *fastly.SecretStore) { fmt.Fprintf(out, "ID: %s\n", s.ID) } +// PrintSecret displays store data. func PrintSecret(out io.Writer, prefix string, s *fastly.Secret) { out = textio.NewPrefixWriter(out, prefix) From 24bd1569bc21f21220dae9c61449912609729ec5 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Tue, 29 Nov 2022 10:10:03 -0700 Subject: [PATCH 08/12] Change formatting and ordering for consistency with other commands --- pkg/commands/secretstore/createsecret.go | 23 +++++++++++------- pkg/commands/secretstore/createstore.go | 26 +++++++++++++------- pkg/commands/secretstore/deletesecret.go | 28 +++++++++++++++------- pkg/commands/secretstore/deletestore.go | 26 +++++++++++++------- pkg/commands/secretstore/getsecret.go | 26 +++++++++++++------- pkg/commands/secretstore/getstore.go | 24 +++++++++++++------ pkg/commands/secretstore/listsecrets.go | 30 ++++++++++++++++-------- pkg/commands/secretstore/liststores.go | 26 +++++++++++++------- pkg/commands/secretstore/root_secret.go | 11 ++++++--- pkg/commands/secretstore/root_store.go | 11 ++++++--- 10 files changed, 157 insertions(+), 74 deletions(-) diff --git a/pkg/commands/secretstore/createsecret.go b/pkg/commands/secretstore/createsecret.go index b0d3d549a..cf68e8f50 100644 --- a/pkg/commands/secretstore/createsecret.go +++ b/pkg/commands/secretstore/createsecret.go @@ -29,21 +29,26 @@ func NewCreateSecretCommand(parent cmd.Registerer, globals *config.Data, data ma }, manifest: data, } - + c.CmdClause = parent.Command("create", "Create secret") - c.RegisterFlag(storeIDFlag(&c.Input.ID)) - c.RegisterFlag(secretNameFlag(&c.Input.Name)) - c.RegisterFlag(secretFileFlag(&c.secretFile)) - c.RegisterFlagBool(secretStdinFlag(&c.secretSTDIN)) - c.RegisterFlagBool(c.jsonFlag()) + + // Required. + c.RegisterFlag(secretNameFlag(&c.Input.Name)) // --name + c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id + + // Optional. + c.RegisterFlag(secretFileFlag(&c.secretFile)) // --file + c.RegisterFlagBool(c.jsonFlag()) // --json + c.RegisterFlagBool(secretStdinFlag(&c.secretSTDIN)) // --stdin + return &c } -// CreateSecretCommand calls the Fastly API to create a secret. +// CreateSecretCommand calls the Fastly API to create an appropriate resource. type CreateSecretCommand struct { cmd.Base jsonOutput - + Input fastly.CreateSecretInput manifest manifest.Data secretFile string @@ -84,7 +89,7 @@ func (cmd *CreateSecretCommand) Exec(in io.Reader, out io.Writer) error { var buf bytes.Buffer if _, err := buf.ReadFrom(in); err != nil { return err - } + } cmd.Input.Secret = buf.Bytes() case cmd.secretFile != "": diff --git a/pkg/commands/secretstore/createstore.go b/pkg/commands/secretstore/createstore.go index 19b781e31..4c9714a34 100644 --- a/pkg/commands/secretstore/createstore.go +++ b/pkg/commands/secretstore/createstore.go @@ -13,21 +13,31 @@ import ( // NewCreateStoreCommand returns a usable command registered under the parent. func NewCreateStoreCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *CreateStoreCommand { - var c CreateStoreCommand - c.Globals = globals - c.manifest = data + c := CreateStoreCommand{ + Base: cmd.Base{ + Globals: globals, + }, + manifest: data, + } + c.CmdClause = parent.Command("create", "Create secret store") - c.RegisterFlag(storeNameFlag(&c.Input.Name)) - c.RegisterFlagBool(c.jsonFlag()) + + // Required. + c.RegisterFlag(storeNameFlag(&c.Input.Name)) // --name + + // Optional. + c.RegisterFlagBool(c.jsonFlag()) // --json + return &c } -// CreateStoreCommand calls the Fastly API to create a secret store. +// CreateStoreCommand calls the Fastly API to create an appropriate resource. type CreateStoreCommand struct { cmd.Base - manifest manifest.Data - Input fastly.CreateSecretStoreInput jsonOutput + + Input fastly.CreateSecretStoreInput + manifest manifest.Data } // Exec invokes the application logic for the command. diff --git a/pkg/commands/secretstore/deletesecret.go b/pkg/commands/secretstore/deletesecret.go index 704f90803..f4129334c 100644 --- a/pkg/commands/secretstore/deletesecret.go +++ b/pkg/commands/secretstore/deletesecret.go @@ -13,22 +13,32 @@ import ( // NewDeleteSecretCommand returns a usable command registered under the parent. func NewDeleteSecretCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *DeleteSecretCommand { - var c DeleteSecretCommand - c.Globals = globals - c.manifest = data + c := DeleteSecretCommand{ + Base: cmd.Base{ + Globals: globals, + }, + manifest: data, + } + c.CmdClause = parent.Command("delete", "Delete secret") - c.RegisterFlag(storeIDFlag(&c.Input.ID)) - c.RegisterFlag(secretNameFlag(&c.Input.Name)) - c.RegisterFlagBool(c.jsonFlag()) + + // Required. + c.RegisterFlag(secretNameFlag(&c.Input.Name)) // --name + c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id + + // Optional. + c.RegisterFlagBool(c.jsonFlag()) // --json + return &c } -// DeleteSecretCommand calls the Fastly API to delete a secret. +// DeleteSecretCommand calls the Fastly API to delete an appropriate resource. type DeleteSecretCommand struct { cmd.Base - manifest manifest.Data - Input fastly.DeleteSecretInput jsonOutput + + Input fastly.DeleteSecretInput + manifest manifest.Data } // Exec invokes the application logic for the command. diff --git a/pkg/commands/secretstore/deletestore.go b/pkg/commands/secretstore/deletestore.go index 1131e4f65..012cee5e3 100644 --- a/pkg/commands/secretstore/deletestore.go +++ b/pkg/commands/secretstore/deletestore.go @@ -13,21 +13,31 @@ import ( // NewDeleteStoreCommand returns a usable command registered under the parent. func NewDeleteStoreCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *DeleteStoreCommand { - var c DeleteStoreCommand - c.Globals = globals - c.manifest = data + c := DeleteStoreCommand{ + Base: cmd.Base{ + Globals: globals, + }, + manifest: data, + } + c.CmdClause = parent.Command("delete", "Delete secret store") - c.RegisterFlag(storeIDFlag(&c.Input.ID)) - c.RegisterFlagBool(c.jsonFlag()) + + // Required. + c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id + + // Optional. + c.RegisterFlagBool(c.jsonFlag()) // --json + return &c } -// DeleteStoreCommand calls the Fastly API to delete a secret store. +// DeleteStoreCommand calls the Fastly API to delete an appropriate resource. type DeleteStoreCommand struct { cmd.Base - manifest manifest.Data - Input fastly.DeleteSecretStoreInput jsonOutput + + Input fastly.DeleteSecretStoreInput + manifest manifest.Data } // Exec invokes the application logic for the command. diff --git a/pkg/commands/secretstore/getsecret.go b/pkg/commands/secretstore/getsecret.go index 15fdc10a7..e3e051c19 100644 --- a/pkg/commands/secretstore/getsecret.go +++ b/pkg/commands/secretstore/getsecret.go @@ -13,22 +13,32 @@ import ( // NewGetSecretCommand returns a usable command registered under the parent. func NewGetSecretCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *GetSecretCommand { - var c GetSecretCommand - c.Globals = globals - c.manifest = data + c := GetSecretCommand{ + Base: cmd.Base{ + Globals: globals, + }, + manifest: data, + } + c.CmdClause = parent.Command("get", "Get secret") - c.RegisterFlag(storeIDFlag(&c.Input.ID)) - c.RegisterFlag(secretNameFlag(&c.Input.Name)) - c.RegisterFlagBool(c.jsonFlag()) + + // Required. + c.RegisterFlag(secretNameFlag(&c.Input.Name)) // --name + c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id + + // Optional. + c.RegisterFlagBool(c.jsonFlag()) // --json + return &c } // GetSecretCommand calls the Fastly API to list the available secret stores. type GetSecretCommand struct { cmd.Base - manifest manifest.Data - Input fastly.GetSecretInput jsonOutput + + Input fastly.GetSecretInput + manifest manifest.Data } // Exec invokes the application logic for the command. diff --git a/pkg/commands/secretstore/getstore.go b/pkg/commands/secretstore/getstore.go index 4e83843b0..f5bfff813 100644 --- a/pkg/commands/secretstore/getstore.go +++ b/pkg/commands/secretstore/getstore.go @@ -13,21 +13,31 @@ import ( // NewGetStoreCommand returns a usable command registered under the parent. func NewGetStoreCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *GetStoreCommand { - var c GetStoreCommand - c.Globals = globals - c.manifest = data + c := GetStoreCommand{ + Base: cmd.Base{ + Globals: globals, + }, + manifest: data, + } + c.CmdClause = parent.Command("get", "Get secret store") - c.RegisterFlag(storeIDFlag(&c.Input.ID)) - c.RegisterFlagBool(c.jsonFlag()) + + // Required. + c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id + + // Optional. + c.RegisterFlagBool(c.jsonFlag()) // --json + return &c } // GetStoreCommand calls the Fastly API to list the available secret stores. type GetStoreCommand struct { cmd.Base - manifest manifest.Data - Input fastly.GetSecretStoreInput jsonOutput + + Input fastly.GetSecretStoreInput + manifest manifest.Data } // Exec invokes the application logic for the command. diff --git a/pkg/commands/secretstore/listsecrets.go b/pkg/commands/secretstore/listsecrets.go index 3507c7d48..4da493635 100644 --- a/pkg/commands/secretstore/listsecrets.go +++ b/pkg/commands/secretstore/listsecrets.go @@ -13,23 +13,33 @@ import ( // NewListSecretsCommand returns a usable command registered under the parent. func NewListSecretsCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *ListSecretsCommand { - var c ListSecretsCommand - c.Globals = globals - c.manifest = data + c := ListSecretsCommand{ + Base: cmd.Base{ + Globals: globals, + }, + manifest: data, + } + c.CmdClause = parent.Command("list", "List secrets") - c.RegisterFlag(storeIDFlag(&c.Input.ID)) - c.RegisterFlag(cursorFlag(&c.Input.Cursor)) - limitFlag(c.CmdClause, &c.Input.Limit) - c.RegisterFlagBool(c.jsonFlag()) + + // Required. + c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id + + // Optional. + c.RegisterFlag(cursorFlag(&c.Input.Cursor)) // --cursor + c.RegisterFlagBool(c.jsonFlag()) // --json + limitFlag(c.CmdClause, &c.Input.Limit) // --limit + return &c } -// ListSecretsCommand calls the Fastly API to list the available secret stores. +// ListSecretsCommand calls the Fastly API to list appropriate resources. type ListSecretsCommand struct { cmd.Base - manifest manifest.Data - Input fastly.ListSecretsInput jsonOutput + + Input fastly.ListSecretsInput + manifest manifest.Data } // Exec invokes the application logic for the command. diff --git a/pkg/commands/secretstore/liststores.go b/pkg/commands/secretstore/liststores.go index 8154c2e73..df53379b4 100644 --- a/pkg/commands/secretstore/liststores.go +++ b/pkg/commands/secretstore/liststores.go @@ -13,22 +13,30 @@ import ( // NewListStoresCommand returns a usable command registered under the parent. func NewListStoresCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *ListStoresCommand { - var c ListStoresCommand - c.Globals = globals - c.manifest = data + c := ListStoresCommand{ + Base: cmd.Base{ + Globals: globals, + }, + manifest: data, + } + c.CmdClause = parent.Command("list", "List secret stores") - c.RegisterFlag(cursorFlag(&c.Input.Cursor)) - limitFlag(c.CmdClause, &c.Input.Limit) - c.RegisterFlagBool(c.jsonFlag()) + + // Optional. + c.RegisterFlag(cursorFlag(&c.Input.Cursor)) // --cursor + c.RegisterFlagBool(c.jsonFlag()) // --json + limitFlag(c.CmdClause, &c.Input.Limit) // --limit + return &c } -// ListStoresCommand calls the Fastly API to list the available secret stores. +// ListStoresCommand calls the Fastly API to list appropriate resources. type ListStoresCommand struct { cmd.Base - manifest manifest.Data - Input fastly.ListSecretStoresInput jsonOutput + + Input fastly.ListSecretStoresInput + manifest manifest.Data } // Exec invokes the application logic for the command. diff --git a/pkg/commands/secretstore/root_secret.go b/pkg/commands/secretstore/root_secret.go index 1ea9e2fb1..961162a43 100644 --- a/pkg/commands/secretstore/root_secret.go +++ b/pkg/commands/secretstore/root_secret.go @@ -12,9 +12,14 @@ const RootNameSecret = "secret-store-entry" // NewSecretRootCommand returns a new command registered in the parent. func NewSecretRootCommand(parent cmd.Registerer, globals *config.Data) *SecretRootCommand { - var c SecretRootCommand - c.Globals = globals - c.CmdClause = parent.Command(RootNameSecret, "Manipulate Fastly secret store secrets") + c := SecretRootCommand{ + Base: cmd.Base{ + Globals: globals, + }, + } + + c.CmdClause = parent.Command(RootNameSecret, "Manipulate Fastly Secret Store secrets") + return &c } diff --git a/pkg/commands/secretstore/root_store.go b/pkg/commands/secretstore/root_store.go index ed8a41fe3..49fdb7f79 100644 --- a/pkg/commands/secretstore/root_store.go +++ b/pkg/commands/secretstore/root_store.go @@ -12,9 +12,14 @@ const RootNameStore = "secret-store" // NewStoreRootCommand returns a new command registered in the parent. func NewStoreRootCommand(parent cmd.Registerer, globals *config.Data) *StoreRootCommand { - var c StoreRootCommand - c.Globals = globals - c.CmdClause = parent.Command(RootNameStore, "Manipulate Fastly secret stores") + c := StoreRootCommand{ + Base: cmd.Base{ + Globals: globals, + }, + } + + c.CmdClause = parent.Command(RootNameStore, "Manipulate Fastly Secret Stores") + return &c } From a6080b0935ccea6c4c6e2dd84b888b04408fcd73 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Tue, 29 Nov 2022 10:40:28 -0700 Subject: [PATCH 09/12] Use 'describe' instead of 'get' for consistency --- pkg/app/commands.go | 4 ++-- pkg/commands/secretstore/createsecret.go | 2 +- pkg/commands/secretstore/createstore.go | 2 +- pkg/commands/secretstore/deletesecret.go | 2 +- pkg/commands/secretstore/deletestore.go | 2 +- .../{getsecret.go => describesecret.go} | 14 +++++++------- .../secretstore/{getstore.go => describestore.go} | 14 +++++++------- pkg/commands/secretstore/listsecrets.go | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) rename pkg/commands/secretstore/{getsecret.go => describesecret.go} (66%) rename pkg/commands/secretstore/{getstore.go => describestore.go} (65%) diff --git a/pkg/app/commands.go b/pkg/app/commands.go index 5c885bfb8..80902060c 100644 --- a/pkg/app/commands.go +++ b/pkg/app/commands.go @@ -315,12 +315,12 @@ func defineCommands( purgeCmdRoot := purge.NewRootCommand(app, globals, data) secretstoreCmdRoot := secretstore.NewStoreRootCommand(app, globals) secretstoreCreateStore := secretstore.NewCreateStoreCommand(secretstoreCmdRoot.CmdClause, globals, data) - secretstoreGetStore := secretstore.NewGetStoreCommand(secretstoreCmdRoot.CmdClause, globals, data) + secretstoreGetStore := secretstore.NewDescribeStoreCommand(secretstoreCmdRoot.CmdClause, globals, data) secretstoreDeleteStore := secretstore.NewDeleteStoreCommand(secretstoreCmdRoot.CmdClause, globals, data) secretstoreListStores := secretstore.NewListStoresCommand(secretstoreCmdRoot.CmdClause, globals, data) secretstoreSecretCmdRoot := secretstore.NewSecretRootCommand(app, globals) secretstoreCreateSecret := secretstore.NewCreateSecretCommand(secretstoreSecretCmdRoot.CmdClause, globals, data) - secretstoreGetSecret := secretstore.NewGetSecretCommand(secretstoreSecretCmdRoot.CmdClause, globals, data) + secretstoreGetSecret := secretstore.NewDescribeSecretCommand(secretstoreSecretCmdRoot.CmdClause, globals, data) secretstoreDeleteSecret := secretstore.NewDeleteSecretCommand(secretstoreSecretCmdRoot.CmdClause, globals, data) secretstoreListSecrets := secretstore.NewListSecretsCommand(secretstoreSecretCmdRoot.CmdClause, globals, data) serviceCmdRoot := service.NewRootCommand(app, globals) diff --git a/pkg/commands/secretstore/createsecret.go b/pkg/commands/secretstore/createsecret.go index cf68e8f50..9b437067c 100644 --- a/pkg/commands/secretstore/createsecret.go +++ b/pkg/commands/secretstore/createsecret.go @@ -30,7 +30,7 @@ func NewCreateSecretCommand(parent cmd.Registerer, globals *config.Data, data ma manifest: data, } - c.CmdClause = parent.Command("create", "Create secret") + c.CmdClause = parent.Command("create", "Create a new secret within specified store") // Required. c.RegisterFlag(secretNameFlag(&c.Input.Name)) // --name diff --git a/pkg/commands/secretstore/createstore.go b/pkg/commands/secretstore/createstore.go index 4c9714a34..473d7e572 100644 --- a/pkg/commands/secretstore/createstore.go +++ b/pkg/commands/secretstore/createstore.go @@ -20,7 +20,7 @@ func NewCreateStoreCommand(parent cmd.Registerer, globals *config.Data, data man manifest: data, } - c.CmdClause = parent.Command("create", "Create secret store") + c.CmdClause = parent.Command("create", "Create a new secret store") // Required. c.RegisterFlag(storeNameFlag(&c.Input.Name)) // --name diff --git a/pkg/commands/secretstore/deletesecret.go b/pkg/commands/secretstore/deletesecret.go index f4129334c..e0ac97504 100644 --- a/pkg/commands/secretstore/deletesecret.go +++ b/pkg/commands/secretstore/deletesecret.go @@ -20,7 +20,7 @@ func NewDeleteSecretCommand(parent cmd.Registerer, globals *config.Data, data ma manifest: data, } - c.CmdClause = parent.Command("delete", "Delete secret") + c.CmdClause = parent.Command("delete", "Delete a secret") // Required. c.RegisterFlag(secretNameFlag(&c.Input.Name)) // --name diff --git a/pkg/commands/secretstore/deletestore.go b/pkg/commands/secretstore/deletestore.go index 012cee5e3..86a130373 100644 --- a/pkg/commands/secretstore/deletestore.go +++ b/pkg/commands/secretstore/deletestore.go @@ -20,7 +20,7 @@ func NewDeleteStoreCommand(parent cmd.Registerer, globals *config.Data, data man manifest: data, } - c.CmdClause = parent.Command("delete", "Delete secret store") + c.CmdClause = parent.Command("delete", "Delete a secret store") // Required. c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id diff --git a/pkg/commands/secretstore/getsecret.go b/pkg/commands/secretstore/describesecret.go similarity index 66% rename from pkg/commands/secretstore/getsecret.go rename to pkg/commands/secretstore/describesecret.go index e3e051c19..6b18736ac 100644 --- a/pkg/commands/secretstore/getsecret.go +++ b/pkg/commands/secretstore/describesecret.go @@ -11,16 +11,16 @@ import ( "github.com/fastly/go-fastly/v7/fastly" ) -// NewGetSecretCommand returns a usable command registered under the parent. -func NewGetSecretCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *GetSecretCommand { - c := GetSecretCommand{ +// NewDescribeSecretCommand returns a usable command registered under the parent. +func NewDescribeSecretCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *DescribeSecretCommand { + c := DescribeSecretCommand{ Base: cmd.Base{ Globals: globals, }, manifest: data, } - c.CmdClause = parent.Command("get", "Get secret") + c.CmdClause = parent.Command("describe", "Retrieve a single secret").Alias("get") // Required. c.RegisterFlag(secretNameFlag(&c.Input.Name)) // --name @@ -32,8 +32,8 @@ func NewGetSecretCommand(parent cmd.Registerer, globals *config.Data, data manif return &c } -// GetSecretCommand calls the Fastly API to list the available secret stores. -type GetSecretCommand struct { +// DescribeSecretCommand calls the Fastly API to describe an appropriate resource. +type DescribeSecretCommand struct { cmd.Base jsonOutput @@ -42,7 +42,7 @@ type GetSecretCommand struct { } // Exec invokes the application logic for the command. -func (cmd *GetSecretCommand) Exec(_ io.Reader, out io.Writer) error { +func (cmd *DescribeSecretCommand) Exec(_ io.Reader, out io.Writer) error { if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { return fsterr.ErrInvalidVerboseJSONCombo } diff --git a/pkg/commands/secretstore/getstore.go b/pkg/commands/secretstore/describestore.go similarity index 65% rename from pkg/commands/secretstore/getstore.go rename to pkg/commands/secretstore/describestore.go index f5bfff813..899863a8c 100644 --- a/pkg/commands/secretstore/getstore.go +++ b/pkg/commands/secretstore/describestore.go @@ -11,16 +11,16 @@ import ( "github.com/fastly/go-fastly/v7/fastly" ) -// NewGetStoreCommand returns a usable command registered under the parent. -func NewGetStoreCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *GetStoreCommand { - c := GetStoreCommand{ +// NewDescribeStoreCommand returns a usable command registered under the parent. +func NewDescribeStoreCommand(parent cmd.Registerer, globals *config.Data, data manifest.Data) *DescribeStoreCommand { + c := DescribeStoreCommand{ Base: cmd.Base{ Globals: globals, }, manifest: data, } - c.CmdClause = parent.Command("get", "Get secret store") + c.CmdClause = parent.Command("describe", "Retrieve a single secret store").Alias("get") // Required. c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id @@ -31,8 +31,8 @@ func NewGetStoreCommand(parent cmd.Registerer, globals *config.Data, data manife return &c } -// GetStoreCommand calls the Fastly API to list the available secret stores. -type GetStoreCommand struct { +// DescribeStoreCommand calls the Fastly API to describe an appropriate resource. +type DescribeStoreCommand struct { cmd.Base jsonOutput @@ -41,7 +41,7 @@ type GetStoreCommand struct { } // Exec invokes the application logic for the command. -func (cmd *GetStoreCommand) Exec(_ io.Reader, out io.Writer) error { +func (cmd *DescribeStoreCommand) Exec(_ io.Reader, out io.Writer) error { if cmd.Globals.Verbose() && cmd.jsonOutput.enabled { return fsterr.ErrInvalidVerboseJSONCombo } diff --git a/pkg/commands/secretstore/listsecrets.go b/pkg/commands/secretstore/listsecrets.go index 4da493635..de3a404e8 100644 --- a/pkg/commands/secretstore/listsecrets.go +++ b/pkg/commands/secretstore/listsecrets.go @@ -20,7 +20,7 @@ func NewListSecretsCommand(parent cmd.Registerer, globals *config.Data, data man manifest: data, } - c.CmdClause = parent.Command("list", "List secrets") + c.CmdClause = parent.Command("list", "List secrets within a specified store") // Required. c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id From 8e6d490280deb7148730fc329375f74b9e113627 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Tue, 29 Nov 2022 10:54:46 -0700 Subject: [PATCH 10/12] Fix error text --- pkg/commands/secretstore/createsecret.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/commands/secretstore/createsecret.go b/pkg/commands/secretstore/createsecret.go index 9b437067c..a462ece68 100644 --- a/pkg/commands/secretstore/createsecret.go +++ b/pkg/commands/secretstore/createsecret.go @@ -56,7 +56,7 @@ type CreateSecretCommand struct { } var errMultipleSecretValue = fsterr.RemediationError{ - Inner: fmt.Errorf("invalid flag combination, --secret-file and --secret-stdin"), + Inner: fmt.Errorf("invalid flag combination, --file and --stdin"), Remediation: "Use one of --file or --stdin flag", } From 57fbb1f767ae037c972bd7f842155660aaf7ca80 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Wed, 30 Nov 2022 11:51:08 -0700 Subject: [PATCH 11/12] Consider `auto-yes` in list cmds; add RegisterFlagInt to Base --- pkg/cmd/flags.go | 31 ++++++++++++++++++++++++- pkg/commands/secretstore/flags.go | 14 ++++++----- pkg/commands/secretstore/listsecrets.go | 8 +++---- pkg/commands/secretstore/liststores.go | 8 +++---- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/flags.go b/pkg/cmd/flags.go index a2dc1dbf0..6a2d3c49b 100644 --- a/pkg/cmd/flags.go +++ b/pkg/cmd/flags.go @@ -61,7 +61,7 @@ type BoolFlagOpts struct { // RegisterFlagBool defines a boolean flag. // -// TODO: Use generics support in go 1.18 to remove the need for two functions. +// TODO: Use generics support in go 1.18 to remove the need for multiple functions. func (b Base) RegisterFlagBool(opts BoolFlagOpts) { clause := b.CmdClause.Flag(opts.Name, opts.Description) if opts.Short > 0 { @@ -76,6 +76,35 @@ func (b Base) RegisterFlagBool(opts BoolFlagOpts) { clause.BoolVar(opts.Dst) } +// IntFlagOpts enables easy configuration of a flag. +type IntFlagOpts struct { + Action kingpin.Action + Default int + Description string + Dst *int + Name string + Required bool + Short rune +} + +// RegisterFlagInt defines an integer flag. +func (b Base) RegisterFlagInt(opts IntFlagOpts) { + clause := b.CmdClause.Flag(opts.Name, opts.Description) + if opts.Short > 0 { + clause = clause.Short(opts.Short) + } + if opts.Required { + clause = clause.Required() + } + if opts.Action != nil { + clause = clause.Action(opts.Action) + } + if opts.Default != 0 { + clause = clause.Default(strconv.Itoa(opts.Default)) + } + clause.IntVar(opts.Dst) +} + // OptionalServiceVersion represents a Fastly service version. type OptionalServiceVersion struct { OptionalString diff --git a/pkg/commands/secretstore/flags.go b/pkg/commands/secretstore/flags.go index c99e8f2c6..8123af00d 100644 --- a/pkg/commands/secretstore/flags.go +++ b/pkg/commands/secretstore/flags.go @@ -2,7 +2,6 @@ package secretstore import ( "github.com/fastly/cli/pkg/cmd" - "github.com/fastly/kingpin" ) func storeIDFlag(dst *string) cmd.StringFlagOpts { @@ -63,9 +62,12 @@ func cursorFlag(dst *string) cmd.StringFlagOpts { } } -func limitFlag(cmd *kingpin.CmdClause, dst *int) { - limit := cmd.Flag("limit", "Maxiumum number of items to list") - limit = limit.Default("50") - limit = limit.Short('l') - limit.IntVar(dst) +func limitFlag(dst *int) cmd.IntFlagOpts { + return cmd.IntFlagOpts{ + Name: "limit", + Short: 'l', + Description: "Maxiumum number of items to list", + Default: 50, + Dst: dst, + } } diff --git a/pkg/commands/secretstore/listsecrets.go b/pkg/commands/secretstore/listsecrets.go index de3a404e8..225e64ddc 100644 --- a/pkg/commands/secretstore/listsecrets.go +++ b/pkg/commands/secretstore/listsecrets.go @@ -26,9 +26,9 @@ func NewListSecretsCommand(parent cmd.Registerer, globals *config.Data, data man c.RegisterFlag(storeIDFlag(&c.Input.ID)) // --store-id // Optional. - c.RegisterFlag(cursorFlag(&c.Input.Cursor)) // --cursor - c.RegisterFlagBool(c.jsonFlag()) // --json - limitFlag(c.CmdClause, &c.Input.Limit) // --limit + c.RegisterFlag(cursorFlag(&c.Input.Cursor)) // --cursor + c.RegisterFlagBool(c.jsonFlag()) // --json + c.RegisterFlagInt(limitFlag(&c.Input.Limit)) // --limit return &c } @@ -64,7 +64,7 @@ func (cmd *ListSecretsCommand) Exec(in io.Reader, out io.Writer) error { if o != nil && o.Meta.NextCursor != "" { // Check if 'out' is interactive before prompting. - if !cmd.Globals.Flag.NonInteractive && text.IsTTY(out) { + if !cmd.Globals.Flag.NonInteractive && !cmd.Globals.Flag.AutoYes && text.IsTTY(out) { printNext, err := text.AskYesNo(out, "Print next page [yes/no]: ", in) if err != nil { return err diff --git a/pkg/commands/secretstore/liststores.go b/pkg/commands/secretstore/liststores.go index df53379b4..ee3cdd2f3 100644 --- a/pkg/commands/secretstore/liststores.go +++ b/pkg/commands/secretstore/liststores.go @@ -23,9 +23,9 @@ func NewListStoresCommand(parent cmd.Registerer, globals *config.Data, data mani c.CmdClause = parent.Command("list", "List secret stores") // Optional. - c.RegisterFlag(cursorFlag(&c.Input.Cursor)) // --cursor - c.RegisterFlagBool(c.jsonFlag()) // --json - limitFlag(c.CmdClause, &c.Input.Limit) // --limit + c.RegisterFlag(cursorFlag(&c.Input.Cursor)) // --cursor + c.RegisterFlagBool(c.jsonFlag()) // --json + c.RegisterFlagInt(limitFlag(&c.Input.Limit)) // --limit return &c } @@ -61,7 +61,7 @@ func (cmd *ListStoresCommand) Exec(in io.Reader, out io.Writer) error { if o != nil && o.Meta.NextCursor != "" { // Check if 'out' is interactive before prompting. - if !cmd.Globals.Flag.NonInteractive && text.IsTTY(out) { + if !cmd.Globals.Flag.NonInteractive && !cmd.Globals.Flag.AutoYes && text.IsTTY(out) { printNext, err := text.AskYesNo(out, "Print next page [yes/no]: ", in) if err != nil { return err From a4d5bb0ff1211c33df8c3e9471a5d94ce447d3a5 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Thu, 19 Jan 2023 13:15:03 -0700 Subject: [PATCH 12/12] fix comment typo --- pkg/mock/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 0e3e4aef8..c0f9c4523 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -1698,7 +1698,7 @@ func (m API) ListSecrets(i *fastly.ListSecretsInput) (*fastly.Secrets, error) { return m.ListSecretsFn(i) } -// CreateResourceFn implements Interface. +// CreateResource implements Interface. func (m API) CreateResource(i *fastly.CreateResourceInput) (*fastly.Resource, error) { return m.CreateResourceFn(i) }