From 3800ac2eb6278a0e104f0cb84ce59ff97540dcd5 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 23 Jul 2024 15:24:44 -0500 Subject: [PATCH 01/75] WIP --- .../vaultsecrets/integrations/displayer.go | 79 +++++++++ .../vaultsecrets/integrations/integrations.go | 26 +++ .../vaultsecrets/integrations/read.go | 121 +++++++++++++ .../vaultsecrets/integrations/read_test.go | 160 ++++++++++++++++++ .../commands/vaultsecrets/vault_secrets.go | 2 + 5 files changed, 388 insertions(+) create mode 100644 internal/commands/vaultsecrets/integrations/displayer.go create mode 100644 internal/commands/vaultsecrets/integrations/integrations.go create mode 100644 internal/commands/vaultsecrets/integrations/read.go create mode 100644 internal/commands/vaultsecrets/integrations/read_test.go diff --git a/internal/commands/vaultsecrets/integrations/displayer.go b/internal/commands/vaultsecrets/integrations/displayer.go new file mode 100644 index 00000000..55e8cfc2 --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/displayer.go @@ -0,0 +1,79 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/hashicorp/hcp/internal/pkg/format" +) + +type displayer struct { + previewTwilioIntegrations []*preview_models.Secrets20231128TwilioIntegration + previewMongoDBIntegrations []*preview_models.Secrets20231128MongoDBAtlasIntegration + + single bool +} + +func newTwilioDisplayer(single bool, integrations ...*preview_models.Secrets20231128TwilioIntegration) *displayer { + return &displayer{ + previewTwilioIntegrations: integrations, + single: single, + } +} + +func newMongoDBDisplayer(single bool, integrations ...*preview_models.Secrets20231128MongoDBAtlasIntegration) *displayer { + return &displayer{ + previewMongoDBIntegrations: integrations, + single: single, + } +} + +func (d *displayer) DefaultFormat() format.Format { + return format.Table +} + +func (d *displayer) Payload() any { + if d.previewTwilioIntegrations != nil { + return d.previewTwilioIntegrationsPayload() + } + + if d.previewMongoDBIntegrations != nil { + return d.previewMongoDBIntegrationsPayload() + } + + return nil +} + +func (d *displayer) previewTwilioIntegrationsPayload() any { + if d.single { + if len(d.previewTwilioIntegrations) != 1 { + return nil + } + return d.previewTwilioIntegrations[0] + } + return d.previewTwilioIntegrations +} + +func (d *displayer) previewMongoDBIntegrationsPayload() any { + if d.single { + if len(d.previewMongoDBIntegrations) != 1 { + return nil + } + return d.previewMongoDBIntegrations[0] + } + return d.previewMongoDBIntegrations +} + +func (d *displayer) FieldTemplates() []format.Field { + return []format.Field{ + { + Name: "Integration Name", + ValueFormat: "{{ .IntegrationName }}", + }, + { + Name: "Account SID", + ValueFormat: "{{ .TwilioAccountSid }}", + }, + } +} diff --git a/internal/commands/vaultsecrets/integrations/integrations.go b/internal/commands/vaultsecrets/integrations/integrations.go new file mode 100644 index 00000000..fc65d7ac --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/integrations.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/heredoc" +) + +func NewCmdIntegrations(ctx *cmd.Context) *cmd.Command { + cmd := &cmd.Command{ + Name: "integrations", + ShortHelp: "Manage Vault Secrets integrations.", + LongHelp: heredoc.New(ctx.IO).Must(` + The {{ template "mdCodeOrBold" "hcp vault-secrets integrations" }} command group lets you + manage Vault Secrets integrations. + `), + PersistentPreRun: func(c *cmd.Command, args []string) error { + return cmd.RequireOrgAndProject(ctx) + }, + } + + cmd.AddChild(NewCmdRead(ctx, nil)) + return cmd +} diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go new file mode 100644 index 00000000..6eef8aea --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "fmt" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/apps/helper" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +type ReadOpts struct { + Ctx context.Context + Profile *profile.Profile + Output *format.Outputter + IO iostreams.IOStreams + + IntegrationName string + Type string + Client secret_service.ClientService + PreviewClient preview_secret_service.ClientService +} + +func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { + opts := &ReadOpts{ + Ctx: ctx.ShutdownCtx, + Profile: ctx.Profile, + Output: ctx.Output, + IO: ctx.IO, + Client: secret_service.New(ctx.HCP, nil), + PreviewClient: preview_secret_service.New(ctx.HCP, nil), + } + + cmd := &cmd.Command{ + Name: "read", + ShortHelp: "Read a Vault Secrets integration.", + LongHelp: heredoc.New(ctx.IO).Must(` + The {{ template "mdCodeOrBold" "hcp vault-secrets integrations read" }} command gets a Vault Secrets integration. + `), + Examples: []cmd.Example{ + { + Preamble: `Read an application:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets apps read company-card + `), + }, + }, + Args: cmd.PositionalArguments{ + Args: []cmd.PositionalArgument{ + { + Name: "NAME", + Documentation: "The name of the app to read.", + }, + }, + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "type", + DisplayValue: "TYPE", + Description: "A required type for the integration to be read.", + Value: flagvalue.Simple("", &opts.Type), + Required: true, + }, + }, + }, + RunF: func(c *cmd.Command, args []string) error { + opts.IntegrationName = args[0] + + if runF != nil { + return runF(opts) + } + return readRun(opts) + }, + } + cmd.Args.Autocomplete = helper.PredictAppName(ctx, cmd, preview_secret_service.New(ctx.HCP, nil)) + + return cmd +} + +func readRun(opts *ReadOpts) error { + switch opts.Type { + case "twilio": + resp, err := opts.PreviewClient.GetTwilioIntegration(&preview_secret_service.GetTwilioIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + IntegrationName: opts.IntegrationName, + }, nil) + if err != nil { + return fmt.Errorf("failed to read integration: %w", err) + } + + return opts.Output.Display(newTwilioDisplayer(true, resp.Payload.Integration)) + + case "mongodb": + resp, err := opts.PreviewClient.GetMongoDBAtlasIntegration(&preview_secret_service.GetMongoDBAtlasIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + IntegrationName: opts.IntegrationName, + }, nil) + if err != nil { + return fmt.Errorf("failed to read integration: %w", err) + } + + return opts.Output.Display(newMongoDBDisplayer(true, resp.Payload.Integration)) + + default: + return fmt.Errorf("not a valid integration type") + } +} diff --git a/internal/commands/vaultsecrets/integrations/read_test.go b/internal/commands/vaultsecrets/integrations/read_test.go new file mode 100644 index 00000000..c4338a55 --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/read_test.go @@ -0,0 +1,160 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "errors" + "fmt" + "testing" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/stretchr/testify/mock" + + "github.com/go-openapi/runtime/client" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +func TestNewCmdRead(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *ReadOpts + }{ + { + Name: "Good", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123").SetProjectID("abc") + }, + Args: []string{"sample-integration", "--type", "twilio"}, + Expect: &ReadOpts{ + IntegrationName: "sample-integration", + }, + }, + { + Name: "Missing type flag", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123").SetProjectID("abc") + }, + Args: []string{"sample-integration"}, + Error: "ERROR: missing required flag: --type=TYPE", + }, + } + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + r := require.New(t) + io := iostreams.Test() + + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + ShutdownCtx: context.Background(), + HCP: &client.Runtime{}, + Output: format.New(io), + } + + var readOpts *ReadOpts + readCmd := NewCmdRead(ctx, func(o *ReadOpts) error { + readOpts = o + return nil + }) + readCmd.SetIO(io) + + code := readCmd.Run(c.Args) + if c.Error != "" { + r.NotZero(code) + r.Contains(io.Error.String(), c.Error) + return + } + + r.Zero(code, io.Error.String()) + r.NotNil(readOpts) + r.Equal(c.Expect.IntegrationName, readOpts.IntegrationName) + }) + } +} + +func TestReadRun(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + ErrMsg string + IntegrationName string + Type string + }{ + { + Name: "Failed: Integration not found", + ErrMsg: "[GET /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/integrations/twilio/config/{integration_name}][403] GetTwilioIntegration", + Type: "twilio", + }, + { + Name: "Success: Read integration", + IntegrationName: "sample-integration", + Type: "twilio", + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + io := iostreams.Test() + vs := mock_preview_secret_service.NewMockClientService(t) + + opts := &ReadOpts{ + Ctx: context.Background(), + Profile: profile.TestProfile(t).SetOrgID("123").SetProjectID("abc"), + IO: io, + PreviewClient: vs, + Output: format.New(io), + IntegrationName: c.IntegrationName, + Type: c.Type, + } + + if c.ErrMsg != "" { + vs.EXPECT().GetTwilioIntegration(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + vs.EXPECT().GetTwilioIntegration(&preview_secret_service.GetTwilioIntegrationParams{ + OrganizationID: "123", + ProjectID: "abc", + IntegrationName: opts.IntegrationName, + Context: opts.Ctx, + }, nil).Return(&preview_secret_service.GetTwilioIntegrationOK{ + Payload: &preview_models.Secrets20231128GetTwilioIntegrationResponse{ + Integration: &preview_models.Secrets20231128TwilioIntegration{ + IntegrationName: opts.IntegrationName, + }, + }, + }, nil).Once() + } + + // Run the command + err := readRun(opts) + if c.ErrMsg != "" { + r.Contains(err.Error(), c.ErrMsg) + return + } + + r.NoError(err) + r.Contains(io.Output.String(), fmt.Sprintf("Integration Name Account SID\n%s \n", opts.IntegrationName)) + }) + } +} diff --git a/internal/commands/vaultsecrets/vault_secrets.go b/internal/commands/vaultsecrets/vault_secrets.go index bdfa362e..452ff627 100644 --- a/internal/commands/vaultsecrets/vault_secrets.go +++ b/internal/commands/vaultsecrets/vault_secrets.go @@ -6,6 +6,7 @@ package vaultsecrets import ( "github.com/hashicorp/hcp/internal/commands/vaultsecrets/apps" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/gatewaypools" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/run" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets" "github.com/hashicorp/hcp/internal/pkg/cmd" @@ -26,6 +27,7 @@ func NewCmdVaultSecrets(ctx *cmd.Context) *cmd.Command { } cmd.AddChild(apps.NewCmdApps(ctx)) + cmd.AddChild(integrations.NewCmdIntegrations(ctx)) cmd.AddChild(secrets.NewCmdSecrets(ctx)) cmd.AddChild(gatewaypools.NewCmdGatewayPools(ctx)) cmd.AddChild(run.NewCmdRun(ctx, nil)) From 115003efb65dab55e22c859ace641ee8be292984 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 23 Jul 2024 15:34:56 -0500 Subject: [PATCH 02/75] fixes a few typos --- internal/commands/vaultsecrets/integrations/read.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index 6eef8aea..29fbb400 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -9,7 +9,6 @@ import ( preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" - "github.com/hashicorp/hcp/internal/commands/vaultsecrets/apps/helper" "github.com/hashicorp/hcp/internal/pkg/cmd" "github.com/hashicorp/hcp/internal/pkg/flagvalue" "github.com/hashicorp/hcp/internal/pkg/format" @@ -48,9 +47,9 @@ func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { `), Examples: []cmd.Example{ { - Preamble: `Read an application:`, + Preamble: `Read an integration:`, Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` - $ hcp vault-secrets apps read company-card + $ hcp vault-secrets integrations read sample-integration --type twilio `), }, }, @@ -58,7 +57,7 @@ func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { Args: []cmd.PositionalArgument{ { Name: "NAME", - Documentation: "The name of the app to read.", + Documentation: "The name of the integration to read.", }, }, }, @@ -82,7 +81,6 @@ func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { return readRun(opts) }, } - cmd.Args.Autocomplete = helper.PredictAppName(ctx, cmd, preview_secret_service.New(ctx.HCP, nil)) return cmd } From f34534ece412582de3d715ea834347059885e22e Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 23 Jul 2024 16:00:35 -0500 Subject: [PATCH 03/75] typo --- internal/commands/vaultsecrets/integrations/read.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index 29fbb400..a1535891 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -66,7 +66,7 @@ func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { { Name: "type", DisplayValue: "TYPE", - Description: "A required type for the integration to be read.", + Description: "The type of the integration to read.", Value: flagvalue.Simple("", &opts.Type), Required: true, }, From a155d4581b0e2468c32a37f5a8703cfbd3e498b5 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 23 Jul 2024 16:14:14 -0500 Subject: [PATCH 04/75] lints --- internal/commands/vaultsecrets/integrations/read_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/read_test.go b/internal/commands/vaultsecrets/integrations/read_test.go index c4338a55..964fc841 100644 --- a/internal/commands/vaultsecrets/integrations/read_test.go +++ b/internal/commands/vaultsecrets/integrations/read_test.go @@ -9,18 +9,17 @@ import ( "fmt" "testing" - preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" - preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" - mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" - "github.com/stretchr/testify/mock" - "github.com/go-openapi/runtime/client" "github.com/stretchr/testify/require" + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" "github.com/hashicorp/hcp/internal/pkg/cmd" "github.com/hashicorp/hcp/internal/pkg/format" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" + "github.com/stretchr/testify/mock" ) func TestNewCmdRead(t *testing.T) { From 5d5819725803c7c26b2181dcf3c67e712337da6e Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 29 Jul 2024 10:33:31 -0500 Subject: [PATCH 05/75] some clean up and latest displayers --- .../vaultsecrets/integrations/displayer.go | 91 ++++++++++++------- 1 file changed, 59 insertions(+), 32 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/displayer.go b/internal/commands/vaultsecrets/integrations/displayer.go index 55e8cfc2..bb8628d7 100644 --- a/internal/commands/vaultsecrets/integrations/displayer.go +++ b/internal/commands/vaultsecrets/integrations/displayer.go @@ -8,72 +8,99 @@ import ( "github.com/hashicorp/hcp/internal/pkg/format" ) -type displayer struct { - previewTwilioIntegrations []*preview_models.Secrets20231128TwilioIntegration - previewMongoDBIntegrations []*preview_models.Secrets20231128MongoDBAtlasIntegration +type twilioDisplayer struct { + previewTwilioIntegrations []*preview_models.Secrets20231128TwilioIntegration single bool } -func newTwilioDisplayer(single bool, integrations ...*preview_models.Secrets20231128TwilioIntegration) *displayer { - return &displayer{ +func newTwilioDisplayer(single bool, integrations ...*preview_models.Secrets20231128TwilioIntegration) *twilioDisplayer { + return &twilioDisplayer{ previewTwilioIntegrations: integrations, single: single, } } -func newMongoDBDisplayer(single bool, integrations ...*preview_models.Secrets20231128MongoDBAtlasIntegration) *displayer { - return &displayer{ - previewMongoDBIntegrations: integrations, - single: single, +func (t *twilioDisplayer) DefaultFormat() format.Format { + return format.Table +} + +func (t *twilioDisplayer) Payload() any { + if t.previewTwilioIntegrations != nil { + return t.previewTwilioIntegrationsPayload() } + + return nil } -func (d *displayer) DefaultFormat() format.Format { - return format.Table +func (t *twilioDisplayer) previewTwilioIntegrationsPayload() any { + if t.single { + if len(t.previewTwilioIntegrations) != 1 { + return nil + } + return t.previewTwilioIntegrations[0] + } + return t.previewTwilioIntegrations } -func (d *displayer) Payload() any { - if d.previewTwilioIntegrations != nil { - return d.previewTwilioIntegrationsPayload() +func (t *twilioDisplayer) FieldTemplates() []format.Field { + return []format.Field{ + { + Name: "Integration Name", + ValueFormat: "{{ .IntegrationName }}", + }, + { + Name: "Account SID", + ValueFormat: "{{ .TwilioAccountSid }}", + }, } +} + +type mongodbDisplayer struct { + previewMongoDBIntegrations []*preview_models.Secrets20231128MongoDBAtlasIntegration + + single bool +} - if d.previewMongoDBIntegrations != nil { - return d.previewMongoDBIntegrationsPayload() +func newMongoDBDisplayer(single bool, integrations ...*preview_models.Secrets20231128MongoDBAtlasIntegration) *mongodbDisplayer { + return &mongodbDisplayer{ + previewMongoDBIntegrations: integrations, + single: single, } +} - return nil +func (m *mongodbDisplayer) DefaultFormat() format.Format { + return format.Table } -func (d *displayer) previewTwilioIntegrationsPayload() any { - if d.single { - if len(d.previewTwilioIntegrations) != 1 { - return nil - } - return d.previewTwilioIntegrations[0] +func (m *mongodbDisplayer) Payload() any { + + if m.previewMongoDBIntegrations != nil { + return m.previewMongoDBIntegrationsPayload() } - return d.previewTwilioIntegrations + + return nil } -func (d *displayer) previewMongoDBIntegrationsPayload() any { - if d.single { - if len(d.previewMongoDBIntegrations) != 1 { +func (m *mongodbDisplayer) previewMongoDBIntegrationsPayload() any { + if m.single { + if len(m.previewMongoDBIntegrations) != 1 { return nil } - return d.previewMongoDBIntegrations[0] + return m.previewMongoDBIntegrations[0] } - return d.previewMongoDBIntegrations + return m.previewMongoDBIntegrations } -func (d *displayer) FieldTemplates() []format.Field { +func (m *mongodbDisplayer) FieldTemplates() []format.Field { return []format.Field{ { Name: "Integration Name", ValueFormat: "{{ .IntegrationName }}", }, { - Name: "Account SID", - ValueFormat: "{{ .TwilioAccountSid }}", + Name: "API Public Key", + ValueFormat: "{{ .APIPublicKey }}", }, } } From 9b4660c4258592005ed945ec046a0517af105bde Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 29 Jul 2024 11:54:42 -0500 Subject: [PATCH 06/75] makes suggested changes --- internal/commands/vaultsecrets/integrations/read.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index a1535891..e4dbe60a 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -29,6 +29,11 @@ type ReadOpts struct { PreviewClient preview_secret_service.ClientService } +const ( + Twilio string = "twilio" + MongoDB = "mongo" +) + func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { opts := &ReadOpts{ Ctx: ctx.ShutdownCtx, @@ -87,7 +92,7 @@ func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { func readRun(opts *ReadOpts) error { switch opts.Type { - case "twilio": + case Twilio: resp, err := opts.PreviewClient.GetTwilioIntegration(&preview_secret_service.GetTwilioIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, @@ -100,7 +105,7 @@ func readRun(opts *ReadOpts) error { return opts.Output.Display(newTwilioDisplayer(true, resp.Payload.Integration)) - case "mongodb": + case MongoDB: resp, err := opts.PreviewClient.GetMongoDBAtlasIntegration(&preview_secret_service.GetMongoDBAtlasIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, From 31db3bf9232fadc8bcad5d961bb6c3bb080e40e5 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 29 Jul 2024 12:02:33 -0500 Subject: [PATCH 07/75] lints --- internal/commands/vaultsecrets/integrations/read.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index e4dbe60a..1c8c317e 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -30,8 +30,8 @@ type ReadOpts struct { } const ( - Twilio string = "twilio" - MongoDB = "mongo" + Twilio = "twilio" + MongoDB = "mongo" ) func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { From 74959186195dda2c6597c5777fa337d136c17bd3 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 24 Jul 2024 10:12:15 -0500 Subject: [PATCH 08/75] Delete a HVS integration --- .../vaultsecrets/integrations/delete.go | 121 ++++++++++++++ .../vaultsecrets/integrations/delete_test.go | 154 ++++++++++++++++++ .../vaultsecrets/integrations/integrations.go | 1 + .../vaultsecrets/integrations/read_test.go | 5 +- 4 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 internal/commands/vaultsecrets/integrations/delete.go create mode 100644 internal/commands/vaultsecrets/integrations/delete_test.go diff --git a/internal/commands/vaultsecrets/integrations/delete.go b/internal/commands/vaultsecrets/integrations/delete.go new file mode 100644 index 00000000..75964daa --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/delete.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "fmt" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +type DeleteOpts struct { + Ctx context.Context + Profile *profile.Profile + Output *format.Outputter + IO iostreams.IOStreams + + IntegrationName string + Type string + Client secret_service.ClientService + PreviewClient preview_secret_service.ClientService +} + +func NewCmdDelete(ctx *cmd.Context, runF func(*DeleteOpts) error) *cmd.Command { + opts := &DeleteOpts{ + Ctx: ctx.ShutdownCtx, + Profile: ctx.Profile, + Output: ctx.Output, + IO: ctx.IO, + Client: secret_service.New(ctx.HCP, nil), + PreviewClient: preview_secret_service.New(ctx.HCP, nil), + } + + cmd := &cmd.Command{ + Name: "delete", + ShortHelp: "Delete a Vault Secrets integration.", + LongHelp: heredoc.New(ctx.IO).Must(` + The {{ template "mdCodeOrBold" "hcp vault-secrets integrations delete" }} command deletes a Vault Secrets integration. + `), + Examples: []cmd.Example{ + { + Preamble: `Delete an integration:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets integrations delete sample-integration --type twilio + `), + }, + }, + Args: cmd.PositionalArguments{ + Args: []cmd.PositionalArgument{ + { + Name: "NAME", + Documentation: "The name of the integration to delete.", + }, + }, + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "type", + DisplayValue: "TYPE", + Description: "The type of the integration to delete.", + Value: flagvalue.Simple("", &opts.Type), + Required: true, + }, + }, + }, + RunF: func(c *cmd.Command, args []string) error { + opts.IntegrationName = args[0] + + if runF != nil { + return runF(opts) + } + return deleteRun(opts) + }, + } + + return cmd +} + +func deleteRun(opts *DeleteOpts) error { + switch opts.Type { + case "twilio": + _, err := opts.PreviewClient.DeleteTwilioIntegration(&preview_secret_service.DeleteTwilioIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + IntegrationName: opts.IntegrationName, + }, nil) + if err != nil { + return fmt.Errorf("failed to delete integration: %w", err) + } + + fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) + return nil + + case "mongodb": + _, err := opts.PreviewClient.DeleteMongoDBAtlasIntegration(&preview_secret_service.DeleteMongoDBAtlasIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + IntegrationName: opts.IntegrationName, + }, nil) + if err != nil { + return fmt.Errorf("failed to delete integration: %w", err) + } + + fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) + return nil + + default: + return fmt.Errorf("not a valid integration type") + } +} diff --git a/internal/commands/vaultsecrets/integrations/delete_test.go b/internal/commands/vaultsecrets/integrations/delete_test.go new file mode 100644 index 00000000..6c9b8b08 --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/delete_test.go @@ -0,0 +1,154 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/stretchr/testify/require" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" + "github.com/stretchr/testify/mock" +) + +func TestNewCmdDelete(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *DeleteOpts + }{ + { + Name: "Good", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123").SetProjectID("abc") + }, + Args: []string{"sample-integration", "--type", "twilio"}, + Expect: &DeleteOpts{ + IntegrationName: "sample-integration", + Type: "twilio", + }, + }, + { + Name: "Missing type flag", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123").SetProjectID("abc") + }, + Args: []string{"sample-integration"}, + Error: "ERROR: missing required flag: --type=TYPE", + }, + } + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + r := require.New(t) + io := iostreams.Test() + + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + ShutdownCtx: context.Background(), + HCP: &client.Runtime{}, + Output: format.New(io), + } + + var deleteOpts *DeleteOpts + readCmd := NewCmdDelete(ctx, func(o *DeleteOpts) error { + deleteOpts = o + return nil + }) + readCmd.SetIO(io) + + code := readCmd.Run(c.Args) + if c.Error != "" { + r.NotZero(code) + r.Contains(io.Error.String(), c.Error) + return + } + + r.Zero(code, io.Error.String()) + r.NotNil(deleteOpts) + r.Equal(c.Expect.IntegrationName, deleteOpts.IntegrationName) + r.Equal(c.Expect.Type, deleteOpts.Type) + }) + } +} + +func TestDeleteRun(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + ErrMsg string + IntegrationName string + Type string + }{ + { + Name: "Failed: Integration not found", + ErrMsg: "[DELETE /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/integrations/twilio/config/{integration_name}][404] DeleteTwilioIntegration", + Type: "twilio", + }, + { + Name: "Success: Delete integration", + IntegrationName: "sample-integration", + Type: "twilio", + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + io := iostreams.Test() + vs := mock_preview_secret_service.NewMockClientService(t) + + opts := &DeleteOpts{ + Ctx: context.Background(), + Profile: profile.TestProfile(t).SetOrgID("123").SetProjectID("abc"), + IO: io, + PreviewClient: vs, + Output: format.New(io), + IntegrationName: c.IntegrationName, + Type: c.Type, + } + + if c.ErrMsg != "" { + vs.EXPECT().DeleteTwilioIntegration(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + vs.EXPECT().DeleteTwilioIntegration(&preview_secret_service.DeleteTwilioIntegrationParams{ + OrganizationID: "123", + ProjectID: "abc", + IntegrationName: opts.IntegrationName, + Context: opts.Ctx, + }, nil).Return(&preview_secret_service.DeleteTwilioIntegrationOK{}, nil).Once() + } + + // Run the command + err := deleteRun(opts) + if c.ErrMsg != "" { + r.Contains(err.Error(), c.ErrMsg) + return + } + + r.NoError(err) + r.Contains(io.Error.String(), fmt.Sprintf("✓ Successfully deleted integration with name \"sample-integration\"\n")) + }) + } +} diff --git a/internal/commands/vaultsecrets/integrations/integrations.go b/internal/commands/vaultsecrets/integrations/integrations.go index fc65d7ac..9a0dc780 100644 --- a/internal/commands/vaultsecrets/integrations/integrations.go +++ b/internal/commands/vaultsecrets/integrations/integrations.go @@ -22,5 +22,6 @@ func NewCmdIntegrations(ctx *cmd.Context) *cmd.Command { } cmd.AddChild(NewCmdRead(ctx, nil)) + cmd.AddChild(NewCmdDelete(ctx, nil)) return cmd } diff --git a/internal/commands/vaultsecrets/integrations/read_test.go b/internal/commands/vaultsecrets/integrations/read_test.go index 964fc841..82f3c27d 100644 --- a/internal/commands/vaultsecrets/integrations/read_test.go +++ b/internal/commands/vaultsecrets/integrations/read_test.go @@ -40,6 +40,7 @@ func TestNewCmdRead(t *testing.T) { Args: []string{"sample-integration", "--type", "twilio"}, Expect: &ReadOpts{ IntegrationName: "sample-integration", + Type: "twilio", }, }, { @@ -84,6 +85,8 @@ func TestNewCmdRead(t *testing.T) { r.Zero(code, io.Error.String()) r.NotNil(readOpts) r.Equal(c.Expect.IntegrationName, readOpts.IntegrationName) + r.Equal(c.Expect.Type, readOpts.Type) + }) } } @@ -99,7 +102,7 @@ func TestReadRun(t *testing.T) { }{ { Name: "Failed: Integration not found", - ErrMsg: "[GET /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/integrations/twilio/config/{integration_name}][403] GetTwilioIntegration", + ErrMsg: "[GET /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/integrations/twilio/config/{integration_name}][404] GetTwilioIntegration", Type: "twilio", }, { From 15af167474f6add6b335e59947feb8f05de1d4ec Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 25 Jul 2024 14:53:50 -0500 Subject: [PATCH 09/75] lints --- internal/commands/vaultsecrets/integrations/delete_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/commands/vaultsecrets/integrations/delete_test.go b/internal/commands/vaultsecrets/integrations/delete_test.go index 6c9b8b08..ad3994aa 100644 --- a/internal/commands/vaultsecrets/integrations/delete_test.go +++ b/internal/commands/vaultsecrets/integrations/delete_test.go @@ -148,7 +148,7 @@ func TestDeleteRun(t *testing.T) { } r.NoError(err) - r.Contains(io.Error.String(), fmt.Sprintf("✓ Successfully deleted integration with name \"sample-integration\"\n")) + r.Contains(io.Error.String(), fmt.Sprintf("✓ Successfully deleted integration with name \"%s\"\n", c.IntegrationName)) }) } } From 2d2ad736b11ea59ef3915df5dd4bef91d19df872 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 30 Jul 2024 14:57:05 -0500 Subject: [PATCH 10/75] clean up --- internal/commands/vaultsecrets/integrations/delete.go | 4 ++-- internal/commands/vaultsecrets/integrations/displayer.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/delete.go b/internal/commands/vaultsecrets/integrations/delete.go index 75964daa..d224b371 100644 --- a/internal/commands/vaultsecrets/integrations/delete.go +++ b/internal/commands/vaultsecrets/integrations/delete.go @@ -87,7 +87,7 @@ func NewCmdDelete(ctx *cmd.Context, runF func(*DeleteOpts) error) *cmd.Command { func deleteRun(opts *DeleteOpts) error { switch opts.Type { - case "twilio": + case Twilio: _, err := opts.PreviewClient.DeleteTwilioIntegration(&preview_secret_service.DeleteTwilioIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, @@ -101,7 +101,7 @@ func deleteRun(opts *DeleteOpts) error { fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) return nil - case "mongodb": + case MongoDB: _, err := opts.PreviewClient.DeleteMongoDBAtlasIntegration(&preview_secret_service.DeleteMongoDBAtlasIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, diff --git a/internal/commands/vaultsecrets/integrations/displayer.go b/internal/commands/vaultsecrets/integrations/displayer.go index bb8628d7..5b1aa459 100644 --- a/internal/commands/vaultsecrets/integrations/displayer.go +++ b/internal/commands/vaultsecrets/integrations/displayer.go @@ -100,7 +100,7 @@ func (m *mongodbDisplayer) FieldTemplates() []format.Field { }, { Name: "API Public Key", - ValueFormat: "{{ .APIPublicKey }}", + ValueFormat: "{{ .MongodbAPIPublicKey }}", }, } } From d98531b097fd8ec8bc1e972ac93eefbaa10943e5 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 1 Aug 2024 17:28:28 -0500 Subject: [PATCH 11/75] respond to review comments --- internal/commands/vaultsecrets/integrations/delete.go | 4 +++- internal/commands/vaultsecrets/integrations/delete_test.go | 7 ++++--- .../commands/vaultsecrets/integrations/integrations.go | 7 +++++++ internal/commands/vaultsecrets/integrations/read.go | 7 +------ internal/commands/vaultsecrets/integrations/read_test.go | 6 +++--- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/delete.go b/internal/commands/vaultsecrets/integrations/delete.go index d224b371..d70c0cde 100644 --- a/internal/commands/vaultsecrets/integrations/delete.go +++ b/internal/commands/vaultsecrets/integrations/delete.go @@ -24,7 +24,7 @@ type DeleteOpts struct { IO iostreams.IOStreams IntegrationName string - Type string + Type IntegrationType Client secret_service.ClientService PreviewClient preview_secret_service.ClientService } @@ -93,6 +93,7 @@ func deleteRun(opts *DeleteOpts) error { ProjectID: opts.Profile.ProjectID, OrganizationID: opts.Profile.OrganizationID, IntegrationName: opts.IntegrationName, + Name: &opts.IntegrationName, }, nil) if err != nil { return fmt.Errorf("failed to delete integration: %w", err) @@ -107,6 +108,7 @@ func deleteRun(opts *DeleteOpts) error { ProjectID: opts.Profile.ProjectID, OrganizationID: opts.Profile.OrganizationID, IntegrationName: opts.IntegrationName, + Name: &opts.IntegrationName, }, nil) if err != nil { return fmt.Errorf("failed to delete integration: %w", err) diff --git a/internal/commands/vaultsecrets/integrations/delete_test.go b/internal/commands/vaultsecrets/integrations/delete_test.go index ad3994aa..830338f3 100644 --- a/internal/commands/vaultsecrets/integrations/delete_test.go +++ b/internal/commands/vaultsecrets/integrations/delete_test.go @@ -96,17 +96,17 @@ func TestDeleteRun(t *testing.T) { Name string ErrMsg string IntegrationName string - Type string + Type IntegrationType }{ { Name: "Failed: Integration not found", ErrMsg: "[DELETE /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/integrations/twilio/config/{integration_name}][404] DeleteTwilioIntegration", - Type: "twilio", + Type: Twilio, }, { Name: "Success: Delete integration", IntegrationName: "sample-integration", - Type: "twilio", + Type: Twilio, }, } @@ -136,6 +136,7 @@ func TestDeleteRun(t *testing.T) { OrganizationID: "123", ProjectID: "abc", IntegrationName: opts.IntegrationName, + Name: &opts.IntegrationName, Context: opts.Ctx, }, nil).Return(&preview_secret_service.DeleteTwilioIntegrationOK{}, nil).Once() } diff --git a/internal/commands/vaultsecrets/integrations/integrations.go b/internal/commands/vaultsecrets/integrations/integrations.go index 9a0dc780..c8135f63 100644 --- a/internal/commands/vaultsecrets/integrations/integrations.go +++ b/internal/commands/vaultsecrets/integrations/integrations.go @@ -8,6 +8,13 @@ import ( "github.com/hashicorp/hcp/internal/pkg/heredoc" ) +type IntegrationType string + +const ( + Twilio IntegrationType = "twilio" + MongoDB IntegrationType = "mongodb-atlas" +) + func NewCmdIntegrations(ctx *cmd.Context) *cmd.Command { cmd := &cmd.Command{ Name: "integrations", diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index 1c8c317e..1602bad7 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -24,16 +24,11 @@ type ReadOpts struct { IO iostreams.IOStreams IntegrationName string - Type string + Type IntegrationType Client secret_service.ClientService PreviewClient preview_secret_service.ClientService } -const ( - Twilio = "twilio" - MongoDB = "mongo" -) - func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { opts := &ReadOpts{ Ctx: ctx.ShutdownCtx, diff --git a/internal/commands/vaultsecrets/integrations/read_test.go b/internal/commands/vaultsecrets/integrations/read_test.go index 82f3c27d..e4f427b7 100644 --- a/internal/commands/vaultsecrets/integrations/read_test.go +++ b/internal/commands/vaultsecrets/integrations/read_test.go @@ -98,17 +98,17 @@ func TestReadRun(t *testing.T) { Name string ErrMsg string IntegrationName string - Type string + Type IntegrationType }{ { Name: "Failed: Integration not found", ErrMsg: "[GET /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/integrations/twilio/config/{integration_name}][404] GetTwilioIntegration", - Type: "twilio", + Type: Twilio, }, { Name: "Success: Read integration", IntegrationName: "sample-integration", - Type: "twilio", + Type: Twilio, }, } From 0c878512d64ad8bfde8945156c74778d4542d5e7 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 2 Aug 2024 14:34:01 -0500 Subject: [PATCH 12/75] adds aws and gcp --- .../vaultsecrets/integrations/delete.go | 32 +++++++++++++++++-- .../vaultsecrets/integrations/integrations.go | 6 ++-- .../vaultsecrets/integrations/read.go | 4 +-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/delete.go b/internal/commands/vaultsecrets/integrations/delete.go index d70c0cde..45777784 100644 --- a/internal/commands/vaultsecrets/integrations/delete.go +++ b/internal/commands/vaultsecrets/integrations/delete.go @@ -87,7 +87,7 @@ func NewCmdDelete(ctx *cmd.Context, runF func(*DeleteOpts) error) *cmd.Command { func deleteRun(opts *DeleteOpts) error { switch opts.Type { - case Twilio: + case IntegrationType_Twilio: _, err := opts.PreviewClient.DeleteTwilioIntegration(&preview_secret_service.DeleteTwilioIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, @@ -102,7 +102,7 @@ func deleteRun(opts *DeleteOpts) error { fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) return nil - case MongoDB: + case IntegrationType_MONGODB_ATLAS: _, err := opts.PreviewClient.DeleteMongoDBAtlasIntegration(&preview_secret_service.DeleteMongoDBAtlasIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, @@ -117,6 +117,34 @@ func deleteRun(opts *DeleteOpts) error { fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) return nil + case IntegrationType_AWS: + _, err := opts.PreviewClient.DeleteAwsIntegration(&preview_secret_service.DeleteAwsIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Name: opts.IntegrationName, + }, nil) + if err != nil { + return fmt.Errorf("failed to delete integration: %w", err) + } + + fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) + return nil + + case IntegrationType_GCP: + _, err := opts.PreviewClient.DeleteGcpIntegration(&preview_secret_service.DeleteGcpIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Name: opts.IntegrationName, + }, nil) + if err != nil { + return fmt.Errorf("failed to delete integration: %w", err) + } + + fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) + return nil + default: return fmt.Errorf("not a valid integration type") } diff --git a/internal/commands/vaultsecrets/integrations/integrations.go b/internal/commands/vaultsecrets/integrations/integrations.go index c8135f63..5bb8b80f 100644 --- a/internal/commands/vaultsecrets/integrations/integrations.go +++ b/internal/commands/vaultsecrets/integrations/integrations.go @@ -11,8 +11,10 @@ import ( type IntegrationType string const ( - Twilio IntegrationType = "twilio" - MongoDB IntegrationType = "mongodb-atlas" + IntegrationType_Twilio IntegrationType = "twilio" + IntegrationType_MONGODB_ATLAS IntegrationType = "mongodb-atlas" + IntegrationType_AWS IntegrationType = "aws" + IntegrationType_GCP IntegrationType = "gcp" ) func NewCmdIntegrations(ctx *cmd.Context) *cmd.Command { diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index 1602bad7..e5d3289f 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -87,7 +87,7 @@ func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { func readRun(opts *ReadOpts) error { switch opts.Type { - case Twilio: + case IntegrationType_Twilio: resp, err := opts.PreviewClient.GetTwilioIntegration(&preview_secret_service.GetTwilioIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, @@ -100,7 +100,7 @@ func readRun(opts *ReadOpts) error { return opts.Output.Display(newTwilioDisplayer(true, resp.Payload.Integration)) - case MongoDB: + case IntegrationType_MONGODB_ATLAS: resp, err := opts.PreviewClient.GetMongoDBAtlasIntegration(&preview_secret_service.GetMongoDBAtlasIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, From 013e3572deb646c5bdf437f4a156ffb7004c4b8f Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 2 Aug 2024 14:42:54 -0500 Subject: [PATCH 13/75] lints --- internal/commands/vaultsecrets/integrations/delete.go | 8 ++++---- .../commands/vaultsecrets/integrations/integrations.go | 8 ++++---- internal/commands/vaultsecrets/integrations/read.go | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/delete.go b/internal/commands/vaultsecrets/integrations/delete.go index 45777784..cab084e8 100644 --- a/internal/commands/vaultsecrets/integrations/delete.go +++ b/internal/commands/vaultsecrets/integrations/delete.go @@ -87,7 +87,7 @@ func NewCmdDelete(ctx *cmd.Context, runF func(*DeleteOpts) error) *cmd.Command { func deleteRun(opts *DeleteOpts) error { switch opts.Type { - case IntegrationType_Twilio: + case Twilio: _, err := opts.PreviewClient.DeleteTwilioIntegration(&preview_secret_service.DeleteTwilioIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, @@ -102,7 +102,7 @@ func deleteRun(opts *DeleteOpts) error { fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) return nil - case IntegrationType_MONGODB_ATLAS: + case MongoDBAtlas: _, err := opts.PreviewClient.DeleteMongoDBAtlasIntegration(&preview_secret_service.DeleteMongoDBAtlasIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, @@ -117,7 +117,7 @@ func deleteRun(opts *DeleteOpts) error { fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) return nil - case IntegrationType_AWS: + case AWS: _, err := opts.PreviewClient.DeleteAwsIntegration(&preview_secret_service.DeleteAwsIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, @@ -131,7 +131,7 @@ func deleteRun(opts *DeleteOpts) error { fmt.Fprintf(opts.IO.Err(), "%s Successfully deleted integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) return nil - case IntegrationType_GCP: + case GCP: _, err := opts.PreviewClient.DeleteGcpIntegration(&preview_secret_service.DeleteGcpIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, diff --git a/internal/commands/vaultsecrets/integrations/integrations.go b/internal/commands/vaultsecrets/integrations/integrations.go index 5bb8b80f..5d5544b0 100644 --- a/internal/commands/vaultsecrets/integrations/integrations.go +++ b/internal/commands/vaultsecrets/integrations/integrations.go @@ -11,10 +11,10 @@ import ( type IntegrationType string const ( - IntegrationType_Twilio IntegrationType = "twilio" - IntegrationType_MONGODB_ATLAS IntegrationType = "mongodb-atlas" - IntegrationType_AWS IntegrationType = "aws" - IntegrationType_GCP IntegrationType = "gcp" + Twilio IntegrationType = "twilio" + MongoDBAtlas IntegrationType = "mongodb-atlas" + AWS IntegrationType = "aws" + GCP IntegrationType = "gcp" ) func NewCmdIntegrations(ctx *cmd.Context) *cmd.Command { diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index e5d3289f..15269996 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -87,7 +87,7 @@ func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { func readRun(opts *ReadOpts) error { switch opts.Type { - case IntegrationType_Twilio: + case Twilio: resp, err := opts.PreviewClient.GetTwilioIntegration(&preview_secret_service.GetTwilioIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, @@ -100,7 +100,7 @@ func readRun(opts *ReadOpts) error { return opts.Output.Display(newTwilioDisplayer(true, resp.Payload.Integration)) - case IntegrationType_MONGODB_ATLAS: + case MongoDBAtlas: resp, err := opts.PreviewClient.GetMongoDBAtlasIntegration(&preview_secret_service.GetMongoDBAtlasIntegrationParams{ Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, From 58281c8ea226ee471d4bb58df9826e82abfe43ff Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 22 Aug 2024 15:35:08 -0500 Subject: [PATCH 14/75] list integrations --- .../vaultsecrets/integrations/displayer.go | 202 ++++++++++++++++- .../vaultsecrets/integrations/integrations.go | 1 + .../vaultsecrets/integrations/list.go | 213 ++++++++++++++++++ .../vaultsecrets/integrations/list_test.go | 170 ++++++++++++++ .../vaultsecrets/integrations/read_test.go | 7 +- 5 files changed, 582 insertions(+), 11 deletions(-) create mode 100644 internal/commands/vaultsecrets/integrations/list.go create mode 100644 internal/commands/vaultsecrets/integrations/list_test.go diff --git a/internal/commands/vaultsecrets/integrations/displayer.go b/internal/commands/vaultsecrets/integrations/displayer.go index 5b1aa459..fe534c0c 100644 --- a/internal/commands/vaultsecrets/integrations/displayer.go +++ b/internal/commands/vaultsecrets/integrations/displayer.go @@ -44,16 +44,27 @@ func (t *twilioDisplayer) previewTwilioIntegrationsPayload() any { } func (t *twilioDisplayer) FieldTemplates() []format.Field { - return []format.Field{ + fields := []format.Field{ { Name: "Integration Name", - ValueFormat: "{{ .IntegrationName }}", - }, - { - Name: "Account SID", - ValueFormat: "{{ .TwilioAccountSid }}", + ValueFormat: "{{ .Name }}", }, } + + if t.single { + return append(fields, []format.Field{ + { + Name: "Account SID", + ValueFormat: "{{ .StaticCredentialDetails.AccountSid }}", + }, + { + Name: "API Key SID", + ValueFormat: "{{ .StaticCredentialDetails.APIKeySid }}", + }, + }...) + } else { + return fields + } } type mongodbDisplayer struct { @@ -93,14 +104,185 @@ func (m *mongodbDisplayer) previewMongoDBIntegrationsPayload() any { } func (m *mongodbDisplayer) FieldTemplates() []format.Field { - return []format.Field{ + fields := []format.Field{ { Name: "Integration Name", - ValueFormat: "{{ .IntegrationName }}", + ValueFormat: "{{ .Name }}", }, + } + + if m.single { + return append(fields, []format.Field{ + { + Name: "API Public Key", + ValueFormat: "{{ .StaticCredentialDetails.APIPublicKey }}", + }, + }...) + } else { + return fields + } +} + +type awsDisplayer struct { + previewAwsIntegrations []*preview_models.Secrets20231128AwsIntegration + + single bool +} + +func newAwsDisplayer(single bool, integrations ...*preview_models.Secrets20231128AwsIntegration) *awsDisplayer { + return &awsDisplayer{ + previewAwsIntegrations: integrations, + single: single, + } +} + +func (a *awsDisplayer) DefaultFormat() format.Format { + return format.Table +} + +func (a *awsDisplayer) Payload() any { + + if a.previewAwsIntegrations != nil { + return a.previewAwsIntegrationsPayload() + } + + return nil +} + +func (a *awsDisplayer) previewAwsIntegrationsPayload() any { + if a.single { + if len(a.previewAwsIntegrations) != 1 { + return nil + } + return a.previewAwsIntegrations[0] + } + return a.previewAwsIntegrations +} + +func (a *awsDisplayer) FieldTemplates() []format.Field { + fields := []format.Field{ { - Name: "API Public Key", - ValueFormat: "{{ .MongodbAPIPublicKey }}", + Name: "Integration Name", + ValueFormat: "{{ .Name }}", + }, + } + + if a.single { + return append(fields, []format.Field{ + { + Name: "Audience", + ValueFormat: "{{ .FederatedWorkloadIdentity.Audience }}", + }, + { + Name: "Role ARN", + ValueFormat: "{{ .FederatedWorkloadIdentity.RoleArn }}", + }, + }...) + } else { + return fields + } +} + +type gcpDisplayer struct { + previewGcpIntegrations []*preview_models.Secrets20231128GcpIntegration + + single bool +} + +func newGcpDisplayer(single bool, integrations ...*preview_models.Secrets20231128GcpIntegration) *gcpDisplayer { + return &gcpDisplayer{ + previewGcpIntegrations: integrations, + single: single, + } +} + +func (g *gcpDisplayer) DefaultFormat() format.Format { + return format.Table +} + +func (g *gcpDisplayer) Payload() any { + + if g.previewGcpIntegrations != nil { + return g.previewGcpIntegrationsPayload() + } + + return nil +} + +func (g *gcpDisplayer) previewGcpIntegrationsPayload() any { + if g.single { + if len(g.previewGcpIntegrations) != 1 { + return nil + } + return g.previewGcpIntegrations[0] + } + return g.previewGcpIntegrations +} + +func (g *gcpDisplayer) FieldTemplates() []format.Field { + fields := []format.Field{ + { + Name: "Integration Name", + ValueFormat: "{{ .Name }}", + }, + } + + if g.single { + return append(fields, []format.Field{ + { + Name: "Audience", + ValueFormat: "{{ .FederatedWorkloadIdentity.Audience }}", + }, + { + Name: "Audience", + ValueFormat: "{{ .FederatedWorkloadIdentity.ServiceAccountEmail }}", + }, + }...) + } else { + return fields + } +} + +type genericDisplayer struct { + integrations []*preview_models.Secrets20231128Integration + + single bool +} + +func newGenericDisplayer(single bool, integrations ...*preview_models.Secrets20231128Integration) *genericDisplayer { + return &genericDisplayer{ + integrations: integrations, + single: single, + } +} + +func (g *genericDisplayer) DefaultFormat() format.Format { + return format.Table +} + +func (g *genericDisplayer) Payload() any { + if g.integrations != nil { + return g.integrationsPayload() + } + + return nil +} + +func (g *genericDisplayer) integrationsPayload() any { + if g.single { + if len(g.integrations) != 1 { + return nil + } + return g.integrations[0] + } + return g.integrations +} + +func (g *genericDisplayer) FieldTemplates() []format.Field { + return []format.Field{ + { + Name: "Integration Name", + ValueFormat: "{{ .Name }}", }, } } diff --git a/internal/commands/vaultsecrets/integrations/integrations.go b/internal/commands/vaultsecrets/integrations/integrations.go index 5d5544b0..74b98bd6 100644 --- a/internal/commands/vaultsecrets/integrations/integrations.go +++ b/internal/commands/vaultsecrets/integrations/integrations.go @@ -32,5 +32,6 @@ func NewCmdIntegrations(ctx *cmd.Context) *cmd.Command { cmd.AddChild(NewCmdRead(ctx, nil)) cmd.AddChild(NewCmdDelete(ctx, nil)) + cmd.AddChild(NewCmdList(ctx, nil)) return cmd } diff --git a/internal/commands/vaultsecrets/integrations/list.go b/internal/commands/vaultsecrets/integrations/list.go new file mode 100644 index 00000000..0cafb639 --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/list.go @@ -0,0 +1,213 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "fmt" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +type ListOpts struct { + Ctx context.Context + Profile *profile.Profile + Output *format.Outputter + IO iostreams.IOStreams + + Type IntegrationType + Client secret_service.ClientService + PreviewClient preview_secret_service.ClientService +} + +func NewCmdList(ctx *cmd.Context, runF func(*ListOpts) error) *cmd.Command { + opts := &ListOpts{ + Ctx: ctx.ShutdownCtx, + Profile: ctx.Profile, + IO: ctx.IO, + Output: ctx.Output, + PreviewClient: preview_secret_service.New(ctx.HCP, nil), + Client: secret_service.New(ctx.HCP, nil), + } + + cmd := &cmd.Command{ + Name: "list", + ShortHelp: "List Vault Secrets integrations.", + LongHelp: heredoc.New(ctx.IO).Must(` + The {{ template "mdCodeOrBold" "hcp vault-secrets integrations list" }} command lists Vault Secrets generic integrations. + `), + Examples: []cmd.Example{ + { + Preamble: `List twilio integrations:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets integrations list --type "twilio" + `), + }, + { + Preamble: `List all generic integrations:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets integrations list + `), + }, + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "type", + DisplayValue: "TYPE", + Description: "The optional type of integration to list.", + Value: flagvalue.Simple("", &opts.Type), + }, + }, + }, + RunF: func(c *cmd.Command, args []string) error { + if runF != nil { + return runF(opts) + } + return listRun(opts) + }, + } + return cmd +} + +func listRun(opts *ListOpts) error { + if opts.Type == "" { + var integrations []*preview_models.Secrets20231128Integration + params := &preview_secret_service.ListIntegrationsParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + } + for { + resp, err := opts.PreviewClient.ListIntegrations(params, nil) + if err != nil { + return fmt.Errorf("failed to list integrations: %w", err) + } + + integrations = append(integrations, resp.Payload.Integrations...) + if resp.Payload.Pagination == nil || resp.Payload.Pagination.NextPageToken == "" { + break + } + + next := resp.Payload.Pagination.NextPageToken + params.PaginationNextPageToken = &next + } + return opts.Output.Display(newGenericDisplayer(false, integrations...)) + + } + + switch opts.Type { + case Twilio: + var integrations []*preview_models.Secrets20231128TwilioIntegration + + params := &preview_secret_service.ListTwilioIntegrationsParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + } + + for { + resp, err := opts.PreviewClient.ListTwilioIntegrations(params, nil) + if err != nil { + return fmt.Errorf("failed to list twilio integrations: %w", err) + } + + integrations = append(integrations, resp.Payload.Integrations...) + if resp.Payload.Pagination == nil || resp.Payload.Pagination.NextPageToken == "" { + break + } + + next := resp.Payload.Pagination.NextPageToken + params.PaginationNextPageToken = &next + } + + return opts.Output.Display(newTwilioDisplayer(false, integrations...)) + + case MongoDBAtlas: + var integrations []*preview_models.Secrets20231128MongoDBAtlasIntegration + + params := &preview_secret_service.ListMongoDBAtlasIntegrationsParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + } + + for { + resp, err := opts.PreviewClient.ListMongoDBAtlasIntegrations(params, nil) + if err != nil { + return fmt.Errorf("failed to list mongo integrations: %w", err) + } + + integrations = append(integrations, resp.Payload.Integrations...) + if resp.Payload.Pagination == nil || resp.Payload.Pagination.NextPageToken == "" { + break + } + + next := resp.Payload.Pagination.NextPageToken + params.PaginationNextPageToken = &next + } + return opts.Output.Display(newMongoDBDisplayer(false, integrations...)) + + case AWS: + var integrations []*preview_models.Secrets20231128AwsIntegration + + params := &preview_secret_service.ListAwsIntegrationsParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + } + + for { + resp, err := opts.PreviewClient.ListAwsIntegrations(params, nil) + if err != nil { + return fmt.Errorf("failed to list AWS integrations: %w", err) + } + + integrations = append(integrations, resp.Payload.Integrations...) + if resp.Payload.Pagination == nil || resp.Payload.Pagination.NextPageToken == "" { + break + } + + next := resp.Payload.Pagination.NextPageToken + params.PaginationNextPageToken = &next + } + return opts.Output.Display(newAwsDisplayer(false, integrations...)) + + case GCP: + var integrations []*preview_models.Secrets20231128GcpIntegration + + params := &preview_secret_service.ListGcpIntegrationsParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + } + + for { + resp, err := opts.PreviewClient.ListGcpIntegrations(params, nil) + if err != nil { + return fmt.Errorf("failed to list GCP integrations: %w", err) + } + + integrations = append(integrations, resp.Payload.Integrations...) + if resp.Payload.Pagination == nil || resp.Payload.Pagination.NextPageToken == "" { + break + } + + next := resp.Payload.Pagination.NextPageToken + params.PaginationNextPageToken = &next + } + return opts.Output.Display(newGcpDisplayer(false, integrations...)) + + default: + return fmt.Errorf("not a valid integration type") + } +} diff --git a/internal/commands/vaultsecrets/integrations/list_test.go b/internal/commands/vaultsecrets/integrations/list_test.go new file mode 100644 index 00000000..16c082b3 --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/list_test.go @@ -0,0 +1,170 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +func TestNewCmdList(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *ListOpts + }{ + { + Name: "Good", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123").SetProjectID("abc") + }, + Args: []string{"--type=twilio"}, + Expect: &ListOpts{ + Type: "twilio", + }, + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + // Create a context. + io := iostreams.Test() + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var gotOpts *ListOpts + listCmd := NewCmdList(ctx, func(o *ListOpts) error { + gotOpts = o + return nil + }) + listCmd.SetIO(io) + + code := listCmd.Run(c.Args) + if c.Error != "" { + r.NotZero(code) + r.Contains(io.Error.String(), c.Error) + return + } + + r.Zero(code, io.Error.String()) + r.NotNil(gotOpts) + }) + } +} + +func TestListRun(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + RespErr bool + ErrMsg string + }{ + { + Name: "Success: List integrations", + }, + { + Name: "Failed: Unable to list integrations", + RespErr: true, + ErrMsg: "[GET /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/integrations][404] ListIntegrations", + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + io := iostreams.Test() + io.ErrorTTY = true + vs := mock_preview_secret_service.NewMockClientService(t) + + opts := &ListOpts{ + Ctx: context.Background(), + IO: io, + Profile: profile.TestProfile(t).SetOrgID("123").SetProjectID("abc"), + Output: format.New(io), + PreviewClient: vs, + Type: "twilio", + } + + if c.RespErr { + vs.EXPECT().ListTwilioIntegrations(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + paginationNextPageToken := "token" + vs.EXPECT().ListTwilioIntegrations(&preview_secret_service.ListTwilioIntegrationsParams{ + OrganizationID: "123", + ProjectID: "abc", + Context: opts.Ctx, + }, mock.Anything).Return(&preview_secret_service.ListTwilioIntegrationsOK{ + Payload: &preview_models.Secrets20231128ListTwilioIntegrationsResponse{ + Integrations: getIntegrations(0, 10), + Pagination: &preview_models.CommonPaginationResponse{ + NextPageToken: paginationNextPageToken, + }, + }, + }, nil).Once() + + vs.EXPECT().ListTwilioIntegrations(&preview_secret_service.ListTwilioIntegrationsParams{ + OrganizationID: "123", + ProjectID: "abc", + Context: opts.Ctx, + PaginationNextPageToken: &paginationNextPageToken, + }, mock.Anything).Return(&preview_secret_service.ListTwilioIntegrationsOK{ + Payload: &preview_models.Secrets20231128ListTwilioIntegrationsResponse{ + Integrations: getIntegrations(10, 5), + }, + }, nil).Once() + } + + // Run the command + err := listRun(opts) + if c.ErrMsg != "" { + r.Contains(err.Error(), c.ErrMsg) + return + } + + r.NoError(err) + r.NotNil(io.Error.String()) + }) + } +} + +func getIntegrations(start, limit int) []*preview_models.Secrets20231128TwilioIntegration { + var secrets []*preview_models.Secrets20231128TwilioIntegration + for i := start; i < (start + limit); i++ { + secrets = append(secrets, &preview_models.Secrets20231128TwilioIntegration{ + Name: fmt.Sprint("test_app_", i), + }) + } + return secrets +} diff --git a/internal/commands/vaultsecrets/integrations/read_test.go b/internal/commands/vaultsecrets/integrations/read_test.go index e4f427b7..7c2d1859 100644 --- a/internal/commands/vaultsecrets/integrations/read_test.go +++ b/internal/commands/vaultsecrets/integrations/read_test.go @@ -143,6 +143,11 @@ func TestReadRun(t *testing.T) { Payload: &preview_models.Secrets20231128GetTwilioIntegrationResponse{ Integration: &preview_models.Secrets20231128TwilioIntegration{ IntegrationName: opts.IntegrationName, + Name: opts.IntegrationName, + StaticCredentialDetails: &preview_models.Secrets20231128TwilioStaticCredentialsResponse{ + AccountSid: "account_sid", + APIKeySid: "api_key_sid", + }, }, }, }, nil).Once() @@ -156,7 +161,7 @@ func TestReadRun(t *testing.T) { } r.NoError(err) - r.Contains(io.Output.String(), fmt.Sprintf("Integration Name Account SID\n%s \n", opts.IntegrationName)) + r.Contains(io.Output.String(), fmt.Sprintf("Integration Name Account SID API Key SID\n%s account_sid api_key_sid\n", opts.IntegrationName)) }) } } From 5b87a9faa12e1f76f1d40bf6e4c7d4380c58b9c9 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 22 Aug 2024 15:37:01 -0500 Subject: [PATCH 15/75] create integrations --- .../vaultsecrets/integrations/create.go | 240 ++++++++++++++++++ .../vaultsecrets/integrations/create_test.go | 201 +++++++++++++++ .../vaultsecrets/integrations/integrations.go | 1 + 3 files changed, 442 insertions(+) create mode 100644 internal/commands/vaultsecrets/integrations/create.go create mode 100644 internal/commands/vaultsecrets/integrations/create_test.go diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go new file mode 100644 index 00000000..25e53d07 --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -0,0 +1,240 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "fmt" + "os" + "slices" + + "golang.org/x/exp/maps" + "gopkg.in/yaml.v3" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +type CreateOpts struct { + Ctx context.Context + Profile *profile.Profile + IO iostreams.IOStreams + Output *format.Outputter + + IntegrationName string + ConfigFilePath string + PreviewClient preview_secret_service.ClientService + Client secret_service.ClientService +} + +func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { + opts := &CreateOpts{ + Ctx: ctx.ShutdownCtx, + Profile: ctx.Profile, + IO: ctx.IO, + Output: ctx.Output, + + PreviewClient: preview_secret_service.New(ctx.HCP, nil), + Client: secret_service.New(ctx.HCP, nil), + } + + cmd := &cmd.Command{ + Name: "create", + ShortHelp: "Create a new integration.", + LongHelp: heredoc.New(ctx.IO).Must(` + The {{ template "mdCodeOrBold" "hcp vault-secrets integrations create" }} command creates a new Vault Secrets integration. + `), + Examples: []cmd.Example{ + { + Preamble: `Create a new Vault Secrets integration:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets integrations create sample-integration --config-file=path-to-file/config.yaml + `), + }, + }, + Args: cmd.PositionalArguments{ + Args: []cmd.PositionalArgument{ + { + Name: "NAME", + Documentation: "The name of the integration to create.", + }, + }, + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "config-file", + DisplayValue: "CONFIG_FILE", + Description: "File path to read integration config data.", + Value: flagvalue.Simple("", &opts.ConfigFilePath), + Required: true, + }, + }, + }, + RunF: func(c *cmd.Command, args []string) error { + opts.IntegrationName = args[0] + + if runF != nil { + return runF(opts) + } + return createRun(opts) + }, + } + + return cmd +} + +type IntegrationConfig struct { + Version string + Type IntegrationType + Details map[string]string +} + +var ( + TwilioKeys = []string{"account_sid", "api_key_secret", "api_key_sid"} + MongoKeys = []string{"private_key", "public_key"} + AWSKeys = []string{"audience", "role_arn"} + GCPKeys = []string{"audience", "service_account_email"} +) + +func createRun(opts *CreateOpts) error { + // Open the file + f, err := os.ReadFile(opts.ConfigFilePath) + if err != nil { + return fmt.Errorf("failed to open config file: %w", err) + } + + var i IntegrationConfig + err = yaml.Unmarshal(f, &i) + if err != nil { + return fmt.Errorf("failed to unmarshal config file: %w", err) + } + + switch i.Type { + case Twilio: + missingFields := validateDetails(i.Details, TwilioKeys) + + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + } + + body := &preview_models.SecretServiceCreateTwilioIntegrationBody{ + Name: opts.IntegrationName, + TwilioAccountSid: i.Details[TwilioKeys[0]], + TwilioAPIKeySecret: i.Details[TwilioKeys[1]], + TwilioAPIKeySid: i.Details[TwilioKeys[2]], + } + + _, err := opts.PreviewClient.CreateTwilioIntegration(&preview_secret_service.CreateTwilioIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Body: body, + }, nil) + + if err != nil { + return fmt.Errorf("failed to create Twilio integration: %w", err) + } + + case MongoDBAtlas: + missingFields := validateDetails(i.Details, MongoKeys) + + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + } + + body := &preview_models.SecretServiceCreateMongoDBAtlasIntegrationBody{ + Name: opts.IntegrationName, + MongodbAPIPrivateKey: i.Details[MongoKeys[0]], + MongodbAPIPublicKey: i.Details[MongoKeys[1]], + } + + _, err := opts.PreviewClient.CreateMongoDBAtlasIntegration(&preview_secret_service.CreateMongoDBAtlasIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Body: body, + }, nil) + + if err != nil { + return fmt.Errorf("failed to create MongoDB Atlas integration: %w", err) + } + + case AWS: + missingFields := validateDetails(i.Details, AWSKeys) + + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + } + + body := &preview_models.SecretServiceCreateAwsIntegrationBody{ + Name: opts.IntegrationName, + FederatedWorkloadIdentity: &preview_models.Secrets20231128AwsFederatedWorkloadIdentityRequest{ + Audience: i.Details[AWSKeys[0]], + RoleArn: i.Details[AWSKeys[1]], + }, + } + + _, err := opts.PreviewClient.CreateAwsIntegration(&preview_secret_service.CreateAwsIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Body: body, + }, nil) + + if err != nil { + return fmt.Errorf("failed to create AWS integration: %w", err) + } + + case GCP: + missingFields := validateDetails(i.Details, GCPKeys) + + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + } + + body := &preview_models.SecretServiceCreateGcpIntegrationBody{ + Name: opts.IntegrationName, + FederatedWorkloadIdentity: &preview_models.Secrets20231128GcpFederatedWorkloadIdentityRequest{ + Audience: i.Details[GCPKeys[0]], + ServiceAccountEmail: i.Details[GCPKeys[1]], + }, + } + + _, err := opts.PreviewClient.CreateGcpIntegration(&preview_secret_service.CreateGcpIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Body: body, + }, nil) + + if err != nil { + return fmt.Errorf("failed to create GCP integration: %w", err) + } + } + + fmt.Fprintln(opts.IO.Err()) + fmt.Fprintf(opts.IO.Err(), "%s Successfully created integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) + + return nil +} + +func validateDetails(details map[string]string, requiredKeys []string) []string { + detailsKeys := maps.Keys(details) + var missingKeys []string + + for _, r := range requiredKeys { + if !slices.Contains(detailsKeys, r) { + missingKeys = append(missingKeys, r) + } + } + return missingKeys +} diff --git a/internal/commands/vaultsecrets/integrations/create_test.go b/internal/commands/vaultsecrets/integrations/create_test.go new file mode 100644 index 00000000..67bb64a2 --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/create_test.go @@ -0,0 +1,201 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/stretchr/testify/require" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +func TestNewCmdCreate(t *testing.T) { + t.Parallel() + + testProfile := func(t *testing.T) *profile.Profile { + tp := profile.TestProfile(t).SetOrgID("123").SetProjectID("456") + return tp + } + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *CreateOpts + }{ + { + Name: "Good", + Profile: testProfile, + Args: []string{"sample-integration", "--config-file", "path/to/file"}, + Expect: &CreateOpts{ + IntegrationName: "sample-integration", + ConfigFilePath: "path/to/file", + }, + }, + { + Name: "Failed: No secret name arg specified", + Profile: testProfile, + Args: []string{"--config-file", "path/to/file"}, + Error: "ERROR: accepts 1 arg(s), received 0", + }, + { + Name: "Failed: No config file flag specified", + Profile: testProfile, + Args: []string{"sample-integration"}, + Error: "ERROR: missing required flag: --config-file=CONFIG_FILE", + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + // Create a context. + io := iostreams.Test() + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var gotOpts *CreateOpts + createCmd := NewCmdCreate(ctx, func(o *CreateOpts) error { + gotOpts = o + return nil + }) + createCmd.SetIO(io) + + code := createCmd.Run(c.Args) + if c.Error != "" { + r.NotZero(code) + r.Contains(io.Error.String(), c.Error) + return + } + + r.Zero(code, io.Error.String()) + r.NotNil(gotOpts) + r.Equal(c.Expect.IntegrationName, gotOpts.IntegrationName) + r.Equal(c.Expect.ConfigFilePath, gotOpts.ConfigFilePath) + }) + } +} + +func TestCreateRun(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + IntegrationName string + Input []byte + Error string + }{ + { + Name: "Good", + IntegrationName: "sample-integration", + Input: []byte(`version: 1.0.0 +type: "aws" +details: + audience: abc + role_arn: def`), + }, + { + Name: "Missing a single required field", + IntegrationName: "sample-integration", + Input: []byte(`version: 1.0.0 +type: "mongodb-atlas" +details: + public_key: abc`), + Error: "missing required field(s) in the config file: [private_key]", + }, + { + Name: "Missing multiple required fields", + IntegrationName: "sample-integration", + Input: []byte(`version: 1.0.0 +type: "twilio" +details: + api_key_sid: ghi`), + Error: "missing required field(s) in the config file: [account_sid api_key_secret]", + }, + } + + for _, c := range cases { + + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + tempDir := t.TempDir() + f, err := os.Create(filepath.Join(tempDir, "config.yaml")) + r.NoError(err) + _, err = f.Write(c.Input) + r.NoError(err) + + io := iostreams.Test() + vs := mock_preview_secret_service.NewMockClientService(t) + + opts := &CreateOpts{ + Ctx: context.Background(), + Profile: profile.TestProfile(t).SetOrgID("123").SetProjectID("abc"), + IO: io, + PreviewClient: vs, + Output: format.New(io), + IntegrationName: c.IntegrationName, + ConfigFilePath: f.Name(), + } + + if c.Error == "" { + vs.EXPECT().CreateAwsIntegration(&preview_secret_service.CreateAwsIntegrationParams{ + Context: opts.Ctx, + OrganizationID: "123", + ProjectID: "abc", + Body: &preview_models.SecretServiceCreateAwsIntegrationBody{ + Name: opts.IntegrationName, + FederatedWorkloadIdentity: &preview_models.Secrets20231128AwsFederatedWorkloadIdentityRequest{ + Audience: "abc", + RoleArn: "def", + }, + }, + }, nil).Return(&preview_secret_service.CreateAwsIntegrationOK{ + Payload: &preview_models.Secrets20231128CreateAwsIntegrationResponse{ + Integration: &preview_models.Secrets20231128AwsIntegration{ + Name: opts.IntegrationName, + FederatedWorkloadIdentity: &preview_models.Secrets20231128AwsFederatedWorkloadIdentityResponse{ + Audience: "abc", + RoleArn: "def", + }, + }, + }, + }, nil).Once() + } + + // Run the command + err = createRun(opts) + if c.Error != "" { + r.ErrorContains(err, c.Error) + return + } + + r.NoError(err) + r.Contains(io.Error.String(), fmt.Sprintf("✓ Successfully created integration with name %q\n", opts.IntegrationName)) + }) + } +} diff --git a/internal/commands/vaultsecrets/integrations/integrations.go b/internal/commands/vaultsecrets/integrations/integrations.go index 74b98bd6..67b1bb2f 100644 --- a/internal/commands/vaultsecrets/integrations/integrations.go +++ b/internal/commands/vaultsecrets/integrations/integrations.go @@ -33,5 +33,6 @@ func NewCmdIntegrations(ctx *cmd.Context) *cmd.Command { cmd.AddChild(NewCmdRead(ctx, nil)) cmd.AddChild(NewCmdDelete(ctx, nil)) cmd.AddChild(NewCmdList(ctx, nil)) + cmd.AddChild(NewCmdCreate(ctx, nil)) return cmd } From dd641742c08da97ddb10fd41cfba877fd577bbb4 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 22 Aug 2024 15:50:38 -0500 Subject: [PATCH 16/75] rotate secrets --- .../commands/vaultsecrets/secrets/rotate.go | 102 ++++++++++ .../vaultsecrets/secrets/rotate_test.go | 181 ++++++++++++++++++ .../commands/vaultsecrets/secrets/secrets.go | 1 + 3 files changed, 284 insertions(+) create mode 100644 internal/commands/vaultsecrets/secrets/rotate.go create mode 100644 internal/commands/vaultsecrets/secrets/rotate_test.go diff --git a/internal/commands/vaultsecrets/secrets/rotate.go b/internal/commands/vaultsecrets/secrets/rotate.go new file mode 100644 index 00000000..ad8b0615 --- /dev/null +++ b/internal/commands/vaultsecrets/secrets/rotate.go @@ -0,0 +1,102 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package secrets + +import ( + "context" + "fmt" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets/appname" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets/helper" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +type RotateOpts struct { + Ctx context.Context + Profile *profile.Profile + Output *format.Outputter + IO iostreams.IOStreams + + AppName string + SecretName string + Client secret_service.ClientService + PreviewClient preview_secret_service.ClientService +} + +func NewCmdRotate(ctx *cmd.Context, runF func(*RotateOpts) error) *cmd.Command { + opts := &RotateOpts{ + Ctx: ctx.ShutdownCtx, + Profile: ctx.Profile, + IO: ctx.IO, + Output: ctx.Output, + PreviewClient: preview_secret_service.New(ctx.HCP, nil), + Client: secret_service.New(ctx.HCP, nil), + } + + cmd := &cmd.Command{ + Name: "rotate", + ShortHelp: "Rotate a rotating secret.", + LongHelp: heredoc.New(ctx.IO).Must(` + The {{ template "mdCodeOrBold" "hcp vault-secrets secrets rotate" }} command rotates a rotating secret from the Vault Secrets application. + `), + Examples: []cmd.Example{ + { + Preamble: `Rotate a secret:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets secret rotate "test_secret" + `), + }, + { + Preamble: `Rotate a secret under the specified Vault Secrets application:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets secret rotate "test_secret" --app test-app + `), + }, + }, + Args: cmd.PositionalArguments{ + Args: []cmd.PositionalArgument{ + { + Name: "NAME", + Documentation: "The name of the secret to rotate.", + }, + }, + }, + RunF: func(c *cmd.Command, args []string) error { + opts.AppName = appname.Get() + opts.SecretName = args[0] + + if runF != nil { + return runF(opts) + } + return rotateRun(opts) + }, + } + cmd.Args.Autocomplete = helper.PredictSecretName(ctx, cmd, opts.PreviewClient) + + return cmd +} + +func rotateRun(opts *RotateOpts) error { + params := &preview_secret_service.RotateSecretParams{ + Context: opts.Ctx, + OrganizationID: opts.Profile.OrganizationID, + ProjectID: opts.Profile.ProjectID, + AppName: opts.AppName, + SecretName: opts.SecretName, + } + + _, err := opts.PreviewClient.RotateSecret(params, nil) + if err != nil { + return fmt.Errorf("failed to rotate the secret %q: %w", opts.SecretName, err) + } + + fmt.Fprintf(opts.IO.Err(), "%s Successfully scheduled rotation of secret with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.SecretName) + return nil +} diff --git a/internal/commands/vaultsecrets/secrets/rotate_test.go b/internal/commands/vaultsecrets/secrets/rotate_test.go new file mode 100644 index 00000000..92f7480b --- /dev/null +++ b/internal/commands/vaultsecrets/secrets/rotate_test.go @@ -0,0 +1,181 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package secrets + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +func TestNewCmdRotate(t *testing.T) { + t.Parallel() + + testSecretName := "test_secret" + testProfile := func(t *testing.T) *profile.Profile { + tp := profile.TestProfile(t).SetOrgID("123").SetProjectID("456") + tp.VaultSecrets = &profile.VaultSecretsConf{ + AppName: "test-app", + } + return tp + } + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *RotateOpts + }{ + { + Name: "No args", + Profile: testProfile, + Args: []string{}, + Error: "accepts 1 arg(s), received 0", + }, + { + Name: "Too many args", + Profile: testProfile, + Args: []string{"foo", "bar"}, + Error: "accepts 1 arg(s), received 2", + }, + { + Name: "Good", + Profile: testProfile, + Args: []string{"foo"}, + Expect: &RotateOpts{ + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: testSecretName, + }, + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + // Create a context. + io := iostreams.Test() + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var gotOpts *RotateOpts + rotateCmd := NewCmdRotate(ctx, func(o *RotateOpts) error { + gotOpts = o + gotOpts.AppName = c.Profile(t).VaultSecrets.AppName + gotOpts.SecretName = testSecretName + return nil + }) + rotateCmd.SetIO(io) + + code := rotateCmd.Run(c.Args) + if c.Error != "" { + r.NotZero(code) + r.Contains(io.Error.String(), c.Error) + return + } + + r.Zero(code, io.Error.String()) + r.NotNil(gotOpts) + r.Equal(c.Expect.AppName, gotOpts.AppName) + r.Equal(c.Expect.SecretName, gotOpts.SecretName) + }) + } +} + +func TestRotateRun(t *testing.T) { + t.Parallel() + + testProfile := func(t *testing.T) *profile.Profile { + tp := profile.TestProfile(t).SetOrgID("123").SetProjectID("456") + tp.VaultSecrets = &profile.VaultSecretsConf{ + AppName: "test-app", + } + return tp + } + testSecretName := "test_secret" + + cases := []struct { + Name string + RespErr bool + ErrMsg string + MockCalled bool + }{ + { + Name: "Failed: Secret not found", + RespErr: true, + ErrMsg: "[POST] /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/apps/{app_name}/secrets/{secret_name}:rotate][404] RotateSecret}", + MockCalled: true, + }, + { + Name: "Success: Rotate secret", + RespErr: false, + MockCalled: true, + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + io := iostreams.Test() + io.ErrorTTY = true + vs := mock_preview_secret_service.NewMockClientService(t) + opts := &RotateOpts{ + Ctx: context.Background(), + IO: io, + Profile: testProfile(t), + Output: format.New(io), + PreviewClient: vs, + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: testSecretName, + } + + if c.MockCalled { + if c.RespErr { + vs.EXPECT().RotateSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + vs.EXPECT().RotateSecret(&preview_secret_service.RotateSecretParams{ + OrganizationID: testProfile(t).OrganizationID, + ProjectID: testProfile(t).ProjectID, + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: opts.SecretName, + Context: opts.Ctx, + }, mock.Anything).Return(&preview_secret_service.RotateSecretOK{}, nil).Once() + } + } + + // Run the command + err := rotateRun(opts) + if c.ErrMsg != "" { + r.Contains(err.Error(), c.ErrMsg) + return + } + + r.NoError(err) + r.Equal(io.Error.String(), fmt.Sprintf("✓ Successfully scheduled rotation of secret with name %q\n", opts.SecretName)) + }) + } +} diff --git a/internal/commands/vaultsecrets/secrets/secrets.go b/internal/commands/vaultsecrets/secrets/secrets.go index 4a5f5fd9..f91e20bf 100644 --- a/internal/commands/vaultsecrets/secrets/secrets.go +++ b/internal/commands/vaultsecrets/secrets/secrets.go @@ -48,6 +48,7 @@ func NewCmdSecrets(ctx *cmd.Context) *cmd.Command { cmd.AddChild(NewCmdDelete(ctx, nil)) cmd.AddChild(NewCmdList(ctx, nil)) cmd.AddChild(NewCmdOpen(ctx, nil)) + cmd.AddChild(NewCmdRotate(ctx, nil)) cmd.AddChild(versions.NewCmdVersions(ctx)) return cmd From c7496daad416415c6cb9f255ffbeda0a0499e072 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 22 Aug 2024 15:56:24 -0500 Subject: [PATCH 17/75] ran go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a4c048fc..a443a1cf 100644 --- a/go.mod +++ b/go.mod @@ -94,5 +94,5 @@ require ( golang.org/x/text v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 ) From 2aeb2148200f917338a18184e5028d5f2cda9cf9 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 7 Aug 2024 12:55:13 -0500 Subject: [PATCH 18/75] Adds support for creating rotating secrets --- .../commands/vaultsecrets/secrets/create.go | 170 ++++++++++++++++-- .../vaultsecrets/secrets/create_test.go | 9 + .../vaultsecrets/secrets/displayer.go | 50 ++++++ 3 files changed, 211 insertions(+), 18 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index ea38f1f4..d5018e07 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -7,10 +7,14 @@ import ( "context" "errors" "fmt" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" + "github.com/mitchellh/mapstructure" + "gopkg.in/yaml.v3" "io" "os" preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets/appname" "github.com/hashicorp/hcp/internal/pkg/cmd" @@ -22,6 +26,14 @@ import ( "github.com/posener/complete" ) +type SecretType string + +const ( + Static SecretType = "static" + Rotating SecretType = "rotating" + Dynamic SecretType = "dynamic" +) + func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { opts := &CreateOpts{ Ctx: ctx.ShutdownCtx, @@ -72,6 +84,12 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { complete.PredictSet("-"), ), }, + { + Name: "secret-type", + DisplayValue: "SECRET_TYPE", + Description: "The type of secret to create: static, rotating, or dynamic.", + Value: flagvalue.Simple("", &opts.Type), + }, }, }, RunF: func(c *cmd.Command, args []string) error { @@ -98,35 +116,151 @@ type CreateOpts struct { SecretName string SecretValuePlaintext string SecretFilePath string + Type SecretType PreviewClient preview_secret_service.ClientService Client secret_service.ClientService } +type SecretConfig struct { + Version string + Type integrations.IntegrationType + RotationIntegrationName string + Details map[string]any +} + +type MongoDBRole struct { + RoleName string `mapstructure:"role_name"` + DatabaseName string `mapstructure:"database_name"` + CollectionName string `mapstructure:"collection_name"` +} + +type MongoDBScope struct { + Name string `mapstructure:"type"` + Type string `mapstructure:"name"` +} + +var ( + TwilioKeys = []string{"rotation_policy_name"} + MongoKeys = []string{"rotation_policy_name", "mongodb_group_id"} +) + func createRun(opts *CreateOpts) error { - if err := readPlainTextSecret(opts); err != nil { - return err - } - req := secret_service.NewCreateAppKVSecretParamsWithContext(opts.Ctx) - req.LocationOrganizationID = opts.Profile.OrganizationID - req.LocationProjectID = opts.Profile.ProjectID - req.AppName = opts.AppName + switch opts.Type { + case Static, "": + if err := readPlainTextSecret(opts); err != nil { + return err + } - req.Body = secret_service.CreateAppKVSecretBody{ - Name: opts.SecretName, - Value: opts.SecretValuePlaintext, - } + req := secret_service.NewCreateAppKVSecretParamsWithContext(opts.Ctx) + req.LocationOrganizationID = opts.Profile.OrganizationID + req.LocationProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName - resp, err := opts.Client.CreateAppKVSecret(req, nil) - if err != nil { - return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) - } + req.Body = secret_service.CreateAppKVSecretBody{ + Name: opts.SecretName, + Value: opts.SecretValuePlaintext, + } + + resp, err := opts.Client.CreateAppKVSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } - if err := opts.Output.Display(newDisplayer().Secrets(resp.Payload.Secret)); err != nil { - return err + if err := opts.Output.Display(newDisplayer().Secrets(resp.Payload.Secret)); err != nil { + return err + } + case Rotating: + f, err := os.ReadFile(opts.SecretFilePath) + if err != nil { + return fmt.Errorf("failed to open config file: %w", err) + } + + var sc SecretConfig + err = yaml.Unmarshal(f, &sc) + if err != nil { + return fmt.Errorf("failed to unmarshal config file: %w", err) + } + + switch sc.Type { + case integrations.Twilio: + req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ + RotationIntegrationName: sc.RotationIntegrationName, + RotationPolicyName: sc.Details["rotation_policy_name"].(string), + SecretName: opts.SecretName, + } + + resp, err := opts.PreviewClient.CreateTwilioRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } + + if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { + return err + } + + case integrations.MongoDBAtlas: + req := preview_secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + + roles := sc.Details["mongodb_roles"].([]interface{}) + var reqRoles []*preview_models.Secrets20231128MongoDBRole + for _, r := range roles { + var role MongoDBRole + decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &role}) + if err := decoder.Decode(r); err != nil { + return fmt.Errorf("unable to decode to a mongodb role") + } + + reqRole := &preview_models.Secrets20231128MongoDBRole{ + CollectionName: role.CollectionName, + RoleName: role.RoleName, + DatabaseName: role.DatabaseName, + } + reqRoles = append(reqRoles, reqRole) + } + + scopes := sc.Details["mongodb_scopes"].([]interface{}) + var reqScopes []*preview_models.Secrets20231128MongoDBScope + for _, r := range scopes { + var scope MongoDBScope + decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &scope}) + if err := decoder.Decode(r); err != nil { + return fmt.Errorf("unable to decode to a mongodb role") + } + + reqScope := &preview_models.Secrets20231128MongoDBScope{ + Name: scope.Name, + Type: scope.Type, + } + reqScopes = append(reqScopes, reqScope) + } + + req.Body = &preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ + MongodbGroupID: sc.Details["mongodb_group_id"].(string), + MongodbRoles: reqRoles, + MongodbScopes: reqScopes, + RotationIntegrationName: sc.RotationIntegrationName, + RotationPolicyName: sc.Details["rotation_policy_name"].(string), + SecretName: opts.SecretName, + } + resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } + + if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { + return err + } + } } - command := fmt.Sprintf(`$ hcp vault-secrets secrets read %s --app %s`, opts.SecretName, req.AppName) + command := fmt.Sprintf(`$ hcp vault-secrets secrets read %s --app %s`, opts.SecretName, opts.AppName) fmt.Fprintln(opts.IO.Err()) fmt.Fprintf(opts.IO.Err(), "%s Successfully created secret with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.SecretName) fmt.Fprintln(opts.IO.Err()) diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index 59599c94..f247fe32 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -56,6 +56,15 @@ func TestNewCmdCreate(t *testing.T) { SecretName: "test", }, }, + { + Name: "Good: Rotating secret", + Profile: testProfile, + Args: []string{"test", "--secret-type=rotating"}, + Expect: &CreateOpts{ + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: "test", + }, + }, } for _, c := range cases { diff --git a/internal/commands/vaultsecrets/secrets/displayer.go b/internal/commands/vaultsecrets/secrets/displayer.go index ad5c6a94..e698c4c1 100644 --- a/internal/commands/vaultsecrets/secrets/displayer.go +++ b/internal/commands/vaultsecrets/secrets/displayer.go @@ -232,3 +232,53 @@ func (d *displayer) openAppSecretsPayload() any { } return d.openAppSecrets } + +type rotatingSecretsDisplayer struct { + previewRotatingSecrets []*preview_models.Secrets20231128RotatingSecretConfig + single bool + + format format.Format +} + +func newRotatingSecretsDisplayer(single bool) *rotatingSecretsDisplayer { + return &rotatingSecretsDisplayer{ + single: single, + format: format.Table, + } +} + +func (r *rotatingSecretsDisplayer) PreviewRotatingSecrets(secrets ...*preview_models.Secrets20231128RotatingSecretConfig) *rotatingSecretsDisplayer { + r.previewRotatingSecrets = secrets + return r +} + +func (r *rotatingSecretsDisplayer) DefaultFormat() format.Format { + return r.format +} + +func (r *rotatingSecretsDisplayer) Payload() any { + if r.single { + if len(r.previewRotatingSecrets) != 1 { + return nil + } + return r.previewRotatingSecrets[0] + } + return r.previewRotatingSecrets +} + +func (r *rotatingSecretsDisplayer) FieldTemplates() []format.Field { + return []format.Field{ + { + Name: "Secret Name", + ValueFormat: "{{ .SecretName }}", + }, + { + Name: " Rotation Integration", + ValueFormat: "{{ . RotationIntegrationName }}", + }, + { + Name: " Rotation Policy", + ValueFormat: "{{ . RotationPolicyName }}", + }, + } +} From 469098600fe0ce12072e4d05ed030cab6f522e5e Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 7 Aug 2024 12:59:22 -0500 Subject: [PATCH 19/75] lints --- internal/commands/vaultsecrets/secrets/create.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index d5018e07..1c5bd1e0 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -7,15 +7,17 @@ import ( "context" "errors" "fmt" - "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" - "github.com/mitchellh/mapstructure" - "gopkg.in/yaml.v3" "io" "os" + "github.com/mitchellh/mapstructure" + "github.com/posener/complete" + "gopkg.in/yaml.v3" + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets/appname" "github.com/hashicorp/hcp/internal/pkg/cmd" "github.com/hashicorp/hcp/internal/pkg/flagvalue" @@ -23,7 +25,6 @@ import ( "github.com/hashicorp/hcp/internal/pkg/heredoc" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" - "github.com/posener/complete" ) type SecretType string From b8e13c92544cc9c6599bec055b0a9070653ba030 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 7 Aug 2024 13:01:18 -0500 Subject: [PATCH 20/75] updates data file to be required --- internal/commands/vaultsecrets/secrets/create.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 1c5bd1e0..b653d1cb 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -80,6 +80,7 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { DisplayValue: "DATA_FILE_PATH", Description: "File path to read secret data from. Set this to '-' to read the secret data from stdin.", Value: flagvalue.Simple("", &opts.SecretFilePath), + Required: true, Autocomplete: complete.PredictOr( complete.PredictFiles("*"), complete.PredictSet("-"), From 94ae1de5961b919929e3f9d75689c493a5e5e86e Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 7 Aug 2024 13:16:12 -0500 Subject: [PATCH 21/75] fixes broken test --- internal/commands/vaultsecrets/secrets/create_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index f247fe32..144c299d 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -45,12 +45,12 @@ func TestNewCmdCreate(t *testing.T) { Name: "Failed: No secret name arg specified", Profile: testProfile, Args: []string{}, - Error: "ERROR: accepts 1 arg(s), received 0", + Error: "ERROR: missing required flag: --data-file=DATA_FILE_PATH", }, { Name: "Good: Secret name arg specified", Profile: testProfile, - Args: []string{"test"}, + Args: []string{"test", "--data-file=DATA_FILE_PATH"}, Expect: &CreateOpts{ AppName: testProfile(t).VaultSecrets.AppName, SecretName: "test", @@ -59,7 +59,7 @@ func TestNewCmdCreate(t *testing.T) { { Name: "Good: Rotating secret", Profile: testProfile, - Args: []string{"test", "--secret-type=rotating"}, + Args: []string{"test", "--secret-type=rotating", "--data-file=DATA_FILE_PATH"}, Expect: &CreateOpts{ AppName: testProfile(t).VaultSecrets.AppName, SecretName: "test", From c5bbc86b6787834468b106f6b36aaa3a811267cd Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 8 Aug 2024 12:41:43 -0500 Subject: [PATCH 22/75] some cleanup --- .../commands/vaultsecrets/secrets/create.go | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index b653d1cb..dd7ea3e7 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -7,8 +7,10 @@ import ( "context" "errors" "fmt" + "golang.org/x/exp/maps" "io" "os" + "slices" "github.com/mitchellh/mapstructure" "github.com/posener/complete" @@ -126,7 +128,7 @@ type CreateOpts struct { type SecretConfig struct { Version string Type integrations.IntegrationType - RotationIntegrationName string + RotationIntegrationName string `yaml:"rotation_integration_name"` Details map[string]any } @@ -146,6 +148,12 @@ var ( MongoKeys = []string{"rotation_policy_name", "mongodb_group_id"} ) +var rotationPolicies = map[string]string{ + "30": "built-in:30-days-2-active", + "60": "built-in:60-days-2-active", + "90": "built-in:90-days-2-active", +} + func createRun(opts *CreateOpts) error { switch opts.Type { @@ -186,13 +194,19 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.Twilio: + missingField := validateDetails(sc.Details, TwilioKeys) + + if missingField != "" { + return fmt.Errorf("missing required field in the config file: %s", missingField) + } + req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ RotationIntegrationName: sc.RotationIntegrationName, - RotationPolicyName: sc.Details["rotation_policy_name"].(string), + RotationPolicyName: rotationPolicies[sc.Details["rotation_policy_name"].(string)], SecretName: opts.SecretName, } @@ -206,6 +220,12 @@ func createRun(opts *CreateOpts) error { } case integrations.MongoDBAtlas: + missingField := validateDetails(sc.Details, MongoKeys) + + if missingField != "" { + return fmt.Errorf("missing required field in the config file: %s", missingField) + } + req := preview_secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID @@ -248,7 +268,7 @@ func createRun(opts *CreateOpts) error { MongodbRoles: reqRoles, MongodbScopes: reqScopes, RotationIntegrationName: sc.RotationIntegrationName, - RotationPolicyName: sc.Details["rotation_policy_name"].(string), + RotationPolicyName: rotationPolicies[sc.Details["rotation_policy_name"].(string)], SecretName: opts.SecretName, } resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) @@ -312,3 +332,14 @@ func readPlainTextSecret(opts *CreateOpts) error { opts.SecretValuePlaintext = string(data) return nil } + +func validateDetails(details map[string]any, requiredKeys []string) string { + detailsKeys := maps.Keys(details) + + for _, r := range requiredKeys { + if !slices.Contains(detailsKeys, r) { + return r + } + } + return "" +} From e30acbdd489abd931633c58b0dd1d1ad84f75f89 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 8 Aug 2024 16:32:57 -0500 Subject: [PATCH 23/75] enhances tests --- .../commands/vaultsecrets/secrets/create.go | 20 +- .../vaultsecrets/secrets/create_test.go | 174 +++++++++++++----- .../vaultsecrets/secrets/displayer.go | 4 +- 3 files changed, 146 insertions(+), 52 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index dd7ea3e7..19d62c61 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -126,10 +126,10 @@ type CreateOpts struct { } type SecretConfig struct { - Version string - Type integrations.IntegrationType - RotationIntegrationName string `yaml:"rotation_integration_name"` - Details map[string]any + Version string + Type integrations.IntegrationType + IntegrationName string `yaml:"rotation_integration_name"` + Details map[string]any } type MongoDBRole struct { @@ -192,6 +192,14 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("failed to unmarshal config file: %w", err) } + if sc.Type == "" || sc.IntegrationName == "" { + return fmt.Errorf("missing required field in the config file: type") + } + + if sc.IntegrationName == "" { + return fmt.Errorf("missing required field in the config file: rotation_integration_name") + } + switch sc.Type { case integrations.Twilio: missingField := validateDetails(sc.Details, TwilioKeys) @@ -205,7 +213,7 @@ func createRun(opts *CreateOpts) error { req.ProjectID = opts.Profile.ProjectID req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ - RotationIntegrationName: sc.RotationIntegrationName, + RotationIntegrationName: sc.IntegrationName, RotationPolicyName: rotationPolicies[sc.Details["rotation_policy_name"].(string)], SecretName: opts.SecretName, } @@ -267,7 +275,7 @@ func createRun(opts *CreateOpts) error { MongodbGroupID: sc.Details["mongodb_group_id"].(string), MongodbRoles: reqRoles, MongodbScopes: reqScopes, - RotationIntegrationName: sc.RotationIntegrationName, + RotationIntegrationName: sc.IntegrationName, RotationPolicyName: rotationPolicies[sc.Details["rotation_policy_name"].(string)], SecretName: opts.SecretName, } diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index 144c299d..f5aa874b 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -7,6 +7,11 @@ import ( "context" "errors" "fmt" + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "os" + "path/filepath" "testing" "github.com/go-openapi/runtime/client" @@ -126,6 +131,7 @@ func TestCreateRun(t *testing.T) { ErrMsg string MockCalled bool AugmentOpts func(*CreateOpts) + Input []byte }{ { Name: "Failed: Read via stdin as hypen not supplied for --data-file flag", @@ -136,27 +142,65 @@ func TestCreateRun(t *testing.T) { EmptySecretValue: true, ReadViaStdin: true, RespErr: true, - AugmentOpts: func(o *CreateOpts) { o.SecretFilePath = "-" }, - ErrMsg: "secret value cannot be empty", + AugmentOpts: func(o *CreateOpts) { + o.SecretFilePath = "-" + o.Type = Static + }, + ErrMsg: "secret value cannot be empty", }, { Name: "Success: Create secret via stdin", ReadViaStdin: true, - AugmentOpts: func(o *CreateOpts) { o.SecretFilePath = "-" }, - MockCalled: true, + AugmentOpts: func(o *CreateOpts) { + o.SecretFilePath = "-" + o.Type = Static + }, + MockCalled: true, + }, + { + Name: "Failed: Max secret versions reached", + RespErr: true, + ErrMsg: "[POST /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/apps/{app_name}/secret/kv][429] CreateAppKVSecret default &{Code:8 Details:[] Message:maximum number of secret versions reached}", + AugmentOpts: func(o *CreateOpts) { + o.SecretValuePlaintext = testSecretValue + o.Type = Static + }, + MockCalled: true, }, { - Name: "Failed: Max secret versions reached", - RespErr: true, - ErrMsg: "[POST /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/apps/{app_name}/secret/kv][429] CreateAppKVSecret default &{Code:8 Details:[] Message:maximum number of secret versions reached}", - AugmentOpts: func(o *CreateOpts) { o.SecretValuePlaintext = testSecretValue }, - MockCalled: true, + Name: "Success: Created secret", + RespErr: false, + AugmentOpts: func(o *CreateOpts) { + o.SecretValuePlaintext = testSecretValue + o.Type = Static + }, + MockCalled: true, }, { - Name: "Success: Created secret", - RespErr: false, - AugmentOpts: func(o *CreateOpts) { o.SecretValuePlaintext = testSecretValue }, - MockCalled: true, + Name: "Success: Create a Twilio rotating secret", + RespErr: false, + AugmentOpts: func(o *CreateOpts) { + o.Type = Rotating + }, + MockCalled: true, + Input: []byte(`version: 1.0.0 +type: "twilio" +rotation_integration_name: "Twil-Int-11" +details: + rotation_policy_name: "60"`), + }, + { + Name: "Failed: Missing required rotating secret field", + RespErr: false, + AugmentOpts: func(o *CreateOpts) { + o.Type = Rotating + }, + Input: []byte(`version: 1.0.0 +type: "twilio" +rotation_integration_name: "Twil-Int-11" +details: + none: "none"`), + ErrMsg: "missing required field in the config file: rotation_policy_name", }, } @@ -177,48 +221,90 @@ func TestCreateRun(t *testing.T) { } } vs := mock_secret_service.NewMockClientService(t) + pvs := mock_preview_secret_service.NewMockClientService(t) + opts := &CreateOpts{ - Ctx: context.Background(), - IO: io, - Profile: testProfile(t), - Output: format.New(io), - Client: vs, - AppName: testProfile(t).VaultSecrets.AppName, - SecretName: "test_secret", + Ctx: context.Background(), + IO: io, + Profile: testProfile(t), + Output: format.New(io), + Client: vs, + PreviewClient: pvs, + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: "test_secret", } if c.AugmentOpts != nil { c.AugmentOpts(opts) } + if opts.Type == Rotating { + tempDir := t.TempDir() + f, err := os.Create(filepath.Join(tempDir, "config.yaml")) + r.NoError(err) + _, err = f.Write(c.Input) + r.NoError(err) + opts.SecretFilePath = f.Name() + } + dt := strfmt.NewDateTime() - if c.MockCalled { - if c.RespErr { - vs.EXPECT().CreateAppKVSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() - } else { - vs.EXPECT().CreateAppKVSecret(&secret_service.CreateAppKVSecretParams{ - LocationOrganizationID: testProfile(t).OrganizationID, - LocationProjectID: testProfile(t).ProjectID, - AppName: testProfile(t).VaultSecrets.AppName, - Body: secret_service.CreateAppKVSecretBody{ - Name: opts.SecretName, - Value: testSecretValue, - }, - Context: opts.Ctx, - }, mock.Anything).Return(&secret_service.CreateAppKVSecretOK{ - Payload: &models.Secrets20230613CreateAppKVSecretResponse{ - Secret: &models.Secrets20230613Secret{ - Name: opts.SecretName, - CreatedAt: dt, - Version: &models.Secrets20230613SecretVersion{ - Version: "2", + if opts.Type == Static { + if c.MockCalled { + if c.RespErr { + vs.EXPECT().CreateAppKVSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + vs.EXPECT().CreateAppKVSecret(&secret_service.CreateAppKVSecretParams{ + LocationOrganizationID: testProfile(t).OrganizationID, + LocationProjectID: testProfile(t).ProjectID, + AppName: testProfile(t).VaultSecrets.AppName, + Body: secret_service.CreateAppKVSecretBody{ + Name: opts.SecretName, + Value: testSecretValue, + }, + Context: opts.Ctx, + }, mock.Anything).Return(&secret_service.CreateAppKVSecretOK{ + Payload: &models.Secrets20230613CreateAppKVSecretResponse{ + Secret: &models.Secrets20230613Secret{ + Name: opts.SecretName, CreatedAt: dt, - Type: "kv", + Version: &models.Secrets20230613SecretVersion{ + Version: "2", + CreatedAt: dt, + Type: "kv", + }, + LatestVersion: "2", + }, + }, + }, nil).Once() + } + } + } else if opts.Type == Rotating { + if c.MockCalled { + if c.RespErr { + pvs.EXPECT().CreateTwilioRotatingSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + pvs.EXPECT().CreateTwilioRotatingSecret(&preview_secret_service.CreateTwilioRotatingSecretParams{ + OrganizationID: testProfile(t).OrganizationID, + ProjectID: testProfile(t).ProjectID, + AppName: testProfile(t).VaultSecrets.AppName, + Body: &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ + SecretName: opts.SecretName, + RotationIntegrationName: "Twil-Int-11", + RotationPolicyName: "built-in:60-days-2-active", + }, + Context: opts.Ctx, + }, mock.Anything).Return(&preview_secret_service.CreateTwilioRotatingSecretOK{ + Payload: &preview_models.Secrets20231128CreateTwilioRotatingSecretResponse{ + Config: &preview_models.Secrets20231128RotatingSecretConfig{ + AppName: opts.AppName, + CreatedAt: dt, + RotationIntegrationName: "Twil-Int-11", + RotationPolicyName: "built-in:60-days-2-active", + SecretName: opts.SecretName, }, - LatestVersion: "2", }, - }, - }, nil).Once() + }, nil).Once() + } } } diff --git a/internal/commands/vaultsecrets/secrets/displayer.go b/internal/commands/vaultsecrets/secrets/displayer.go index e698c4c1..cad9d985 100644 --- a/internal/commands/vaultsecrets/secrets/displayer.go +++ b/internal/commands/vaultsecrets/secrets/displayer.go @@ -274,11 +274,11 @@ func (r *rotatingSecretsDisplayer) FieldTemplates() []format.Field { }, { Name: " Rotation Integration", - ValueFormat: "{{ . RotationIntegrationName }}", + ValueFormat: "{{ .RotationIntegrationName }}", }, { Name: " Rotation Policy", - ValueFormat: "{{ . RotationPolicyName }}", + ValueFormat: "{{ .RotationPolicyName }}", }, } } From 1bb19ab39708f755f34f81ca73b331aa5d33709a Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 8 Aug 2024 16:36:03 -0500 Subject: [PATCH 24/75] lints --- internal/commands/vaultsecrets/secrets/create.go | 2 +- .../commands/vaultsecrets/secrets/create_test.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 19d62c61..00fadb59 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -7,13 +7,13 @@ import ( "context" "errors" "fmt" - "golang.org/x/exp/maps" "io" "os" "slices" "github.com/mitchellh/mapstructure" "github.com/posener/complete" + "golang.org/x/exp/maps" "gopkg.in/yaml.v3" preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index f5aa874b..77235038 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -7,25 +7,25 @@ import ( "context" "errors" "fmt" - preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" - preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" - mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" "os" "path/filepath" "testing" "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" - models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/models" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/models" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" mock_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" - "github.com/hashicorp/hcp/internal/pkg/cmd" "github.com/hashicorp/hcp/internal/pkg/format" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" ) func TestNewCmdCreate(t *testing.T) { From d30bf7b427e7798f8f1ffbba2f68ea82a8377b1f Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 14 Aug 2024 12:41:55 -0500 Subject: [PATCH 25/75] small tweaks --- .../commands/vaultsecrets/secrets/create.go | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 00fadb59..cd9fbc2c 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -202,10 +202,10 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.Twilio: - missingField := validateDetails(sc.Details, TwilioKeys) + missingFields := validateDetails(sc.Details, TwilioKeys) - if missingField != "" { - return fmt.Errorf("missing required field in the config file: %s", missingField) + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) } req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) @@ -214,7 +214,7 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ RotationIntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.Details["rotation_policy_name"].(string)], + RotationPolicyName: rotationPolicies[sc.Details[TwilioKeys[0]].(string)], SecretName: opts.SecretName, } @@ -228,10 +228,10 @@ func createRun(opts *CreateOpts) error { } case integrations.MongoDBAtlas: - missingField := validateDetails(sc.Details, MongoKeys) + missingFields := validateDetails(sc.Details, MongoKeys) - if missingField != "" { - return fmt.Errorf("missing required field in the config file: %s", missingField) + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) } req := preview_secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) @@ -272,11 +272,11 @@ func createRun(opts *CreateOpts) error { } req.Body = &preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ - MongodbGroupID: sc.Details["mongodb_group_id"].(string), + MongodbGroupID: sc.Details[MongoKeys[1]].(string), MongodbRoles: reqRoles, MongodbScopes: reqScopes, RotationIntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.Details["rotation_policy_name"].(string)], + RotationPolicyName: rotationPolicies[sc.Details[MongoKeys[0]].(string)], SecretName: opts.SecretName, } resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) @@ -341,13 +341,14 @@ func readPlainTextSecret(opts *CreateOpts) error { return nil } -func validateDetails(details map[string]any, requiredKeys []string) string { +func validateDetails(details map[string]any, requiredKeys []string) []string { detailsKeys := maps.Keys(details) + var missingKeys []string for _, r := range requiredKeys { if !slices.Contains(detailsKeys, r) { - return r + missingKeys = append(missingKeys, r) } } - return "" + return missingKeys } From c8461ff2ea0f0aa35084e9cb65c8173fb4f9ef5a Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 21 Aug 2024 12:42:37 -0500 Subject: [PATCH 26/75] fixes failing test --- internal/commands/vaultsecrets/secrets/create_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index 77235038..a60eb960 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -200,7 +200,7 @@ type: "twilio" rotation_integration_name: "Twil-Int-11" details: none: "none"`), - ErrMsg: "missing required field in the config file: rotation_policy_name", + ErrMsg: "missing required field(s) in the config file: [rotation_policy_name]", }, } From 9191a046141348a305c33512c4e0ed1083d66694 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 21 Aug 2024 13:03:43 -0500 Subject: [PATCH 27/75] WIP --- internal/commands/vaultsecrets/secrets/create.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index cd9fbc2c..0ae2e66b 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -261,7 +261,7 @@ func createRun(opts *CreateOpts) error { var scope MongoDBScope decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &scope}) if err := decoder.Decode(r); err != nil { - return fmt.Errorf("unable to decode to a mongodb role") + return fmt.Errorf("unable to decode to a mongodb scope") } reqScope := &preview_models.Secrets20231128MongoDBScope{ @@ -272,9 +272,11 @@ func createRun(opts *CreateOpts) error { } req.Body = &preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ - MongodbGroupID: sc.Details[MongoKeys[1]].(string), - MongodbRoles: reqRoles, - MongodbScopes: reqScopes, + SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ + MongodbGroupID: sc.Details[MongoKeys[1]].(string), + MongodbRoles: reqRoles, + MongodbScopes: reqScopes, + }, RotationIntegrationName: sc.IntegrationName, RotationPolicyName: rotationPolicies[sc.Details[MongoKeys[0]].(string)], SecretName: opts.SecretName, From 7900e645863917e6f235eaa9da8164836b0a046b Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 22 Aug 2024 15:13:46 -0500 Subject: [PATCH 28/75] WIP --- go.mod | 2 +- go.sum | 6 ++++-- .../vaultsecrets/integrations/delete.go | 18 ++++++++---------- .../vaultsecrets/integrations/delete_test.go | 9 ++++----- .../commands/vaultsecrets/integrations/read.go | 16 ++++++++-------- .../vaultsecrets/integrations/read_test.go | 11 +++++------ .../secret_service/mock_ClientService.go | 2 +- 7 files changed, 31 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index a443a1cf..e5977b34 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl/v2 v2.19.1 - github.com/hashicorp/hcp-sdk-go v0.115.0 + github.com/hashicorp/hcp-sdk-go v0.108.0 github.com/lithammer/dedent v1.1.0 github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/cli v1.1.5 diff --git a/go.sum b/go.sum index ecb1c70b..9c76eb97 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,10 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= -github.com/hashicorp/hcp-sdk-go v0.115.0 h1:q6viFNFPd4H4cHm/B9KGYvkpkT5ZSBQASh9KR/zYHEI= -github.com/hashicorp/hcp-sdk-go v0.115.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +github.com/hashicorp/hcp-sdk-go v0.104.0 h1:uKAgAzXdjb+JgLgoT0Ta1/N/NEYTyCBHZUrknhJsBsQ= +github.com/hashicorp/hcp-sdk-go v0.104.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +github.com/hashicorp/hcp-sdk-go v0.108.0 h1:MgP7vGTx5A34l9HpktvQSY4nNfwCBpIXRJhJCHCxcyQ= +github.com/hashicorp/hcp-sdk-go v0.108.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= diff --git a/internal/commands/vaultsecrets/integrations/delete.go b/internal/commands/vaultsecrets/integrations/delete.go index cab084e8..c0a136c0 100644 --- a/internal/commands/vaultsecrets/integrations/delete.go +++ b/internal/commands/vaultsecrets/integrations/delete.go @@ -89,11 +89,10 @@ func deleteRun(opts *DeleteOpts) error { switch opts.Type { case Twilio: _, err := opts.PreviewClient.DeleteTwilioIntegration(&preview_secret_service.DeleteTwilioIntegrationParams{ - Context: opts.Ctx, - ProjectID: opts.Profile.ProjectID, - OrganizationID: opts.Profile.OrganizationID, - IntegrationName: opts.IntegrationName, - Name: &opts.IntegrationName, + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Name: opts.IntegrationName, }, nil) if err != nil { return fmt.Errorf("failed to delete integration: %w", err) @@ -104,11 +103,10 @@ func deleteRun(opts *DeleteOpts) error { case MongoDBAtlas: _, err := opts.PreviewClient.DeleteMongoDBAtlasIntegration(&preview_secret_service.DeleteMongoDBAtlasIntegrationParams{ - Context: opts.Ctx, - ProjectID: opts.Profile.ProjectID, - OrganizationID: opts.Profile.OrganizationID, - IntegrationName: opts.IntegrationName, - Name: &opts.IntegrationName, + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Name: opts.IntegrationName, }, nil) if err != nil { return fmt.Errorf("failed to delete integration: %w", err) diff --git a/internal/commands/vaultsecrets/integrations/delete_test.go b/internal/commands/vaultsecrets/integrations/delete_test.go index 830338f3..666d4045 100644 --- a/internal/commands/vaultsecrets/integrations/delete_test.go +++ b/internal/commands/vaultsecrets/integrations/delete_test.go @@ -133,11 +133,10 @@ func TestDeleteRun(t *testing.T) { vs.EXPECT().DeleteTwilioIntegration(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() } else { vs.EXPECT().DeleteTwilioIntegration(&preview_secret_service.DeleteTwilioIntegrationParams{ - OrganizationID: "123", - ProjectID: "abc", - IntegrationName: opts.IntegrationName, - Name: &opts.IntegrationName, - Context: opts.Ctx, + OrganizationID: "123", + ProjectID: "abc", + Name: opts.IntegrationName, + Context: opts.Ctx, }, nil).Return(&preview_secret_service.DeleteTwilioIntegrationOK{}, nil).Once() } diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index 15269996..d5283f93 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -89,10 +89,10 @@ func readRun(opts *ReadOpts) error { switch opts.Type { case Twilio: resp, err := opts.PreviewClient.GetTwilioIntegration(&preview_secret_service.GetTwilioIntegrationParams{ - Context: opts.Ctx, - ProjectID: opts.Profile.ProjectID, - OrganizationID: opts.Profile.OrganizationID, - IntegrationName: opts.IntegrationName, + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Name: opts.IntegrationName, }, nil) if err != nil { return fmt.Errorf("failed to read integration: %w", err) @@ -102,10 +102,10 @@ func readRun(opts *ReadOpts) error { case MongoDBAtlas: resp, err := opts.PreviewClient.GetMongoDBAtlasIntegration(&preview_secret_service.GetMongoDBAtlasIntegrationParams{ - Context: opts.Ctx, - ProjectID: opts.Profile.ProjectID, - OrganizationID: opts.Profile.OrganizationID, - IntegrationName: opts.IntegrationName, + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Name: opts.IntegrationName, }, nil) if err != nil { return fmt.Errorf("failed to read integration: %w", err) diff --git a/internal/commands/vaultsecrets/integrations/read_test.go b/internal/commands/vaultsecrets/integrations/read_test.go index 7c2d1859..2ddc378e 100644 --- a/internal/commands/vaultsecrets/integrations/read_test.go +++ b/internal/commands/vaultsecrets/integrations/read_test.go @@ -135,15 +135,14 @@ func TestReadRun(t *testing.T) { vs.EXPECT().GetTwilioIntegration(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() } else { vs.EXPECT().GetTwilioIntegration(&preview_secret_service.GetTwilioIntegrationParams{ - OrganizationID: "123", - ProjectID: "abc", - IntegrationName: opts.IntegrationName, - Context: opts.Ctx, + OrganizationID: "123", + ProjectID: "abc", + Name: opts.IntegrationName, + Context: opts.Ctx, }, nil).Return(&preview_secret_service.GetTwilioIntegrationOK{ Payload: &preview_models.Secrets20231128GetTwilioIntegrationResponse{ Integration: &preview_models.Secrets20231128TwilioIntegration{ - IntegrationName: opts.IntegrationName, - Name: opts.IntegrationName, + Name: opts.IntegrationName, StaticCredentialDetails: &preview_models.Secrets20231128TwilioStaticCredentialsResponse{ AccountSid: "account_sid", APIKeySid: "api_key_sid", diff --git a/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service/mock_ClientService.go b/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service/mock_ClientService.go index 74f35926..d2e1b0e8 100644 --- a/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service/mock_ClientService.go +++ b/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service/mock_ClientService.go @@ -5543,4 +5543,4 @@ func NewMockClientService(t interface { t.Cleanup(func() { mock.AssertExpectations(t) }) return mock -} +} \ No newline at end of file From 1f7e99e7f3144587dcddf4378c362547c2c75e2d Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 22 Aug 2024 16:47:59 -0500 Subject: [PATCH 29/75] some cleanup --- .../vaultsecrets/integrations/create.go | 18 ++++--- .../commands/vaultsecrets/secrets/create.go | 48 ++++++++++++------- .../vaultsecrets/secrets/create_test.go | 3 +- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index 25e53d07..dbf0a191 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -127,10 +127,12 @@ func createRun(opts *CreateOpts) error { } body := &preview_models.SecretServiceCreateTwilioIntegrationBody{ - Name: opts.IntegrationName, - TwilioAccountSid: i.Details[TwilioKeys[0]], - TwilioAPIKeySecret: i.Details[TwilioKeys[1]], - TwilioAPIKeySid: i.Details[TwilioKeys[2]], + Name: opts.IntegrationName, + StaticCredentialDetails: &preview_models.Secrets20231128TwilioStaticCredentialsRequest{ + AccountSid: i.Details[TwilioKeys[0]], + APIKeySecret: i.Details[TwilioKeys[1]], + APIKeySid: i.Details[TwilioKeys[2]], + }, } _, err := opts.PreviewClient.CreateTwilioIntegration(&preview_secret_service.CreateTwilioIntegrationParams{ @@ -152,9 +154,11 @@ func createRun(opts *CreateOpts) error { } body := &preview_models.SecretServiceCreateMongoDBAtlasIntegrationBody{ - Name: opts.IntegrationName, - MongodbAPIPrivateKey: i.Details[MongoKeys[0]], - MongodbAPIPublicKey: i.Details[MongoKeys[1]], + Name: opts.IntegrationName, + StaticCredentialDetails: &preview_models.Secrets20231128MongoDBAtlasStaticCredentialsRequest{ + APIPrivateKey: i.Details[MongoKeys[0]], + APIPublicKey: i.Details[MongoKeys[1]], + }, } _, err := opts.PreviewClient.CreateMongoDBAtlasIntegration(&preview_secret_service.CreateMongoDBAtlasIntegrationParams{ diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 0ae2e66b..cb66c668 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -125,10 +125,11 @@ type CreateOpts struct { Client secret_service.ClientService } -type SecretConfig struct { +type RotatingSecretConfig struct { Version string Type integrations.IntegrationType IntegrationName string `yaml:"rotation_integration_name"` + PolicyName string `yaml:"rotation_policy_name"` Details map[string]any } @@ -144,8 +145,8 @@ type MongoDBScope struct { } var ( - TwilioKeys = []string{"rotation_policy_name"} - MongoKeys = []string{"rotation_policy_name", "mongodb_group_id"} + // There are no Twilio-specific keys + MongoKeys = []string{"mongodb_group_id", "mongodb_roles"} ) var rotationPolicies = map[string]string{ @@ -186,27 +187,20 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("failed to open config file: %w", err) } - var sc SecretConfig + var sc RotatingSecretConfig err = yaml.Unmarshal(f, &sc) if err != nil { return fmt.Errorf("failed to unmarshal config file: %w", err) } - if sc.Type == "" || sc.IntegrationName == "" { - return fmt.Errorf("missing required field in the config file: type") - } + missingFields := validateRotatingSecretConfig(sc) - if sc.IntegrationName == "" { - return fmt.Errorf("missing required field in the config file: rotation_integration_name") + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) } switch sc.Type { case integrations.Twilio: - missingFields := validateDetails(sc.Details, TwilioKeys) - - if len(missingFields) > 0 { - return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) - } req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID @@ -214,7 +208,7 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ RotationIntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.Details[TwilioKeys[0]].(string)], + RotationPolicyName: rotationPolicies[sc.PolicyName], SecretName: opts.SecretName, } @@ -228,10 +222,10 @@ func createRun(opts *CreateOpts) error { } case integrations.MongoDBAtlas: - missingFields := validateDetails(sc.Details, MongoKeys) + missingDetails := validateDetails(sc.Details, MongoKeys) - if len(missingFields) > 0 { - return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + if len(missingDetails) > 0 { + return fmt.Errorf("missing required detail(s) in the config file: %s", missingDetails) } req := preview_secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) @@ -343,6 +337,24 @@ func readPlainTextSecret(opts *CreateOpts) error { return nil } +func validateRotatingSecretConfig(sc RotatingSecretConfig) []string { + var missingKeys []string + + if sc.Type == "" { + missingKeys = append(missingKeys, "type") + } + + if sc.IntegrationName == "" { + missingKeys = append(missingKeys, "rotation_integration_name") + } + + if sc.PolicyName == "" { + missingKeys = append(missingKeys, "rotation_policy_name") + } + + return missingKeys +} + func validateDetails(details map[string]any, requiredKeys []string) []string { detailsKeys := maps.Keys(details) var missingKeys []string diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index a60eb960..32405289 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -186,8 +186,7 @@ func TestCreateRun(t *testing.T) { Input: []byte(`version: 1.0.0 type: "twilio" rotation_integration_name: "Twil-Int-11" -details: - rotation_policy_name: "60"`), +rotation_policy_name: "60"`), }, { Name: "Failed: Missing required rotating secret field", From 079fa99803f3e3e58a1d45b43680a9955f3a061b Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 22 Aug 2024 16:49:11 -0500 Subject: [PATCH 30/75] go mod tidy --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 9c76eb97..e5b2289b 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,6 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= -github.com/hashicorp/hcp-sdk-go v0.104.0 h1:uKAgAzXdjb+JgLgoT0Ta1/N/NEYTyCBHZUrknhJsBsQ= -github.com/hashicorp/hcp-sdk-go v0.104.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/hashicorp/hcp-sdk-go v0.108.0 h1:MgP7vGTx5A34l9HpktvQSY4nNfwCBpIXRJhJCHCxcyQ= github.com/hashicorp/hcp-sdk-go v0.108.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= From 2eaafed0104d38af0e89272682b32e48a08216fd Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 26 Aug 2024 13:54:22 -0500 Subject: [PATCH 31/75] final comments --- internal/commands/vaultsecrets/secrets/create.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index cb66c668..cf4eeb6c 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -146,7 +146,7 @@ type MongoDBScope struct { var ( // There are no Twilio-specific keys - MongoKeys = []string{"mongodb_group_id", "mongodb_roles"} + MongoDBAtlasRequiredKeys = []string{"mongodb_group_id", "mongodb_roles"} ) var rotationPolicies = map[string]string{ @@ -222,7 +222,7 @@ func createRun(opts *CreateOpts) error { } case integrations.MongoDBAtlas: - missingDetails := validateDetails(sc.Details, MongoKeys) + missingDetails := validateDetails(sc.Details, MongoDBAtlasRequiredKeys) if len(missingDetails) > 0 { return fmt.Errorf("missing required detail(s) in the config file: %s", missingDetails) @@ -267,12 +267,12 @@ func createRun(opts *CreateOpts) error { req.Body = &preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ - MongodbGroupID: sc.Details[MongoKeys[1]].(string), + MongodbGroupID: sc.Details[MongoDBAtlasRequiredKeys[1]].(string), MongodbRoles: reqRoles, MongodbScopes: reqScopes, }, RotationIntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.Details[MongoKeys[0]].(string)], + RotationPolicyName: rotationPolicies[sc.Details[MongoDBAtlasRequiredKeys[0]].(string)], SecretName: opts.SecretName, } resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) From 6e12338c9f118ccdd813f414d5595f9bc4120bef Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Thu, 29 Aug 2024 11:09:22 -0400 Subject: [PATCH 32/75] Replace RotationIntegratioName => IntegratioName in CreateIntegration calls --- go.mod | 2 +- go.sum | 4 ++-- .../commands/vaultsecrets/secrets/create.go | 19 ++++++++----------- .../vaultsecrets/secrets/create_test.go | 16 ++++++++-------- .../vaultsecrets/secrets/displayer.go | 4 ++-- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index e5977b34..4450a14e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl/v2 v2.19.1 - github.com/hashicorp/hcp-sdk-go v0.108.0 + github.com/hashicorp/hcp-sdk-go v0.110.0 github.com/lithammer/dedent v1.1.0 github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/cli v1.1.5 diff --git a/go.sum b/go.sum index e5b2289b..8c976c8c 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= -github.com/hashicorp/hcp-sdk-go v0.108.0 h1:MgP7vGTx5A34l9HpktvQSY4nNfwCBpIXRJhJCHCxcyQ= -github.com/hashicorp/hcp-sdk-go v0.108.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +github.com/hashicorp/hcp-sdk-go v0.110.0 h1:eaDO6XoEb0H+g00Ka3C+ZbRibhwWyA2YNmv48xFCL2w= +github.com/hashicorp/hcp-sdk-go v0.110.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index cf4eeb6c..73779c48 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -144,10 +144,8 @@ type MongoDBScope struct { Type string `mapstructure:"name"` } -var ( - // There are no Twilio-specific keys - MongoDBAtlasRequiredKeys = []string{"mongodb_group_id", "mongodb_roles"} -) +// There are no Twilio-specific keys +var MongoDBAtlasRequiredKeys = []string{"mongodb_group_id", "mongodb_roles"} var rotationPolicies = map[string]string{ "30": "built-in:30-days-2-active", @@ -156,7 +154,6 @@ var rotationPolicies = map[string]string{ } func createRun(opts *CreateOpts) error { - switch opts.Type { case Static, "": if err := readPlainTextSecret(opts); err != nil { @@ -207,9 +204,9 @@ func createRun(opts *CreateOpts) error { req.ProjectID = opts.Profile.ProjectID req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ - RotationIntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.PolicyName], - SecretName: opts.SecretName, + IntegrationName: sc.IntegrationName, + RotationPolicyName: rotationPolicies[sc.PolicyName], + SecretName: opts.SecretName, } resp, err := opts.PreviewClient.CreateTwilioRotatingSecret(req, nil) @@ -271,9 +268,9 @@ func createRun(opts *CreateOpts) error { MongodbRoles: reqRoles, MongodbScopes: reqScopes, }, - RotationIntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.Details[MongoDBAtlasRequiredKeys[0]].(string)], - SecretName: opts.SecretName, + IntegrationName: sc.IntegrationName, + RotationPolicyName: rotationPolicies[sc.Details[MongoDBAtlasRequiredKeys[0]].(string)], + SecretName: opts.SecretName, } resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) if err != nil { diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index 32405289..4a69fceb 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -287,19 +287,19 @@ details: ProjectID: testProfile(t).ProjectID, AppName: testProfile(t).VaultSecrets.AppName, Body: &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ - SecretName: opts.SecretName, - RotationIntegrationName: "Twil-Int-11", - RotationPolicyName: "built-in:60-days-2-active", + SecretName: opts.SecretName, + IntegrationName: "Twil-Int-11", + RotationPolicyName: "built-in:60-days-2-active", }, Context: opts.Ctx, }, mock.Anything).Return(&preview_secret_service.CreateTwilioRotatingSecretOK{ Payload: &preview_models.Secrets20231128CreateTwilioRotatingSecretResponse{ Config: &preview_models.Secrets20231128RotatingSecretConfig{ - AppName: opts.AppName, - CreatedAt: dt, - RotationIntegrationName: "Twil-Int-11", - RotationPolicyName: "built-in:60-days-2-active", - SecretName: opts.SecretName, + AppName: opts.AppName, + CreatedAt: dt, + IntegrationName: "Twil-Int-11", + RotationPolicyName: "built-in:60-days-2-active", + SecretName: opts.SecretName, }, }, }, nil).Once() diff --git a/internal/commands/vaultsecrets/secrets/displayer.go b/internal/commands/vaultsecrets/secrets/displayer.go index cad9d985..85e1162f 100644 --- a/internal/commands/vaultsecrets/secrets/displayer.go +++ b/internal/commands/vaultsecrets/secrets/displayer.go @@ -273,8 +273,8 @@ func (r *rotatingSecretsDisplayer) FieldTemplates() []format.Field { ValueFormat: "{{ .SecretName }}", }, { - Name: " Rotation Integration", - ValueFormat: "{{ .RotationIntegrationName }}", + Name: " Integration Name", + ValueFormat: "{{ .IntegrationName }}", }, { Name: " Rotation Policy", From 00f6f089069fdff70fcc639c316f1b2a74f943bc Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Thu, 29 Aug 2024 11:26:18 -0400 Subject: [PATCH 33/75] rotation_integration_name => integration_name --- internal/commands/vaultsecrets/secrets/create.go | 4 ++-- internal/commands/vaultsecrets/secrets/create_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 73779c48..8242d1fb 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -128,7 +128,7 @@ type CreateOpts struct { type RotatingSecretConfig struct { Version string Type integrations.IntegrationType - IntegrationName string `yaml:"rotation_integration_name"` + IntegrationName string `yaml:"integration_name"` PolicyName string `yaml:"rotation_policy_name"` Details map[string]any } @@ -342,7 +342,7 @@ func validateRotatingSecretConfig(sc RotatingSecretConfig) []string { } if sc.IntegrationName == "" { - missingKeys = append(missingKeys, "rotation_integration_name") + missingKeys = append(missingKeys, "integration_name") } if sc.PolicyName == "" { diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index 4a69fceb..c0137840 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -185,7 +185,7 @@ func TestCreateRun(t *testing.T) { MockCalled: true, Input: []byte(`version: 1.0.0 type: "twilio" -rotation_integration_name: "Twil-Int-11" +integration_name: "Twil-Int-11" rotation_policy_name: "60"`), }, { @@ -196,7 +196,7 @@ rotation_policy_name: "60"`), }, Input: []byte(`version: 1.0.0 type: "twilio" -rotation_integration_name: "Twil-Int-11" +integration_name: "Twil-Int-11" details: none: "none"`), ErrMsg: "missing required field(s) in the config file: [rotation_policy_name]", From 81499e09c890130232cc7cda6c70691d9cb61e87 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 7 Aug 2024 12:55:13 -0500 Subject: [PATCH 34/75] Adds support for creating rotating secrets --- internal/commands/vaultsecrets/secrets/create.go | 12 +++++------- .../commands/vaultsecrets/secrets/create_test.go | 9 +++++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 8242d1fb..6323497b 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -7,19 +7,16 @@ import ( "context" "errors" "fmt" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" + "github.com/mitchellh/mapstructure" + "gopkg.in/yaml.v3" "io" "os" "slices" - "github.com/mitchellh/mapstructure" - "github.com/posener/complete" - "golang.org/x/exp/maps" - "gopkg.in/yaml.v3" - preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" - "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets/appname" "github.com/hashicorp/hcp/internal/pkg/cmd" "github.com/hashicorp/hcp/internal/pkg/flagvalue" @@ -27,6 +24,8 @@ import ( "github.com/hashicorp/hcp/internal/pkg/heredoc" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" + "github.com/posener/complete" + "golang.org/x/exp/maps" ) type SecretType string @@ -198,7 +197,6 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.Twilio: - req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index c0137840..83ad5840 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -70,6 +70,15 @@ func TestNewCmdCreate(t *testing.T) { SecretName: "test", }, }, + { + Name: "Good: Rotating secret", + Profile: testProfile, + Args: []string{"test", "--secret-type=rotating"}, + Expect: &CreateOpts{ + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: "test", + }, + }, } for _, c := range cases { From c4c9c974a9878529473f59d525f98c965a138aaf Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 15 Aug 2024 10:45:27 -0500 Subject: [PATCH 35/75] adds back removed changes --- .../commands/vaultsecrets/secrets/create.go | 111 ++++++++++++++++-- .../vaultsecrets/secrets/create_test.go | 4 +- 2 files changed, 102 insertions(+), 13 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 6323497b..409ef953 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -7,16 +7,19 @@ import ( "context" "errors" "fmt" - "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" - "github.com/mitchellh/mapstructure" - "gopkg.in/yaml.v3" "io" "os" "slices" + "github.com/mitchellh/mapstructure" + "github.com/posener/complete" + "golang.org/x/exp/maps" + "gopkg.in/yaml.v3" + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets/appname" "github.com/hashicorp/hcp/internal/pkg/cmd" "github.com/hashicorp/hcp/internal/pkg/flagvalue" @@ -24,8 +27,6 @@ import ( "github.com/hashicorp/hcp/internal/pkg/heredoc" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" - "github.com/posener/complete" - "golang.org/x/exp/maps" ) type SecretType string @@ -143,8 +144,12 @@ type MongoDBScope struct { Type string `mapstructure:"name"` } -// There are no Twilio-specific keys -var MongoDBAtlasRequiredKeys = []string{"mongodb_group_id", "mongodb_roles"} +var ( + // There are no Twilio-specific keys + MongoDBAtlasRequiredKeys = []string{"mongodb_group_id", "mongodb_roles"} + AwsKeys = []string{"default_ttl", "role_arn"} + GcpKeys = []string{"default_ttl", "service_account_email"} +) var rotationPolicies = map[string]string{ "30": "built-in:30-days-2-active", @@ -178,16 +183,15 @@ func createRun(opts *CreateOpts) error { return err } case Rotating: - f, err := os.ReadFile(opts.SecretFilePath) + sc, err := readConfigFile(opts) if err != nil { - return fmt.Errorf("failed to open config file: %w", err) + return fmt.Errorf("failed to process config file: %w", err) } var sc RotatingSecretConfig err = yaml.Unmarshal(f, &sc) if err != nil { return fmt.Errorf("failed to unmarshal config file: %w", err) - } missingFields := validateRotatingSecretConfig(sc) @@ -197,6 +201,12 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.Twilio: + missingDetails := validateDetails(sc.Details, TwilioKeys) + + if len(missingDetails) > 0 { + return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) + } + req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID @@ -220,7 +230,7 @@ func createRun(opts *CreateOpts) error { missingDetails := validateDetails(sc.Details, MongoDBAtlasRequiredKeys) if len(missingDetails) > 0 { - return fmt.Errorf("missing required detail(s) in the config file: %s", missingDetails) + return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) } req := preview_secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) @@ -279,6 +289,70 @@ func createRun(opts *CreateOpts) error { return err } } + case Dynamic: + sc, err := readConfigFile(opts) + if err != nil { + return fmt.Errorf("failed to process config file: %w", err) + } + + missingFields := validateFields(sc) + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + } + + switch sc.Type { + case integrations.AWS: + + missingDetails := validateDetails(sc.Details, AwsKeys) + + if len(missingDetails) > 0 { + return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) + } + + req := preview_secret_service.NewCreateAwsDynamicSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + req.Body = &preview_models.SecretServiceCreateAwsDynamicSecretBody{ + IntegrationName: sc.IntegrationName, + DefaultTTL: sc.Details[AwsKeys[0]].(string), + AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ + RoleArn: sc.Details[AwsKeys[1]].(string), + }, + Name: opts.SecretName, + } + + _, err := opts.PreviewClient.CreateAwsDynamicSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } + + case integrations.GCP: + + missingDetails := validateDetails(sc.Details, GcpKeys) + + if len(missingDetails) > 0 { + return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) + } + + req := preview_secret_service.NewCreateGcpDynamicSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + req.Body = &preview_models.SecretServiceCreateGcpDynamicSecretBody{ + IntegrationName: sc.IntegrationName, + DefaultTTL: sc.Details[GcpKeys[0]].(string), + ServiceAccountImpersonation: &preview_models.Secrets20231128ServiceAccountImpersonationRequest{ + ServiceAccountEmail: sc.Details[GcpKeys[1]].(string), + }, + Name: opts.SecretName, + } + + _, err := opts.PreviewClient.CreateGcpDynamicSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } + } } command := fmt.Sprintf(`$ hcp vault-secrets secrets read %s --app %s`, opts.SecretName, opts.AppName) @@ -332,6 +406,21 @@ func readPlainTextSecret(opts *CreateOpts) error { return nil } +func readConfigFile(opts *CreateOpts) (SecretConfig, error) { + var sc SecretConfig + f, err := os.ReadFile(opts.SecretFilePath) + if err != nil { + return sc, fmt.Errorf("unable to open config file: %w", err) + } + + err = yaml.Unmarshal(f, &sc) + if err != nil { + return sc, fmt.Errorf("unable to unmarshal config file: %w", err) + } + + return sc, nil +} + func validateRotatingSecretConfig(sc RotatingSecretConfig) []string { var missingKeys []string diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index 83ad5840..d8396649 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -71,9 +71,9 @@ func TestNewCmdCreate(t *testing.T) { }, }, { - Name: "Good: Rotating secret", + Name: "Good: Dynamic secret", Profile: testProfile, - Args: []string{"test", "--secret-type=rotating"}, + Args: []string{"test", "--secret-type=dynamic", "--data-file=DATA_FILE_PATH"}, Expect: &CreateOpts{ AppName: testProfile(t).VaultSecrets.AppName, SecretName: "test", From 080afbf45a713451acbeeb1c94e820426b3a7789 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 23 Aug 2024 16:33:10 -0500 Subject: [PATCH 36/75] WIP --- .../commands/vaultsecrets/secrets/create.go | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 409ef953..79697531 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -133,6 +133,12 @@ type RotatingSecretConfig struct { Details map[string]any } +type DynamicSecretConfig struct { + Version string + Type integrations.IntegrationType + DefaultTtl string `yaml:"default_ttl"` +} + type MongoDBRole struct { RoleName string `mapstructure:"role_name"` DatabaseName string `mapstructure:"database_name"` @@ -147,8 +153,8 @@ type MongoDBScope struct { var ( // There are no Twilio-specific keys MongoDBAtlasRequiredKeys = []string{"mongodb_group_id", "mongodb_roles"} - AwsKeys = []string{"default_ttl", "role_arn"} - GcpKeys = []string{"default_ttl", "service_account_email"} + AwsKeys = []string{"default_ttl", "role_arn"} + GcpKeys = []string{"default_ttl", "service_account_email"} ) var rotationPolicies = map[string]string{ @@ -188,11 +194,6 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("failed to process config file: %w", err) } - var sc RotatingSecretConfig - err = yaml.Unmarshal(f, &sc) - if err != nil { - return fmt.Errorf("failed to unmarshal config file: %w", err) - missingFields := validateRotatingSecretConfig(sc) if len(missingFields) > 0 { @@ -201,12 +202,6 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.Twilio: - missingDetails := validateDetails(sc.Details, TwilioKeys) - - if len(missingDetails) > 0 { - return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) - } - req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID @@ -295,7 +290,7 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("failed to process config file: %w", err) } - missingFields := validateFields(sc) + missingFields := validateDynamicSecretConfig(sc) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) } @@ -406,8 +401,8 @@ func readPlainTextSecret(opts *CreateOpts) error { return nil } -func readConfigFile(opts *CreateOpts) (SecretConfig, error) { - var sc SecretConfig +func readConfigFile(opts *CreateOpts) (RotatingSecretConfig, error) { + var sc RotatingSecretConfig f, err := os.ReadFile(opts.SecretFilePath) if err != nil { return sc, fmt.Errorf("unable to open config file: %w", err) @@ -439,6 +434,16 @@ func validateRotatingSecretConfig(sc RotatingSecretConfig) []string { return missingKeys } +func validateDynamicSecretConfig(sc DynamicSecretConfig) []string { + var missingKeys []string + + if sc.DefaultTtl == "" { + missingKeys = append(missingKeys, "default_ttl") + } + + return missingKeys +} + func validateDetails(details map[string]any, requiredKeys []string) []string { detailsKeys := maps.Keys(details) var missingKeys []string From f877833e1afdfd391cfaaed210164f6a1263e662 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 27 Aug 2024 17:00:46 -0500 Subject: [PATCH 37/75] some cleanup --- .../commands/vaultsecrets/secrets/create.go | 60 ++++++++----------- .../vaultsecrets/secrets/create_test.go | 53 +++++++++++++++- .../commands/vaultsecrets/secrets/open.go | 2 +- 3 files changed, 75 insertions(+), 40 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 79697531..f7fefc27 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -125,7 +125,7 @@ type CreateOpts struct { Client secret_service.ClientService } -type RotatingSecretConfig struct { +type SecretConfig struct { Version string Type integrations.IntegrationType IntegrationName string `yaml:"integration_name"` @@ -133,12 +133,6 @@ type RotatingSecretConfig struct { Details map[string]any } -type DynamicSecretConfig struct { - Version string - Type integrations.IntegrationType - DefaultTtl string `yaml:"default_ttl"` -} - type MongoDBRole struct { RoleName string `mapstructure:"role_name"` DatabaseName string `mapstructure:"database_name"` @@ -151,10 +145,10 @@ type MongoDBScope struct { } var ( - // There are no Twilio-specific keys - MongoDBAtlasRequiredKeys = []string{"mongodb_group_id", "mongodb_roles"} - AwsKeys = []string{"default_ttl", "role_arn"} - GcpKeys = []string{"default_ttl", "service_account_email"} + TwilioRequiredKeys = []string{"rotation_policy_name"} + MongoDBAtlasRequiredKeys = []string{"rotation_policy_name", "mongodb_group_id", "mongodb_roles"} + AwsRequiredKeys = []string{"default_ttl", "role_arn"} + GcpRequiredKeys = []string{"default_ttl", "service_account_email"} ) var rotationPolicies = map[string]string{ @@ -194,7 +188,7 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("failed to process config file: %w", err) } - missingFields := validateRotatingSecretConfig(sc) + missingFields := validateSecretConfig(sc) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) @@ -202,6 +196,12 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.Twilio: + missingDetails := validateDetails(sc.Details, TwilioRequiredKeys) + + if len(missingDetails) > 0 { + return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) + } + req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID @@ -290,7 +290,8 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("failed to process config file: %w", err) } - missingFields := validateDynamicSecretConfig(sc) + missingFields := validateSecretConfig(sc) + if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) } @@ -298,7 +299,7 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.AWS: - missingDetails := validateDetails(sc.Details, AwsKeys) + missingDetails := validateDetails(sc.Details, AwsRequiredKeys) if len(missingDetails) > 0 { return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) @@ -310,9 +311,9 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateAwsDynamicSecretBody{ IntegrationName: sc.IntegrationName, - DefaultTTL: sc.Details[AwsKeys[0]].(string), + DefaultTTL: sc.Details[AwsRequiredKeys[0]].(string), AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ - RoleArn: sc.Details[AwsKeys[1]].(string), + RoleArn: sc.Details[AwsRequiredKeys[1]].(string), }, Name: opts.SecretName, } @@ -324,7 +325,7 @@ func createRun(opts *CreateOpts) error { case integrations.GCP: - missingDetails := validateDetails(sc.Details, GcpKeys) + missingDetails := validateDetails(sc.Details, GcpRequiredKeys) if len(missingDetails) > 0 { return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) @@ -336,9 +337,9 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateGcpDynamicSecretBody{ IntegrationName: sc.IntegrationName, - DefaultTTL: sc.Details[GcpKeys[0]].(string), + DefaultTTL: sc.Details[GcpRequiredKeys[0]].(string), ServiceAccountImpersonation: &preview_models.Secrets20231128ServiceAccountImpersonationRequest{ - ServiceAccountEmail: sc.Details[GcpKeys[1]].(string), + ServiceAccountEmail: sc.Details[GcpRequiredKeys[1]].(string), }, Name: opts.SecretName, } @@ -401,8 +402,9 @@ func readPlainTextSecret(opts *CreateOpts) error { return nil } -func readConfigFile(opts *CreateOpts) (RotatingSecretConfig, error) { - var sc RotatingSecretConfig +func readConfigFile(opts *CreateOpts) (SecretConfig, error) { + var sc SecretConfig + f, err := os.ReadFile(opts.SecretFilePath) if err != nil { return sc, fmt.Errorf("unable to open config file: %w", err) @@ -416,7 +418,7 @@ func readConfigFile(opts *CreateOpts) (RotatingSecretConfig, error) { return sc, nil } -func validateRotatingSecretConfig(sc RotatingSecretConfig) []string { +func validateSecretConfig(sc SecretConfig) []string { var missingKeys []string if sc.Type == "" { @@ -427,20 +429,6 @@ func validateRotatingSecretConfig(sc RotatingSecretConfig) []string { missingKeys = append(missingKeys, "integration_name") } - if sc.PolicyName == "" { - missingKeys = append(missingKeys, "rotation_policy_name") - } - - return missingKeys -} - -func validateDynamicSecretConfig(sc DynamicSecretConfig) []string { - var missingKeys []string - - if sc.DefaultTtl == "" { - missingKeys = append(missingKeys, "default_ttl") - } - return missingKeys } diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index d8396649..b260e19d 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -199,7 +199,7 @@ rotation_policy_name: "60"`), }, { Name: "Failed: Missing required rotating secret field", - RespErr: false, + RespErr: true, AugmentOpts: func(o *CreateOpts) { o.Type = Rotating }, @@ -208,7 +208,21 @@ type: "twilio" integration_name: "Twil-Int-11" details: none: "none"`), - ErrMsg: "missing required field(s) in the config file: [rotation_policy_name]", + ErrMsg: "missing required field(s) in the config file details: [rotation_policy_name]", + }, + { + Name: "Success: Create an Aws dynamic secret", + RespErr: false, + AugmentOpts: func(o *CreateOpts) { + o.Type = Dynamic + }, + MockCalled: true, + Input: []byte(`version: 1.0.0 +type: "aws" +rotation_integration_name: "Aws-Int-12" +details: + default_ttl: "30" + role_arn: "ra"`), }, } @@ -246,7 +260,7 @@ details: c.AugmentOpts(opts) } - if opts.Type == Rotating { + if opts.Type == Rotating || opts.Type == Dynamic { tempDir := t.TempDir() f, err := os.Create(filepath.Join(tempDir, "config.yaml")) r.NoError(err) @@ -314,6 +328,39 @@ details: }, nil).Once() } } + } else if opts.Type == Dynamic { + if c.MockCalled { + if c.RespErr { + pvs.EXPECT().CreateAwsDynamicSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + pvs.EXPECT().CreateAwsDynamicSecret(&preview_secret_service.CreateAwsDynamicSecretParams{ + OrganizationID: testProfile(t).OrganizationID, + ProjectID: testProfile(t).ProjectID, + AppName: testProfile(t).VaultSecrets.AppName, + Body: &preview_models.SecretServiceCreateAwsDynamicSecretBody{ + IntegrationName: "Aws-Int-12", + Name: opts.SecretName, + DefaultTTL: "30", + AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ + RoleArn: "ra", + }, + }, + Context: opts.Ctx, + }, mock.Anything).Return(&preview_secret_service.CreateAwsDynamicSecretOK{ + Payload: &preview_models.Secrets20231128CreateAwsDynamicSecretResponse{ + Secret: &preview_models.Secrets20231128AwsDynamicSecret{ + AssumeRole: &preview_models.Secrets20231128AssumeRoleResponse{ + RoleArn: "ra", + }, + DefaultTTL: "30", + CreatedAt: dt, + IntegrationName: "Aws-Int-12", + Name: opts.SecretName, + }, + }, + }, nil).Once() + } + } } // Run the command diff --git a/internal/commands/vaultsecrets/secrets/open.go b/internal/commands/vaultsecrets/secrets/open.go index b045e5e4..8c28617c 100644 --- a/internal/commands/vaultsecrets/secrets/open.go +++ b/internal/commands/vaultsecrets/secrets/open.go @@ -37,7 +37,7 @@ func NewCmdOpen(ctx *cmd.Context, runF func(*OpenOpts) error) *cmd.Command { Name: "open", ShortHelp: "Open a secret.", LongHelp: heredoc.New(ctx.IO).Must(` - The {{ template "mdCodeOrBold" "hcp vault-secrets secrets open" }} command reads the plaintext value of a static, rotating, or dynamic secret from the Vault Secrets application. + The {{ template "mdCodeOrBold" "hcp vault-secrets secrets open" }} command reads the plaintext value of a static, rotating, or dynamic secret from a Vault Secrets application. `), Examples: []cmd.Example{ { From d1b93107eb191abedfa7d9801b27d7d321860f11 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 27 Aug 2024 17:50:57 -0500 Subject: [PATCH 38/75] Adds additional test case --- internal/commands/vaultsecrets/secrets/create.go | 10 ++++++++++ internal/commands/vaultsecrets/secrets/create_test.go | 11 ++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index f7fefc27..74792e6a 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -283,7 +283,11 @@ func createRun(opts *CreateOpts) error { if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { return err } + + default: + return fmt.Errorf("unsupported rotating secret provider type") } + case Dynamic: sc, err := readConfigFile(opts) if err != nil { @@ -348,7 +352,13 @@ func createRun(opts *CreateOpts) error { if err != nil { return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) } + + default: + return fmt.Errorf("unsupported dynamic secret provider type") } + + default: + return fmt.Errorf("%q is an unsupported secret type; \"static\", \"rotating\", \"dynamic\" are available types", opts.Type) } command := fmt.Sprintf(`$ hcp vault-secrets secrets read %s --app %s`, opts.SecretName, opts.AppName) diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index b260e19d..29e9545b 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -143,7 +143,7 @@ func TestCreateRun(t *testing.T) { Input []byte }{ { - Name: "Failed: Read via stdin as hypen not supplied for --data-file flag", + Name: "Failed: Read via stdin as hyphen not supplied for --data-file flag", ErrMsg: "data file path is required", }, { @@ -224,6 +224,15 @@ details: default_ttl: "30" role_arn: "ra"`), }, + { + Name: "Failed: Unsupported secret type", + RespErr: true, + AugmentOpts: func(o *CreateOpts) { + o.Type = "random" + }, + Input: []byte{}, + ErrMsg: "\"random\" is an unsupported secret type; \"static\", \"rotating\", \"dynamic\" are available types", + }, } for _, c := range cases { From f0c988705f27cbc8cb4cd1786f153bd558b84465 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 27 Aug 2024 18:45:58 -0500 Subject: [PATCH 39/75] updates integration name --- .../commands/vaultsecrets/secrets/create.go | 22 +++++---------- .../vaultsecrets/secrets/create_test.go | 27 ++++++++++--------- .../commands/vaultsecrets/secrets/open.go | 2 +- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 74792e6a..e68bd7e2 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -29,14 +29,6 @@ import ( "github.com/hashicorp/hcp/internal/pkg/profile" ) -type SecretType string - -const ( - Static SecretType = "static" - Rotating SecretType = "rotating" - Dynamic SecretType = "dynamic" -) - func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { opts := &CreateOpts{ Ctx: ctx.ShutdownCtx, @@ -51,7 +43,7 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { Name: "create", ShortHelp: "Create a new static secret.", LongHelp: heredoc.New(ctx.IO).Must(` - The {{ template "mdCodeOrBold" "hcp vault-secrets secrets create" }} command creates a new static secret under a Vault Secrets application. + The {{ template "mdCodeOrBold" "hcp vault-secrets secrets create" }} command creates a new static, rotating, or dynamic secret under a Vault Secrets application. `), Examples: []cmd.Example{ { @@ -120,7 +112,7 @@ type CreateOpts struct { SecretName string SecretValuePlaintext string SecretFilePath string - Type SecretType + Type string PreviewClient preview_secret_service.ClientService Client secret_service.ClientService } @@ -159,7 +151,7 @@ var rotationPolicies = map[string]string{ func createRun(opts *CreateOpts) error { switch opts.Type { - case Static, "": + case secretTypeKV, "": if err := readPlainTextSecret(opts); err != nil { return err } @@ -182,7 +174,7 @@ func createRun(opts *CreateOpts) error { if err := opts.Output.Display(newDisplayer().Secrets(resp.Payload.Secret)); err != nil { return err } - case Rotating: + case secretTypeRotating: sc, err := readConfigFile(opts) if err != nil { return fmt.Errorf("failed to process config file: %w", err) @@ -232,7 +224,7 @@ func createRun(opts *CreateOpts) error { req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID - roles := sc.Details["mongodb_roles"].([]interface{}) + roles := sc.Details["mongodb_roles"].([]any) var reqRoles []*preview_models.Secrets20231128MongoDBRole for _, r := range roles { var role MongoDBRole @@ -249,7 +241,7 @@ func createRun(opts *CreateOpts) error { reqRoles = append(reqRoles, reqRole) } - scopes := sc.Details["mongodb_scopes"].([]interface{}) + scopes := sc.Details["mongodb_scopes"].([]any) var reqScopes []*preview_models.Secrets20231128MongoDBScope for _, r := range scopes { var scope MongoDBScope @@ -288,7 +280,7 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("unsupported rotating secret provider type") } - case Dynamic: + case secretTypeDynamic: sc, err := readConfigFile(opts) if err != nil { return fmt.Errorf("failed to process config file: %w", err) diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index 29e9545b..7bf818e4 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -153,7 +153,7 @@ func TestCreateRun(t *testing.T) { RespErr: true, AugmentOpts: func(o *CreateOpts) { o.SecretFilePath = "-" - o.Type = Static + o.Type = secretTypeKV }, ErrMsg: "secret value cannot be empty", }, @@ -162,7 +162,7 @@ func TestCreateRun(t *testing.T) { ReadViaStdin: true, AugmentOpts: func(o *CreateOpts) { o.SecretFilePath = "-" - o.Type = Static + o.Type = secretTypeKV }, MockCalled: true, }, @@ -172,7 +172,7 @@ func TestCreateRun(t *testing.T) { ErrMsg: "[POST /secrets/2023-11-28/organizations/{organization_id}/projects/{project_id}/apps/{app_name}/secret/kv][429] CreateAppKVSecret default &{Code:8 Details:[] Message:maximum number of secret versions reached}", AugmentOpts: func(o *CreateOpts) { o.SecretValuePlaintext = testSecretValue - o.Type = Static + o.Type = secretTypeKV }, MockCalled: true, }, @@ -181,7 +181,7 @@ func TestCreateRun(t *testing.T) { RespErr: false, AugmentOpts: func(o *CreateOpts) { o.SecretValuePlaintext = testSecretValue - o.Type = Static + o.Type = secretTypeKV }, MockCalled: true, }, @@ -189,19 +189,20 @@ func TestCreateRun(t *testing.T) { Name: "Success: Create a Twilio rotating secret", RespErr: false, AugmentOpts: func(o *CreateOpts) { - o.Type = Rotating + o.Type = secretTypeRotating }, MockCalled: true, Input: []byte(`version: 1.0.0 type: "twilio" integration_name: "Twil-Int-11" -rotation_policy_name: "60"`), +details: + rotation_policy_name: "60"`), }, { Name: "Failed: Missing required rotating secret field", RespErr: true, AugmentOpts: func(o *CreateOpts) { - o.Type = Rotating + o.Type = secretTypeRotating }, Input: []byte(`version: 1.0.0 type: "twilio" @@ -214,12 +215,12 @@ details: Name: "Success: Create an Aws dynamic secret", RespErr: false, AugmentOpts: func(o *CreateOpts) { - o.Type = Dynamic + o.Type = secretTypeDynamic }, MockCalled: true, Input: []byte(`version: 1.0.0 type: "aws" -rotation_integration_name: "Aws-Int-12" +integration_name: "Aws-Int-12" details: default_ttl: "30" role_arn: "ra"`), @@ -269,7 +270,7 @@ details: c.AugmentOpts(opts) } - if opts.Type == Rotating || opts.Type == Dynamic { + if opts.Type == secretTypeRotating || opts.Type == secretTypeDynamic { tempDir := t.TempDir() f, err := os.Create(filepath.Join(tempDir, "config.yaml")) r.NoError(err) @@ -279,7 +280,7 @@ details: } dt := strfmt.NewDateTime() - if opts.Type == Static { + if opts.Type == secretTypeKV { if c.MockCalled { if c.RespErr { vs.EXPECT().CreateAppKVSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() @@ -309,7 +310,7 @@ details: }, nil).Once() } } - } else if opts.Type == Rotating { + } else if opts.Type == secretTypeRotating { if c.MockCalled { if c.RespErr { pvs.EXPECT().CreateTwilioRotatingSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() @@ -337,7 +338,7 @@ details: }, nil).Once() } } - } else if opts.Type == Dynamic { + } else if opts.Type == secretTypeDynamic { if c.MockCalled { if c.RespErr { pvs.EXPECT().CreateAwsDynamicSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() diff --git a/internal/commands/vaultsecrets/secrets/open.go b/internal/commands/vaultsecrets/secrets/open.go index 8c28617c..b045e5e4 100644 --- a/internal/commands/vaultsecrets/secrets/open.go +++ b/internal/commands/vaultsecrets/secrets/open.go @@ -37,7 +37,7 @@ func NewCmdOpen(ctx *cmd.Context, runF func(*OpenOpts) error) *cmd.Command { Name: "open", ShortHelp: "Open a secret.", LongHelp: heredoc.New(ctx.IO).Must(` - The {{ template "mdCodeOrBold" "hcp vault-secrets secrets open" }} command reads the plaintext value of a static, rotating, or dynamic secret from a Vault Secrets application. + The {{ template "mdCodeOrBold" "hcp vault-secrets secrets open" }} command reads the plaintext value of a static, rotating, or dynamic secret from the Vault Secrets application. `), Examples: []cmd.Example{ { From e240e1c7c4d722d3b84aad3aef1c6194ccf368de Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 29 Aug 2024 15:22:24 -0500 Subject: [PATCH 40/75] some cleanup --- internal/commands/vaultsecrets/secrets/create.go | 3 +++ internal/commands/vaultsecrets/secrets/create_test.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index e68bd7e2..e912836d 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -314,6 +314,9 @@ func createRun(opts *CreateOpts) error { Name: opts.SecretName, } + //var sb preview_models.SecretServiceCreateAwsDynamicSecretBody + //err := sb.UnmarshalBinary([]byte{}) + _, err := opts.PreviewClient.CreateAwsDynamicSecret(req, nil) if err != nil { return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index 7bf818e4..dad042a0 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -222,7 +222,7 @@ details: type: "aws" integration_name: "Aws-Int-12" details: - default_ttl: "30" + default_ttl: "3600s" role_arn: "ra"`), }, { From f79bc271eb920902efce53ebdc8e9105405a65d9 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 29 Aug 2024 15:23:21 -0500 Subject: [PATCH 41/75] some cleanup --- internal/commands/vaultsecrets/secrets/create.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index e912836d..e68bd7e2 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -314,9 +314,6 @@ func createRun(opts *CreateOpts) error { Name: opts.SecretName, } - //var sb preview_models.SecretServiceCreateAwsDynamicSecretBody - //err := sb.UnmarshalBinary([]byte{}) - _, err := opts.PreviewClient.CreateAwsDynamicSecret(req, nil) if err != nil { return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) From bebdb74ae996c054ddec6200fa8d050d0157c07d Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 29 Aug 2024 16:10:22 -0500 Subject: [PATCH 42/75] updates to include assume_role --- internal/commands/vaultsecrets/secrets/create.go | 14 ++++++++++++-- .../commands/vaultsecrets/secrets/create_test.go | 7 ++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index e68bd7e2..34409e7e 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -136,10 +136,14 @@ type MongoDBScope struct { Type string `mapstructure:"name"` } +type AwsAssumeRole struct { + RoleArn string `mapstructure:"role_arn"` +} + var ( TwilioRequiredKeys = []string{"rotation_policy_name"} MongoDBAtlasRequiredKeys = []string{"rotation_policy_name", "mongodb_group_id", "mongodb_roles"} - AwsRequiredKeys = []string{"default_ttl", "role_arn"} + AwsRequiredKeys = []string{"default_ttl", "assume_role"} GcpRequiredKeys = []string{"default_ttl", "service_account_email"} ) @@ -301,6 +305,12 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) } + var role AwsAssumeRole + decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &role}) + if err := decoder.Decode(sc.Details[AwsRequiredKeys[1]].(any)); err != nil { + return fmt.Errorf("unable to decode aws assume_role") + } + req := preview_secret_service.NewCreateAwsDynamicSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID @@ -309,7 +319,7 @@ func createRun(opts *CreateOpts) error { IntegrationName: sc.IntegrationName, DefaultTTL: sc.Details[AwsRequiredKeys[0]].(string), AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ - RoleArn: sc.Details[AwsRequiredKeys[1]].(string), + RoleArn: role.RoleArn, }, Name: opts.SecretName, } diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index dad042a0..893a5d13 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -223,7 +223,8 @@ type: "aws" integration_name: "Aws-Int-12" details: default_ttl: "3600s" - role_arn: "ra"`), + assume_role: + role_arn: "ra"`), }, { Name: "Failed: Unsupported secret type", @@ -350,7 +351,7 @@ details: Body: &preview_models.SecretServiceCreateAwsDynamicSecretBody{ IntegrationName: "Aws-Int-12", Name: opts.SecretName, - DefaultTTL: "30", + DefaultTTL: "3600s", AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ RoleArn: "ra", }, @@ -362,7 +363,7 @@ details: AssumeRole: &preview_models.Secrets20231128AssumeRoleResponse{ RoleArn: "ra", }, - DefaultTTL: "30", + DefaultTTL: "3600s", CreatedAt: dt, IntegrationName: "Aws-Int-12", Name: opts.SecretName, From 48b7daac40fd1364bcffebe4605eef2514840d22 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 3 Sep 2024 18:10:51 -0500 Subject: [PATCH 43/75] makes requesed change --- internal/commands/vaultsecrets/secrets/create.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 34409e7e..22381ebb 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -140,11 +140,15 @@ type AwsAssumeRole struct { RoleArn string `mapstructure:"role_arn"` } +type GcpServiceAccount struct { + ServiceAccountEmail string `mapstructure:"service_account_email"` +} + var ( TwilioRequiredKeys = []string{"rotation_policy_name"} MongoDBAtlasRequiredKeys = []string{"rotation_policy_name", "mongodb_group_id", "mongodb_roles"} AwsRequiredKeys = []string{"default_ttl", "assume_role"} - GcpRequiredKeys = []string{"default_ttl", "service_account_email"} + GcpRequiredKeys = []string{"default_ttl", "service_account_impersonation"} ) var rotationPolicies = map[string]string{ @@ -337,6 +341,12 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) } + var account GcpServiceAccount + decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &account}) + if err := decoder.Decode(sc.Details[GcpRequiredKeys[1]].(any)); err != nil { + return fmt.Errorf("unable to decode gcp service_account_impersonation") + } + req := preview_secret_service.NewCreateGcpDynamicSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID @@ -345,7 +355,7 @@ func createRun(opts *CreateOpts) error { IntegrationName: sc.IntegrationName, DefaultTTL: sc.Details[GcpRequiredKeys[0]].(string), ServiceAccountImpersonation: &preview_models.Secrets20231128ServiceAccountImpersonationRequest{ - ServiceAccountEmail: sc.Details[GcpRequiredKeys[1]].(string), + ServiceAccountEmail: account.ServiceAccountEmail, }, Name: opts.SecretName, } From d3d2eec1840f350a8af2693288eab9b6e8e7aede Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 3 Sep 2024 19:09:18 -0500 Subject: [PATCH 44/75] fixes failing tests --- internal/commands/vaultsecrets/secrets/create.go | 3 +-- .../2023-11-28/client/secret_service/mock_ClientService.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 22381ebb..25be032d 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -121,7 +121,6 @@ type SecretConfig struct { Version string Type integrations.IntegrationType IntegrationName string `yaml:"integration_name"` - PolicyName string `yaml:"rotation_policy_name"` Details map[string]any } @@ -208,7 +207,7 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ IntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.PolicyName], + RotationPolicyName: rotationPolicies[sc.Details[TwilioRequiredKeys[0]].(string)], SecretName: opts.SecretName, } diff --git a/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service/mock_ClientService.go b/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service/mock_ClientService.go index d2e1b0e8..74f35926 100644 --- a/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service/mock_ClientService.go +++ b/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service/mock_ClientService.go @@ -5543,4 +5543,4 @@ func NewMockClientService(t interface { t.Cleanup(func() { mock.AssertExpectations(t) }) return mock -} \ No newline at end of file +} From ac7ab88baeccde581ae27efb6632fda7cfdb114e Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 3 Sep 2024 19:46:36 -0500 Subject: [PATCH 45/75] lints --- internal/commands/vaultsecrets/secrets/create.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 25be032d..2957f53f 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -310,7 +310,7 @@ func createRun(opts *CreateOpts) error { var role AwsAssumeRole decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &role}) - if err := decoder.Decode(sc.Details[AwsRequiredKeys[1]].(any)); err != nil { + if err := decoder.Decode(sc.Details[AwsRequiredKeys[1]]); err != nil { return fmt.Errorf("unable to decode aws assume_role") } @@ -327,7 +327,7 @@ func createRun(opts *CreateOpts) error { Name: opts.SecretName, } - _, err := opts.PreviewClient.CreateAwsDynamicSecret(req, nil) + _, err = opts.PreviewClient.CreateAwsDynamicSecret(req, nil) if err != nil { return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) } @@ -342,7 +342,7 @@ func createRun(opts *CreateOpts) error { var account GcpServiceAccount decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &account}) - if err := decoder.Decode(sc.Details[GcpRequiredKeys[1]].(any)); err != nil { + if err := decoder.Decode(sc.Details[GcpRequiredKeys[1]]); err != nil { return fmt.Errorf("unable to decode gcp service_account_impersonation") } From 7e3b609b1278ece4b22270cada1b71baf75010b3 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 4 Sep 2024 13:58:07 -0500 Subject: [PATCH 46/75] converts from yaml to hcl integration create config file --- .../vaultsecrets/integrations/create.go | 36 ++++++++++--------- .../vaultsecrets/integrations/create_test.go | 34 ++++++++++-------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index dbf0a191..90f9033f 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -6,12 +6,9 @@ package integrations import ( "context" "fmt" - "os" "slices" - "golang.org/x/exp/maps" - "gopkg.in/yaml.v3" - + "github.com/hashicorp/hcl/v2/hclsimple" preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" @@ -21,6 +18,7 @@ import ( "github.com/hashicorp/hcp/internal/pkg/heredoc" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" + "golang.org/x/exp/maps" ) type CreateOpts struct { @@ -56,7 +54,7 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { { Preamble: `Create a new Vault Secrets integration:`, Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` - $ hcp vault-secrets integrations create sample-integration --config-file=path-to-file/config.yaml + $ hcp vault-secrets integrations create sample-integration --config-file=path-to-file/config.hcl `), }, }, @@ -93,9 +91,8 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { } type IntegrationConfig struct { - Version string - Type IntegrationType - Details map[string]string + Type IntegrationType `hcl:"type"` + Details map[string]string `hcl:"details"` } var ( @@ -106,16 +103,9 @@ var ( ) func createRun(opts *CreateOpts) error { - // Open the file - f, err := os.ReadFile(opts.ConfigFilePath) - if err != nil { - return fmt.Errorf("failed to open config file: %w", err) - } - var i IntegrationConfig - err = yaml.Unmarshal(f, &i) - if err != nil { - return fmt.Errorf("failed to unmarshal config file: %w", err) + if err := hclsimple.DecodeFile(opts.ConfigFilePath, nil, &i); err != nil { + return fmt.Errorf("failed to decode config file: %w", err) } switch i.Type { @@ -133,6 +123,9 @@ func createRun(opts *CreateOpts) error { APIKeySecret: i.Details[TwilioKeys[1]], APIKeySid: i.Details[TwilioKeys[2]], }, + Capabilities: []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityROTATION.Pointer(), + }, } _, err := opts.PreviewClient.CreateTwilioIntegration(&preview_secret_service.CreateTwilioIntegrationParams{ @@ -159,6 +152,9 @@ func createRun(opts *CreateOpts) error { APIPrivateKey: i.Details[MongoKeys[0]], APIPublicKey: i.Details[MongoKeys[1]], }, + Capabilities: []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityROTATION.Pointer(), + }, } _, err := opts.PreviewClient.CreateMongoDBAtlasIntegration(&preview_secret_service.CreateMongoDBAtlasIntegrationParams{ @@ -185,6 +181,9 @@ func createRun(opts *CreateOpts) error { Audience: i.Details[AWSKeys[0]], RoleArn: i.Details[AWSKeys[1]], }, + Capabilities: []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), + }, } _, err := opts.PreviewClient.CreateAwsIntegration(&preview_secret_service.CreateAwsIntegrationParams{ @@ -211,6 +210,9 @@ func createRun(opts *CreateOpts) error { Audience: i.Details[GCPKeys[0]], ServiceAccountEmail: i.Details[GCPKeys[1]], }, + Capabilities: []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), + }, } _, err := opts.PreviewClient.CreateGcpIntegration(&preview_secret_service.CreateGcpIntegrationParams{ diff --git a/internal/commands/vaultsecrets/integrations/create_test.go b/internal/commands/vaultsecrets/integrations/create_test.go index 67bb64a2..0e551a43 100644 --- a/internal/commands/vaultsecrets/integrations/create_test.go +++ b/internal/commands/vaultsecrets/integrations/create_test.go @@ -110,28 +110,28 @@ func TestCreateRun(t *testing.T) { { Name: "Good", IntegrationName: "sample-integration", - Input: []byte(`version: 1.0.0 -type: "aws" -details: - audience: abc - role_arn: def`), + Input: []byte(`type = "aws" +details = { + "audience" = "abc", + "role_arn" = "def" +}`), }, { Name: "Missing a single required field", IntegrationName: "sample-integration", - Input: []byte(`version: 1.0.0 -type: "mongodb-atlas" -details: - public_key: abc`), + Input: []byte(`type = "mongodb-atlas" +details = { + "public_key" = "abc" +}`), Error: "missing required field(s) in the config file: [private_key]", }, { Name: "Missing multiple required fields", IntegrationName: "sample-integration", - Input: []byte(`version: 1.0.0 -type: "twilio" -details: - api_key_sid: ghi`), + Input: []byte(`type = "twilio" +details = { + "api_key_sid" = "ghi" +}`), Error: "missing required field(s) in the config file: [account_sid api_key_secret]", }, } @@ -144,7 +144,7 @@ details: r := require.New(t) tempDir := t.TempDir() - f, err := os.Create(filepath.Join(tempDir, "config.yaml")) + f, err := os.Create(filepath.Join(tempDir, "config.hcl")) r.NoError(err) _, err = f.Write(c.Input) r.NoError(err) @@ -173,6 +173,9 @@ details: Audience: "abc", RoleArn: "def", }, + Capabilities: []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), + }, }, }, nil).Return(&preview_secret_service.CreateAwsIntegrationOK{ Payload: &preview_models.Secrets20231128CreateAwsIntegrationResponse{ @@ -182,6 +185,9 @@ details: Audience: "abc", RoleArn: "def", }, + Capabilities: []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), + }, }, }, }, nil).Once() From 746af0e9717da0160c0b1ef96df61faa91845cba Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 4 Sep 2024 14:09:14 -0500 Subject: [PATCH 47/75] lints --- internal/commands/vaultsecrets/integrations/create.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index 90f9033f..ffa452ec 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -8,6 +8,8 @@ import ( "fmt" "slices" + "golang.org/x/exp/maps" + "github.com/hashicorp/hcl/v2/hclsimple" preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/hcp/internal/pkg/heredoc" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" - "golang.org/x/exp/maps" ) type CreateOpts struct { From 287efe9350cceb3f3049848dd6853ce3ac36a31a Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 6 Sep 2024 11:05:20 -0500 Subject: [PATCH 48/75] WIP --- .../commands/vaultsecrets/secrets/create.go | 95 ++++++++++++++----- .../vaultsecrets/secrets/create_test.go | 39 ++++---- 2 files changed, 91 insertions(+), 43 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 2957f53f..c9230fa6 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -7,15 +7,12 @@ import ( "context" "errors" "fmt" + "github.com/hashicorp/hcl/v2/hclsimple" + "github.com/zclconf/go-cty/cty" "io" "os" "slices" - "github.com/mitchellh/mapstructure" - "github.com/posener/complete" - "golang.org/x/exp/maps" - "gopkg.in/yaml.v3" - preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" @@ -27,6 +24,9 @@ import ( "github.com/hashicorp/hcp/internal/pkg/heredoc" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" + "github.com/mitchellh/mapstructure" + "github.com/posener/complete" + "golang.org/x/exp/maps" ) func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { @@ -47,7 +47,7 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { `), Examples: []cmd.Example{ { - Preamble: `Create a new secret in the Vault Secrets application on your active profile:`, + Preamble: `Create a new static secret in the Vault Secrets application on your active profile:`, Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` $ hcp vault-secrets secrets create secret_1 --data-file=tmp/secrets1.txt `), @@ -58,6 +58,12 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { $ echo -n "my super secret" | hcp vault-secrets secrets create secret_2 --data-file=- `), }, + { + Preamble: `Create a new rotating secret in the Vault Secrets application on your active profile:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets secrets create secret_1 --secret-type=rotating --data-file=path/to/file/config.hcl + `), + }, }, Args: cmd.PositionalArguments{ Args: []cmd.PositionalArgument{ @@ -117,11 +123,16 @@ type CreateOpts struct { Client secret_service.ClientService } +type SecretConfigZ struct { + Type integrations.IntegrationType `hcl:"type"` + IntegrationName string `hcl:"integration_name"` + Details map[string]string `hcl:"details"` +} + type SecretConfig struct { - Version string - Type integrations.IntegrationType - IntegrationName string `yaml:"integration_name"` - Details map[string]any + Type integrations.IntegrationType `hcl:"type"` + IntegrationName string `hcl:"integration_name"` + Details map[string]cty.Value `hcl:"details"` } type MongoDBRole struct { @@ -201,13 +212,21 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) } + //for detail, value := range sc.Details { + // + // //for _, key := range TwilioRequiredKeys { + // // if detail + // //} + // value.AsString() + //} + req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ IntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.Details[TwilioRequiredKeys[0]].(string)], + RotationPolicyName: rotationPolicies[sc.Details[TwilioRequiredKeys[0]].AsString()], SecretName: opts.SecretName, } @@ -231,7 +250,7 @@ func createRun(opts *CreateOpts) error { req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID - roles := sc.Details["mongodb_roles"].([]any) + roles := sc.Details["mongodb_roles"].AsValueSlice() var reqRoles []*preview_models.Secrets20231128MongoDBRole for _, r := range roles { var role MongoDBRole @@ -248,7 +267,7 @@ func createRun(opts *CreateOpts) error { reqRoles = append(reqRoles, reqRole) } - scopes := sc.Details["mongodb_scopes"].([]any) + scopes := sc.Details["mongodb_scopes"].AsValueSlice() var reqScopes []*preview_models.Secrets20231128MongoDBScope for _, r := range scopes { var scope MongoDBScope @@ -266,12 +285,12 @@ func createRun(opts *CreateOpts) error { req.Body = &preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ - MongodbGroupID: sc.Details[MongoDBAtlasRequiredKeys[1]].(string), + MongodbGroupID: sc.Details[MongoDBAtlasRequiredKeys[1]].AsString(), MongodbRoles: reqRoles, MongodbScopes: reqScopes, }, IntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.Details[MongoDBAtlasRequiredKeys[0]].(string)], + RotationPolicyName: rotationPolicies[sc.Details[MongoDBAtlasRequiredKeys[0]].AsString()], SecretName: opts.SecretName, } resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) @@ -320,7 +339,7 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateAwsDynamicSecretBody{ IntegrationName: sc.IntegrationName, - DefaultTTL: sc.Details[AwsRequiredKeys[0]].(string), + DefaultTTL: sc.Details[AwsRequiredKeys[0]].AsString(), AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ RoleArn: role.RoleArn, }, @@ -352,7 +371,7 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName req.Body = &preview_models.SecretServiceCreateGcpDynamicSecretBody{ IntegrationName: sc.IntegrationName, - DefaultTTL: sc.Details[GcpRequiredKeys[0]].(string), + DefaultTTL: sc.Details[GcpRequiredKeys[0]].AsString(), ServiceAccountImpersonation: &preview_models.Secrets20231128ServiceAccountImpersonationRequest{ ServiceAccountEmail: account.ServiceAccountEmail, }, @@ -425,16 +444,19 @@ func readPlainTextSecret(opts *CreateOpts) error { func readConfigFile(opts *CreateOpts) (SecretConfig, error) { var sc SecretConfig + //var scz SecretConfigZ - f, err := os.ReadFile(opts.SecretFilePath) - if err != nil { - return sc, fmt.Errorf("unable to open config file: %w", err) + if err := hclsimple.DecodeFile(opts.SecretFilePath, nil, &sc); err != nil { + return sc, fmt.Errorf("failed to decode config file: %w", err) } - err = yaml.Unmarshal(f, &sc) - if err != nil { - return sc, fmt.Errorf("unable to unmarshal config file: %w", err) - } + //if err := hclsimple.DecodeFile(opts.SecretFilePath, nil, &scz); err != nil { + // return sc, fmt.Errorf("failed to decode config file: %w", err) + //} + + //fmt.Println("SCZ type: ", scz.Type) + //fmt.Println("SCZ type: ", scz.IntegrationName) + //fmt.Println("SCZ type: ", scz.Details) return sc, nil } @@ -453,7 +475,7 @@ func validateSecretConfig(sc SecretConfig) []string { return missingKeys } -func validateDetails(details map[string]any, requiredKeys []string) []string { +func validateDetails(details map[string]cty.Value, requiredKeys []string) []string { detailsKeys := maps.Keys(details) var missingKeys []string @@ -464,3 +486,26 @@ func validateDetails(details map[string]any, requiredKeys []string) []string { } return missingKeys } + +//func ctyToType(objMap map[string]cty.Value) (map[string]any) { +// obj := make(map[string]any) +// +// for k, v := range objMap { +// switch sv := v. { +// case map[string]any: +// // Recuse and walk the map for its children +// obj[k], _ = anyToCty(sv) +// case float64: +// obj[k] = cty.NumberFloatVal(sv) +// case bool: +// obj[k] = cty.BoolVal(sv) +// case string: +// obj[k] = cty.StringVal(sv) +// default: +// // Unhandled var type +// obj[k] = cty.StringVal(fmt.Sprintf("%v", v)) +// } +// } +// +// return cty.ObjectVal(obj), obj +//} diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index 893a5d13..ea2e0500 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -192,11 +192,11 @@ func TestCreateRun(t *testing.T) { o.Type = secretTypeRotating }, MockCalled: true, - Input: []byte(`version: 1.0.0 -type: "twilio" -integration_name: "Twil-Int-11" -details: - rotation_policy_name: "60"`), + Input: []byte(`type = "twilio" +integration_name = "Twil-Int-11" +details = { + "rotation_policy_name": "60" +}`), }, { Name: "Failed: Missing required rotating secret field", @@ -204,11 +204,11 @@ details: AugmentOpts: func(o *CreateOpts) { o.Type = secretTypeRotating }, - Input: []byte(`version: 1.0.0 -type: "twilio" -integration_name: "Twil-Int-11" -details: - none: "none"`), + Input: []byte(`type = "twilio" +integration_name = "Twil-Int-11" +details = { + "none": "none" +}`), ErrMsg: "missing required field(s) in the config file details: [rotation_policy_name]", }, { @@ -218,13 +218,16 @@ details: o.Type = secretTypeDynamic }, MockCalled: true, - Input: []byte(`version: 1.0.0 -type: "aws" -integration_name: "Aws-Int-12" -details: - default_ttl: "3600s" - assume_role: - role_arn: "ra"`), + Input: []byte(`type = "aws" +integration_name = "Aws-Int-12" + +details = { + "default_ttl" = "3600s" + + "assume_role" = { + "role_arn" = "ra" + } +}`), }, { Name: "Failed: Unsupported secret type", @@ -273,7 +276,7 @@ details: if opts.Type == secretTypeRotating || opts.Type == secretTypeDynamic { tempDir := t.TempDir() - f, err := os.Create(filepath.Join(tempDir, "config.yaml")) + f, err := os.Create(filepath.Join(tempDir, "config.hcl")) r.NoError(err) _, err = f.Write(c.Input) r.NoError(err) From f9149361b4aa14cff392e91b35cfe52c9947fe7d Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 10 Sep 2024 17:19:24 -0500 Subject: [PATCH 49/75] WIP --- .../commands/vaultsecrets/secrets/create.go | 369 ++++++++++-------- 1 file changed, 207 insertions(+), 162 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index c9230fa6..e0102aea 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -5,6 +5,7 @@ package secrets import ( "context" + "encoding/json" "errors" "fmt" "github.com/hashicorp/hcl/v2/hclsimple" @@ -14,7 +15,6 @@ import ( "slices" preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" - preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets/appname" @@ -24,7 +24,6 @@ import ( "github.com/hashicorp/hcp/internal/pkg/heredoc" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" - "github.com/mitchellh/mapstructure" "github.com/posener/complete" "golang.org/x/exp/maps" ) @@ -132,7 +131,13 @@ type SecretConfigZ struct { type SecretConfig struct { Type integrations.IntegrationType `hcl:"type"` IntegrationName string `hcl:"integration_name"` - Details map[string]cty.Value `hcl:"details"` + Details cty.Value `hcl:"details"` +} + +type SecretInternalConfig struct { + Type integrations.IntegrationType `json:"type"` + IntegrationName string `json:"integration_name"` + Details map[string]any `json:"details"` } type MongoDBRole struct { @@ -206,101 +211,101 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.Twilio: - missingDetails := validateDetails(sc.Details, TwilioRequiredKeys) - - if len(missingDetails) > 0 { - return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) - } - - //for detail, value := range sc.Details { + //missingDetails := validateDetails(sc.Details, TwilioRequiredKeys) // - // //for _, key := range TwilioRequiredKeys { - // // if detail - // //} - // value.AsString() + //if len(missingDetails) > 0 { + // return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) + //} + // + ////for detail, value := range sc.Details { + //// + //// //for _, key := range TwilioRequiredKeys { + //// // if detail + //// //} + //// value.AsString() + ////} + // + //req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) + //req.OrganizationID = opts.Profile.OrganizationID + //req.ProjectID = opts.Profile.ProjectID + //req.AppName = opts.AppName + //req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ + // IntegrationName: sc.IntegrationName, + // RotationPolicyName: rotationPolicies[sc.Details[TwilioRequiredKeys[0]].AsString()], + // SecretName: opts.SecretName, + //} + // + //resp, err := opts.PreviewClient.CreateTwilioRotatingSecret(req, nil) + //if err != nil { + // return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + //} + // + //if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { + // return err //} - - req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) - req.OrganizationID = opts.Profile.OrganizationID - req.ProjectID = opts.Profile.ProjectID - req.AppName = opts.AppName - req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ - IntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.Details[TwilioRequiredKeys[0]].AsString()], - SecretName: opts.SecretName, - } - - resp, err := opts.PreviewClient.CreateTwilioRotatingSecret(req, nil) - if err != nil { - return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) - } - - if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { - return err - } case integrations.MongoDBAtlas: - missingDetails := validateDetails(sc.Details, MongoDBAtlasRequiredKeys) - - if len(missingDetails) > 0 { - return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) - } - - req := preview_secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) - req.OrganizationID = opts.Profile.OrganizationID - req.ProjectID = opts.Profile.ProjectID - - roles := sc.Details["mongodb_roles"].AsValueSlice() - var reqRoles []*preview_models.Secrets20231128MongoDBRole - for _, r := range roles { - var role MongoDBRole - decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &role}) - if err := decoder.Decode(r); err != nil { - return fmt.Errorf("unable to decode to a mongodb role") - } - - reqRole := &preview_models.Secrets20231128MongoDBRole{ - CollectionName: role.CollectionName, - RoleName: role.RoleName, - DatabaseName: role.DatabaseName, - } - reqRoles = append(reqRoles, reqRole) - } - - scopes := sc.Details["mongodb_scopes"].AsValueSlice() - var reqScopes []*preview_models.Secrets20231128MongoDBScope - for _, r := range scopes { - var scope MongoDBScope - decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &scope}) - if err := decoder.Decode(r); err != nil { - return fmt.Errorf("unable to decode to a mongodb scope") - } - - reqScope := &preview_models.Secrets20231128MongoDBScope{ - Name: scope.Name, - Type: scope.Type, - } - reqScopes = append(reqScopes, reqScope) - } - - req.Body = &preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ - SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ - MongodbGroupID: sc.Details[MongoDBAtlasRequiredKeys[1]].AsString(), - MongodbRoles: reqRoles, - MongodbScopes: reqScopes, - }, - IntegrationName: sc.IntegrationName, - RotationPolicyName: rotationPolicies[sc.Details[MongoDBAtlasRequiredKeys[0]].AsString()], - SecretName: opts.SecretName, - } - resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) - if err != nil { - return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) - } - - if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { - return err - } + //missingDetails := validateDetails(sc.Details, MongoDBAtlasRequiredKeys) + // + //if len(missingDetails) > 0 { + // return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) + //} + // + //req := preview_secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) + //req.OrganizationID = opts.Profile.OrganizationID + //req.ProjectID = opts.Profile.ProjectID + // + //roles := sc.Details["mongodb_roles"].AsValueSlice() + //var reqRoles []*preview_models.Secrets20231128MongoDBRole + //for _, r := range roles { + // var role MongoDBRole + // decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &role}) + // if err := decoder.Decode(r); err != nil { + // return fmt.Errorf("unable to decode to a mongodb role") + // } + // + // reqRole := &preview_models.Secrets20231128MongoDBRole{ + // CollectionName: role.CollectionName, + // RoleName: role.RoleName, + // DatabaseName: role.DatabaseName, + // } + // reqRoles = append(reqRoles, reqRole) + //} + // + //scopes := sc.Details["mongodb_scopes"].AsValueSlice() + //var reqScopes []*preview_models.Secrets20231128MongoDBScope + //for _, r := range scopes { + // var scope MongoDBScope + // decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &scope}) + // if err := decoder.Decode(r); err != nil { + // return fmt.Errorf("unable to decode to a mongodb scope") + // } + // + // reqScope := &preview_models.Secrets20231128MongoDBScope{ + // Name: scope.Name, + // Type: scope.Type, + // } + // reqScopes = append(reqScopes, reqScope) + //} + // + //req.Body = &preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ + // SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ + // MongodbGroupID: sc.Details[MongoDBAtlasRequiredKeys[1]].AsString(), + // MongodbRoles: reqRoles, + // MongodbScopes: reqScopes, + // }, + // IntegrationName: sc.IntegrationName, + // RotationPolicyName: rotationPolicies[sc.Details[MongoDBAtlasRequiredKeys[0]].AsString()], + // SecretName: opts.SecretName, + //} + //resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) + //if err != nil { + // return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + //} + // + //if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { + // return err + //} default: return fmt.Errorf("unsupported rotating secret provider type") @@ -321,70 +326,70 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.AWS: - missingDetails := validateDetails(sc.Details, AwsRequiredKeys) - - if len(missingDetails) > 0 { - return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) - } - - var role AwsAssumeRole - decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &role}) - if err := decoder.Decode(sc.Details[AwsRequiredKeys[1]]); err != nil { - return fmt.Errorf("unable to decode aws assume_role") - } - - req := preview_secret_service.NewCreateAwsDynamicSecretParamsWithContext(opts.Ctx) - req.OrganizationID = opts.Profile.OrganizationID - req.ProjectID = opts.Profile.ProjectID - req.AppName = opts.AppName - req.Body = &preview_models.SecretServiceCreateAwsDynamicSecretBody{ - IntegrationName: sc.IntegrationName, - DefaultTTL: sc.Details[AwsRequiredKeys[0]].AsString(), - AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ - RoleArn: role.RoleArn, - }, - Name: opts.SecretName, - } - - _, err = opts.PreviewClient.CreateAwsDynamicSecret(req, nil) - if err != nil { - return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) - } + //missingDetails := validateDetails(sc.Details, AwsRequiredKeys) + // + //if len(missingDetails) > 0 { + // return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) + //} + // + //var role AwsAssumeRole + //decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &role}) + //if err := decoder.Decode(sc.Details[AwsRequiredKeys[1]]); err != nil { + // return fmt.Errorf("unable to decode aws assume_role") + //} + // + //req := preview_secret_service.NewCreateAwsDynamicSecretParamsWithContext(opts.Ctx) + //req.OrganizationID = opts.Profile.OrganizationID + //req.ProjectID = opts.Profile.ProjectID + //req.AppName = opts.AppName + //req.Body = &preview_models.SecretServiceCreateAwsDynamicSecretBody{ + // IntegrationName: sc.IntegrationName, + // DefaultTTL: sc.Details[AwsRequiredKeys[0]].AsString(), + // AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ + // RoleArn: role.RoleArn, + // }, + // Name: opts.SecretName, + //} + // + //_, err = opts.PreviewClient.CreateAwsDynamicSecret(req, nil) + //if err != nil { + // return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + //} case integrations.GCP: - missingDetails := validateDetails(sc.Details, GcpRequiredKeys) - - if len(missingDetails) > 0 { - return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) - } - - var account GcpServiceAccount - decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &account}) - if err := decoder.Decode(sc.Details[GcpRequiredKeys[1]]); err != nil { - return fmt.Errorf("unable to decode gcp service_account_impersonation") - } - - req := preview_secret_service.NewCreateGcpDynamicSecretParamsWithContext(opts.Ctx) - req.OrganizationID = opts.Profile.OrganizationID - req.ProjectID = opts.Profile.ProjectID - req.AppName = opts.AppName - req.Body = &preview_models.SecretServiceCreateGcpDynamicSecretBody{ - IntegrationName: sc.IntegrationName, - DefaultTTL: sc.Details[GcpRequiredKeys[0]].AsString(), - ServiceAccountImpersonation: &preview_models.Secrets20231128ServiceAccountImpersonationRequest{ - ServiceAccountEmail: account.ServiceAccountEmail, - }, - Name: opts.SecretName, - } - - _, err := opts.PreviewClient.CreateGcpDynamicSecret(req, nil) - if err != nil { - return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) - } - - default: - return fmt.Errorf("unsupported dynamic secret provider type") + // missingDetails := validateDetails(sc.Details, GcpRequiredKeys) + // + // if len(missingDetails) > 0 { + // return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) + // } + // + // var account GcpServiceAccount + // decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &account}) + // if err := decoder.Decode(sc.Details[GcpRequiredKeys[1]]); err != nil { + // return fmt.Errorf("unable to decode gcp service_account_impersonation") + // } + // + // req := preview_secret_service.NewCreateGcpDynamicSecretParamsWithContext(opts.Ctx) + // req.OrganizationID = opts.Profile.OrganizationID + // req.ProjectID = opts.Profile.ProjectID + // req.AppName = opts.AppName + // req.Body = &preview_models.SecretServiceCreateGcpDynamicSecretBody{ + // IntegrationName: sc.IntegrationName, + // DefaultTTL: sc.Details[GcpRequiredKeys[0]].AsString(), + // ServiceAccountImpersonation: &preview_models.Secrets20231128ServiceAccountImpersonationRequest{ + // ServiceAccountEmail: account.ServiceAccountEmail, + // }, + // Name: opts.SecretName, + // } + // + // _, err := opts.PreviewClient.CreateGcpDynamicSecret(req, nil) + // if err != nil { + // return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + // } + // + //default: + // return fmt.Errorf("unsupported dynamic secret provider type") } default: @@ -444,23 +449,63 @@ func readPlainTextSecret(opts *CreateOpts) error { func readConfigFile(opts *CreateOpts) (SecretConfig, error) { var sc SecretConfig - //var scz SecretConfigZ if err := hclsimple.DecodeFile(opts.SecretFilePath, nil, &sc); err != nil { return sc, fmt.Errorf("failed to decode config file: %w", err) } - //if err := hclsimple.DecodeFile(opts.SecretFilePath, nil, &scz); err != nil { - // return sc, fmt.Errorf("failed to decode config file: %w", err) - //} + varMap, err := ctyValueToMap(sc.Details) + if err != nil { + return sc, fmt.Errorf("error processing config file: %w", err) + } - //fmt.Println("SCZ type: ", scz.Type) - //fmt.Println("SCZ type: ", scz.IntegrationName) - //fmt.Println("SCZ type: ", scz.Details) + fmt.Println("New map: ", varMap) + fmt.Println("Assume role: ", varMap["assume_role"]) + + //ar := varMap["assume_role"] + + type InternalDetails struct { + Details map[string]any + } + + var sic InternalDetails + jsonString, _ := json.Marshal(varMap) + //st := string(jsonString) + fmt.Println("JSON string: ", string(jsonString)) + json.Unmarshal(jsonString, &sic) + fmt.Println("SIC Details: ", sic) return sc, nil } +func ctyValueToMap(ctyVal cty.Value) (map[string]any, error) { + varMap := make(map[string]any) + + //if !ctyVal.Type().IsMapType() { + // fmt.Println("Actual map type: ", ctyVal.Type()) + // return varMap, errors.New("expected details as type map") + //} + + for k, v := range ctyVal.AsValueMap() { + //fmt.Println("K: ", k) + //fmt.Println("V in loop: ", v.IsKnown()) + switch v.Type() { + case cty.String: + fmt.Println("V: ", v.AsString()) + varMap[k] = v.AsString() + case cty.Map(cty.String): + varMap[k] = v.AsValueMap() + default: + fmt.Println("V: ", v.AsValueMap()) + varMap[k] = v.AsValueMap() + } + } + + fmt.Println("Keys: ", varMap) + + return varMap, nil +} + func validateSecretConfig(sc SecretConfig) []string { var missingKeys []string From 35597e61b9ad0361554e142807df7848c992f70c Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 12 Sep 2024 16:01:41 -0500 Subject: [PATCH 50/75] Updates secret creation to support HCL config files --- go.mod | 27 +- go.sum | 52 +-- .../commands/vaultsecrets/secrets/create.go | 431 +++++++----------- .../vaultsecrets/secrets/create_test.go | 66 ++- 4 files changed, 235 insertions(+), 341 deletions(-) diff --git a/go.mod b/go.mod index 4450a14e..39a89a53 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 - github.com/hashicorp/hcl/v2 v2.19.1 + github.com/hashicorp/hcl/v2 v2.22.0 github.com/hashicorp/hcp-sdk-go v0.110.0 github.com/lithammer/dedent v1.1.0 github.com/manifoldco/promptui v0.9.0 @@ -27,17 +27,20 @@ require ( github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/oauth2 v0.15.0 - golang.org/x/term v0.19.0 + golang.org/x/term v0.24.0 ) -require golang.org/x/sync v0.7.0 // indirect +require ( + golang.org/x/mod v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/tools v0.25.0 // indirect +) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect - github.com/agext/levenshtein v1.2.1 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect @@ -72,7 +75,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect @@ -83,16 +86,16 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/zclconf/go-cty v1.13.0 + github.com/zclconf/go-cty v1.15.0 go.mongodb.org/mongo-driver v1.15.0 // indirect go.opentelemetry.io/otel v1.25.0 // indirect go.opentelemetry.io/otel/metric v1.25.0 // indirect go.opentelemetry.io/otel/trace v1.25.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/yaml.v3 v3.0.1 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8c976c8c..4fb04a01 100644 --- a/go.sum +++ b/go.sum @@ -8,10 +8,8 @@ github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYr github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -93,8 +91,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= -github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/hcp-sdk-go v0.110.0 h1:eaDO6XoEb0H+g00Ka3C+ZbRibhwWyA2YNmv48xFCL2w= github.com/hashicorp/hcp-sdk-go v0.110.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -111,8 +109,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -144,8 +140,8 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -175,8 +171,6 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -201,8 +195,10 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= -github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= +github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= @@ -218,25 +214,27 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -252,23 +250,25 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index e0102aea..c4bcb647 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -8,13 +8,15 @@ import ( "encoding/json" "errors" "fmt" - "github.com/hashicorp/hcl/v2/hclsimple" - "github.com/zclconf/go-cty/cty" "io" "os" - "slices" + "github.com/posener/complete" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/hcl/v2/hclsimple" preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets/appname" @@ -24,8 +26,6 @@ import ( "github.com/hashicorp/hcp/internal/pkg/heredoc" "github.com/hashicorp/hcp/internal/pkg/iostreams" "github.com/hashicorp/hcp/internal/pkg/profile" - "github.com/posener/complete" - "golang.org/x/exp/maps" ) func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { @@ -122,10 +122,8 @@ type CreateOpts struct { Client secret_service.ClientService } -type SecretConfigZ struct { - Type integrations.IntegrationType `hcl:"type"` - IntegrationName string `hcl:"integration_name"` - Details map[string]string `hcl:"details"` +type DetailsInternal struct { + Details map[string]any } type SecretConfig struct { @@ -134,38 +132,6 @@ type SecretConfig struct { Details cty.Value `hcl:"details"` } -type SecretInternalConfig struct { - Type integrations.IntegrationType `json:"type"` - IntegrationName string `json:"integration_name"` - Details map[string]any `json:"details"` -} - -type MongoDBRole struct { - RoleName string `mapstructure:"role_name"` - DatabaseName string `mapstructure:"database_name"` - CollectionName string `mapstructure:"collection_name"` -} - -type MongoDBScope struct { - Name string `mapstructure:"type"` - Type string `mapstructure:"name"` -} - -type AwsAssumeRole struct { - RoleArn string `mapstructure:"role_arn"` -} - -type GcpServiceAccount struct { - ServiceAccountEmail string `mapstructure:"service_account_email"` -} - -var ( - TwilioRequiredKeys = []string{"rotation_policy_name"} - MongoDBAtlasRequiredKeys = []string{"rotation_policy_name", "mongodb_group_id", "mongodb_roles"} - AwsRequiredKeys = []string{"default_ttl", "assume_role"} - GcpRequiredKeys = []string{"default_ttl", "service_account_impersonation"} -) - var rotationPolicies = map[string]string{ "30": "built-in:30-days-2-active", "60": "built-in:60-days-2-active", @@ -198,7 +164,7 @@ func createRun(opts *CreateOpts) error { return err } case secretTypeRotating: - sc, err := readConfigFile(opts) + sc, di, err := readConfigFile(opts) if err != nil { return fmt.Errorf("failed to process config file: %w", err) } @@ -211,112 +177,75 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.Twilio: - //missingDetails := validateDetails(sc.Details, TwilioRequiredKeys) - // - //if len(missingDetails) > 0 { - // return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) - //} - // - ////for detail, value := range sc.Details { - //// - //// //for _, key := range TwilioRequiredKeys { - //// // if detail - //// //} - //// value.AsString() - ////} - // - //req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) - //req.OrganizationID = opts.Profile.OrganizationID - //req.ProjectID = opts.Profile.ProjectID - //req.AppName = opts.AppName - //req.Body = &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ - // IntegrationName: sc.IntegrationName, - // RotationPolicyName: rotationPolicies[sc.Details[TwilioRequiredKeys[0]].AsString()], - // SecretName: opts.SecretName, - //} - // - //resp, err := opts.PreviewClient.CreateTwilioRotatingSecret(req, nil) - //if err != nil { - // return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) - //} - // - //if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { - // return err - //} + req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + + var twilioBody preview_models.SecretServiceCreateTwilioRotatingSecretBody + detailBytes, err := json.Marshal(di.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = twilioBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + twilioBody.IntegrationName = sc.IntegrationName + twilioBody.SecretName = opts.SecretName + req.Body = &twilioBody + + resp, err := opts.PreviewClient.CreateTwilioRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } + + if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { + return err + } case integrations.MongoDBAtlas: - //missingDetails := validateDetails(sc.Details, MongoDBAtlasRequiredKeys) - // - //if len(missingDetails) > 0 { - // return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) - //} - // - //req := preview_secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) - //req.OrganizationID = opts.Profile.OrganizationID - //req.ProjectID = opts.Profile.ProjectID - // - //roles := sc.Details["mongodb_roles"].AsValueSlice() - //var reqRoles []*preview_models.Secrets20231128MongoDBRole - //for _, r := range roles { - // var role MongoDBRole - // decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &role}) - // if err := decoder.Decode(r); err != nil { - // return fmt.Errorf("unable to decode to a mongodb role") - // } - // - // reqRole := &preview_models.Secrets20231128MongoDBRole{ - // CollectionName: role.CollectionName, - // RoleName: role.RoleName, - // DatabaseName: role.DatabaseName, - // } - // reqRoles = append(reqRoles, reqRole) - //} - // - //scopes := sc.Details["mongodb_scopes"].AsValueSlice() - //var reqScopes []*preview_models.Secrets20231128MongoDBScope - //for _, r := range scopes { - // var scope MongoDBScope - // decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &scope}) - // if err := decoder.Decode(r); err != nil { - // return fmt.Errorf("unable to decode to a mongodb scope") - // } - // - // reqScope := &preview_models.Secrets20231128MongoDBScope{ - // Name: scope.Name, - // Type: scope.Type, - // } - // reqScopes = append(reqScopes, reqScope) - //} - // - //req.Body = &preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ - // SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ - // MongodbGroupID: sc.Details[MongoDBAtlasRequiredKeys[1]].AsString(), - // MongodbRoles: reqRoles, - // MongodbScopes: reqScopes, - // }, - // IntegrationName: sc.IntegrationName, - // RotationPolicyName: rotationPolicies[sc.Details[MongoDBAtlasRequiredKeys[0]].AsString()], - // SecretName: opts.SecretName, - //} - //resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) - //if err != nil { - // return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) - //} - // - //if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { - // return err - //} + + req := preview_secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + + var mongoDBBody preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody + detailBytes, err := json.Marshal(di.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = mongoDBBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + mongoDBBody.IntegrationName = sc.IntegrationName + mongoDBBody.SecretName = opts.SecretName + req.Body = &mongoDBBody + + resp, err := opts.PreviewClient.CreateMongoDBAtlasRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } + + if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { + return err + } default: return fmt.Errorf("unsupported rotating secret provider type") } case secretTypeDynamic: - sc, err := readConfigFile(opts) + sc, di, err := readConfigFile(opts) if err != nil { return fmt.Errorf("failed to process config file: %w", err) } - missingFields := validateSecretConfig(sc) if len(missingFields) > 0 { @@ -325,71 +254,59 @@ func createRun(opts *CreateOpts) error { switch sc.Type { case integrations.AWS: + req := preview_secret_service.NewCreateAwsDynamicSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + + var awsBody preview_models.SecretServiceCreateAwsDynamicSecretBody + detailBytes, err := json.Marshal(di.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = awsBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + awsBody.IntegrationName = sc.IntegrationName + awsBody.Name = opts.SecretName + req.Body = &awsBody - //missingDetails := validateDetails(sc.Details, AwsRequiredKeys) - // - //if len(missingDetails) > 0 { - // return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) - //} - // - //var role AwsAssumeRole - //decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &role}) - //if err := decoder.Decode(sc.Details[AwsRequiredKeys[1]]); err != nil { - // return fmt.Errorf("unable to decode aws assume_role") - //} - // - //req := preview_secret_service.NewCreateAwsDynamicSecretParamsWithContext(opts.Ctx) - //req.OrganizationID = opts.Profile.OrganizationID - //req.ProjectID = opts.Profile.ProjectID - //req.AppName = opts.AppName - //req.Body = &preview_models.SecretServiceCreateAwsDynamicSecretBody{ - // IntegrationName: sc.IntegrationName, - // DefaultTTL: sc.Details[AwsRequiredKeys[0]].AsString(), - // AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ - // RoleArn: role.RoleArn, - // }, - // Name: opts.SecretName, - //} - // - //_, err = opts.PreviewClient.CreateAwsDynamicSecret(req, nil) - //if err != nil { - // return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) - //} + _, err = opts.PreviewClient.CreateAwsDynamicSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } case integrations.GCP: + req := preview_secret_service.NewCreateGcpDynamicSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + + var gcpBody preview_models.SecretServiceCreateGcpDynamicSecretBody + detailBytes, err := json.Marshal(di.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = gcpBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + gcpBody.IntegrationName = sc.IntegrationName + gcpBody.Name = opts.SecretName + req.Body = &gcpBody - // missingDetails := validateDetails(sc.Details, GcpRequiredKeys) - // - // if len(missingDetails) > 0 { - // return fmt.Errorf("missing required field(s) in the config file details: %s", missingDetails) - // } - // - // var account GcpServiceAccount - // decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{WeaklyTypedInput: true, Result: &account}) - // if err := decoder.Decode(sc.Details[GcpRequiredKeys[1]]); err != nil { - // return fmt.Errorf("unable to decode gcp service_account_impersonation") - // } - // - // req := preview_secret_service.NewCreateGcpDynamicSecretParamsWithContext(opts.Ctx) - // req.OrganizationID = opts.Profile.OrganizationID - // req.ProjectID = opts.Profile.ProjectID - // req.AppName = opts.AppName - // req.Body = &preview_models.SecretServiceCreateGcpDynamicSecretBody{ - // IntegrationName: sc.IntegrationName, - // DefaultTTL: sc.Details[GcpRequiredKeys[0]].AsString(), - // ServiceAccountImpersonation: &preview_models.Secrets20231128ServiceAccountImpersonationRequest{ - // ServiceAccountEmail: account.ServiceAccountEmail, - // }, - // Name: opts.SecretName, - // } - // - // _, err := opts.PreviewClient.CreateGcpDynamicSecret(req, nil) - // if err != nil { - // return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) - // } - // - //default: - // return fmt.Errorf("unsupported dynamic secret provider type") + _, err = opts.PreviewClient.CreateGcpDynamicSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } + + default: + return fmt.Errorf("unsupported dynamic secret provider type") } default: @@ -447,63 +364,56 @@ func readPlainTextSecret(opts *CreateOpts) error { return nil } -func readConfigFile(opts *CreateOpts) (SecretConfig, error) { - var sc SecretConfig +func readConfigFile(opts *CreateOpts) (SecretConfig, DetailsInternal, error) { + var ( + sc SecretConfig + di DetailsInternal + ) if err := hclsimple.DecodeFile(opts.SecretFilePath, nil, &sc); err != nil { - return sc, fmt.Errorf("failed to decode config file: %w", err) + return sc, di, fmt.Errorf("failed to decode config file: %w", err) } - varMap, err := ctyValueToMap(sc.Details) + detailsMap, err := ctyValueToMap(sc.Details) if err != nil { - return sc, fmt.Errorf("error processing config file: %w", err) + return sc, di, err } + di.Details = detailsMap - fmt.Println("New map: ", varMap) - fmt.Println("Assume role: ", varMap["assume_role"]) - - //ar := varMap["assume_role"] - - type InternalDetails struct { - Details map[string]any - } - - var sic InternalDetails - jsonString, _ := json.Marshal(varMap) - //st := string(jsonString) - fmt.Println("JSON string: ", string(jsonString)) - json.Unmarshal(jsonString, &sic) - fmt.Println("SIC Details: ", sic) - - return sc, nil + return sc, di, nil } -func ctyValueToMap(ctyVal cty.Value) (map[string]any, error) { - varMap := make(map[string]any) - - //if !ctyVal.Type().IsMapType() { - // fmt.Println("Actual map type: ", ctyVal.Type()) - // return varMap, errors.New("expected details as type map") - //} - - for k, v := range ctyVal.AsValueMap() { - //fmt.Println("K: ", k) - //fmt.Println("V in loop: ", v.IsKnown()) - switch v.Type() { - case cty.String: - fmt.Println("V: ", v.AsString()) - varMap[k] = v.AsString() - case cty.Map(cty.String): - varMap[k] = v.AsValueMap() - default: - fmt.Println("V: ", v.AsValueMap()) - varMap[k] = v.AsValueMap() +func ctyValueToMap(value cty.Value) (map[string]any, error) { + varMapNow := make(map[string]any) + for k, v := range value.AsValueMap() { + if v.Type() == cty.String { + if k == "rotation_policy_name" { + varMapNow[k] = rotationPolicies[v.AsString()] + } else { + varMapNow[k] = v.AsString() + } + } else if v.Type().IsObjectType() { + nestedMap, err := ctyValueToMap(v) + if err != nil { + return nil, err + } + varMapNow[k] = nestedMap + } else if v.Type().IsTupleType() { + var roles []map[string]any + for _, val := range v.AsValueSlice() { + nestedMap, err := ctyValueToMap(val) + if err != nil { + return nil, err + } + roles = append(roles, nestedMap) + } + varMapNow[k] = roles + } else { + return nil, fmt.Errorf("found unsupported value type") } } - fmt.Println("Keys: ", varMap) - - return varMap, nil + return varMapNow, nil } func validateSecretConfig(sc SecretConfig) []string { @@ -519,38 +429,3 @@ func validateSecretConfig(sc SecretConfig) []string { return missingKeys } - -func validateDetails(details map[string]cty.Value, requiredKeys []string) []string { - detailsKeys := maps.Keys(details) - var missingKeys []string - - for _, r := range requiredKeys { - if !slices.Contains(detailsKeys, r) { - missingKeys = append(missingKeys, r) - } - } - return missingKeys -} - -//func ctyToType(objMap map[string]cty.Value) (map[string]any) { -// obj := make(map[string]any) -// -// for k, v := range objMap { -// switch sv := v. { -// case map[string]any: -// // Recuse and walk the map for its children -// obj[k], _ = anyToCty(sv) -// case float64: -// obj[k] = cty.NumberFloatVal(sv) -// case bool: -// obj[k] = cty.BoolVal(sv) -// case string: -// obj[k] = cty.StringVal(sv) -// default: -// // Unhandled var type -// obj[k] = cty.StringVal(fmt.Sprintf("%v", v)) -// } -// } -// -// return cty.ObjectVal(obj), obj -//} diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index ea2e0500..accfb0e1 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -186,30 +186,30 @@ func TestCreateRun(t *testing.T) { MockCalled: true, }, { - Name: "Success: Create a Twilio rotating secret", + Name: "Success: Create a MongoDB rotating secret", RespErr: false, AugmentOpts: func(o *CreateOpts) { o.Type = secretTypeRotating }, MockCalled: true, - Input: []byte(`type = "twilio" -integration_name = "Twil-Int-11" -details = { - "rotation_policy_name": "60" -}`), - }, - { - Name: "Failed: Missing required rotating secret field", - RespErr: true, - AugmentOpts: func(o *CreateOpts) { - o.Type = secretTypeRotating - }, - Input: []byte(`type = "twilio" -integration_name = "Twil-Int-11" -details = { - "none": "none" + Input: []byte(`type = "mongodb-atlas" +integration_name = "mongo-db-integration" +details = { + rotation_policy_name: "60" + secret_details = { + mongodb_group_id = "mbdgi" + mongodb_roles = [{ + "role_name" = "rn1" + "database_name" = "dn1" + "collection_name" = "cn1" + }, + { + "role_name" = "rn2" + "database_name" = "dn2" + "collection_name" = "cn2" + }] + } }`), - ErrMsg: "missing required field(s) in the config file details: [rotation_policy_name]", }, { Name: "Success: Create an Aws dynamic secret", @@ -219,6 +219,7 @@ details = { }, MockCalled: true, Input: []byte(`type = "aws" + integration_name = "Aws-Int-12" details = { @@ -317,24 +318,39 @@ details = { } else if opts.Type == secretTypeRotating { if c.MockCalled { if c.RespErr { - pvs.EXPECT().CreateTwilioRotatingSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + pvs.EXPECT().CreateMongoDBAtlasRotatingSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() } else { - pvs.EXPECT().CreateTwilioRotatingSecret(&preview_secret_service.CreateTwilioRotatingSecretParams{ + pvs.EXPECT().CreateMongoDBAtlasRotatingSecret(&preview_secret_service.CreateMongoDBAtlasRotatingSecretParams{ OrganizationID: testProfile(t).OrganizationID, ProjectID: testProfile(t).ProjectID, AppName: testProfile(t).VaultSecrets.AppName, - Body: &preview_models.SecretServiceCreateTwilioRotatingSecretBody{ + Body: &preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ SecretName: opts.SecretName, - IntegrationName: "Twil-Int-11", + IntegrationName: "mongo-db-integration", RotationPolicyName: "built-in:60-days-2-active", + SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ + MongodbGroupID: "mbdgi", + MongodbRoles: []*preview_models.Secrets20231128MongoDBRole{ + { + RoleName: "rn1", + DatabaseName: "dn1", + CollectionName: "cn1", + }, + { + RoleName: "rn2", + DatabaseName: "dn2", + CollectionName: "cn2", + }, + }, + }, }, Context: opts.Ctx, - }, mock.Anything).Return(&preview_secret_service.CreateTwilioRotatingSecretOK{ - Payload: &preview_models.Secrets20231128CreateTwilioRotatingSecretResponse{ + }, mock.Anything).Return(&preview_secret_service.CreateMongoDBAtlasRotatingSecretOK{ + Payload: &preview_models.Secrets20231128CreateMongoDBAtlasRotatingSecretResponse{ Config: &preview_models.Secrets20231128RotatingSecretConfig{ AppName: opts.AppName, CreatedAt: dt, - IntegrationName: "Twil-Int-11", + IntegrationName: "mongo-db-integration", RotationPolicyName: "built-in:60-days-2-active", SecretName: opts.SecretName, }, From 3fe0bbb1dd130d103e5c942d0c95b58243692c6d Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 16 Sep 2024 11:33:16 -0500 Subject: [PATCH 51/75] makes requested changes --- internal/commands/vaultsecrets/secrets/create.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index c4bcb647..7fcdb294 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -122,7 +122,7 @@ type CreateOpts struct { Client secret_service.ClientService } -type DetailsInternal struct { +type detailsInternal struct { Details map[string]any } @@ -364,10 +364,10 @@ func readPlainTextSecret(opts *CreateOpts) error { return nil } -func readConfigFile(opts *CreateOpts) (SecretConfig, DetailsInternal, error) { +func readConfigFile(opts *CreateOpts) (SecretConfig, detailsInternal, error) { var ( sc SecretConfig - di DetailsInternal + di detailsInternal ) if err := hclsimple.DecodeFile(opts.SecretFilePath, nil, &sc); err != nil { From 62ab1f816b533d4ca006eb6af4bf8bee40b02207 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 16 Sep 2024 15:35:09 -0500 Subject: [PATCH 52/75] updates variable name --- internal/commands/vaultsecrets/secrets/create.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 7fcdb294..6da6ee57 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -399,15 +399,15 @@ func ctyValueToMap(value cty.Value) (map[string]any, error) { } varMapNow[k] = nestedMap } else if v.Type().IsTupleType() { - var roles []map[string]any + var items []map[string]any for _, val := range v.AsValueSlice() { nestedMap, err := ctyValueToMap(val) if err != nil { return nil, err } - roles = append(roles, nestedMap) + items = append(items, nestedMap) } - varMapNow[k] = roles + varMapNow[k] = items } else { return nil, fmt.Errorf("found unsupported value type") } From 83ac2d61756facc5fcb32765fe047aa970855a92 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 17 Sep 2024 14:52:27 -0500 Subject: [PATCH 53/75] makes requested changes --- .../commands/vaultsecrets/secrets/create.go | 74 ++++++++----------- .../vaultsecrets/secrets/create_test.go | 2 +- 2 files changed, 33 insertions(+), 43 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 6da6ee57..e84d9880 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -122,7 +122,7 @@ type CreateOpts struct { Client secret_service.ClientService } -type detailsInternal struct { +type secretConfigInternal struct { Details map[string]any } @@ -132,12 +132,6 @@ type SecretConfig struct { Details cty.Value `hcl:"details"` } -var rotationPolicies = map[string]string{ - "30": "built-in:30-days-2-active", - "60": "built-in:60-days-2-active", - "90": "built-in:90-days-2-active", -} - func createRun(opts *CreateOpts) error { switch opts.Type { case secretTypeKV, "": @@ -164,18 +158,18 @@ func createRun(opts *CreateOpts) error { return err } case secretTypeRotating: - sc, di, err := readConfigFile(opts) + secretConfig, internalConfig, err := readConfigFile(opts) if err != nil { return fmt.Errorf("failed to process config file: %w", err) } - missingFields := validateSecretConfig(sc) + missingFields := validateSecretConfig(secretConfig) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) } - switch sc.Type { + switch secretConfig.Type { case integrations.Twilio: req := preview_secret_service.NewCreateTwilioRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID @@ -183,7 +177,7 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName var twilioBody preview_models.SecretServiceCreateTwilioRotatingSecretBody - detailBytes, err := json.Marshal(di.Details) + detailBytes, err := json.Marshal(internalConfig.Details) if err != nil { return fmt.Errorf("error marshaling details config: %w", err) } @@ -193,7 +187,7 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("error marshaling details config: %w", err) } - twilioBody.IntegrationName = sc.IntegrationName + twilioBody.IntegrationName = secretConfig.IntegrationName twilioBody.SecretName = opts.SecretName req.Body = &twilioBody @@ -214,7 +208,7 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName var mongoDBBody preview_models.SecretServiceCreateMongoDBAtlasRotatingSecretBody - detailBytes, err := json.Marshal(di.Details) + detailBytes, err := json.Marshal(internalConfig.Details) if err != nil { return fmt.Errorf("error marshaling details config: %w", err) } @@ -224,7 +218,7 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("error marshaling details config: %w", err) } - mongoDBBody.IntegrationName = sc.IntegrationName + mongoDBBody.IntegrationName = secretConfig.IntegrationName mongoDBBody.SecretName = opts.SecretName req.Body = &mongoDBBody @@ -242,17 +236,17 @@ func createRun(opts *CreateOpts) error { } case secretTypeDynamic: - sc, di, err := readConfigFile(opts) + secretConfig, internalConfig, err := readConfigFile(opts) if err != nil { return fmt.Errorf("failed to process config file: %w", err) } - missingFields := validateSecretConfig(sc) + missingFields := validateSecretConfig(secretConfig) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) } - switch sc.Type { + switch secretConfig.Type { case integrations.AWS: req := preview_secret_service.NewCreateAwsDynamicSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID @@ -260,7 +254,7 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName var awsBody preview_models.SecretServiceCreateAwsDynamicSecretBody - detailBytes, err := json.Marshal(di.Details) + detailBytes, err := json.Marshal(internalConfig.Details) if err != nil { return fmt.Errorf("error marshaling details config: %w", err) } @@ -270,7 +264,7 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("error marshaling details config: %w", err) } - awsBody.IntegrationName = sc.IntegrationName + awsBody.IntegrationName = secretConfig.IntegrationName awsBody.Name = opts.SecretName req.Body = &awsBody @@ -286,7 +280,7 @@ func createRun(opts *CreateOpts) error { req.AppName = opts.AppName var gcpBody preview_models.SecretServiceCreateGcpDynamicSecretBody - detailBytes, err := json.Marshal(di.Details) + detailBytes, err := json.Marshal(internalConfig.Details) if err != nil { return fmt.Errorf("error marshaling details config: %w", err) } @@ -296,7 +290,7 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("error marshaling details config: %w", err) } - gcpBody.IntegrationName = sc.IntegrationName + gcpBody.IntegrationName = secretConfig.IntegrationName gcpBody.Name = opts.SecretName req.Body = &gcpBody @@ -364,40 +358,36 @@ func readPlainTextSecret(opts *CreateOpts) error { return nil } -func readConfigFile(opts *CreateOpts) (SecretConfig, detailsInternal, error) { +func readConfigFile(opts *CreateOpts) (SecretConfig, secretConfigInternal, error) { var ( - sc SecretConfig - di detailsInternal + secretConfig SecretConfig + internalConfig secretConfigInternal ) - if err := hclsimple.DecodeFile(opts.SecretFilePath, nil, &sc); err != nil { - return sc, di, fmt.Errorf("failed to decode config file: %w", err) + if err := hclsimple.DecodeFile(opts.SecretFilePath, nil, &secretConfig); err != nil { + return secretConfig, internalConfig, fmt.Errorf("failed to decode config file: %w", err) } - detailsMap, err := ctyValueToMap(sc.Details) + detailsMap, err := ctyValueToMap(secretConfig.Details) if err != nil { - return sc, di, err + return secretConfig, internalConfig, err } - di.Details = detailsMap + internalConfig.Details = detailsMap - return sc, di, nil + return secretConfig, internalConfig, nil } func ctyValueToMap(value cty.Value) (map[string]any, error) { - varMapNow := make(map[string]any) + fieldsMap := make(map[string]any) for k, v := range value.AsValueMap() { if v.Type() == cty.String { - if k == "rotation_policy_name" { - varMapNow[k] = rotationPolicies[v.AsString()] - } else { - varMapNow[k] = v.AsString() - } + fieldsMap[k] = v.AsString() } else if v.Type().IsObjectType() { nestedMap, err := ctyValueToMap(v) if err != nil { return nil, err } - varMapNow[k] = nestedMap + fieldsMap[k] = nestedMap } else if v.Type().IsTupleType() { var items []map[string]any for _, val := range v.AsValueSlice() { @@ -407,23 +397,23 @@ func ctyValueToMap(value cty.Value) (map[string]any, error) { } items = append(items, nestedMap) } - varMapNow[k] = items + fieldsMap[k] = items } else { return nil, fmt.Errorf("found unsupported value type") } } - return varMapNow, nil + return fieldsMap, nil } -func validateSecretConfig(sc SecretConfig) []string { +func validateSecretConfig(secretConfig SecretConfig) []string { var missingKeys []string - if sc.Type == "" { + if secretConfig.Type == "" { missingKeys = append(missingKeys, "type") } - if sc.IntegrationName == "" { + if secretConfig.IntegrationName == "" { missingKeys = append(missingKeys, "integration_name") } diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index accfb0e1..b686af2a 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -195,7 +195,7 @@ func TestCreateRun(t *testing.T) { Input: []byte(`type = "mongodb-atlas" integration_name = "mongo-db-integration" details = { - rotation_policy_name: "60" + rotation_policy_name: "built-in:60-days-2-active" secret_details = { mongodb_group_id = "mbdgi" mongodb_roles = [{ From 4cae8b5d9cea01546bea1b8a8425bef87fdee99e Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 13 Sep 2024 16:47:25 -0500 Subject: [PATCH 54/75] Update rotating secrets --- go.mod | 2 +- go.sum | 2 + .../commands/vaultsecrets/secrets/create.go | 10 +- .../commands/vaultsecrets/secrets/secrets.go | 1 + .../commands/vaultsecrets/secrets/update.go | 268 +++++++++++++++++ .../vaultsecrets/secrets/update_test.go | 278 ++++++++++++++++++ 6 files changed, 556 insertions(+), 5 deletions(-) create mode 100644 internal/commands/vaultsecrets/secrets/update.go create mode 100644 internal/commands/vaultsecrets/secrets/update_test.go diff --git a/go.mod b/go.mod index 39a89a53..472118bd 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl/v2 v2.22.0 - github.com/hashicorp/hcp-sdk-go v0.110.0 + github.com/hashicorp/hcp-sdk-go v0.112.0 github.com/lithammer/dedent v1.1.0 github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/cli v1.1.5 diff --git a/go.sum b/go.sum index 4fb04a01..0ce6d821 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,8 @@ github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul1 github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/hcp-sdk-go v0.110.0 h1:eaDO6XoEb0H+g00Ka3C+ZbRibhwWyA2YNmv48xFCL2w= github.com/hashicorp/hcp-sdk-go v0.110.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +github.com/hashicorp/hcp-sdk-go v0.112.0 h1:gKzxaPhzJj4NobFw7Sc1rGf3nMSqUKBgTtsbZ6bzd14= +github.com/hashicorp/hcp-sdk-go v0.112.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index e84d9880..669f036e 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -158,7 +158,7 @@ func createRun(opts *CreateOpts) error { return err } case secretTypeRotating: - secretConfig, internalConfig, err := readConfigFile(opts) + secretConfig, internalConfig, err := readConfigFile(opts.SecretFilePath) if err != nil { return fmt.Errorf("failed to process config file: %w", err) } @@ -236,7 +236,7 @@ func createRun(opts *CreateOpts) error { } case secretTypeDynamic: - secretConfig, internalConfig, err := readConfigFile(opts) + secretConfig, internalConfig, err := readConfigFile(opts.SecretFilePath) if err != nil { return fmt.Errorf("failed to process config file: %w", err) } @@ -358,13 +358,13 @@ func readPlainTextSecret(opts *CreateOpts) error { return nil } -func readConfigFile(opts *CreateOpts) (SecretConfig, secretConfigInternal, error) { +func readConfigFile(filePath string) (SecretConfig, secretConfigInternal, error) { var ( secretConfig SecretConfig internalConfig secretConfigInternal ) - if err := hclsimple.DecodeFile(opts.SecretFilePath, nil, &secretConfig); err != nil { + if err := hclsimple.DecodeFile(filePath, nil, &secretConfig); err != nil { return secretConfig, internalConfig, fmt.Errorf("failed to decode config file: %w", err) } @@ -382,6 +382,8 @@ func ctyValueToMap(value cty.Value) (map[string]any, error) { for k, v := range value.AsValueMap() { if v.Type() == cty.String { fieldsMap[k] = v.AsString() + } else if v.Type() == cty.Bool { + fieldsMap[k] = v.True() } else if v.Type().IsObjectType() { nestedMap, err := ctyValueToMap(v) if err != nil { diff --git a/internal/commands/vaultsecrets/secrets/secrets.go b/internal/commands/vaultsecrets/secrets/secrets.go index f91e20bf..2f941dc3 100644 --- a/internal/commands/vaultsecrets/secrets/secrets.go +++ b/internal/commands/vaultsecrets/secrets/secrets.go @@ -49,6 +49,7 @@ func NewCmdSecrets(ctx *cmd.Context) *cmd.Command { cmd.AddChild(NewCmdList(ctx, nil)) cmd.AddChild(NewCmdOpen(ctx, nil)) cmd.AddChild(NewCmdRotate(ctx, nil)) + cmd.AddChild(NewCmdUpdate(ctx, nil)) cmd.AddChild(versions.NewCmdVersions(ctx)) return cmd diff --git a/internal/commands/vaultsecrets/secrets/update.go b/internal/commands/vaultsecrets/secrets/update.go new file mode 100644 index 00000000..5ec9b9cc --- /dev/null +++ b/internal/commands/vaultsecrets/secrets/update.go @@ -0,0 +1,268 @@ +package secrets + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/posener/complete" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/hcl/v2/hclsimple" + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/integrations" + "github.com/hashicorp/hcp/internal/commands/vaultsecrets/secrets/appname" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +type UpdateOpts struct { + Ctx context.Context + Profile *profile.Profile + IO iostreams.IOStreams + Output *format.Outputter + + AppName string + SecretName string + SecretValuePlaintext string + SecretFilePath string + Type string + PreviewClient preview_secret_service.ClientService + Client secret_service.ClientService +} + +func NewCmdUpdate(ctx *cmd.Context, runF func(*UpdateOpts) error) *cmd.Command { + opts := &UpdateOpts{ + Ctx: ctx.ShutdownCtx, + Profile: ctx.Profile, + IO: ctx.IO, + Output: ctx.Output, + PreviewClient: preview_secret_service.New(ctx.HCP, nil), + Client: secret_service.New(ctx.HCP, nil), + } + + cmd := &cmd.Command{ + Name: "update", + ShortHelp: "Update an existing secret.", + LongHelp: heredoc.New(ctx.IO).Must(` + The {{ template "mdCodeOrBold" "hcp vault-secrets secrets update" }} command updates an existing rotating or dynamic secret under a Vault Secrets application. + `), + Examples: []cmd.Example{ + { + Preamble: `Update a rotating secret in the Vault Secrets application on your active profile:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets secrets update secret_1 --secret-type=rotating --data-file=tmp/secrets1.txt + `), + }, + }, + Args: cmd.PositionalArguments{ + Args: []cmd.PositionalArgument{ + { + Name: "NAME", + Documentation: "The name of the secret to update.", + }, + }, + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "data-file", + DisplayValue: "DATA_FILE_PATH", + Description: "File path to read secret data from. Set this to '-' to read the secret data from stdin.", + Value: flagvalue.Simple("", &opts.SecretFilePath), + Required: true, + Autocomplete: complete.PredictOr( + complete.PredictFiles("*"), + complete.PredictSet("-"), + ), + }, + { + Name: "secret-type", + DisplayValue: "SECRET_TYPE", + Description: "The type of secret to update: rotating or dynamic.", + Value: flagvalue.Simple("", &opts.Type), + }, + }, + }, + RunF: func(c *cmd.Command, args []string) error { + opts.AppName = appname.Get() + opts.SecretName = args[0] + + if runF != nil { + return runF(opts) + } + return updateRun(opts) + }, + } + + return cmd +} + +type SecretUpdateConfig struct { + Type integrations.IntegrationType `hcl:"type"` + Details cty.Value `hcl:"details"` +} + +func updateRun(opts *UpdateOpts) error { + switch opts.Type { + case secretTypeRotating: + sc, di, err := readUpdateConfigFile(opts.SecretFilePath) + if err != nil { + return fmt.Errorf("failed to process config file: %w", err) + } + + missingFields := validateSecretUpdateConfig(sc) + + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + } + + switch sc.Type { + case integrations.Twilio: + req := preview_secret_service.NewUpdateTwilioRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + req.SecretName = opts.SecretName + + var twilioBody preview_models.SecretServiceUpdateTwilioRotatingSecretBody + detailBytes, err := json.Marshal(di.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = twilioBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + req.Body = &twilioBody + + resp, err := opts.PreviewClient.UpdateTwilioRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to update secret with name %q: %w", opts.SecretName, err) + } + + if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { + return err + } + + case integrations.MongoDBAtlas: + req := preview_secret_service.NewUpdateMongoDBAtlasRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + req.SecretName = opts.SecretName + + var mongoDBBody preview_models.SecretServiceUpdateMongoDBAtlasRotatingSecretBody + detailBytes, err := json.Marshal(di.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = mongoDBBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + req.Body = &mongoDBBody + + resp, err := opts.PreviewClient.UpdateMongoDBAtlasRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to update secret with name %q: %w", opts.SecretName, err) + } + + if err := opts.Output.Display(newRotatingSecretsDisplayer(true).PreviewRotatingSecrets(resp.Payload.Config)); err != nil { + return err + } + + case integrations.AWS: + req := preview_secret_service.NewUpdateAwsIAMUserAccessKeyRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + + var awsBody preview_models.SecretServiceUpdateAwsIAMUserAccessKeyRotatingSecretBody + detailBytes, err := json.Marshal(di.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = awsBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + req.Body = &awsBody + + _, err = opts.PreviewClient.UpdateAwsIAMUserAccessKeyRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to update secret with name %q: %w", opts.SecretName, err) + } + + case integrations.GCP: + req := preview_secret_service.NewUpdateGcpServiceAccountKeyRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + + var gcpBody preview_models.SecretServiceUpdateGcpServiceAccountKeyRotatingSecretBody + detailBytes, err := json.Marshal(di.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = gcpBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + req.Body = &gcpBody + + _, err = opts.PreviewClient.UpdateGcpServiceAccountKeyRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to update secret with name %q: %w", opts.SecretName, err) + } + } + + default: + return fmt.Errorf("%q is an unsupported secret type; \"rotating\" and \"dynamic\" are available types", opts.Type) + } + + fmt.Fprintf(opts.IO.Err(), "%s Successfully updated secret with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.SecretName) + + return nil +} + +func readUpdateConfigFile(filePath string) (SecretUpdateConfig, DetailsInternal, error) { + var ( + sc SecretUpdateConfig + di DetailsInternal + ) + + if err := hclsimple.DecodeFile(filePath, nil, &sc); err != nil { + return sc, di, fmt.Errorf("failed to decode config file: %w", err) + } + + detailsMap, err := ctyValueToMap(sc.Details) + if err != nil { + return sc, di, err + } + di.Details = detailsMap + + return sc, di, nil +} + +func validateSecretUpdateConfig(sc SecretUpdateConfig) []string { + var missingKeys []string + + if sc.Type == "" { + missingKeys = append(missingKeys, "type") + } + + return missingKeys +} diff --git a/internal/commands/vaultsecrets/secrets/update_test.go b/internal/commands/vaultsecrets/secrets/update_test.go new file mode 100644 index 00000000..9cf4ef75 --- /dev/null +++ b/internal/commands/vaultsecrets/secrets/update_test.go @@ -0,0 +1,278 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package secrets + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/go-openapi/strfmt" + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + mock_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/stretchr/testify/mock" + + "github.com/go-openapi/runtime/client" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +func TestNewCmdUpdate(t *testing.T) { + t.Parallel() + + testProfile := func(t *testing.T) *profile.Profile { + tp := profile.TestProfile(t).SetOrgID("123").SetProjectID("456") + tp.VaultSecrets = &profile.VaultSecretsConf{ + AppName: "test-app", + } + return tp + } + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *UpdateOpts + }{ + { + Name: "Failed: No secret name arg specified", + Profile: testProfile, + Args: []string{}, + Error: "ERROR: missing required flag: --data-file=DATA_FILE_PATH", + }, + { + Name: "Good: Secret name arg specified", + Profile: testProfile, + Args: []string{"test", "--data-file=DATA_FILE_PATH"}, + Expect: &UpdateOpts{ + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: "test", + }, + }, + { + Name: "Good: Rotating secret", + Profile: testProfile, + Args: []string{"test", "--secret-type=rotating", "--data-file=DATA_FILE_PATH"}, + Expect: &UpdateOpts{ + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: "test", + }, + }, + { + Name: "Good: Dynamic secret", + Profile: testProfile, + Args: []string{"test", "--secret-type=dynamic", "--data-file=DATA_FILE_PATH"}, + Expect: &UpdateOpts{ + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: "test", + }, + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + // Create a context. + io := iostreams.Test() + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var gotOpts *UpdateOpts + updateCmd := NewCmdUpdate(ctx, func(o *UpdateOpts) error { + gotOpts = o + gotOpts.AppName = "test-app" + return nil + }) + updateCmd.SetIO(io) + + code := updateCmd.Run(c.Args) + if c.Error != "" { + r.NotZero(code) + r.Contains(io.Error.String(), c.Error) + return + } + + r.Zero(code, io.Error.String()) + r.NotNil(gotOpts) + r.Equal(c.Expect.AppName, gotOpts.AppName) + r.Equal(c.Expect.SecretName, gotOpts.SecretName) + }) + } +} + +func TestUpdateRun(t *testing.T) { + t.Parallel() + + testProfile := func(t *testing.T) *profile.Profile { + tp := profile.TestProfile(t).SetOrgID("123").SetProjectID("456") + tp.VaultSecrets = &profile.VaultSecretsConf{ + AppName: "test-app", + } + return tp + } + + cases := []struct { + Name string + RespErr bool + ReadViaStdin bool + EmptySecretValue bool + ErrMsg string + MockCalled bool + AugmentOpts func(opts *UpdateOpts) + Input []byte + }{ + { + Name: "Success: Update a MongoDB rotating secret", + RespErr: false, + AugmentOpts: func(o *UpdateOpts) { + o.Type = secretTypeRotating + }, + MockCalled: true, + Input: []byte(`type = "mongodb-atlas" +details = { + rotate_on_update = true + rotation_policy_name = "60" + secret_details = { + mongodb_group_id = "mbdgi" + mongodb_roles = [{ + "role_name" = "rn1" + "database_name" = "dn1" + "collection_name" = "cn1" + }, + { + "role_name" = "rn2" + "database_name" = "dn2" + "collection_name" = "cn2" + }] + } +}`), + }, + { + Name: "Failed: Unsupported secret type", + RespErr: true, + AugmentOpts: func(o *UpdateOpts) { + o.Type = "random" + }, + Input: []byte{}, + ErrMsg: "\"random\" is an unsupported secret type; \"rotating\" and \"dynamic\" are available types", + }, + { + Name: "Failed: Unsupported static secret type", + RespErr: true, + AugmentOpts: func(o *UpdateOpts) { + o.Type = secretTypeKV + }, + Input: []byte{}, + ErrMsg: "\"kv\" is an unsupported secret type; \"rotating\" and \"dynamic\" are available types", + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + io := iostreams.Test() + + vs := mock_secret_service.NewMockClientService(t) + pvs := mock_preview_secret_service.NewMockClientService(t) + + opts := &UpdateOpts{ + Ctx: context.Background(), + IO: io, + Profile: testProfile(t), + Output: format.New(io), + Client: vs, + PreviewClient: pvs, + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: "test_secret", + } + + if c.AugmentOpts != nil { + c.AugmentOpts(opts) + } + + tempDir := t.TempDir() + f, err := os.Create(filepath.Join(tempDir, "config.hcl")) + r.NoError(err) + _, err = f.Write(c.Input) + r.NoError(err) + opts.SecretFilePath = f.Name() + + dt := strfmt.NewDateTime() + if c.MockCalled { + if c.RespErr { + pvs.EXPECT().UpdateMongoDBAtlasRotatingSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + pvs.EXPECT().UpdateMongoDBAtlasRotatingSecret(&preview_secret_service.UpdateMongoDBAtlasRotatingSecretParams{ + OrganizationID: testProfile(t).OrganizationID, + ProjectID: testProfile(t).ProjectID, + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: "test_secret", + Body: &preview_models.SecretServiceUpdateMongoDBAtlasRotatingSecretBody{ + RotateOnUpdate: true, + RotationPolicyName: "built-in:60-days-2-active", + SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ + MongodbGroupID: "mbdgi", + MongodbRoles: []*preview_models.Secrets20231128MongoDBRole{ + { + RoleName: "rn1", + DatabaseName: "dn1", + CollectionName: "cn1", + }, + { + RoleName: "rn2", + DatabaseName: "dn2", + CollectionName: "cn2", + }, + }, + }, + }, + Context: opts.Ctx, + }, mock.Anything).Return(&preview_secret_service.UpdateMongoDBAtlasRotatingSecretOK{ + Payload: &preview_models.Secrets20231128UpdateMongoDBAtlasRotatingSecretResponse{ + Config: &preview_models.Secrets20231128RotatingSecretConfig{ + AppName: opts.AppName, + CreatedAt: dt, + IntegrationName: "mongo-db-integration", + RotationPolicyName: "built-in:60-days-2-active", + SecretName: opts.SecretName, + }, + }, + }, nil).Once() + } + } + + // Run the command + err = updateRun(opts) + if c.ErrMsg != "" { + r.Contains(err.Error(), c.ErrMsg) + return + } + + r.NoError(err) + r.Contains(io.Error.String(), fmt.Sprintf("✓ Successfully updated secret with name %q\n", opts.SecretName)) + }) + } + +} From 2996790d22cc6191aea04a665072c50c339bf4cd Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 13 Sep 2024 16:59:04 -0500 Subject: [PATCH 55/75] go mod tidy --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 0ce6d821..b26e2dff 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,6 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= -github.com/hashicorp/hcp-sdk-go v0.110.0 h1:eaDO6XoEb0H+g00Ka3C+ZbRibhwWyA2YNmv48xFCL2w= -github.com/hashicorp/hcp-sdk-go v0.110.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/hashicorp/hcp-sdk-go v0.112.0 h1:gKzxaPhzJj4NobFw7Sc1rGf3nMSqUKBgTtsbZ6bzd14= github.com/hashicorp/hcp-sdk-go v0.112.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= From f60515b727ad833f999f6f241c0af3fb3338d309 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 17 Sep 2024 15:02:13 -0500 Subject: [PATCH 56/75] renames variables --- internal/commands/vaultsecrets/secrets/update.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/update.go b/internal/commands/vaultsecrets/secrets/update.go index 5ec9b9cc..2abce72f 100644 --- a/internal/commands/vaultsecrets/secrets/update.go +++ b/internal/commands/vaultsecrets/secrets/update.go @@ -112,18 +112,18 @@ type SecretUpdateConfig struct { func updateRun(opts *UpdateOpts) error { switch opts.Type { case secretTypeRotating: - sc, di, err := readUpdateConfigFile(opts.SecretFilePath) + secretConfig, internalConfig, err := readUpdateConfigFile(opts.SecretFilePath) if err != nil { return fmt.Errorf("failed to process config file: %w", err) } - missingFields := validateSecretUpdateConfig(sc) + missingFields := validateSecretUpdateConfig(secretConfig) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) } - switch sc.Type { + switch secretConfig.Type { case integrations.Twilio: req := preview_secret_service.NewUpdateTwilioRotatingSecretParamsWithContext(opts.Ctx) req.OrganizationID = opts.Profile.OrganizationID @@ -132,7 +132,7 @@ func updateRun(opts *UpdateOpts) error { req.SecretName = opts.SecretName var twilioBody preview_models.SecretServiceUpdateTwilioRotatingSecretBody - detailBytes, err := json.Marshal(di.Details) + detailBytes, err := json.Marshal(internalConfig.Details) if err != nil { return fmt.Errorf("error marshaling details config: %w", err) } @@ -160,7 +160,7 @@ func updateRun(opts *UpdateOpts) error { req.SecretName = opts.SecretName var mongoDBBody preview_models.SecretServiceUpdateMongoDBAtlasRotatingSecretBody - detailBytes, err := json.Marshal(di.Details) + detailBytes, err := json.Marshal(internalConfig.Details) if err != nil { return fmt.Errorf("error marshaling details config: %w", err) } @@ -187,7 +187,7 @@ func updateRun(opts *UpdateOpts) error { req.AppName = opts.AppName var awsBody preview_models.SecretServiceUpdateAwsIAMUserAccessKeyRotatingSecretBody - detailBytes, err := json.Marshal(di.Details) + detailBytes, err := json.Marshal(internalConfig.Details) if err != nil { return fmt.Errorf("error marshaling details config: %w", err) } @@ -211,7 +211,7 @@ func updateRun(opts *UpdateOpts) error { req.AppName = opts.AppName var gcpBody preview_models.SecretServiceUpdateGcpServiceAccountKeyRotatingSecretBody - detailBytes, err := json.Marshal(di.Details) + detailBytes, err := json.Marshal(internalConfig.Details) if err != nil { return fmt.Errorf("error marshaling details config: %w", err) } From 2b0d37c756b62f2510de1cd8216b29567bb148ab Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 17 Sep 2024 15:12:29 -0500 Subject: [PATCH 57/75] addressing review comments --- .../commands/vaultsecrets/secrets/update.go | 22 +++++++++---------- .../vaultsecrets/secrets/update_test.go | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/commands/vaultsecrets/secrets/update.go b/internal/commands/vaultsecrets/secrets/update.go index 2abce72f..a94a7413 100644 --- a/internal/commands/vaultsecrets/secrets/update.go +++ b/internal/commands/vaultsecrets/secrets/update.go @@ -238,29 +238,29 @@ func updateRun(opts *UpdateOpts) error { return nil } -func readUpdateConfigFile(filePath string) (SecretUpdateConfig, DetailsInternal, error) { +func readUpdateConfigFile(filePath string) (SecretUpdateConfig, secretConfigInternal, error) { var ( - sc SecretUpdateConfig - di DetailsInternal + secretConfig SecretUpdateConfig + internalConfig secretConfigInternal ) - if err := hclsimple.DecodeFile(filePath, nil, &sc); err != nil { - return sc, di, fmt.Errorf("failed to decode config file: %w", err) + if err := hclsimple.DecodeFile(filePath, nil, &secretConfig); err != nil { + return secretConfig, internalConfig, fmt.Errorf("failed to decode config file: %w", err) } - detailsMap, err := ctyValueToMap(sc.Details) + detailsMap, err := ctyValueToMap(secretConfig.Details) if err != nil { - return sc, di, err + return secretConfig, internalConfig, err } - di.Details = detailsMap + internalConfig.Details = detailsMap - return sc, di, nil + return secretConfig, internalConfig, nil } -func validateSecretUpdateConfig(sc SecretUpdateConfig) []string { +func validateSecretUpdateConfig(secretConfig SecretUpdateConfig) []string { var missingKeys []string - if sc.Type == "" { + if secretConfig.Type == "" { missingKeys = append(missingKeys, "type") } diff --git a/internal/commands/vaultsecrets/secrets/update_test.go b/internal/commands/vaultsecrets/secrets/update_test.go index 9cf4ef75..b96c6c9b 100644 --- a/internal/commands/vaultsecrets/secrets/update_test.go +++ b/internal/commands/vaultsecrets/secrets/update_test.go @@ -150,7 +150,7 @@ func TestUpdateRun(t *testing.T) { Input: []byte(`type = "mongodb-atlas" details = { rotate_on_update = true - rotation_policy_name = "60" + rotation_policy_name = "built-in:60-days-2-active" secret_details = { mongodb_group_id = "mbdgi" mongodb_roles = [{ From 848f1cd9cf8ff04892b90917cca943d8cbc19fd3 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 16 Sep 2024 18:15:48 -0500 Subject: [PATCH 58/75] create integration interactively --- .../vaultsecrets/integrations/create.go | 96 +++++++++++++++---- 1 file changed, 78 insertions(+), 18 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index ffa452ec..53fdfc06 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -6,6 +6,8 @@ package integrations import ( "context" "fmt" + "github.com/manifoldco/promptui" + "io" "slices" "golang.org/x/exp/maps" @@ -74,7 +76,6 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { DisplayValue: "CONFIG_FILE", Description: "File path to read integration config data.", Value: flagvalue.Simple("", &opts.ConfigFilePath), - Required: true, }, }, }, @@ -103,15 +104,33 @@ var ( GCPKeys = []string{"audience", "service_account_email"} ) +var providerToRequiredFields = map[string][]string{ + string(Twilio): TwilioKeys, + string(MongoDBAtlas): MongoKeys, + string(AWS): AWSKeys, + string(GCP): GCPKeys, +} + func createRun(opts *CreateOpts) error { - var i IntegrationConfig - if err := hclsimple.DecodeFile(opts.ConfigFilePath, nil, &i); err != nil { - return fmt.Errorf("failed to decode config file: %w", err) + var ( + ic IntegrationConfig + err error + ) + + if opts.ConfigFilePath == "" { + ic, err = promptUserForConfig(opts) + if err != nil { + return fmt.Errorf("failed to create integration via cli prompt: %w", err) + } + } else { + if err = hclsimple.DecodeFile(opts.ConfigFilePath, nil, &ic); err != nil { + return fmt.Errorf("failed to decode config file: %w", err) + } } - switch i.Type { + switch ic.Type { case Twilio: - missingFields := validateDetails(i.Details, TwilioKeys) + missingFields := validateDetails(ic.Details, TwilioKeys) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) @@ -120,9 +139,9 @@ func createRun(opts *CreateOpts) error { body := &preview_models.SecretServiceCreateTwilioIntegrationBody{ Name: opts.IntegrationName, StaticCredentialDetails: &preview_models.Secrets20231128TwilioStaticCredentialsRequest{ - AccountSid: i.Details[TwilioKeys[0]], - APIKeySecret: i.Details[TwilioKeys[1]], - APIKeySid: i.Details[TwilioKeys[2]], + AccountSid: ic.Details[TwilioKeys[0]], + APIKeySecret: ic.Details[TwilioKeys[1]], + APIKeySid: ic.Details[TwilioKeys[2]], }, Capabilities: []*preview_models.Secrets20231128Capability{ preview_models.Secrets20231128CapabilityROTATION.Pointer(), @@ -141,7 +160,7 @@ func createRun(opts *CreateOpts) error { } case MongoDBAtlas: - missingFields := validateDetails(i.Details, MongoKeys) + missingFields := validateDetails(ic.Details, MongoKeys) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) @@ -150,8 +169,8 @@ func createRun(opts *CreateOpts) error { body := &preview_models.SecretServiceCreateMongoDBAtlasIntegrationBody{ Name: opts.IntegrationName, StaticCredentialDetails: &preview_models.Secrets20231128MongoDBAtlasStaticCredentialsRequest{ - APIPrivateKey: i.Details[MongoKeys[0]], - APIPublicKey: i.Details[MongoKeys[1]], + APIPrivateKey: ic.Details[MongoKeys[0]], + APIPublicKey: ic.Details[MongoKeys[1]], }, Capabilities: []*preview_models.Secrets20231128Capability{ preview_models.Secrets20231128CapabilityROTATION.Pointer(), @@ -170,7 +189,7 @@ func createRun(opts *CreateOpts) error { } case AWS: - missingFields := validateDetails(i.Details, AWSKeys) + missingFields := validateDetails(ic.Details, AWSKeys) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) @@ -179,8 +198,8 @@ func createRun(opts *CreateOpts) error { body := &preview_models.SecretServiceCreateAwsIntegrationBody{ Name: opts.IntegrationName, FederatedWorkloadIdentity: &preview_models.Secrets20231128AwsFederatedWorkloadIdentityRequest{ - Audience: i.Details[AWSKeys[0]], - RoleArn: i.Details[AWSKeys[1]], + Audience: ic.Details[AWSKeys[0]], + RoleArn: ic.Details[AWSKeys[1]], }, Capabilities: []*preview_models.Secrets20231128Capability{ preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), @@ -199,7 +218,7 @@ func createRun(opts *CreateOpts) error { } case GCP: - missingFields := validateDetails(i.Details, GCPKeys) + missingFields := validateDetails(ic.Details, GCPKeys) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) @@ -208,8 +227,8 @@ func createRun(opts *CreateOpts) error { body := &preview_models.SecretServiceCreateGcpIntegrationBody{ Name: opts.IntegrationName, FederatedWorkloadIdentity: &preview_models.Secrets20231128GcpFederatedWorkloadIdentityRequest{ - Audience: i.Details[GCPKeys[0]], - ServiceAccountEmail: i.Details[GCPKeys[1]], + Audience: ic.Details[GCPKeys[0]], + ServiceAccountEmail: ic.Details[GCPKeys[1]], }, Capabilities: []*preview_models.Secrets20231128Capability{ preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), @@ -234,6 +253,47 @@ func createRun(opts *CreateOpts) error { return nil } +func promptUserForConfig(opts *CreateOpts) (IntegrationConfig, error) { + var config IntegrationConfig + + if !opts.IO.CanPrompt() { + return config, fmt.Errorf("unable to creative integration interactively") + } + + providerPrompt := promptui.Select{ + Label: "Please select the provider you would like to configure", + Items: maps.Keys(providerToRequiredFields), + Stdin: io.NopCloser(opts.IO.In()), + Stdout: iostreams.NopWriteCloser(opts.IO.Err()), + } + + _, provider, err := providerPrompt.Run() + if err != nil { + return config, fmt.Errorf("provider selection prompt failed: %w", err) + } + + config.Type = IntegrationType(provider) + + var fieldPrompt promptui.Prompt + fieldValues := make(map[string]string) + for _, field := range providerToRequiredFields[provider] { + fieldPrompt = promptui.Prompt{ + Label: field, + Mask: '*', + } + + input, err := fieldPrompt.Run() + if err != nil { + return config, fmt.Errorf("Prompt for field %s failed %v\n", field, err) + } + + fieldValues[field] = input + } + + config.Details = fieldValues + return config, err +} + func validateDetails(details map[string]string, requiredKeys []string) []string { detailsKeys := maps.Keys(details) var missingKeys []string From 2fb8a77723c79bafef08370a202af11e799c34a9 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 16 Sep 2024 18:22:08 -0500 Subject: [PATCH 59/75] lints --- internal/commands/vaultsecrets/integrations/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index 53fdfc06..bc8fd2da 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -6,10 +6,10 @@ package integrations import ( "context" "fmt" - "github.com/manifoldco/promptui" "io" "slices" + "github.com/manifoldco/promptui" "golang.org/x/exp/maps" "github.com/hashicorp/hcl/v2/hclsimple" @@ -284,7 +284,7 @@ func promptUserForConfig(opts *CreateOpts) (IntegrationConfig, error) { input, err := fieldPrompt.Run() if err != nil { - return config, fmt.Errorf("Prompt for field %s failed %v\n", field, err) + return config, fmt.Errorf("prompt for field %s failed: %w", field, err) } fieldValues[field] = input From e4d54eb444986f9e1c1a2460c71aa03f4bbeebf2 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 16 Sep 2024 18:24:47 -0500 Subject: [PATCH 60/75] fixes failign test --- internal/commands/vaultsecrets/integrations/create_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/create_test.go b/internal/commands/vaultsecrets/integrations/create_test.go index 0e551a43..e66f7e41 100644 --- a/internal/commands/vaultsecrets/integrations/create_test.go +++ b/internal/commands/vaultsecrets/integrations/create_test.go @@ -52,12 +52,6 @@ func TestNewCmdCreate(t *testing.T) { Args: []string{"--config-file", "path/to/file"}, Error: "ERROR: accepts 1 arg(s), received 0", }, - { - Name: "Failed: No config file flag specified", - Profile: testProfile, - Args: []string{"sample-integration"}, - Error: "ERROR: missing required flag: --config-file=CONFIG_FILE", - }, } for _, c := range cases { From d39ee693d31a1a71792302b2fcb99184509440ed Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 17 Sep 2024 11:49:36 -0500 Subject: [PATCH 61/75] fixes typo --- internal/commands/vaultsecrets/integrations/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index bc8fd2da..8aeb94b2 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -257,7 +257,7 @@ func promptUserForConfig(opts *CreateOpts) (IntegrationConfig, error) { var config IntegrationConfig if !opts.IO.CanPrompt() { - return config, fmt.Errorf("unable to creative integration interactively") + return config, fmt.Errorf("unable to create integration interactively") } providerPrompt := promptui.Select{ From 285b4f3f4af92416cc9c856d7631182bd0ba3182 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 19 Sep 2024 11:43:30 -0500 Subject: [PATCH 62/75] come clean up --- .../vaultsecrets/integrations/create.go | 36 +++++++++---------- .../vaultsecrets/secrets/create_test.go | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index 8aeb94b2..e26b3045 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -113,24 +113,24 @@ var providerToRequiredFields = map[string][]string{ func createRun(opts *CreateOpts) error { var ( - ic IntegrationConfig - err error + config IntegrationConfig + err error ) if opts.ConfigFilePath == "" { - ic, err = promptUserForConfig(opts) + config, err = promptUserForConfig(opts) if err != nil { return fmt.Errorf("failed to create integration via cli prompt: %w", err) } } else { - if err = hclsimple.DecodeFile(opts.ConfigFilePath, nil, &ic); err != nil { + if err = hclsimple.DecodeFile(opts.ConfigFilePath, nil, &config); err != nil { return fmt.Errorf("failed to decode config file: %w", err) } } - switch ic.Type { + switch config.Type { case Twilio: - missingFields := validateDetails(ic.Details, TwilioKeys) + missingFields := validateDetails(config.Details, TwilioKeys) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) @@ -139,9 +139,9 @@ func createRun(opts *CreateOpts) error { body := &preview_models.SecretServiceCreateTwilioIntegrationBody{ Name: opts.IntegrationName, StaticCredentialDetails: &preview_models.Secrets20231128TwilioStaticCredentialsRequest{ - AccountSid: ic.Details[TwilioKeys[0]], - APIKeySecret: ic.Details[TwilioKeys[1]], - APIKeySid: ic.Details[TwilioKeys[2]], + AccountSid: config.Details[TwilioKeys[0]], + APIKeySecret: config.Details[TwilioKeys[1]], + APIKeySid: config.Details[TwilioKeys[2]], }, Capabilities: []*preview_models.Secrets20231128Capability{ preview_models.Secrets20231128CapabilityROTATION.Pointer(), @@ -160,7 +160,7 @@ func createRun(opts *CreateOpts) error { } case MongoDBAtlas: - missingFields := validateDetails(ic.Details, MongoKeys) + missingFields := validateDetails(config.Details, MongoKeys) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) @@ -169,8 +169,8 @@ func createRun(opts *CreateOpts) error { body := &preview_models.SecretServiceCreateMongoDBAtlasIntegrationBody{ Name: opts.IntegrationName, StaticCredentialDetails: &preview_models.Secrets20231128MongoDBAtlasStaticCredentialsRequest{ - APIPrivateKey: ic.Details[MongoKeys[0]], - APIPublicKey: ic.Details[MongoKeys[1]], + APIPrivateKey: config.Details[MongoKeys[0]], + APIPublicKey: config.Details[MongoKeys[1]], }, Capabilities: []*preview_models.Secrets20231128Capability{ preview_models.Secrets20231128CapabilityROTATION.Pointer(), @@ -189,7 +189,7 @@ func createRun(opts *CreateOpts) error { } case AWS: - missingFields := validateDetails(ic.Details, AWSKeys) + missingFields := validateDetails(config.Details, AWSKeys) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) @@ -198,8 +198,8 @@ func createRun(opts *CreateOpts) error { body := &preview_models.SecretServiceCreateAwsIntegrationBody{ Name: opts.IntegrationName, FederatedWorkloadIdentity: &preview_models.Secrets20231128AwsFederatedWorkloadIdentityRequest{ - Audience: ic.Details[AWSKeys[0]], - RoleArn: ic.Details[AWSKeys[1]], + Audience: config.Details[AWSKeys[0]], + RoleArn: config.Details[AWSKeys[1]], }, Capabilities: []*preview_models.Secrets20231128Capability{ preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), @@ -218,7 +218,7 @@ func createRun(opts *CreateOpts) error { } case GCP: - missingFields := validateDetails(ic.Details, GCPKeys) + missingFields := validateDetails(config.Details, GCPKeys) if len(missingFields) > 0 { return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) @@ -227,8 +227,8 @@ func createRun(opts *CreateOpts) error { body := &preview_models.SecretServiceCreateGcpIntegrationBody{ Name: opts.IntegrationName, FederatedWorkloadIdentity: &preview_models.Secrets20231128GcpFederatedWorkloadIdentityRequest{ - Audience: ic.Details[GCPKeys[0]], - ServiceAccountEmail: ic.Details[GCPKeys[1]], + Audience: config.Details[GCPKeys[0]], + ServiceAccountEmail: config.Details[GCPKeys[1]], }, Capabilities: []*preview_models.Secrets20231128Capability{ preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), diff --git a/internal/commands/vaultsecrets/secrets/create_test.go b/internal/commands/vaultsecrets/secrets/create_test.go index b686af2a..34556fcc 100644 --- a/internal/commands/vaultsecrets/secrets/create_test.go +++ b/internal/commands/vaultsecrets/secrets/create_test.go @@ -195,7 +195,7 @@ func TestCreateRun(t *testing.T) { Input: []byte(`type = "mongodb-atlas" integration_name = "mongo-db-integration" details = { - rotation_policy_name: "built-in:60-days-2-active" + rotation_policy_name = "built-in:60-days-2-active" secret_details = { mongodb_group_id = "mbdgi" mongodb_roles = [{ From 24c2a2b0240b857e97e5ff55dc91ccc7406452f4 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Tue, 24 Sep 2024 18:33:52 -0500 Subject: [PATCH 63/75] supports either auth method --- .../vaultsecrets/integrations/create.go | 274 +++++++++++------- .../vaultsecrets/integrations/create_test.go | 24 +- .../commands/vaultsecrets/secrets/create.go | 33 +-- .../commands/vaultsecrets/secrets/update.go | 2 +- 4 files changed, 180 insertions(+), 153 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index e26b3045..42c12505 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -5,11 +5,12 @@ package integrations import ( "context" + "encoding/json" "fmt" "io" - "slices" "github.com/manifoldco/promptui" + "github.com/zclconf/go-cty/cty" "golang.org/x/exp/maps" "github.com/hashicorp/hcl/v2/hclsimple" @@ -92,16 +93,20 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { return cmd } +type integrationConfigInternal struct { + Details map[string]any +} + type IntegrationConfig struct { - Type IntegrationType `hcl:"type"` - Details map[string]string `hcl:"details"` + Type IntegrationType `hcl:"type"` + Details cty.Value `hcl:"details"` } var ( TwilioKeys = []string{"account_sid", "api_key_secret", "api_key_sid"} MongoKeys = []string{"private_key", "public_key"} - AWSKeys = []string{"audience", "role_arn"} - GCPKeys = []string{"audience", "service_account_email"} + AWSKeys = []string{"access_keys", "federated_workload_identity"} + GCPKeys = []string{"service_account_key", "federated_workload_identity"} ) var providerToRequiredFields = map[string][]string{ @@ -111,14 +116,25 @@ var providerToRequiredFields = map[string][]string{ string(GCP): GCPKeys, } +var awsAuthMethodsToReqKeys = map[string][]string{ + "federated_workload_identity": {"audience", "role_arn"}, + "access_keys": {"access_key_id", "secret_access_key"}, +} + +var gcpAuthMethodsToReqKeys = map[string][]string{ + "federated_workload_identity": {"audience", "service_account_email"}, + "service_account_key": {"credentials"}, +} + func createRun(opts *CreateOpts) error { var ( - config IntegrationConfig - err error + config IntegrationConfig + internalConfig integrationConfigInternal + err error ) if opts.ConfigFilePath == "" { - config, err = promptUserForConfig(opts) + config, internalConfig, err = promptUserForConfig(opts) if err != nil { return fmt.Errorf("failed to create integration via cli prompt: %w", err) } @@ -126,121 +142,117 @@ func createRun(opts *CreateOpts) error { if err = hclsimple.DecodeFile(opts.ConfigFilePath, nil, &config); err != nil { return fmt.Errorf("failed to decode config file: %w", err) } + + detailsMap, err := CtyValueToMap(config.Details) + if err != nil { + return fmt.Errorf("failed to process config file: %w", err) + } + internalConfig.Details = detailsMap } switch config.Type { case Twilio: - missingFields := validateDetails(config.Details, TwilioKeys) + req := preview_secret_service.NewCreateTwilioIntegrationParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID - if len(missingFields) > 0 { - return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + var twilioBody preview_models.SecretServiceCreateTwilioIntegrationBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) } - body := &preview_models.SecretServiceCreateTwilioIntegrationBody{ - Name: opts.IntegrationName, - StaticCredentialDetails: &preview_models.Secrets20231128TwilioStaticCredentialsRequest{ - AccountSid: config.Details[TwilioKeys[0]], - APIKeySecret: config.Details[TwilioKeys[1]], - APIKeySid: config.Details[TwilioKeys[2]], - }, - Capabilities: []*preview_models.Secrets20231128Capability{ - preview_models.Secrets20231128CapabilityROTATION.Pointer(), - }, + err = twilioBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) } + req.Body = &twilioBody + req.Body.Capabilities = []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityROTATION.Pointer(), + } + req.Body.Name = opts.IntegrationName - _, err := opts.PreviewClient.CreateTwilioIntegration(&preview_secret_service.CreateTwilioIntegrationParams{ - Context: opts.Ctx, - ProjectID: opts.Profile.ProjectID, - OrganizationID: opts.Profile.OrganizationID, - Body: body, - }, nil) - + _, err = opts.PreviewClient.CreateTwilioIntegration(req, nil) if err != nil { return fmt.Errorf("failed to create Twilio integration: %w", err) } case MongoDBAtlas: - missingFields := validateDetails(config.Details, MongoKeys) + req := preview_secret_service.NewCreateMongoDBAtlasIntegrationParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID - if len(missingFields) > 0 { - return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + var mongoDBBody preview_models.SecretServiceCreateMongoDBAtlasIntegrationBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) } - body := &preview_models.SecretServiceCreateMongoDBAtlasIntegrationBody{ - Name: opts.IntegrationName, - StaticCredentialDetails: &preview_models.Secrets20231128MongoDBAtlasStaticCredentialsRequest{ - APIPrivateKey: config.Details[MongoKeys[0]], - APIPublicKey: config.Details[MongoKeys[1]], - }, - Capabilities: []*preview_models.Secrets20231128Capability{ - preview_models.Secrets20231128CapabilityROTATION.Pointer(), - }, + err = mongoDBBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + req.Body = &mongoDBBody + req.Body.Capabilities = []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityROTATION.Pointer(), } + req.Body.Name = opts.IntegrationName - _, err := opts.PreviewClient.CreateMongoDBAtlasIntegration(&preview_secret_service.CreateMongoDBAtlasIntegrationParams{ - Context: opts.Ctx, - ProjectID: opts.Profile.ProjectID, - OrganizationID: opts.Profile.OrganizationID, - Body: body, - }, nil) + _, err = opts.PreviewClient.CreateMongoDBAtlasIntegration(req, nil) if err != nil { return fmt.Errorf("failed to create MongoDB Atlas integration: %w", err) } case AWS: - missingFields := validateDetails(config.Details, AWSKeys) + req := preview_secret_service.NewCreateAwsIntegrationParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID - if len(missingFields) > 0 { - return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + var awsBody preview_models.SecretServiceCreateAwsIntegrationBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) } - body := &preview_models.SecretServiceCreateAwsIntegrationBody{ - Name: opts.IntegrationName, - FederatedWorkloadIdentity: &preview_models.Secrets20231128AwsFederatedWorkloadIdentityRequest{ - Audience: config.Details[AWSKeys[0]], - RoleArn: config.Details[AWSKeys[1]], - }, - Capabilities: []*preview_models.Secrets20231128Capability{ - preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), - }, + err = awsBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) } + req.Body = &awsBody + req.Body.Capabilities = []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), + } + req.Body.Name = opts.IntegrationName - _, err := opts.PreviewClient.CreateAwsIntegration(&preview_secret_service.CreateAwsIntegrationParams{ - Context: opts.Ctx, - ProjectID: opts.Profile.ProjectID, - OrganizationID: opts.Profile.OrganizationID, - Body: body, - }, nil) + _, err = opts.PreviewClient.CreateAwsIntegration(req, nil) if err != nil { return fmt.Errorf("failed to create AWS integration: %w", err) } case GCP: - missingFields := validateDetails(config.Details, GCPKeys) + req := preview_secret_service.NewCreateGcpIntegrationParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID - if len(missingFields) > 0 { - return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + var gcpBody preview_models.SecretServiceCreateGcpIntegrationBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) } - body := &preview_models.SecretServiceCreateGcpIntegrationBody{ - Name: opts.IntegrationName, - FederatedWorkloadIdentity: &preview_models.Secrets20231128GcpFederatedWorkloadIdentityRequest{ - Audience: config.Details[GCPKeys[0]], - ServiceAccountEmail: config.Details[GCPKeys[1]], - }, - Capabilities: []*preview_models.Secrets20231128Capability{ - preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), - }, + err = gcpBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) } + req.Body = &gcpBody + req.Body.Capabilities = []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), + } + req.Body.Name = opts.IntegrationName - _, err := opts.PreviewClient.CreateGcpIntegration(&preview_secret_service.CreateGcpIntegrationParams{ - Context: opts.Ctx, - ProjectID: opts.Profile.ProjectID, - OrganizationID: opts.Profile.OrganizationID, - Body: body, - }, nil) + _, err = opts.PreviewClient.CreateGcpIntegration(req, nil) if err != nil { return fmt.Errorf("failed to create GCP integration: %w", err) @@ -253,11 +265,14 @@ func createRun(opts *CreateOpts) error { return nil } -func promptUserForConfig(opts *CreateOpts) (IntegrationConfig, error) { - var config IntegrationConfig +func promptUserForConfig(opts *CreateOpts) (IntegrationConfig, integrationConfigInternal, error) { + var ( + config IntegrationConfig + internalConfig integrationConfigInternal + ) if !opts.IO.CanPrompt() { - return config, fmt.Errorf("unable to create integration interactively") + return config, internalConfig, fmt.Errorf("unable to create integration interactively") } providerPrompt := promptui.Select{ @@ -269,14 +284,49 @@ func promptUserForConfig(opts *CreateOpts) (IntegrationConfig, error) { _, provider, err := providerPrompt.Run() if err != nil { - return config, fmt.Errorf("provider selection prompt failed: %w", err) + return config, internalConfig, fmt.Errorf("provider selection prompt failed: %w", err) } - config.Type = IntegrationType(provider) + var ( + fields []string + authMethod string + ) + if config.Type == AWS { + authPrompt := promptui.Select{ + Label: "Please select an authentication method", + Items: providerToRequiredFields[provider], + Stdin: io.NopCloser(opts.IO.In()), + Stdout: iostreams.NopWriteCloser(opts.IO.Err()), + } + + _, authMethod, err = authPrompt.Run() + if err != nil { + return config, internalConfig, fmt.Errorf("authentication method selection prompt failed: %w", err) + } + + fields = awsAuthMethodsToReqKeys[authMethod] + } else if config.Type == GCP { + authPrompt := promptui.Select{ + Label: "Please select an authentication method", + Items: providerToRequiredFields[provider], + Stdin: io.NopCloser(opts.IO.In()), + Stdout: iostreams.NopWriteCloser(opts.IO.Err()), + } + + _, authMethod, err = authPrompt.Run() + if err != nil { + return config, internalConfig, fmt.Errorf("authentication method selection prompt failed: %w", err) + } + fields = gcpAuthMethodsToReqKeys[authMethod] + + } else { + fields = providerToRequiredFields[provider] + } + var fieldPrompt promptui.Prompt - fieldValues := make(map[string]string) - for _, field := range providerToRequiredFields[provider] { + fieldValues := make(map[string]any) + for _, field := range fields { fieldPrompt = promptui.Prompt{ Label: field, Mask: '*', @@ -284,24 +334,48 @@ func promptUserForConfig(opts *CreateOpts) (IntegrationConfig, error) { input, err := fieldPrompt.Run() if err != nil { - return config, fmt.Errorf("prompt for field %s failed: %w", field, err) + return config, internalConfig, fmt.Errorf("prompt for field %s failed: %w", field, err) } fieldValues[field] = input } - config.Details = fieldValues - return config, err -} + if config.Type == AWS || config.Type == GCP { + internalConfig.Details = map[string]any{authMethod: fieldValues} + return config, internalConfig, err + } -func validateDetails(details map[string]string, requiredKeys []string) []string { - detailsKeys := maps.Keys(details) - var missingKeys []string + internalConfig.Details = fieldValues + return config, internalConfig, err +} - for _, r := range requiredKeys { - if !slices.Contains(detailsKeys, r) { - missingKeys = append(missingKeys, r) +func CtyValueToMap(value cty.Value) (map[string]any, error) { + fieldsMap := make(map[string]any) + for k, v := range value.AsValueMap() { + if v.Type() == cty.String { + fieldsMap[k] = v.AsString() + } else if v.Type() == cty.Bool { + fieldsMap[k] = v.True() + } else if v.Type().IsObjectType() { + nestedMap, err := CtyValueToMap(v) + if err != nil { + return nil, err + } + fieldsMap[k] = nestedMap + } else if v.Type().IsTupleType() { + var items []map[string]any + for _, val := range v.AsValueSlice() { + nestedMap, err := CtyValueToMap(val) + if err != nil { + return nil, err + } + items = append(items, nestedMap) + } + fieldsMap[k] = items + } else { + return nil, fmt.Errorf("found unsupported value type") } } - return missingKeys + + return fieldsMap, nil } diff --git a/internal/commands/vaultsecrets/integrations/create_test.go b/internal/commands/vaultsecrets/integrations/create_test.go index e66f7e41..01c112e8 100644 --- a/internal/commands/vaultsecrets/integrations/create_test.go +++ b/internal/commands/vaultsecrets/integrations/create_test.go @@ -106,28 +106,12 @@ func TestCreateRun(t *testing.T) { IntegrationName: "sample-integration", Input: []byte(`type = "aws" details = { - "audience" = "abc", - "role_arn" = "def" + "federated_workload_identity" = { + "audience" = "abc", + "role_arn" = "def" + } }`), }, - { - Name: "Missing a single required field", - IntegrationName: "sample-integration", - Input: []byte(`type = "mongodb-atlas" -details = { - "public_key" = "abc" -}`), - Error: "missing required field(s) in the config file: [private_key]", - }, - { - Name: "Missing multiple required fields", - IntegrationName: "sample-integration", - Input: []byte(`type = "twilio" -details = { - "api_key_sid" = "ghi" -}`), - Error: "missing required field(s) in the config file: [account_sid api_key_secret]", - }, } for _, c := range cases { diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index 669f036e..e93095da 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -368,7 +368,7 @@ func readConfigFile(filePath string) (SecretConfig, secretConfigInternal, error) return secretConfig, internalConfig, fmt.Errorf("failed to decode config file: %w", err) } - detailsMap, err := ctyValueToMap(secretConfig.Details) + detailsMap, err := integrations.CtyValueToMap(secretConfig.Details) if err != nil { return secretConfig, internalConfig, err } @@ -377,37 +377,6 @@ func readConfigFile(filePath string) (SecretConfig, secretConfigInternal, error) return secretConfig, internalConfig, nil } -func ctyValueToMap(value cty.Value) (map[string]any, error) { - fieldsMap := make(map[string]any) - for k, v := range value.AsValueMap() { - if v.Type() == cty.String { - fieldsMap[k] = v.AsString() - } else if v.Type() == cty.Bool { - fieldsMap[k] = v.True() - } else if v.Type().IsObjectType() { - nestedMap, err := ctyValueToMap(v) - if err != nil { - return nil, err - } - fieldsMap[k] = nestedMap - } else if v.Type().IsTupleType() { - var items []map[string]any - for _, val := range v.AsValueSlice() { - nestedMap, err := ctyValueToMap(val) - if err != nil { - return nil, err - } - items = append(items, nestedMap) - } - fieldsMap[k] = items - } else { - return nil, fmt.Errorf("found unsupported value type") - } - } - - return fieldsMap, nil -} - func validateSecretConfig(secretConfig SecretConfig) []string { var missingKeys []string diff --git a/internal/commands/vaultsecrets/secrets/update.go b/internal/commands/vaultsecrets/secrets/update.go index a94a7413..ec1e6f67 100644 --- a/internal/commands/vaultsecrets/secrets/update.go +++ b/internal/commands/vaultsecrets/secrets/update.go @@ -248,7 +248,7 @@ func readUpdateConfigFile(filePath string) (SecretUpdateConfig, secretConfigInte return secretConfig, internalConfig, fmt.Errorf("failed to decode config file: %w", err) } - detailsMap, err := ctyValueToMap(secretConfig.Details) + detailsMap, err := integrations.CtyValueToMap(secretConfig.Details) if err != nil { return secretConfig, internalConfig, err } From 99c1cc83c0ff49f6ac21af33bb31fae6a40d1ae2 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 20 Sep 2024 17:47:08 -0500 Subject: [PATCH 64/75] Update dynamis secrets --- go.mod | 2 +- go.sum | 4 +- .../commands/vaultsecrets/secrets/update.go | 74 ++++++++- .../vaultsecrets/secrets/update_test.go | 140 +++++++++++++----- 4 files changed, 177 insertions(+), 43 deletions(-) diff --git a/go.mod b/go.mod index 472118bd..9722227e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl/v2 v2.22.0 - github.com/hashicorp/hcp-sdk-go v0.112.0 + github.com/hashicorp/hcp-sdk-go v0.113.0 github.com/lithammer/dedent v1.1.0 github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/cli v1.1.5 diff --git a/go.sum b/go.sum index b26e2dff..5f779ecc 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= -github.com/hashicorp/hcp-sdk-go v0.112.0 h1:gKzxaPhzJj4NobFw7Sc1rGf3nMSqUKBgTtsbZ6bzd14= -github.com/hashicorp/hcp-sdk-go v0.112.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +github.com/hashicorp/hcp-sdk-go v0.113.0 h1:JuA6mZosEezE550lNMEq43VLUCzVc+/jPmPC1Nd39Gk= +github.com/hashicorp/hcp-sdk-go v0.113.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= diff --git a/internal/commands/vaultsecrets/secrets/update.go b/internal/commands/vaultsecrets/secrets/update.go index ec1e6f67..db1516cd 100644 --- a/internal/commands/vaultsecrets/secrets/update.go +++ b/internal/commands/vaultsecrets/secrets/update.go @@ -139,7 +139,7 @@ func updateRun(opts *UpdateOpts) error { err = twilioBody.UnmarshalBinary(detailBytes) if err != nil { - return fmt.Errorf("error marshaling details config: %w", err) + return fmt.Errorf("error unmarshaling details config: %w", err) } req.Body = &twilioBody @@ -167,7 +167,7 @@ func updateRun(opts *UpdateOpts) error { err = mongoDBBody.UnmarshalBinary(detailBytes) if err != nil { - return fmt.Errorf("error marshaling details config: %w", err) + return fmt.Errorf("error unmarshaling details config: %w", err) } req.Body = &mongoDBBody @@ -185,6 +185,7 @@ func updateRun(opts *UpdateOpts) error { req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID req.AppName = opts.AppName + req.Name = opts.SecretName var awsBody preview_models.SecretServiceUpdateAwsIAMUserAccessKeyRotatingSecretBody detailBytes, err := json.Marshal(internalConfig.Details) @@ -194,7 +195,7 @@ func updateRun(opts *UpdateOpts) error { err = awsBody.UnmarshalBinary(detailBytes) if err != nil { - return fmt.Errorf("error marshaling details config: %w", err) + return fmt.Errorf("error unmarshaling details config: %w", err) } req.Body = &awsBody @@ -209,6 +210,7 @@ func updateRun(opts *UpdateOpts) error { req.OrganizationID = opts.Profile.OrganizationID req.ProjectID = opts.Profile.ProjectID req.AppName = opts.AppName + req.Name = opts.SecretName var gcpBody preview_models.SecretServiceUpdateGcpServiceAccountKeyRotatingSecretBody detailBytes, err := json.Marshal(internalConfig.Details) @@ -218,7 +220,7 @@ func updateRun(opts *UpdateOpts) error { err = gcpBody.UnmarshalBinary(detailBytes) if err != nil { - return fmt.Errorf("error marshaling details config: %w", err) + return fmt.Errorf("error unmarshaling details config: %w", err) } req.Body = &gcpBody @@ -229,6 +231,70 @@ func updateRun(opts *UpdateOpts) error { } } + case secretTypeDynamic: + secretConfig, internalConfig, err := readUpdateConfigFile(opts.SecretFilePath) + if err != nil { + return fmt.Errorf("failed to process config file: %w", err) + } + + missingFields := validateSecretUpdateConfig(secretConfig) + + if len(missingFields) > 0 { + return fmt.Errorf("missing required field(s) in the config file: %s", missingFields) + } + + switch secretConfig.Type { + case integrations.AWS: + req := preview_secret_service.NewUpdateAwsDynamicSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + req.Name = opts.SecretName + + var awsBody preview_models.SecretServiceUpdateAwsDynamicSecretBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = awsBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error unmarshaling details config: %w", err) + } + + req.Body = &awsBody + + _, err = opts.PreviewClient.UpdateAwsDynamicSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to update secret with name %q: %w", opts.SecretName, err) + } + + case integrations.GCP: + req := preview_secret_service.NewUpdateGcpDynamicSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + req.Name = opts.SecretName + + var gcpBody preview_models.SecretServiceUpdateGcpDynamicSecretBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = gcpBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error unmarshaling details config: %w", err) + } + + req.Body = &gcpBody + + _, err = opts.PreviewClient.UpdateGcpDynamicSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to update secret with name %q: %w", opts.SecretName, err) + } + } + default: return fmt.Errorf("%q is an unsupported secret type; \"rotating\" and \"dynamic\" are available types", opts.Type) } diff --git a/internal/commands/vaultsecrets/secrets/update_test.go b/internal/commands/vaultsecrets/secrets/update_test.go index b96c6c9b..18e5acd7 100644 --- a/internal/commands/vaultsecrets/secrets/update_test.go +++ b/internal/commands/vaultsecrets/secrets/update_test.go @@ -164,6 +164,41 @@ details = { "collection_name" = "cn2" }] } +}`), + }, + { + Name: "Success: Update an Aws dynamic secret", + RespErr: false, + AugmentOpts: func(o *UpdateOpts) { + o.Type = secretTypeDynamic + }, + MockCalled: true, + Input: []byte(`type = "aws" + +details = { + "default_ttl" = "3600s" + + "assume_role" = { + "role_arn" = "ra2" + } +}`), + }, + { + Name: "Failed: Unable to update integration name of a secret", + RespErr: true, + AugmentOpts: func(o *UpdateOpts) { + o.Type = secretTypeDynamic + }, + ErrMsg: "Unsupported argument; An argument named \"integration_name\" is not expected here", + Input: []byte(`type = "aws" +integration_name = "Aws-Int-12" + +details = { + "default_ttl" = "3600s" + + "assume_role" = { + "role_arn" = "ra2" + } }`), }, { @@ -220,46 +255,79 @@ details = { opts.SecretFilePath = f.Name() dt := strfmt.NewDateTime() - if c.MockCalled { - if c.RespErr { - pvs.EXPECT().UpdateMongoDBAtlasRotatingSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() - } else { - pvs.EXPECT().UpdateMongoDBAtlasRotatingSecret(&preview_secret_service.UpdateMongoDBAtlasRotatingSecretParams{ - OrganizationID: testProfile(t).OrganizationID, - ProjectID: testProfile(t).ProjectID, - AppName: testProfile(t).VaultSecrets.AppName, - SecretName: "test_secret", - Body: &preview_models.SecretServiceUpdateMongoDBAtlasRotatingSecretBody{ - RotateOnUpdate: true, - RotationPolicyName: "built-in:60-days-2-active", - SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ - MongodbGroupID: "mbdgi", - MongodbRoles: []*preview_models.Secrets20231128MongoDBRole{ - { - RoleName: "rn1", - DatabaseName: "dn1", - CollectionName: "cn1", - }, - { - RoleName: "rn2", - DatabaseName: "dn2", - CollectionName: "cn2", + if opts.Type == secretTypeRotating { + if c.MockCalled { + if c.RespErr { + pvs.EXPECT().UpdateMongoDBAtlasRotatingSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + pvs.EXPECT().UpdateMongoDBAtlasRotatingSecret(&preview_secret_service.UpdateMongoDBAtlasRotatingSecretParams{ + OrganizationID: testProfile(t).OrganizationID, + ProjectID: testProfile(t).ProjectID, + AppName: testProfile(t).VaultSecrets.AppName, + SecretName: "test_secret", + Body: &preview_models.SecretServiceUpdateMongoDBAtlasRotatingSecretBody{ + RotateOnUpdate: true, + RotationPolicyName: "built-in:60-days-2-active", + SecretDetails: &preview_models.Secrets20231128MongoDBAtlasSecretDetails{ + MongodbGroupID: "mbdgi", + MongodbRoles: []*preview_models.Secrets20231128MongoDBRole{ + { + RoleName: "rn1", + DatabaseName: "dn1", + CollectionName: "cn1", + }, + { + RoleName: "rn2", + DatabaseName: "dn2", + CollectionName: "cn2", + }, }, }, }, - }, - Context: opts.Ctx, - }, mock.Anything).Return(&preview_secret_service.UpdateMongoDBAtlasRotatingSecretOK{ - Payload: &preview_models.Secrets20231128UpdateMongoDBAtlasRotatingSecretResponse{ - Config: &preview_models.Secrets20231128RotatingSecretConfig{ - AppName: opts.AppName, - CreatedAt: dt, - IntegrationName: "mongo-db-integration", - RotationPolicyName: "built-in:60-days-2-active", - SecretName: opts.SecretName, + Context: opts.Ctx, + }, mock.Anything).Return(&preview_secret_service.UpdateMongoDBAtlasRotatingSecretOK{ + Payload: &preview_models.Secrets20231128UpdateMongoDBAtlasRotatingSecretResponse{ + Config: &preview_models.Secrets20231128RotatingSecretConfig{ + AppName: opts.AppName, + CreatedAt: dt, + IntegrationName: "mongo-db-integration", + RotationPolicyName: "built-in:60-days-2-active", + SecretName: opts.SecretName, + }, + }, + }, nil).Once() + } + } + } else if opts.Type == secretTypeDynamic { + if c.MockCalled { + if c.RespErr { + pvs.EXPECT().UpdateAwsDynamicSecret(mock.Anything, mock.Anything).Return(nil, errors.New(c.ErrMsg)).Once() + } else { + pvs.EXPECT().UpdateAwsDynamicSecret(&preview_secret_service.UpdateAwsDynamicSecretParams{ + OrganizationID: testProfile(t).OrganizationID, + ProjectID: testProfile(t).ProjectID, + AppName: testProfile(t).VaultSecrets.AppName, + Body: &preview_models.SecretServiceUpdateAwsDynamicSecretBody{ + DefaultTTL: "3600s", + AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ + RoleArn: "ra2", + }, + }, + Context: opts.Ctx, + }, mock.Anything).Return(&preview_secret_service.UpdateAwsDynamicSecretOK{ + Payload: &preview_models.Secrets20231128UpdateAwsDynamicSecretResponse{ + Secret: &preview_models.Secrets20231128AwsDynamicSecret{ + AssumeRole: &preview_models.Secrets20231128AssumeRoleResponse{ + RoleArn: "ra2", + }, + DefaultTTL: "3600s", + CreatedAt: dt, + IntegrationName: "Aws-Int-12", + Name: opts.SecretName, + }, }, - }, - }, nil).Once() + }, nil).Once() + } } } From 322fdc2b62d6d706d3b06667cae25ac8ad6562ee Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 20 Sep 2024 17:54:30 -0500 Subject: [PATCH 65/75] fixes failing test --- internal/commands/vaultsecrets/secrets/update_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/commands/vaultsecrets/secrets/update_test.go b/internal/commands/vaultsecrets/secrets/update_test.go index 18e5acd7..15dad037 100644 --- a/internal/commands/vaultsecrets/secrets/update_test.go +++ b/internal/commands/vaultsecrets/secrets/update_test.go @@ -307,6 +307,7 @@ details = { OrganizationID: testProfile(t).OrganizationID, ProjectID: testProfile(t).ProjectID, AppName: testProfile(t).VaultSecrets.AppName, + Name: opts.SecretName, Body: &preview_models.SecretServiceUpdateAwsDynamicSecretBody{ DefaultTTL: "3600s", AssumeRole: &preview_models.Secrets20231128AssumeRoleRequest{ From b2b2df1c4b8b63241f8779b543fe1196001c3b13 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Wed, 25 Sep 2024 17:13:12 -0500 Subject: [PATCH 66/75] adds AWS and GCP to rotating secrets creation --- .../commands/vaultsecrets/secrets/create.go | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index e93095da..ae4a2ad7 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -231,6 +231,58 @@ func createRun(opts *CreateOpts) error { return err } + case integrations.AWS: + req := preview_secret_service.NewCreateAwsIAMUserAccessKeyRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + + var awsBody preview_models.SecretServiceCreateAwsIAMUserAccessKeyRotatingSecretBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = awsBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + awsBody.IntegrationName = secretConfig.IntegrationName + awsBody.Name = opts.SecretName + req.Body = &awsBody + + _, err = opts.PreviewClient.CreateAwsIAMUserAccessKeyRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } + + case integrations.GCP: + req := preview_secret_service.NewCreateGcpServiceAccountKeyRotatingSecretParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.AppName = opts.AppName + + var gcpBody preview_models.SecretServiceCreateGcpServiceAccountKeyRotatingSecretBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = gcpBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + gcpBody.IntegrationName = secretConfig.IntegrationName + gcpBody.Name = opts.SecretName + req.Body = &gcpBody + + _, err = opts.PreviewClient.CreateGcpServiceAccountKeyRotatingSecret(req, nil) + if err != nil { + return fmt.Errorf("failed to create secret with name %q: %w", opts.SecretName, err) + } + default: return fmt.Errorf("unsupported rotating secret provider type") } From d21da3f9a1cc0f09f6a1d27e6389ced23810b51e Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 26 Sep 2024 12:44:27 -0500 Subject: [PATCH 67/75] Adds AWS, GCP, and generic get integration calls --- .../vaultsecrets/integrations/read.go | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index d5283f93..59614082 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -87,6 +87,19 @@ func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { func readRun(opts *ReadOpts) error { switch opts.Type { + case "": + resp, err := opts.PreviewClient.GetIntegration(&preview_secret_service.GetIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Name: opts.IntegrationName, + }, nil) + if err != nil { + return fmt.Errorf("failed to read integration: %w", err) + } + + return opts.Output.Display(newGenericDisplayer(true, resp.Payload.Integration)) + case Twilio: resp, err := opts.PreviewClient.GetTwilioIntegration(&preview_secret_service.GetTwilioIntegrationParams{ Context: opts.Ctx, @@ -113,6 +126,32 @@ func readRun(opts *ReadOpts) error { return opts.Output.Display(newMongoDBDisplayer(true, resp.Payload.Integration)) + case AWS: + resp, err := opts.PreviewClient.GetAwsIntegration(&preview_secret_service.GetAwsIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Name: opts.IntegrationName, + }, nil) + if err != nil { + return fmt.Errorf("failed to read integration: %w", err) + } + + return opts.Output.Display(newAwsDisplayer(true, resp.Payload.Integration)) + + case GCP: + resp, err := opts.PreviewClient.GetGcpIntegration(&preview_secret_service.GetGcpIntegrationParams{ + Context: opts.Ctx, + ProjectID: opts.Profile.ProjectID, + OrganizationID: opts.Profile.OrganizationID, + Name: opts.IntegrationName, + }, nil) + if err != nil { + return fmt.Errorf("failed to read integration: %w", err) + } + + return opts.Output.Display(newGcpDisplayer(true, resp.Payload.Integration)) + default: return fmt.Errorf("not a valid integration type") } From 370704f58eb997a6a7938f019cd660ee2147655d Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 30 Sep 2024 13:32:34 -0500 Subject: [PATCH 68/75] allows updating integrations --- .../vaultsecrets/integrations/create.go | 36 ++- .../vaultsecrets/integrations/create_test.go | 4 + .../vaultsecrets/integrations/integrations.go | 1 + .../vaultsecrets/integrations/update.go | 210 ++++++++++++++++++ .../vaultsecrets/integrations/update_test.go | 197 ++++++++++++++++ 5 files changed, 429 insertions(+), 19 deletions(-) create mode 100644 internal/commands/vaultsecrets/integrations/update.go create mode 100644 internal/commands/vaultsecrets/integrations/update_test.go diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index 42c12505..d1210e78 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -167,9 +167,6 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("error marshaling details config: %w", err) } req.Body = &twilioBody - req.Body.Capabilities = []*preview_models.Secrets20231128Capability{ - preview_models.Secrets20231128CapabilityROTATION.Pointer(), - } req.Body.Name = opts.IntegrationName _, err = opts.PreviewClient.CreateTwilioIntegration(req, nil) @@ -193,9 +190,6 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("error marshaling details config: %w", err) } req.Body = &mongoDBBody - req.Body.Capabilities = []*preview_models.Secrets20231128Capability{ - preview_models.Secrets20231128CapabilityROTATION.Pointer(), - } req.Body.Name = opts.IntegrationName _, err = opts.PreviewClient.CreateMongoDBAtlasIntegration(req, nil) @@ -220,9 +214,6 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("error marshaling details config: %w", err) } req.Body = &awsBody - req.Body.Capabilities = []*preview_models.Secrets20231128Capability{ - preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), - } req.Body.Name = opts.IntegrationName _, err = opts.PreviewClient.CreateAwsIntegration(req, nil) @@ -247,9 +238,6 @@ func createRun(opts *CreateOpts) error { return fmt.Errorf("error marshaling details config: %w", err) } req.Body = &gcpBody - req.Body.Capabilities = []*preview_models.Secrets20231128Capability{ - preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), - } req.Body.Name = opts.IntegrationName _, err = opts.PreviewClient.CreateGcpIntegration(req, nil) @@ -363,15 +351,25 @@ func CtyValueToMap(value cty.Value) (map[string]any, error) { } fieldsMap[k] = nestedMap } else if v.Type().IsTupleType() { - var items []map[string]any - for _, val := range v.AsValueSlice() { - nestedMap, err := CtyValueToMap(val) - if err != nil { - return nil, err + // Check the type of the first element in the slice + // (we will assume all other elements in slice are of the same type) + if v.AsValueSlice()[0].Type() == cty.String { + var items []string + for _, val := range v.AsValueSlice() { + items = append(items, val.AsString()) + } + fieldsMap[k] = items + } else { + var items []map[string]any + for _, val := range v.AsValueSlice() { + nestedMap, err := CtyValueToMap(val) + if err != nil { + return nil, err + } + items = append(items, nestedMap) } - items = append(items, nestedMap) + fieldsMap[k] = items } - fieldsMap[k] = items } else { return nil, fmt.Errorf("found unsupported value type") } diff --git a/internal/commands/vaultsecrets/integrations/create_test.go b/internal/commands/vaultsecrets/integrations/create_test.go index 01c112e8..9c4caadd 100644 --- a/internal/commands/vaultsecrets/integrations/create_test.go +++ b/internal/commands/vaultsecrets/integrations/create_test.go @@ -110,6 +110,10 @@ details = { "audience" = "abc", "role_arn" = "def" } + + "capabilities" = [ + "DYNAMIC" + ] }`), }, } diff --git a/internal/commands/vaultsecrets/integrations/integrations.go b/internal/commands/vaultsecrets/integrations/integrations.go index 67b1bb2f..b5814e02 100644 --- a/internal/commands/vaultsecrets/integrations/integrations.go +++ b/internal/commands/vaultsecrets/integrations/integrations.go @@ -34,5 +34,6 @@ func NewCmdIntegrations(ctx *cmd.Context) *cmd.Command { cmd.AddChild(NewCmdDelete(ctx, nil)) cmd.AddChild(NewCmdList(ctx, nil)) cmd.AddChild(NewCmdCreate(ctx, nil)) + cmd.AddChild(NewCmdUpdate(ctx, nil)) return cmd } diff --git a/internal/commands/vaultsecrets/integrations/update.go b/internal/commands/vaultsecrets/integrations/update.go new file mode 100644 index 00000000..a0b46c06 --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/update.go @@ -0,0 +1,210 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hashicorp/hcl/v2/hclsimple" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-06-13/client/secret_service" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +type UpdateOpts struct { + Ctx context.Context + Profile *profile.Profile + IO iostreams.IOStreams + Output *format.Outputter + + IntegrationName string + ConfigFilePath string + PreviewClient preview_secret_service.ClientService + Client secret_service.ClientService +} + +func NewCmdUpdate(ctx *cmd.Context, runF func(*UpdateOpts) error) *cmd.Command { + opts := &UpdateOpts{ + Ctx: ctx.ShutdownCtx, + Profile: ctx.Profile, + IO: ctx.IO, + Output: ctx.Output, + + PreviewClient: preview_secret_service.New(ctx.HCP, nil), + Client: secret_service.New(ctx.HCP, nil), + } + + cmd := &cmd.Command{ + Name: "update", + ShortHelp: "Update an integration.", + LongHelp: heredoc.New(ctx.IO).Must(` + The {{ template "mdCodeOrBold" "hcp vault-secrets integrations update" }} command updates a Vault Secrets integration. + `), + Examples: []cmd.Example{ + { + Preamble: `Update a Vault Secrets integration:`, + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` + $ hcp vault-secrets integrations update sample-integration --config-file=path-to-file/config.hcl + `), + }, + }, + Args: cmd.PositionalArguments{ + Args: []cmd.PositionalArgument{ + { + Name: "NAME", + Documentation: "The name of the integration to update.", + }, + }, + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "config-file", + DisplayValue: "CONFIG_FILE", + Description: "File path to read integration config data.", + Value: flagvalue.Simple("", &opts.ConfigFilePath), + Required: true, + }, + }, + }, + RunF: func(c *cmd.Command, args []string) error { + opts.IntegrationName = args[0] + + if runF != nil { + return runF(opts) + } + return updateRun(opts) + }, + } + + return cmd +} + +func updateRun(opts *UpdateOpts) error { + var ( + config IntegrationConfig + internalConfig integrationConfigInternal + ) + + if err := hclsimple.DecodeFile(opts.ConfigFilePath, nil, &config); err != nil { + return fmt.Errorf("failed to decode config file: %w", err) + } + + detailsMap, err := CtyValueToMap(config.Details) + if err != nil { + return fmt.Errorf("failed to process config file: %w", err) + } + internalConfig.Details = detailsMap + + switch config.Type { + case Twilio: + req := preview_secret_service.NewUpdateTwilioIntegrationParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.Name = opts.IntegrationName + + var twilioBody preview_models.SecretServiceUpdateTwilioIntegrationBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = twilioBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + req.Body = &twilioBody + + _, err = opts.PreviewClient.UpdateTwilioIntegration(req, nil) + if err != nil { + return fmt.Errorf("failed to update Twilio integration: %w", err) + } + + case MongoDBAtlas: + req := preview_secret_service.NewUpdateMongoDBAtlasIntegrationParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.Name = opts.IntegrationName + + var mongoDBBody preview_models.SecretServiceUpdateMongoDBAtlasIntegrationBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = mongoDBBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + req.Body = &mongoDBBody + + _, err = opts.PreviewClient.UpdateMongoDBAtlasIntegration(req, nil) + + if err != nil { + return fmt.Errorf("failed to update MongoDB Atlas integration: %w", err) + } + + case AWS: + req := preview_secret_service.NewUpdateAwsIntegrationParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.Name = opts.IntegrationName + + var awsBody preview_models.SecretServiceUpdateAwsIntegrationBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = awsBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + req.Body = &awsBody + + _, err = opts.PreviewClient.UpdateAwsIntegration(req, nil) + + if err != nil { + return fmt.Errorf("failed to update AWS integration: %w", err) + } + + case GCP: + req := preview_secret_service.NewUpdateGcpIntegrationParamsWithContext(opts.Ctx) + req.OrganizationID = opts.Profile.OrganizationID + req.ProjectID = opts.Profile.ProjectID + req.Name = opts.IntegrationName + + var gcpBody preview_models.SecretServiceUpdateGcpIntegrationBody + detailBytes, err := json.Marshal(internalConfig.Details) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + + err = gcpBody.UnmarshalBinary(detailBytes) + if err != nil { + return fmt.Errorf("error marshaling details config: %w", err) + } + req.Body = &gcpBody + + _, err = opts.PreviewClient.UpdateGcpIntegration(req, nil) + + if err != nil { + return fmt.Errorf("failed to update GCP integration: %w", err) + } + } + + fmt.Fprintln(opts.IO.Err()) + fmt.Fprintf(opts.IO.Err(), "%s Successfully updated integration with name %q\n", opts.IO.ColorScheme().SuccessIcon(), opts.IntegrationName) + + return nil +} diff --git a/internal/commands/vaultsecrets/integrations/update_test.go b/internal/commands/vaultsecrets/integrations/update_test.go new file mode 100644 index 00000000..b2f9ce19 --- /dev/null +++ b/internal/commands/vaultsecrets/integrations/update_test.go @@ -0,0 +1,197 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package integrations + +import ( + "context" + "fmt" + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "os" + "path/filepath" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +func TestNewCmdUpdate(t *testing.T) { + t.Parallel() + + testProfile := func(t *testing.T) *profile.Profile { + tp := profile.TestProfile(t).SetOrgID("123").SetProjectID("456") + return tp + } + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *UpdateOpts + }{ + { + Name: "Good", + Profile: testProfile, + Args: []string{"sample-integration", "--config-file", "path/to/file"}, + Expect: &UpdateOpts{ + IntegrationName: "sample-integration", + ConfigFilePath: "path/to/file", + }, + }, + { + Name: "Failed: No secret name arg specified", + Profile: testProfile, + Args: []string{"--config-file", "path/to/file"}, + Error: "ERROR: accepts 1 arg(s), received 0", + }, + { + Name: "Failed: No secret name arg specified", + Profile: testProfile, + Args: []string{"sample-integration"}, + Error: "ERROR: missing required flag: --config-file=CONFIG_FILE", + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + // Create a context. + io := iostreams.Test() + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var gotOpts *UpdateOpts + createCmd := NewCmdUpdate(ctx, func(o *UpdateOpts) error { + gotOpts = o + return nil + }) + createCmd.SetIO(io) + + code := createCmd.Run(c.Args) + if c.Error != "" { + r.NotZero(code) + r.Contains(io.Error.String(), c.Error) + return + } + + r.Zero(code, io.Error.String()) + r.NotNil(gotOpts) + r.Equal(c.Expect.IntegrationName, gotOpts.IntegrationName) + r.Equal(c.Expect.ConfigFilePath, gotOpts.ConfigFilePath) + }) + } +} + +func TestUpdateRun(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + IntegrationName string + Input []byte + Error string + }{ + { + Name: "Good", + IntegrationName: "sample-integration", + Input: []byte(`type = "aws" +details = { + "federated_workload_identity" = { + "audience" = "abc", + "role_arn" = "def" + } + + "capabilities" = [ + "ROTATION", "DYNAMIC" + ] +}`), + }, + } + + for _, c := range cases { + + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + r := require.New(t) + + tempDir := t.TempDir() + f, err := os.Create(filepath.Join(tempDir, "config.hcl")) + r.NoError(err) + _, err = f.Write(c.Input) + r.NoError(err) + + io := iostreams.Test() + vs := mock_preview_secret_service.NewMockClientService(t) + + opts := &UpdateOpts{ + Ctx: context.Background(), + Profile: profile.TestProfile(t).SetOrgID("123").SetProjectID("abc"), + IO: io, + PreviewClient: vs, + Output: format.New(io), + IntegrationName: c.IntegrationName, + ConfigFilePath: f.Name(), + } + + if c.Error == "" { + vs.EXPECT().UpdateAwsIntegration(&preview_secret_service.UpdateAwsIntegrationParams{ + Context: opts.Ctx, + OrganizationID: "123", + ProjectID: "abc", + Name: opts.IntegrationName, + Body: &preview_models.SecretServiceUpdateAwsIntegrationBody{ + FederatedWorkloadIdentity: &preview_models.Secrets20231128AwsFederatedWorkloadIdentityRequest{ + Audience: "abc", + RoleArn: "def", + }, + Capabilities: []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityROTATION.Pointer(), + preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), + }, + }, + }, nil).Return(&preview_secret_service.UpdateAwsIntegrationOK{ + Payload: &preview_models.Secrets20231128UpdateAwsIntegrationResponse{ + Integration: &preview_models.Secrets20231128AwsIntegration{ + Name: opts.IntegrationName, + FederatedWorkloadIdentity: &preview_models.Secrets20231128AwsFederatedWorkloadIdentityResponse{ + Audience: "abc", + RoleArn: "def", + }, + Capabilities: []*preview_models.Secrets20231128Capability{ + preview_models.Secrets20231128CapabilityROTATION.Pointer(), + preview_models.Secrets20231128CapabilityDYNAMIC.Pointer(), + }, + }, + }, + }, nil).Once() + } + + // Run the command + err = updateRun(opts) + if c.Error != "" { + r.ErrorContains(err, c.Error) + return + } + + r.NoError(err) + r.Contains(io.Error.String(), fmt.Sprintf("✓ Successfully updated integration with name %q\n", opts.IntegrationName)) + }) + } +} From 22635eb1f68cad632a627ec167f1f5d498bad319 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Mon, 30 Sep 2024 13:35:13 -0500 Subject: [PATCH 69/75] lints --- internal/commands/vaultsecrets/integrations/update_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/update_test.go b/internal/commands/vaultsecrets/integrations/update_test.go index b2f9ce19..94e205d8 100644 --- a/internal/commands/vaultsecrets/integrations/update_test.go +++ b/internal/commands/vaultsecrets/integrations/update_test.go @@ -6,9 +6,6 @@ package integrations import ( "context" "fmt" - preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" - preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" - mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" "os" "path/filepath" "testing" @@ -16,6 +13,9 @@ import ( "github.com/go-openapi/runtime/client" "github.com/stretchr/testify/require" + preview_secret_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + preview_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + mock_preview_secret_service "github.com/hashicorp/hcp/internal/pkg/api/mocks/github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" "github.com/hashicorp/hcp/internal/pkg/cmd" "github.com/hashicorp/hcp/internal/pkg/format" "github.com/hashicorp/hcp/internal/pkg/iostreams" From 23b871bd567a09b44bfe94f0e2e60784973e1b01 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 3 Oct 2024 17:45:55 -0500 Subject: [PATCH 70/75] Implements bug bash fixes for cli parity work --- .../vaultsecrets/integrations/create.go | 7 ++- .../vaultsecrets/integrations/delete.go | 5 +- .../vaultsecrets/integrations/displayer.go | 59 +++++++++++++++---- .../vaultsecrets/integrations/list.go | 10 ++-- .../vaultsecrets/integrations/read.go | 5 +- .../vaultsecrets/integrations/update.go | 1 + .../commands/vaultsecrets/secrets/create.go | 1 + .../commands/vaultsecrets/secrets/update.go | 1 + 8 files changed, 66 insertions(+), 23 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index d1210e78..a982ea49 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -37,6 +37,8 @@ type CreateOpts struct { Client secret_service.ClientService } +var IntegrationProviders = maps.Keys(providerToRequiredFields) + func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { opts := &CreateOpts{ Ctx: ctx.ShutdownCtx, @@ -53,6 +55,9 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { ShortHelp: "Create a new integration.", LongHelp: heredoc.New(ctx.IO).Must(` The {{ template "mdCodeOrBold" "hcp vault-secrets integrations create" }} command creates a new Vault Secrets integration. + When the {{ template "mdCodeOrBold" "--config-file" }} flag is specified, the configuration for your integration will be read + from the provided HCL config file. The following fields are required: [type details]. When the {{ template "mdCodeOrBold" "--config-file" }} + flag is not specified, you will be prompted to create the integration interactively. `), Examples: []cmd.Example{ { @@ -265,7 +270,7 @@ func promptUserForConfig(opts *CreateOpts) (IntegrationConfig, integrationConfig providerPrompt := promptui.Select{ Label: "Please select the provider you would like to configure", - Items: maps.Keys(providerToRequiredFields), + Items: IntegrationProviders, Stdin: io.NopCloser(opts.IO.In()), Stdout: iostreams.NopWriteCloser(opts.IO.Err()), } diff --git a/internal/commands/vaultsecrets/integrations/delete.go b/internal/commands/vaultsecrets/integrations/delete.go index c0a136c0..9190bc06 100644 --- a/internal/commands/vaultsecrets/integrations/delete.go +++ b/internal/commands/vaultsecrets/integrations/delete.go @@ -42,9 +42,10 @@ func NewCmdDelete(ctx *cmd.Context, runF func(*DeleteOpts) error) *cmd.Command { cmd := &cmd.Command{ Name: "delete", ShortHelp: "Delete a Vault Secrets integration.", - LongHelp: heredoc.New(ctx.IO).Must(` + LongHelp: heredoc.New(ctx.IO).Must(fmt.Sprintf(` The {{ template "mdCodeOrBold" "hcp vault-secrets integrations delete" }} command deletes a Vault Secrets integration. - `), + The required {{ template "mdCodeOrBold" "--type" }} flag may be any of the following: %v + `, IntegrationProviders)), Examples: []cmd.Example{ { Preamble: `Delete an integration:`, diff --git a/internal/commands/vaultsecrets/integrations/displayer.go b/internal/commands/vaultsecrets/integrations/displayer.go index fe534c0c..78287ddf 100644 --- a/internal/commands/vaultsecrets/integrations/displayer.go +++ b/internal/commands/vaultsecrets/integrations/displayer.go @@ -126,13 +126,15 @@ func (m *mongodbDisplayer) FieldTemplates() []format.Field { type awsDisplayer struct { previewAwsIntegrations []*preview_models.Secrets20231128AwsIntegration - single bool + single bool + federatedWorkloadIdentity bool } -func newAwsDisplayer(single bool, integrations ...*preview_models.Secrets20231128AwsIntegration) *awsDisplayer { +func newAwsDisplayer(single bool, federatedWorkloadIdentity bool, integrations ...*preview_models.Secrets20231128AwsIntegration) *awsDisplayer { return &awsDisplayer{ - previewAwsIntegrations: integrations, - single: single, + previewAwsIntegrations: integrations, + single: single, + federatedWorkloadIdentity: federatedWorkloadIdentity, } } @@ -166,8 +168,11 @@ func (a *awsDisplayer) FieldTemplates() []format.Field { ValueFormat: "{{ .Name }}", }, } + if !a.single { + return fields + } - if a.single { + if a.federatedWorkloadIdentity { return append(fields, []format.Field{ { Name: "Audience", @@ -178,21 +183,29 @@ func (a *awsDisplayer) FieldTemplates() []format.Field { ValueFormat: "{{ .FederatedWorkloadIdentity.RoleArn }}", }, }...) + } else { - return fields + return append(fields, []format.Field{ + { + Name: "Access Key ID", + ValueFormat: "{{ .AccessKeys.AccessKeyID }}", + }, + }...) } } type gcpDisplayer struct { previewGcpIntegrations []*preview_models.Secrets20231128GcpIntegration - single bool + single bool + federatedWorkloadIdentity bool } -func newGcpDisplayer(single bool, integrations ...*preview_models.Secrets20231128GcpIntegration) *gcpDisplayer { +func newGcpDisplayer(single bool, federatedWorkloadIdentity bool, integrations ...*preview_models.Secrets20231128GcpIntegration) *gcpDisplayer { return &gcpDisplayer{ - previewGcpIntegrations: integrations, - single: single, + previewGcpIntegrations: integrations, + single: single, + federatedWorkloadIdentity: federatedWorkloadIdentity, } } @@ -226,20 +239,32 @@ func (g *gcpDisplayer) FieldTemplates() []format.Field { ValueFormat: "{{ .Name }}", }, } + if !g.single { + return fields + } - if g.single { + if g.federatedWorkloadIdentity { return append(fields, []format.Field{ { Name: "Audience", ValueFormat: "{{ .FederatedWorkloadIdentity.Audience }}", }, { - Name: "Audience", + Name: "Service Account Email", ValueFormat: "{{ .FederatedWorkloadIdentity.ServiceAccountEmail }}", }, }...) } else { - return fields + return append(fields, []format.Field{ + { + Name: "Client Email", + ValueFormat: "{{ .ServiceAccountKey.ClientEmail }}", + }, + { + Name: "Project ID", + ValueFormat: "{{ .ServiceAccountKey.ProjectID }}", + }, + }...) } } @@ -284,5 +309,13 @@ func (g *genericDisplayer) FieldTemplates() []format.Field { Name: "Integration Name", ValueFormat: "{{ .Name }}", }, + { + Name: "Provider", + ValueFormat: "{{ .Provider }}", + }, + { + Name: "Created", + ValueFormat: "{{ .CreatedAt }}", + }, } } diff --git a/internal/commands/vaultsecrets/integrations/list.go b/internal/commands/vaultsecrets/integrations/list.go index 0cafb639..9faaad6b 100644 --- a/internal/commands/vaultsecrets/integrations/list.go +++ b/internal/commands/vaultsecrets/integrations/list.go @@ -42,9 +42,10 @@ func NewCmdList(ctx *cmd.Context, runF func(*ListOpts) error) *cmd.Command { cmd := &cmd.Command{ Name: "list", ShortHelp: "List Vault Secrets integrations.", - LongHelp: heredoc.New(ctx.IO).Must(` + LongHelp: heredoc.New(ctx.IO).Must(fmt.Sprintf(` The {{ template "mdCodeOrBold" "hcp vault-secrets integrations list" }} command lists Vault Secrets generic integrations. - `), + The optional {{ template "mdCodeOrBold" "--type" }} flag may be any of the following: %v + `, IntegrationProviders)), Examples: []cmd.Example{ { Preamble: `List twilio integrations:`, @@ -86,6 +87,7 @@ func listRun(opts *ListOpts) error { Context: opts.Ctx, ProjectID: opts.Profile.ProjectID, OrganizationID: opts.Profile.OrganizationID, + Capabilities: []string{"ROTATION", "DYNAMIC"}, } for { resp, err := opts.PreviewClient.ListIntegrations(params, nil) @@ -180,7 +182,7 @@ func listRun(opts *ListOpts) error { next := resp.Payload.Pagination.NextPageToken params.PaginationNextPageToken = &next } - return opts.Output.Display(newAwsDisplayer(false, integrations...)) + return opts.Output.Display(newAwsDisplayer(false, false, integrations...)) case GCP: var integrations []*preview_models.Secrets20231128GcpIntegration @@ -205,7 +207,7 @@ func listRun(opts *ListOpts) error { next := resp.Payload.Pagination.NextPageToken params.PaginationNextPageToken = &next } - return opts.Output.Display(newGcpDisplayer(false, integrations...)) + return opts.Output.Display(newGcpDisplayer(false, false, integrations...)) default: return fmt.Errorf("not a valid integration type") diff --git a/internal/commands/vaultsecrets/integrations/read.go b/internal/commands/vaultsecrets/integrations/read.go index 59614082..7f94ed20 100644 --- a/internal/commands/vaultsecrets/integrations/read.go +++ b/internal/commands/vaultsecrets/integrations/read.go @@ -68,7 +68,6 @@ func NewCmdRead(ctx *cmd.Context, runF func(*ReadOpts) error) *cmd.Command { DisplayValue: "TYPE", Description: "The type of the integration to read.", Value: flagvalue.Simple("", &opts.Type), - Required: true, }, }, }, @@ -137,7 +136,7 @@ func readRun(opts *ReadOpts) error { return fmt.Errorf("failed to read integration: %w", err) } - return opts.Output.Display(newAwsDisplayer(true, resp.Payload.Integration)) + return opts.Output.Display(newAwsDisplayer(true, resp.Payload.Integration.FederatedWorkloadIdentity != nil, resp.Payload.Integration)) case GCP: resp, err := opts.PreviewClient.GetGcpIntegration(&preview_secret_service.GetGcpIntegrationParams{ @@ -150,7 +149,7 @@ func readRun(opts *ReadOpts) error { return fmt.Errorf("failed to read integration: %w", err) } - return opts.Output.Display(newGcpDisplayer(true, resp.Payload.Integration)) + return opts.Output.Display(newGcpDisplayer(true, resp.Payload.Integration.FederatedWorkloadIdentity != nil, resp.Payload.Integration)) default: return fmt.Errorf("not a valid integration type") diff --git a/internal/commands/vaultsecrets/integrations/update.go b/internal/commands/vaultsecrets/integrations/update.go index a0b46c06..caa6c211 100644 --- a/internal/commands/vaultsecrets/integrations/update.go +++ b/internal/commands/vaultsecrets/integrations/update.go @@ -49,6 +49,7 @@ func NewCmdUpdate(ctx *cmd.Context, runF func(*UpdateOpts) error) *cmd.Command { ShortHelp: "Update an integration.", LongHelp: heredoc.New(ctx.IO).Must(` The {{ template "mdCodeOrBold" "hcp vault-secrets integrations update" }} command updates a Vault Secrets integration. + The following fields are required: [type details]. `), Examples: []cmd.Example{ { diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index ae4a2ad7..a1e98250 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -43,6 +43,7 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { ShortHelp: "Create a new static secret.", LongHelp: heredoc.New(ctx.IO).Must(` The {{ template "mdCodeOrBold" "hcp vault-secrets secrets create" }} command creates a new static, rotating, or dynamic secret under a Vault Secrets application. + For rotating and dynamic secrets, the following fields are required in the config file: [type integration_name details]. `), Examples: []cmd.Example{ { diff --git a/internal/commands/vaultsecrets/secrets/update.go b/internal/commands/vaultsecrets/secrets/update.go index db1516cd..d8a3f871 100644 --- a/internal/commands/vaultsecrets/secrets/update.go +++ b/internal/commands/vaultsecrets/secrets/update.go @@ -52,6 +52,7 @@ func NewCmdUpdate(ctx *cmd.Context, runF func(*UpdateOpts) error) *cmd.Command { ShortHelp: "Update an existing secret.", LongHelp: heredoc.New(ctx.IO).Must(` The {{ template "mdCodeOrBold" "hcp vault-secrets secrets update" }} command updates an existing rotating or dynamic secret under a Vault Secrets application. + The following fields are required in the config file: [type details]. `), Examples: []cmd.Example{ { From 8c8fb70004627950e3f549ad16ee83ed546ec736 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 3 Oct 2024 17:51:10 -0500 Subject: [PATCH 71/75] fixes failing test --- internal/commands/vaultsecrets/integrations/read_test.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/read_test.go b/internal/commands/vaultsecrets/integrations/read_test.go index 2ddc378e..10cb9aed 100644 --- a/internal/commands/vaultsecrets/integrations/read_test.go +++ b/internal/commands/vaultsecrets/integrations/read_test.go @@ -43,14 +43,6 @@ func TestNewCmdRead(t *testing.T) { Type: "twilio", }, }, - { - Name: "Missing type flag", - Profile: func(t *testing.T) *profile.Profile { - return profile.TestProfile(t).SetOrgID("123").SetProjectID("abc") - }, - Args: []string{"sample-integration"}, - Error: "ERROR: missing required flag: --type=TYPE", - }, } for _, c := range cases { c := c From b6c990c90a26e41c5d5886a7be62e02b8a4c11f8 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Thu, 3 Oct 2024 18:20:03 -0500 Subject: [PATCH 72/75] updates help text --- internal/commands/vaultsecrets/integrations/create.go | 5 ++++- internal/commands/vaultsecrets/integrations/update.go | 4 +++- internal/commands/vaultsecrets/secrets/create.go | 4 +++- internal/commands/vaultsecrets/secrets/update.go | 4 +++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/internal/commands/vaultsecrets/integrations/create.go b/internal/commands/vaultsecrets/integrations/create.go index a982ea49..fab9d84a 100644 --- a/internal/commands/vaultsecrets/integrations/create.go +++ b/internal/commands/vaultsecrets/integrations/create.go @@ -56,7 +56,10 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { LongHelp: heredoc.New(ctx.IO).Must(` The {{ template "mdCodeOrBold" "hcp vault-secrets integrations create" }} command creates a new Vault Secrets integration. When the {{ template "mdCodeOrBold" "--config-file" }} flag is specified, the configuration for your integration will be read - from the provided HCL config file. The following fields are required: [type details]. When the {{ template "mdCodeOrBold" "--config-file" }} + from the provided HCL config file. The following fields are required: [type details]. For help populating the details for an + integration type, please refer to the + {{ Link "API reference documentation" "https://developer.hashicorp.com/hcp/api-docs/vault-secrets/2023-11-28" }}. + When the {{ template "mdCodeOrBold" "--config-file" }} flag is not specified, you will be prompted to create the integration interactively. `), Examples: []cmd.Example{ diff --git a/internal/commands/vaultsecrets/integrations/update.go b/internal/commands/vaultsecrets/integrations/update.go index caa6c211..ac799413 100644 --- a/internal/commands/vaultsecrets/integrations/update.go +++ b/internal/commands/vaultsecrets/integrations/update.go @@ -49,7 +49,9 @@ func NewCmdUpdate(ctx *cmd.Context, runF func(*UpdateOpts) error) *cmd.Command { ShortHelp: "Update an integration.", LongHelp: heredoc.New(ctx.IO).Must(` The {{ template "mdCodeOrBold" "hcp vault-secrets integrations update" }} command updates a Vault Secrets integration. - The following fields are required: [type details]. + The configuration for updating your integration will be read from the provided HCL config file. The following fields are + required: [type details]. For help populating the details for an integration type, please refer to the + {{ Link "API reference documentation" "https://developer.hashicorp.com/hcp/api-docs/vault-secrets/2023-11-28" }}. `), Examples: []cmd.Example{ { diff --git a/internal/commands/vaultsecrets/secrets/create.go b/internal/commands/vaultsecrets/secrets/create.go index a1e98250..14b3eacf 100644 --- a/internal/commands/vaultsecrets/secrets/create.go +++ b/internal/commands/vaultsecrets/secrets/create.go @@ -43,7 +43,9 @@ func NewCmdCreate(ctx *cmd.Context, runF func(*CreateOpts) error) *cmd.Command { ShortHelp: "Create a new static secret.", LongHelp: heredoc.New(ctx.IO).Must(` The {{ template "mdCodeOrBold" "hcp vault-secrets secrets create" }} command creates a new static, rotating, or dynamic secret under a Vault Secrets application. - For rotating and dynamic secrets, the following fields are required in the config file: [type integration_name details]. + The configuration for creating your rotating or dynamic secret will be read from the provided HCL config file. The following fields are required in the config + file: [type integration_name details]. For help populating the details for a dynamic or rotating secret, please refer to the + {{ Link "API reference documentation" "https://developer.hashicorp.com/hcp/api-docs/vault-secrets/2023-11-28" }}. `), Examples: []cmd.Example{ { diff --git a/internal/commands/vaultsecrets/secrets/update.go b/internal/commands/vaultsecrets/secrets/update.go index d8a3f871..9b886546 100644 --- a/internal/commands/vaultsecrets/secrets/update.go +++ b/internal/commands/vaultsecrets/secrets/update.go @@ -52,7 +52,9 @@ func NewCmdUpdate(ctx *cmd.Context, runF func(*UpdateOpts) error) *cmd.Command { ShortHelp: "Update an existing secret.", LongHelp: heredoc.New(ctx.IO).Must(` The {{ template "mdCodeOrBold" "hcp vault-secrets secrets update" }} command updates an existing rotating or dynamic secret under a Vault Secrets application. - The following fields are required in the config file: [type details]. + The configuration for updating your rotating or dynamic secret will be read from the provided HCL config file. The following fields are required in the config + file: [type details]. For help populating the details for a dynamic or rotating secret, please refer to the + {{ Link "API reference documentation" "https://developer.hashicorp.com/hcp/api-docs/vault-secrets/2023-11-28" }}. `), Examples: []cmd.Example{ { From 6f16e93ab7c3a7c3ff194200ae9ca56e8659c257 Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 4 Oct 2024 12:39:43 -0500 Subject: [PATCH 73/75] quick changes --- go.mod | 2 +- go.sum | 4 ++-- internal/commands/vaultsecrets/secrets/update.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 9722227e..501bc334 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl/v2 v2.22.0 - github.com/hashicorp/hcp-sdk-go v0.113.0 + github.com/hashicorp/hcp-sdk-go v0.115.0 github.com/lithammer/dedent v1.1.0 github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/cli v1.1.5 diff --git a/go.sum b/go.sum index 5f779ecc..78f0963a 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= -github.com/hashicorp/hcp-sdk-go v0.113.0 h1:JuA6mZosEezE550lNMEq43VLUCzVc+/jPmPC1Nd39Gk= -github.com/hashicorp/hcp-sdk-go v0.113.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +github.com/hashicorp/hcp-sdk-go v0.115.0 h1:q6viFNFPd4H4cHm/B9KGYvkpkT5ZSBQASh9KR/zYHEI= +github.com/hashicorp/hcp-sdk-go v0.115.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= diff --git a/internal/commands/vaultsecrets/secrets/update.go b/internal/commands/vaultsecrets/secrets/update.go index 9b886546..db142950 100644 --- a/internal/commands/vaultsecrets/secrets/update.go +++ b/internal/commands/vaultsecrets/secrets/update.go @@ -49,7 +49,7 @@ func NewCmdUpdate(ctx *cmd.Context, runF func(*UpdateOpts) error) *cmd.Command { cmd := &cmd.Command{ Name: "update", - ShortHelp: "Update an existing secret.", + ShortHelp: "Update an existing dynamic or rotating secret.", LongHelp: heredoc.New(ctx.IO).Must(` The {{ template "mdCodeOrBold" "hcp vault-secrets secrets update" }} command updates an existing rotating or dynamic secret under a Vault Secrets application. The configuration for updating your rotating or dynamic secret will be read from the provided HCL config file. The following fields are required in the config From 9f73c2d23562ece13ab32edfbdddaa607f053a1c Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 4 Oct 2024 12:46:32 -0500 Subject: [PATCH 74/75] Adds changelog --- .changelog/176.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/176.txt diff --git a/.changelog/176.txt b/.changelog/176.txt new file mode 100644 index 00000000..21c622bf --- /dev/null +++ b/.changelog/176.txt @@ -0,0 +1,3 @@ +```release-note:feature +vault-secrets: Add support for managing vault-secrets integrations and rotating/dynamic secrets +``` From 64f58bc2d069374487b5104838de167c6a0b524c Mon Sep 17 00:00:00 2001 From: Mercedes Hall Date: Fri, 4 Oct 2024 14:18:39 -0500 Subject: [PATCH 75/75] adds missing copyright --- internal/commands/vaultsecrets/secrets/update.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/commands/vaultsecrets/secrets/update.go b/internal/commands/vaultsecrets/secrets/update.go index db142950..2948cbd1 100644 --- a/internal/commands/vaultsecrets/secrets/update.go +++ b/internal/commands/vaultsecrets/secrets/update.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package secrets import (