From 31a3f3563068d41f1a6adcf33c29ae4dd0eb3010 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Wed, 10 May 2023 16:25:10 -0600 Subject: [PATCH 1/3] secret store: Add --recreate and --recreate-must options This works in combination with https://github.com/fastly/go-fastly/pull/433 which adds support for the `CreateSecretInput.Method` field. Secret names must be unique within a store. The method effects how duplicate names are handled: - `POST`: Default. Create a secret; error if one already exists with the same name. - `PUT` / `--recreate`: Create or recreate a secret. - `PATCH` / `--recreate-must`: Recreate a secret and error if one does not already exist with the same name. --- pkg/commands/secretstoreentry/create.go | 46 +++++++++++--- .../secretstoreentry/secretstoreentry_test.go | 61 +++++++++++++++++++ 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/pkg/commands/secretstoreentry/create.go b/pkg/commands/secretstoreentry/create.go index e9519e017..254fe54b9 100644 --- a/pkg/commands/secretstoreentry/create.go +++ b/pkg/commands/secretstoreentry/create.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "io" + "net/http" "os" "github.com/fastly/go-fastly/v8/fastly" @@ -57,8 +58,20 @@ func NewCreateCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *C c.RegisterFlag(cmd.StoreIDFlag(&c.Input.ID)) // --store-id // Optional. - c.RegisterFlag(secretFileFlag(&c.secretFile)) // --file - c.RegisterFlagBool(c.JSONFlag()) // --json + c.RegisterFlag(secretFileFlag(&c.secretFile)) // --file + c.RegisterFlagBool(c.JSONFlag()) // --json + c.RegisterFlagBool(cmd.BoolFlagOpts{ + Name: "recreate", + Description: "Create or recreate if a secret of the same name already exists within the store", + Dst: &c.recreate, + Required: false, + }) + c.RegisterFlagBool(cmd.BoolFlagOpts{ + Name: "recreate-must", + Description: "Recreate secret of the same name which must already exist within the store", + Dst: &c.recreateMust, + Required: false, + }) c.RegisterFlagBool(secretStdinFlag(&c.secretSTDIN)) // --stdin return &c @@ -69,10 +82,12 @@ type CreateCommand struct { cmd.Base cmd.JSONOutput - Input fastly.CreateSecretInput - manifest manifest.Data - secretFile string - secretSTDIN bool + Input fastly.CreateSecretInput + manifest manifest.Data + recreate bool + recreateMust bool + secretFile string + secretSTDIN bool } var errMultipleSecretValue = fsterr.RemediationError{ @@ -94,6 +109,18 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { return errMultipleSecretValue } + switch { + case c.recreate && c.recreateMust: + return fsterr.RemediationError{ + Inner: fmt.Errorf("invalid flag combination, --recreate and --recreate-must"), + Remediation: "Use either --recreate or --recreate-must, not both.", + } + case c.recreate: + c.Input.Method = http.MethodPut + case c.recreateMust: + c.Input.Method = http.MethodPatch + } + // Read secret's value: either from STDIN, a file, or prompt. switch { case c.secretSTDIN: @@ -165,12 +192,15 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { return err } - // TODO: Use this approach across the code base. if ok, err := c.WriteJSON(out, o); ok { return err } - text.Success(out, "Created secret '%s' in Secret Store '%s' (digest: %s)", o.Name, c.Input.ID, hex.EncodeToString(o.Digest)) + action := "Created" + if o.Recreated { + action = "Recreated" + } + text.Success(out, "%s secret '%s' in Secret Store '%s' (digest: %s)", action, o.Name, c.Input.ID, hex.EncodeToString(o.Digest)) return nil } diff --git a/pkg/commands/secretstoreentry/secretstoreentry_test.go b/pkg/commands/secretstoreentry/secretstoreentry_test.go index 4f6a9e09d..e9ec2dc59 100644 --- a/pkg/commands/secretstoreentry/secretstoreentry_test.go +++ b/pkg/commands/secretstoreentry/secretstoreentry_test.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "errors" "fmt" + "net/http" "os" "path" "runtime" @@ -94,6 +95,10 @@ func TestCreateSecretCommand(t *testing.T) { args: fmt.Sprintf("create --store-id %s --name %s --stdin", storeID, secretName), wantError: "unable to read from STDIN", }, + { + args: fmt.Sprintf("create --store-id %s --name %s --stdin --recreate --recreate-must", storeID, secretName), + wantError: "invalid flag combination, --recreate and --recreate-must", + }, // Read from STDIN. { args: fmt.Sprintf("create --store-id %s --name %s --stdin", storeID, secretName), @@ -160,6 +165,62 @@ func TestCreateSecretCommand(t *testing.T) { Digest: []byte(secretDigest), }), }, + // CreateOrRecreate + { + args: fmt.Sprintf("create --store-id %s --name %s --file %s --json --recreate", storeID, secretName, secretFile), + api: mock.API{ + CreateClientKeyFn: mockCreateClientKey, + GetSigningKeyFn: mockGetSigningKey, + CreateSecretFn: func(i *fastly.CreateSecretInput) (*fastly.Secret, error) { + if got, want := i.Method, http.MethodPut; got != want { + return nil, fmt.Errorf("got method %q, want %q", got, want) + } + if got, err := decrypt(i.Secret); err != nil { + return nil, err + } else if got != secretValue { + return nil, fmt.Errorf("invalid secret: %s", got) + } + return &fastly.Secret{ + Name: i.Name, + Digest: []byte(secretDigest), + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fstfmt.EncodeJSON(&fastly.Secret{ + Name: secretName, + Digest: []byte(secretDigest), + }), + }, + // Recreate + { + args: fmt.Sprintf("create --store-id %s --name %s --file %s --json --recreate-must", storeID, secretName, secretFile), + api: mock.API{ + CreateClientKeyFn: mockCreateClientKey, + GetSigningKeyFn: mockGetSigningKey, + CreateSecretFn: func(i *fastly.CreateSecretInput) (*fastly.Secret, error) { + if got, want := i.Method, http.MethodPatch; got != want { + return nil, fmt.Errorf("got method %q, want %q", got, want) + } + if got, err := decrypt(i.Secret); err != nil { + return nil, err + } else if got != secretValue { + return nil, fmt.Errorf("invalid secret: %s", got) + } + return &fastly.Secret{ + Name: i.Name, + Digest: []byte(secretDigest), + Recreated: true, + }, nil + }, + }, + wantAPIInvoked: true, + wantOutput: fstfmt.EncodeJSON(&fastly.Secret{ + Name: secretName, + Digest: []byte(secretDigest), + Recreated: true, + }), + }, } for _, testcase := range scenarios { From 263ef6267bf6f6433e6b7482f4850a77d795c32e Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Fri, 26 May 2023 10:45:12 -0600 Subject: [PATCH 2/3] Use `recreate` and `recreate-allow` flags --- pkg/commands/secretstoreentry/create.go | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/commands/secretstoreentry/create.go b/pkg/commands/secretstoreentry/create.go index 254fe54b9..3ca2eecc0 100644 --- a/pkg/commands/secretstoreentry/create.go +++ b/pkg/commands/secretstoreentry/create.go @@ -62,14 +62,14 @@ func NewCreateCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *C c.RegisterFlagBool(c.JSONFlag()) // --json c.RegisterFlagBool(cmd.BoolFlagOpts{ Name: "recreate", - Description: "Create or recreate if a secret of the same name already exists within the store", + Description: "Recreate secret by name (errors if secret doesn't already exist)", Dst: &c.recreate, Required: false, }) c.RegisterFlagBool(cmd.BoolFlagOpts{ - Name: "recreate-must", - Description: "Recreate secret of the same name which must already exist within the store", - Dst: &c.recreateMust, + Name: "recreate-allow", + Description: "Create or recreate secret by name", + Dst: &c.recreateAllow, Required: false, }) c.RegisterFlagBool(secretStdinFlag(&c.secretSTDIN)) // --stdin @@ -82,12 +82,12 @@ type CreateCommand struct { cmd.Base cmd.JSONOutput - Input fastly.CreateSecretInput - manifest manifest.Data - recreate bool - recreateMust bool - secretFile string - secretSTDIN bool + Input fastly.CreateSecretInput + manifest manifest.Data + recreate bool + recreateAllow bool + secretFile string + secretSTDIN bool } var errMultipleSecretValue = fsterr.RemediationError{ @@ -110,15 +110,15 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error { } switch { - case c.recreate && c.recreateMust: + case c.recreate && c.recreateAllow: return fsterr.RemediationError{ - Inner: fmt.Errorf("invalid flag combination, --recreate and --recreate-must"), - Remediation: "Use either --recreate or --recreate-must, not both.", + Inner: fmt.Errorf("invalid flag combination, --recreate and --recreate-allow"), + Remediation: "Use either --recreate or --recreate-allow, not both.", } case c.recreate: - c.Input.Method = http.MethodPut - case c.recreateMust: c.Input.Method = http.MethodPatch + case c.recreateAllow: + c.Input.Method = http.MethodPut } // Read secret's value: either from STDIN, a file, or prompt. From 935959b13c8f2c241f6e20f8036752a9a2163093 Mon Sep 17 00:00:00 2001 From: Adam Williams Date: Fri, 26 May 2023 11:01:07 -0600 Subject: [PATCH 3/3] Fix failing tests --- pkg/commands/secretstoreentry/secretstoreentry_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/commands/secretstoreentry/secretstoreentry_test.go b/pkg/commands/secretstoreentry/secretstoreentry_test.go index e9ec2dc59..5fccac36b 100644 --- a/pkg/commands/secretstoreentry/secretstoreentry_test.go +++ b/pkg/commands/secretstoreentry/secretstoreentry_test.go @@ -96,8 +96,8 @@ func TestCreateSecretCommand(t *testing.T) { wantError: "unable to read from STDIN", }, { - args: fmt.Sprintf("create --store-id %s --name %s --stdin --recreate --recreate-must", storeID, secretName), - wantError: "invalid flag combination, --recreate and --recreate-must", + args: fmt.Sprintf("create --store-id %s --name %s --stdin --recreate --recreate-allow", storeID, secretName), + wantError: "invalid flag combination, --recreate and --recreate-allow", }, // Read from STDIN. { @@ -167,7 +167,7 @@ func TestCreateSecretCommand(t *testing.T) { }, // CreateOrRecreate { - args: fmt.Sprintf("create --store-id %s --name %s --file %s --json --recreate", storeID, secretName, secretFile), + args: fmt.Sprintf("create --store-id %s --name %s --file %s --json --recreate-allow", storeID, secretName, secretFile), api: mock.API{ CreateClientKeyFn: mockCreateClientKey, GetSigningKeyFn: mockGetSigningKey, @@ -194,7 +194,7 @@ func TestCreateSecretCommand(t *testing.T) { }, // Recreate { - args: fmt.Sprintf("create --store-id %s --name %s --file %s --json --recreate-must", storeID, secretName, secretFile), + args: fmt.Sprintf("create --store-id %s --name %s --file %s --json --recreate", storeID, secretName, secretFile), api: mock.API{ CreateClientKeyFn: mockCreateClientKey, GetSigningKeyFn: mockGetSigningKey,