From aa3be88573ce3545220aea06a132142ecba61b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 5 Mar 2024 11:21:56 +0000 Subject: [PATCH 1/3] Onboard secrets manager: add delete subcommand --- .../secrets-manager/instance/delete/delete.go | 96 ++++++++ .../instance/delete/delete_test.go | 215 ++++++++++++++++++ .../cmd/secrets-manager/instance/instance.go | 2 + .../services/secrets-manager/utils/utils.go | 20 ++ .../secrets-manager/utils/utils_test.go | 81 +++++++ 5 files changed, 414 insertions(+) create mode 100644 internal/cmd/secrets-manager/instance/delete/delete.go create mode 100644 internal/cmd/secrets-manager/instance/delete/delete_test.go create mode 100644 internal/pkg/services/secrets-manager/utils/utils.go create mode 100644 internal/pkg/services/secrets-manager/utils/utils_test.go diff --git a/internal/cmd/secrets-manager/instance/delete/delete.go b/internal/cmd/secrets-manager/instance/delete/delete.go new file mode 100644 index 00000000..0e227b5a --- /dev/null +++ b/internal/cmd/secrets-manager/instance/delete/delete.go @@ -0,0 +1,96 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/client" + secretsmanagerUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +const ( + instanceIdArg = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", instanceIdArg), + Short: "Deletes a Secrets Manager instance", + Long: "Deletes a Secrets Manager instance.", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a Secrets Manager instance with ID "xxx"`, + "$ stackit secrets-manager instance delete xxx"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + instanceLabel, err := secretsmanagerUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + instanceLabel = model.InstanceId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = confirm.PromptForConfirmation(cmd, prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete Secrets Manager instance: %w", err) + } + + cmd.Printf("Deleted instance %s \n", model.InstanceId) + return nil + }, + } + return cmd +} + +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + instanceId := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: instanceId, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiDeleteInstanceRequest { + req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) + return req +} diff --git a/internal/cmd/secrets-manager/instance/delete/delete_test.go b/internal/cmd/secrets-manager/instance/delete/delete_test.go new file mode 100644 index 00000000..fff0cf83 --- /dev/null +++ b/internal/cmd/secrets-manager/instance/delete/delete_test.go @@ -0,0 +1,215 @@ +package delete + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &secretsmanager.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testInstanceId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *secretsmanager.ApiDeleteInstanceRequest)) secretsmanager.ApiDeleteInstanceRequest { + request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest secretsmanager.ApiDeleteInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/secrets-manager/instance/instance.go b/internal/cmd/secrets-manager/instance/instance.go index 6dbb36bb..f64ffac1 100644 --- a/internal/cmd/secrets-manager/instance/instance.go +++ b/internal/cmd/secrets-manager/instance/instance.go @@ -3,6 +3,7 @@ package instance import ( "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/create" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/delete" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -24,4 +25,5 @@ func NewCmd() *cobra.Command { func addSubcommands(cmd *cobra.Command) { cmd.AddCommand(list.NewCmd()) cmd.AddCommand(create.NewCmd()) + cmd.AddCommand(delete.NewCmd()) } diff --git a/internal/pkg/services/secrets-manager/utils/utils.go b/internal/pkg/services/secrets-manager/utils/utils.go new file mode 100644 index 00000000..97aa1f6c --- /dev/null +++ b/internal/pkg/services/secrets-manager/utils/utils.go @@ -0,0 +1,20 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +type SecretsManagerClient interface { + GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*secretsmanager.Instance, error) +} + +func GetInstanceName(ctx context.Context, apiClient SecretsManagerClient, projectId, instanceId string) (string, error) { + resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) + if err != nil { + return "", fmt.Errorf("get Secrets Manager instance: %w", err) + } + return *resp.Name, nil +} diff --git a/internal/pkg/services/secrets-manager/utils/utils_test.go b/internal/pkg/services/secrets-manager/utils/utils_test.go new file mode 100644 index 00000000..6ae75fbd --- /dev/null +++ b/internal/pkg/services/secrets-manager/utils/utils_test.go @@ -0,0 +1,81 @@ +package utils + +import ( + "context" + "fmt" + "testing" + + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +var ( + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +const ( + testInstanceName = "instance" + testUserName = "user" +) + +type secretsManagerClientMocked struct { + getInstanceFails bool + getInstanceResp *secretsmanager.Instance +} + +func (s *secretsManagerClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*secretsmanager.Instance, error) { + if s.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return s.getInstanceResp, nil +} + +func TestGetInstanceName(t *testing.T) { + tests := []struct { + description string + getInstanceFails bool + getInstanceResp *secretsmanager.Instance + isValid bool + expectedOutput string + }{ + { + description: "base", + getInstanceResp: &secretsmanager.Instance{ + Name: utils.Ptr(testInstanceName), + }, + isValid: true, + expectedOutput: testInstanceName, + }, + { + description: "get instance fails", + getInstanceFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &secretsManagerClientMocked{ + getInstanceFails: tt.getInstanceFails, + getInstanceResp: tt.getInstanceResp, + } + + output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} From 73d2e645b596025d9c2e59ced4f6616a4816db62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 5 Mar 2024 13:11:29 +0000 Subject: [PATCH 2/3] Onboarding Secrets Manager: describe command and new docs --- docs/stackit.md | 1 + docs/stackit_config_set.md | 1 + docs/stackit_config_unset.md | 1 + docs/stackit_object-storage.md | 1 + ...tackit_object-storage_credentials-group.md | 34 +++ ...object-storage_credentials-group_create.md | 39 ++++ ...object-storage_credentials-group_delete.md | 38 ++++ ...t_object-storage_credentials-group_list.md | 45 ++++ docs/stackit_object-storage_disable.md | 4 +- docs/stackit_secrets-manager.md | 32 +++ docs/stackit_secrets-manager_instance.md | 35 +++ ...stackit_secrets-manager_instance_create.md | 39 ++++ ...stackit_secrets-manager_instance_delete.md | 38 ++++ ...ackit_secrets-manager_instance_describe.md | 41 ++++ docs/stackit_secrets-manager_instance_list.md | 45 ++++ .../instance/describe/describe.go | 119 ++++++++++ .../instance/describe/describe_test.go | 215 ++++++++++++++++++ .../cmd/secrets-manager/instance/instance.go | 4 +- 18 files changed, 729 insertions(+), 3 deletions(-) create mode 100644 docs/stackit_object-storage_credentials-group.md create mode 100644 docs/stackit_object-storage_credentials-group_create.md create mode 100644 docs/stackit_object-storage_credentials-group_delete.md create mode 100644 docs/stackit_object-storage_credentials-group_list.md create mode 100644 docs/stackit_secrets-manager.md create mode 100644 docs/stackit_secrets-manager_instance.md create mode 100644 docs/stackit_secrets-manager_instance_create.md create mode 100644 docs/stackit_secrets-manager_instance_delete.md create mode 100644 docs/stackit_secrets-manager_instance_describe.md create mode 100644 docs/stackit_secrets-manager_instance_list.md create mode 100644 internal/cmd/secrets-manager/instance/describe/describe.go create mode 100644 internal/cmd/secrets-manager/instance/describe/describe_test.go diff --git a/docs/stackit.md b/docs/stackit.md index de954dec..5f5f6f24 100644 --- a/docs/stackit.md +++ b/docs/stackit.md @@ -39,6 +39,7 @@ stackit [flags] * [stackit project](./stackit_project.md) - Provides functionality regarding projects * [stackit rabbitmq](./stackit_rabbitmq.md) - Provides functionality for RabbitMQ * [stackit redis](./stackit_redis.md) - Provides functionality for Redis +* [stackit secrets-manager](./stackit_secrets-manager.md) - Provides functionality for Secrets Manager * [stackit service-account](./stackit_service-account.md) - Provides functionality for service accounts * [stackit ske](./stackit_ske.md) - Provides functionality for SKE diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index 313afee3..096dd2aa 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -41,6 +41,7 @@ stackit config set [flags] --rabbitmq-custom-endpoint string RabbitMQ API base URL, used in calls to this API --redis-custom-endpoint string Redis API base URL, used in calls to this API --resource-manager-custom-endpoint string Resource Manager API base URL, used in calls to this API + --secrets-manager-custom-endpoint string Secrets Manager API base URL, used in calls to this API --service-account-custom-endpoint string Service Account API base URL, used in calls to this API --session-time-limit string Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect) --ske-custom-endpoint string SKE API base URL, used in calls to this API diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index 012b65e8..35f65a29 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -41,6 +41,7 @@ stackit config unset [flags] --rabbitmq-custom-endpoint RabbitMQ API base URL. If unset, uses the default base URL --redis-custom-endpoint Redis API base URL. If unset, uses the default base URL --resource-manager-custom-endpoint Resource Manager API base URL. If unset, uses the default base URL + --secrets-manager-custom-endpoint Secrets Manager API base URL. If unset, uses the default base URL --service-account-custom-endpoint SKE API base URL. If unset, uses the default base URL --session-time-limit Maximum time before authentication is required again. If unset, defaults to 2h --ske-custom-endpoint SKE API base URL. If unset, uses the default base URL diff --git a/docs/stackit_object-storage.md b/docs/stackit_object-storage.md index a07097d7..a30996fb 100644 --- a/docs/stackit_object-storage.md +++ b/docs/stackit_object-storage.md @@ -29,6 +29,7 @@ stackit object-storage [flags] * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit object-storage bucket](./stackit_object-storage_bucket.md) - Provides functionality for Object Storage buckets +* [stackit object-storage credentials-group](./stackit_object-storage_credentials-group.md) - Provides functionality for Object Storage credentials group * [stackit object-storage disable](./stackit_object-storage_disable.md) - Disables Object Storage for a project * [stackit object-storage enable](./stackit_object-storage_enable.md) - Enables Object Storage for a project diff --git a/docs/stackit_object-storage_credentials-group.md b/docs/stackit_object-storage_credentials-group.md new file mode 100644 index 00000000..d575dfde --- /dev/null +++ b/docs/stackit_object-storage_credentials-group.md @@ -0,0 +1,34 @@ +## stackit object-storage credentials-group + +Provides functionality for Object Storage credentials group + +### Synopsis + +Provides functionality for Object Storage credentials group. + +``` +stackit object-storage credentials-group [flags] +``` + +### Options + +``` + -h, --help Help for "stackit object-storage credentials-group" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit object-storage](./stackit_object-storage.md) - Provides functionality regarding Object Storage +* [stackit object-storage credentials-group create](./stackit_object-storage_credentials-group_create.md) - Creates a credentials group to hold Object Storage access credentials +* [stackit object-storage credentials-group delete](./stackit_object-storage_credentials-group_delete.md) - Deletes a credentials group +* [stackit object-storage credentials-group list](./stackit_object-storage_credentials-group_list.md) - Lists all credentials groups + diff --git a/docs/stackit_object-storage_credentials-group_create.md b/docs/stackit_object-storage_credentials-group_create.md new file mode 100644 index 00000000..f349b9dd --- /dev/null +++ b/docs/stackit_object-storage_credentials-group_create.md @@ -0,0 +1,39 @@ +## stackit object-storage credentials-group create + +Creates a credentials group to hold Object Storage access credentials + +### Synopsis + +Creates a credentials group to hold Object Storage access credentials. + +``` +stackit object-storage credentials-group create [flags] +``` + +### Examples + +``` + Create credentials group to hold Object Storage access credentials + $ stackit object-storage credentials-group create --name example +``` + +### Options + +``` + -h, --help Help for "stackit object-storage credentials-group create" + --name string Name of the group holding credentials +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit object-storage credentials-group](./stackit_object-storage_credentials-group.md) - Provides functionality for Object Storage credentials group + diff --git a/docs/stackit_object-storage_credentials-group_delete.md b/docs/stackit_object-storage_credentials-group_delete.md new file mode 100644 index 00000000..56a59ae5 --- /dev/null +++ b/docs/stackit_object-storage_credentials-group_delete.md @@ -0,0 +1,38 @@ +## stackit object-storage credentials-group delete + +Deletes a credentials group + +### Synopsis + +Deletes a credentials group. Only possible if there are no valid credentials (access-keys) left in the group, otherwise it will throw an error. + +``` +stackit object-storage credentials-group delete CREDENTIALS_GROUP_ID [flags] +``` + +### Examples + +``` + Delete a credentials group with ID "xxx" + $ stackit object-storage credentials-group delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit object-storage credentials-group delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit object-storage credentials-group](./stackit_object-storage_credentials-group.md) - Provides functionality for Object Storage credentials group + diff --git a/docs/stackit_object-storage_credentials-group_list.md b/docs/stackit_object-storage_credentials-group_list.md new file mode 100644 index 00000000..6da01fef --- /dev/null +++ b/docs/stackit_object-storage_credentials-group_list.md @@ -0,0 +1,45 @@ +## stackit object-storage credentials-group list + +Lists all credentials groups + +### Synopsis + +Lists all credentials groups. + +``` +stackit object-storage credentials-group list [flags] +``` + +### Examples + +``` + List all credentials groups + $ stackit object-storage credentials-group list + + List all credentials groups in JSON format + $ stackit object-storage credentials-group list --output-format json + + List up to 10 credentials groups + $ stackit object-storage credentials-group list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit object-storage credentials-group list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit object-storage credentials-group](./stackit_object-storage_credentials-group.md) - Provides functionality for Object Storage credentials group + diff --git a/docs/stackit_object-storage_disable.md b/docs/stackit_object-storage_disable.md index 629a3201..0e704470 100644 --- a/docs/stackit_object-storage_disable.md +++ b/docs/stackit_object-storage_disable.md @@ -4,7 +4,7 @@ Disables Object Storage for a project ### Synopsis -Disables Object Storage for a project. It will delete all associated buckets. +Disables Object Storage for a project. All buckets must be deleted beforehand. ``` stackit object-storage disable [flags] @@ -13,7 +13,7 @@ stackit object-storage disable [flags] ### Examples ``` - Disable Object Storage functionality for your project, deleting all associated buckets + Disable Object Storage functionality for your project. $ stackit object-storage disable ``` diff --git a/docs/stackit_secrets-manager.md b/docs/stackit_secrets-manager.md new file mode 100644 index 00000000..df31a405 --- /dev/null +++ b/docs/stackit_secrets-manager.md @@ -0,0 +1,32 @@ +## stackit secrets-manager + +Provides functionality for Secrets Manager + +### Synopsis + +Provides functionality for Secrets Manager. + +``` +stackit secrets-manager [flags] +``` + +### Options + +``` + -h, --help Help for "stackit secrets-manager" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit secrets-manager instance](./stackit_secrets-manager_instance.md) - Provides functionality for Secrets Manager instances + diff --git a/docs/stackit_secrets-manager_instance.md b/docs/stackit_secrets-manager_instance.md new file mode 100644 index 00000000..37c11626 --- /dev/null +++ b/docs/stackit_secrets-manager_instance.md @@ -0,0 +1,35 @@ +## stackit secrets-manager instance + +Provides functionality for Secrets Manager instances + +### Synopsis + +Provides functionality for Secrets Manager instances. + +``` +stackit secrets-manager instance [flags] +``` + +### Options + +``` + -h, --help Help for "stackit secrets-manager instance" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit secrets-manager](./stackit_secrets-manager.md) - Provides functionality for Secrets Manager +* [stackit secrets-manager instance create](./stackit_secrets-manager_instance_create.md) - Creates a Secrets Manager instance +* [stackit secrets-manager instance delete](./stackit_secrets-manager_instance_delete.md) - Deletes a Secrets Manager instance +* [stackit secrets-manager instance describe](./stackit_secrets-manager_instance_describe.md) - Shows details of a Secrets Manager instance +* [stackit secrets-manager instance list](./stackit_secrets-manager_instance_list.md) - Lists all Secrets Manager instances + diff --git a/docs/stackit_secrets-manager_instance_create.md b/docs/stackit_secrets-manager_instance_create.md new file mode 100644 index 00000000..bb59b198 --- /dev/null +++ b/docs/stackit_secrets-manager_instance_create.md @@ -0,0 +1,39 @@ +## stackit secrets-manager instance create + +Creates a Secrets Manager instance + +### Synopsis + +Creates a Secrets Manager instance. + +``` +stackit secrets-manager instance create [flags] +``` + +### Examples + +``` + Create a Secrets Manager instance with name "my-instance" + $ stackit secrets-manager instance create --name my-instance +``` + +### Options + +``` + -h, --help Help for "stackit secrets-manager instance create" + -n, --name string Instance name +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit secrets-manager instance](./stackit_secrets-manager_instance.md) - Provides functionality for Secrets Manager instances + diff --git a/docs/stackit_secrets-manager_instance_delete.md b/docs/stackit_secrets-manager_instance_delete.md new file mode 100644 index 00000000..72766bc7 --- /dev/null +++ b/docs/stackit_secrets-manager_instance_delete.md @@ -0,0 +1,38 @@ +## stackit secrets-manager instance delete + +Deletes a Secrets Manager instance + +### Synopsis + +Deletes a Secrets Manager instance. + +``` +stackit secrets-manager instance delete INSTANCE_ID [flags] +``` + +### Examples + +``` + Delete a Secrets Manager instance with ID "xxx" + $ stackit secrets-manager instance delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit secrets-manager instance delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit secrets-manager instance](./stackit_secrets-manager_instance.md) - Provides functionality for Secrets Manager instances + diff --git a/docs/stackit_secrets-manager_instance_describe.md b/docs/stackit_secrets-manager_instance_describe.md new file mode 100644 index 00000000..4ddd537f --- /dev/null +++ b/docs/stackit_secrets-manager_instance_describe.md @@ -0,0 +1,41 @@ +## stackit secrets-manager instance describe + +Shows details of a Secrets Manager instance + +### Synopsis + +Shows details of a Secrets Manager instance. + +``` +stackit secrets-manager instance describe INSTANCE_ID [flags] +``` + +### Examples + +``` + Get details of a Secrets Manager instance with ID "xxx" + $ stackit secrets-manager instance describe xxx + + Get details of a Secrets Manager instance with ID "xxx" in a table format + $ stackit secrets-manager instance describe xxx --output-format pretty +``` + +### Options + +``` + -h, --help Help for "stackit secrets-manager instance describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit secrets-manager instance](./stackit_secrets-manager_instance.md) - Provides functionality for Secrets Manager instances + diff --git a/docs/stackit_secrets-manager_instance_list.md b/docs/stackit_secrets-manager_instance_list.md new file mode 100644 index 00000000..2431f2e9 --- /dev/null +++ b/docs/stackit_secrets-manager_instance_list.md @@ -0,0 +1,45 @@ +## stackit secrets-manager instance list + +Lists all Secrets Manager instances + +### Synopsis + +Lists all Secrets Manager instances. + +``` +stackit secrets-manager instance list [flags] +``` + +### Examples + +``` + List all Secrets Manager instances + $ stackit secrets-manager instance list + + List all Secrets Manager instances in JSON format + $ stackit secrets-manager instance list --output-format json + + List up to 10 Secrets Manager instances + $ stackit secrets-manager instance list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit secrets-manager instance list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit secrets-manager instance](./stackit_secrets-manager_instance.md) - Provides functionality for Secrets Manager instances + diff --git a/internal/cmd/secrets-manager/instance/describe/describe.go b/internal/cmd/secrets-manager/instance/describe/describe.go new file mode 100644 index 00000000..db8a1645 --- /dev/null +++ b/internal/cmd/secrets-manager/instance/describe/describe.go @@ -0,0 +1,119 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +const ( + instanceIdArg = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", instanceIdArg), + Short: "Shows details of a Secrets Manager instance", + Long: "Shows details of a Secrets Manager instance.", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of a Secrets Manager instance with ID "xxx"`, + "$ stackit secrets-manager instance describe xxx"), + examples.NewExample( + `Get details of a Secrets Manager instance with ID "xxx" in a table format`, + "$ stackit secrets-manager instance describe xxx --output-format pretty"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd, args) + if err != nil { + return err + } + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read Secrets Manager instance: %w", err) + } + + return outputResult(cmd, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + instanceId := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: instanceId, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiGetInstanceRequest { + req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + return req +} + +func outputResult(cmd *cobra.Command, outputFormat string, instance *secretsmanager.Instance) error { + switch outputFormat { + case globalflags.PrettyOutputFormat: + + table := tables.NewTable() + table.AddRow("ID", *instance.Id) + table.AddSeparator() + table.AddRow("NAME", *instance.Name) + table.AddSeparator() + table.AddRow("STATUS", *instance.State) + table.AddSeparator() + table.AddRow("SECRETS", *instance.SecretCount) + table.AddSeparator() + table.AddRow("ENGINE", *instance.SecretsEngine) + table.AddSeparator() + table.AddRow("CREATION DATE", *instance.CreationStartDate) + table.AddSeparator() + err := table.Display(cmd) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + default: + details, err := json.MarshalIndent(instance, "", " ") + if err != nil { + return fmt.Errorf("marshal Secrets Manager instance: %w", err) + } + cmd.Println(string(details)) + + return nil + } +} diff --git a/internal/cmd/secrets-manager/instance/describe/describe_test.go b/internal/cmd/secrets-manager/instance/describe/describe_test.go new file mode 100644 index 00000000..0c03949f --- /dev/null +++ b/internal/cmd/secrets-manager/instance/describe/describe_test.go @@ -0,0 +1,215 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &secretsmanager.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testInstanceId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *secretsmanager.ApiGetInstanceRequest)) secretsmanager.ApiGetInstanceRequest { + request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest secretsmanager.ApiGetInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/secrets-manager/instance/instance.go b/internal/cmd/secrets-manager/instance/instance.go index f64ffac1..fbcb44e1 100644 --- a/internal/cmd/secrets-manager/instance/instance.go +++ b/internal/cmd/secrets-manager/instance/instance.go @@ -2,8 +2,9 @@ package instance import ( "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/create" - "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -26,4 +27,5 @@ func addSubcommands(cmd *cobra.Command) { cmd.AddCommand(list.NewCmd()) cmd.AddCommand(create.NewCmd()) cmd.AddCommand(delete.NewCmd()) + cmd.AddCommand(describe.NewCmd()) } From 3f7a155162f564ab3b88a0b0dc7a2fd0224b636d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 5 Mar 2024 14:35:22 +0000 Subject: [PATCH 3/3] Address comments --- internal/cmd/secrets-manager/instance/describe/describe.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/secrets-manager/instance/describe/describe.go b/internal/cmd/secrets-manager/instance/describe/describe.go index db8a1645..de466a50 100644 --- a/internal/cmd/secrets-manager/instance/describe/describe.go +++ b/internal/cmd/secrets-manager/instance/describe/describe.go @@ -93,7 +93,7 @@ func outputResult(cmd *cobra.Command, outputFormat string, instance *secretsmana table.AddSeparator() table.AddRow("NAME", *instance.Name) table.AddSeparator() - table.AddRow("STATUS", *instance.State) + table.AddRow("STATE", *instance.State) table.AddSeparator() table.AddRow("SECRETS", *instance.SecretCount) table.AddSeparator()