diff --git a/docs/stackit_beta_kms_key.md b/docs/stackit_beta_kms_key.md index 631808f53..a22f3d97b 100644 --- a/docs/stackit_beta_kms_key.md +++ b/docs/stackit_beta_kms_key.md @@ -32,6 +32,7 @@ stackit beta kms key [flags] * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS * [stackit beta kms key create](./stackit_beta_kms_key_create.md) - Creates a KMS key * [stackit beta kms key delete](./stackit_beta_kms_key_delete.md) - Deletes a KMS key +* [stackit beta kms key describe](./stackit_beta_kms_key_describe.md) - Describe a KMS key * [stackit beta kms key import](./stackit_beta_kms_key_import.md) - Import a KMS key * [stackit beta kms key list](./stackit_beta_kms_key_list.md) - List all KMS keys * [stackit beta kms key restore](./stackit_beta_kms_key_restore.md) - Restore a key diff --git a/docs/stackit_beta_kms_key_describe.md b/docs/stackit_beta_kms_key_describe.md new file mode 100644 index 000000000..05e876491 --- /dev/null +++ b/docs/stackit_beta_kms_key_describe.md @@ -0,0 +1,41 @@ +## stackit beta kms key describe + +Describe a KMS key + +### Synopsis + +Describe a KMS key + +``` +stackit beta kms key describe KEY_ID [flags] +``` + +### Examples + +``` + Describe a KMS key with ID xxx of keyring yyy + $ stackit beta kms key describe xxx --keyring-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta kms key describe" + --keyring-id string Key Ring ID +``` + +### 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" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta kms key](./stackit_beta_kms_key.md) - Manage KMS keys + diff --git a/docs/stackit_beta_kms_keyring.md b/docs/stackit_beta_kms_keyring.md index 6e65f3a47..2d87f99d3 100644 --- a/docs/stackit_beta_kms_keyring.md +++ b/docs/stackit_beta_kms_keyring.md @@ -32,5 +32,6 @@ stackit beta kms keyring [flags] * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS * [stackit beta kms keyring create](./stackit_beta_kms_keyring_create.md) - Creates a KMS key ring * [stackit beta kms keyring delete](./stackit_beta_kms_keyring_delete.md) - Deletes a KMS key ring +* [stackit beta kms keyring describe](./stackit_beta_kms_keyring_describe.md) - Describe a KMS key ring * [stackit beta kms keyring list](./stackit_beta_kms_keyring_list.md) - Lists all KMS key rings diff --git a/docs/stackit_beta_kms_keyring_describe.md b/docs/stackit_beta_kms_keyring_describe.md new file mode 100644 index 000000000..9b1381dc0 --- /dev/null +++ b/docs/stackit_beta_kms_keyring_describe.md @@ -0,0 +1,40 @@ +## stackit beta kms keyring describe + +Describe a KMS key ring + +### Synopsis + +Describe a KMS key ring + +``` +stackit beta kms keyring describe KEYRING_ID [flags] +``` + +### Examples + +``` + Describe a KMS key ring with ID xxx + $ stackit beta kms keyring describe xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta kms keyring 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" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta kms keyring](./stackit_beta_kms_keyring.md) - Manage KMS key rings + diff --git a/docs/stackit_beta_kms_wrapping-key.md b/docs/stackit_beta_kms_wrapping-key.md index c10cb4946..2cef6b863 100644 --- a/docs/stackit_beta_kms_wrapping-key.md +++ b/docs/stackit_beta_kms_wrapping-key.md @@ -32,5 +32,6 @@ stackit beta kms wrapping-key [flags] * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS * [stackit beta kms wrapping-key create](./stackit_beta_kms_wrapping-key_create.md) - Creates a KMS wrapping key * [stackit beta kms wrapping-key delete](./stackit_beta_kms_wrapping-key_delete.md) - Deletes a KMS wrapping key +* [stackit beta kms wrapping-key describe](./stackit_beta_kms_wrapping-key_describe.md) - Describe a KMS wrapping key * [stackit beta kms wrapping-key list](./stackit_beta_kms_wrapping-key_list.md) - Lists all KMS wrapping keys diff --git a/docs/stackit_beta_kms_wrapping-key_describe.md b/docs/stackit_beta_kms_wrapping-key_describe.md new file mode 100644 index 000000000..6e82cd595 --- /dev/null +++ b/docs/stackit_beta_kms_wrapping-key_describe.md @@ -0,0 +1,41 @@ +## stackit beta kms wrapping-key describe + +Describe a KMS wrapping key + +### Synopsis + +Describe a KMS wrapping key + +``` +stackit beta kms wrapping-key describe WRAPPING_KEY_ID [flags] +``` + +### Examples + +``` + Describe a KMS wrapping key with ID xxx of keyring yyy + $ stackit beta kms wrappingkey describe xxx --keyring-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta kms wrapping-key describe" + --keyring-id string Key Ring ID +``` + +### 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" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta kms wrapping-key](./stackit_beta_kms_wrapping-key.md) - Manage KMS wrapping keys + diff --git a/internal/cmd/beta/kms/key/describe/describe.go b/internal/cmd/beta/kms/key/describe/describe.go new file mode 100644 index 000000000..113cf96de --- /dev/null +++ b/internal/cmd/beta/kms/key/describe/describe.go @@ -0,0 +1,131 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +const ( + argKeyID = "KEY_ID" + flagKeyRingID = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyID string + KeyRingID string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argKeyID), + Short: "Describe a KMS key", + Long: "Describe a KMS key", + Args: args.SingleArg(argKeyID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a KMS key with ID xxx of keyring yyy`, + `$ stackit beta kms key describe xxx --keyring-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get key: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), flagKeyRingID, "Key Ring ID") + err := flags.MarkFlagsRequired(cmd, flagKeyRingID) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + KeyID: args[0], + KeyRingID: flags.FlagToStringValue(p, cmd, flagKeyRingID), + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetKeyRequest { + return apiClient.GetKey(ctx, model.ProjectId, model.Region, model.KeyRingID, model.KeyID) +} + +func outputResult(p *print.Printer, outputFormat string, key *kms.Key) error { + if key == nil { + return fmt.Errorf("key response is empty") + } + return p.OutputResult(outputFormat, key, func() error { + table := tables.NewTable() + table.AddRow("ID", utils.PtrString(key.Id)) + table.AddSeparator() + table.AddRow("DISPLAY NAME", utils.PtrString(key.DisplayName)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.PtrString(key.CreatedAt)) + table.AddSeparator() + table.AddRow("STATE", utils.PtrString(key.State)) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(key.Description)) + table.AddSeparator() + table.AddRow("ACCESS SCOPE", utils.PtrString(key.AccessScope)) + table.AddSeparator() + table.AddRow("ALGORITHM", utils.PtrString(key.Algorithm)) + table.AddSeparator() + table.AddRow("DELETION DATE", utils.PtrString(key.DeletionDate)) + table.AddSeparator() + table.AddRow("IMPORT ONLY", utils.PtrString(key.ImportOnly)) + table.AddSeparator() + table.AddRow("KEYRING ID", utils.PtrString(key.KeyRingId)) + table.AddSeparator() + table.AddRow("PROTECTION", utils.PtrString(key.Protection)) + table.AddSeparator() + table.AddRow("PURPOSE", utils.PtrString(key.Purpose)) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/kms/key/describe/describe_test.go b/internal/cmd/beta/kms/key/describe/describe_test.go new file mode 100644 index 000000000..405ebcd66 --- /dev/null +++ b/internal/cmd/beta/kms/key/describe/describe_test.go @@ -0,0 +1,222 @@ +package describe + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &kms.APIClient{} +var testProjectId = uuid.NewString() +var testKeyRingID = uuid.NewString() +var testKeyID = uuid.NewString() +var testTime = time.Now() + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + flagKeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyID: testKeyID, + KeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testKeyID}, + 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: []string{testKeyID}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid key id", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing key ring id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, flagKeyRingID) }), + isValid: false, + }, + { + description: "invalid key ring id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { + m[flagKeyRingID] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing project id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }), + isValid: false, + }, + { + description: "invalid project id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { m[globalflags.ProjectIdFlag] = "invalid-uuid" }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + got := buildRequest(testCtx, fixtureInputModel(), testClient) + want := testClient.GetKey(testCtx, testProjectId, testRegion, testKeyRingID, testKeyID) + diff := cmp.Diff(got, want, + cmp.AllowUnexported(want), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFmt string + keyRing *kms.Key + wantErr bool + expected string + }{ + { + description: "empty", + outputFmt: "table", + wantErr: true, + }, + { + description: "table format", + outputFmt: "table", + keyRing: &kms.Key{ + AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), + Algorithm: utils.Ptr(kms.ALGORITHM_AES_256_GCM), + CreatedAt: utils.Ptr(testTime), + DeletionDate: nil, + Description: utils.Ptr("very secure and secret key"), + DisplayName: utils.Ptr("Test Key"), + Id: utils.Ptr(testKeyID), + ImportOnly: utils.Ptr(true), + KeyRingId: utils.Ptr(testKeyRingID), + Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), + Purpose: utils.Ptr(kms.PURPOSE_SYMMETRIC_ENCRYPT_DECRYPT), + State: utils.Ptr(kms.KEYSTATE_ACTIVE), + }, + expected: fmt.Sprintf(` + ID │ %-52s +───────────────┼───────────────────────────────────────────────────── + DISPLAY NAME │ Test Key +───────────────┼───────────────────────────────────────────────────── + CREATED AT │ %-52s +───────────────┼───────────────────────────────────────────────────── + STATE │ active +───────────────┼───────────────────────────────────────────────────── + DESCRIPTION │ very secure and secret key +───────────────┼───────────────────────────────────────────────────── + ACCESS SCOPE │ PUBLIC +───────────────┼───────────────────────────────────────────────────── + ALGORITHM │ aes_256_gcm +───────────────┼───────────────────────────────────────────────────── + DELETION DATE │ +───────────────┼───────────────────────────────────────────────────── + IMPORT ONLY │ true +───────────────┼───────────────────────────────────────────────────── + KEYRING ID │ %-52s +───────────────┼───────────────────────────────────────────────────── + PROTECTION │ software +───────────────┼───────────────────────────────────────────────────── + PURPOSE │ symmetric_encrypt_decrypt + +`, + testKeyID, + testTime, + testKeyRingID, + ), + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var buf bytes.Buffer + p.Cmd.SetOut(&buf) + if err := outputResult(p, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(buf.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/kms/key/key.go b/internal/cmd/beta/kms/key/key.go index 4b2f7d8fa..b6ff239bc 100644 --- a/internal/cmd/beta/kms/key/key.go +++ b/internal/cmd/beta/kms/key/key.go @@ -3,6 +3,7 @@ package key import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/create" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/importKey" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/list" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/restore" @@ -33,4 +34,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(restore.NewCmd(params)) cmd.AddCommand(rotate.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) } diff --git a/internal/cmd/beta/kms/keyring/describe/describe.go b/internal/cmd/beta/kms/keyring/describe/describe.go new file mode 100644 index 000000000..f9dc11d0a --- /dev/null +++ b/internal/cmd/beta/kms/keyring/describe/describe.go @@ -0,0 +1,106 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +const ( + argKeyRingID = "KEYRING_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingID string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argKeyRingID), + Short: "Describe a KMS key ring", + Long: "Describe a KMS key ring", + Args: args.SingleArg(argKeyRingID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a KMS key ring with ID xxx`, + `$ stackit beta kms keyring describe xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get key ring: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + KeyRingID: args[0], + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetKeyRingRequest { + return apiClient.GetKeyRing(ctx, model.ProjectId, model.Region, model.KeyRingID) +} + +func outputResult(p *print.Printer, outputFormat string, keyRing *kms.KeyRing) error { + if keyRing == nil { + return fmt.Errorf("key ring response is empty") + } + return p.OutputResult(outputFormat, keyRing, func() error { + table := tables.NewTable() + table.AddRow("ID", utils.PtrString(keyRing.Id)) + table.AddSeparator() + table.AddRow("DISPLAY NAME", utils.PtrString(keyRing.DisplayName)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.PtrString(keyRing.CreatedAt)) + table.AddSeparator() + table.AddRow("STATE", utils.PtrString(keyRing.State)) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(keyRing.Description)) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/kms/keyring/describe/describe_test.go b/internal/cmd/beta/kms/keyring/describe/describe_test.go new file mode 100644 index 000000000..99317bca4 --- /dev/null +++ b/internal/cmd/beta/kms/keyring/describe/describe_test.go @@ -0,0 +1,183 @@ +package describe + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &kms.APIClient{} +var testProjectId = uuid.NewString() +var testKeyRingID = uuid.NewString() +var testTime = time.Now() + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testKeyRingID}, + 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: []string{testKeyRingID}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid key ring id", + argValues: []string{"!invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing project id", + argValues: []string{testKeyRingID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }), + isValid: false, + }, + { + description: "invalid project id", + argValues: []string{testKeyRingID}, + flagValues: fixtureFlagValues(func(m map[string]string) { m[globalflags.ProjectIdFlag] = "invalid-uuid" }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + got := buildRequest(testCtx, fixtureInputModel(), testClient) + want := testClient.GetKeyRing(testCtx, testProjectId, testRegion, testKeyRingID) + diff := cmp.Diff(got, want, + cmp.AllowUnexported(want), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFmt string + keyRing *kms.KeyRing + wantErr bool + expected string + }{ + { + description: "empty", + outputFmt: "table", + wantErr: true, + }, + { + description: "table format", + outputFmt: "table", + keyRing: &kms.KeyRing{ + Id: utils.Ptr(testKeyRingID), + DisplayName: utils.Ptr("Test Key Ring"), + CreatedAt: utils.Ptr(testTime), + Description: utils.Ptr("This is a test key ring."), + State: utils.Ptr(kms.KEYRINGSTATE_ACTIVE), + }, + expected: fmt.Sprintf(` + ID │ %-52s +──────────────┼───────────────────────────────────────────────────── + DISPLAY NAME │ Test Key Ring +──────────────┼───────────────────────────────────────────────────── + CREATED AT │ %-52s +──────────────┼───────────────────────────────────────────────────── + STATE │ active +──────────────┼───────────────────────────────────────────────────── + DESCRIPTION │ This is a test key ring. + +`, + testKeyRingID, + testTime, + ), + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var buf bytes.Buffer + p.Cmd.SetOut(&buf) + if err := outputResult(p, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(buf.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/kms/keyring/keyring.go b/internal/cmd/beta/kms/keyring/keyring.go index 7a42ce131..f948fae41 100644 --- a/internal/cmd/beta/kms/keyring/keyring.go +++ b/internal/cmd/beta/kms/keyring/keyring.go @@ -3,6 +3,7 @@ package keyring import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/create" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/list" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -27,4 +28,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) } diff --git a/internal/cmd/beta/kms/wrappingkey/describe/describe.go b/internal/cmd/beta/kms/wrappingkey/describe/describe.go new file mode 100644 index 000000000..2c25a288e --- /dev/null +++ b/internal/cmd/beta/kms/wrappingkey/describe/describe.go @@ -0,0 +1,131 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +const ( + argWrappingKeyID = "WRAPPING_KEY_ID" + flagKeyRingID = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + WrappingKeyID string + KeyRingID string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argWrappingKeyID), + Short: "Describe a KMS wrapping key", + Long: "Describe a KMS wrapping key", + Args: args.SingleArg(argWrappingKeyID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a KMS wrapping key with ID xxx of keyring yyy`, + `$ stackit beta kms wrappingkey describe xxx --keyring-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get wrapping key: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), flagKeyRingID, "Key Ring ID") + err := flags.MarkFlagsRequired(cmd, flagKeyRingID) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + WrappingKeyID: args[0], + KeyRingID: flags.FlagToStringValue(p, cmd, flagKeyRingID), + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetWrappingKeyRequest { + return apiClient.GetWrappingKey(ctx, model.ProjectId, model.Region, model.KeyRingID, model.WrappingKeyID) +} + +func outputResult(p *print.Printer, outputFormat string, wrappingKey *kms.WrappingKey) error { + if wrappingKey == nil { + return fmt.Errorf("wrapping key response is empty") + } + return p.OutputResult(outputFormat, wrappingKey, func() error { + table := tables.NewTable() + table.AddRow("ID", utils.PtrString(wrappingKey.Id)) + table.AddSeparator() + table.AddRow("DISPLAY NAME", utils.PtrString(wrappingKey.DisplayName)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.PtrString(wrappingKey.CreatedAt)) + table.AddSeparator() + table.AddRow("STATE", utils.PtrString(wrappingKey.State)) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(wrappingKey.Description)) + table.AddSeparator() + table.AddRow("ACCESS SCOPE", utils.PtrString(wrappingKey.AccessScope)) + table.AddSeparator() + table.AddRow("ALGORITHM", utils.PtrString(wrappingKey.Algorithm)) + table.AddSeparator() + table.AddRow("EXPIRES AT", utils.PtrString(wrappingKey.ExpiresAt)) + table.AddSeparator() + table.AddRow("KEYRING ID", utils.PtrString(wrappingKey.KeyRingId)) + table.AddSeparator() + table.AddRow("PROTECTION", utils.PtrString(wrappingKey.Protection)) + table.AddSeparator() + table.AddRow("PUBLIC KEY", utils.PtrString(wrappingKey.PublicKey)) + table.AddSeparator() + table.AddRow("PURPOSE", utils.PtrString(wrappingKey.Purpose)) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/kms/wrappingkey/describe/describe_test.go b/internal/cmd/beta/kms/wrappingkey/describe/describe_test.go new file mode 100644 index 000000000..589f3be37 --- /dev/null +++ b/internal/cmd/beta/kms/wrappingkey/describe/describe_test.go @@ -0,0 +1,215 @@ +package describe + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &kms.APIClient{} +var testProjectId = uuid.NewString() +var testKeyRingID = uuid.NewString() +var testWrappingKeyID = uuid.NewString() +var testTime = time.Now() + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + flagKeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingID: testKeyRingID, + WrappingKeyID: testWrappingKeyID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testWrappingKeyID}, + 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: []string{testWrappingKeyID}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid key ring id", + argValues: []string{testWrappingKeyID}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[flagKeyRingID] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing project id", + argValues: []string{testWrappingKeyID}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "invalid project id", + argValues: []string{testWrappingKeyID}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + got := buildRequest(testCtx, fixtureInputModel(), testClient) + want := testClient.GetWrappingKey(testCtx, testProjectId, testRegion, testKeyRingID, testWrappingKeyID) + diff := cmp.Diff(got, want, + cmp.AllowUnexported(want), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff) + } +} +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFmt string + keyRing *kms.WrappingKey + wantErr bool + expected string + }{ + { + description: "empty", + outputFmt: "table", + wantErr: true, + }, + { + description: "table format", + outputFmt: "table", + keyRing: &kms.WrappingKey{ + Id: utils.Ptr(testWrappingKeyID), + DisplayName: utils.Ptr("Test Key Ring"), + CreatedAt: utils.Ptr(testTime), + Description: utils.Ptr("This is a test key ring."), + State: utils.Ptr(kms.WRAPPINGKEYSTATE_ACTIVE), + AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), + Algorithm: utils.Ptr(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256), + ExpiresAt: utils.Ptr(testTime), + KeyRingId: utils.Ptr(testKeyRingID), + Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), + PublicKey: utils.Ptr("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQ...\n-----END PUBLIC KEY-----"), + Purpose: utils.Ptr(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY), + }, + expected: fmt.Sprintf(` + ID │ %-52s +──────────────┼───────────────────────────────────────────────────── + DISPLAY NAME │ Test Key Ring +──────────────┼───────────────────────────────────────────────────── + CREATED AT │ %-52s +──────────────┼───────────────────────────────────────────────────── + STATE │ active +──────────────┼───────────────────────────────────────────────────── + DESCRIPTION │ This is a test key ring. +──────────────┼───────────────────────────────────────────────────── + ACCESS SCOPE │ PUBLIC +──────────────┼───────────────────────────────────────────────────── + ALGORITHM │ rsa_2048_oaep_sha256 +──────────────┼───────────────────────────────────────────────────── + EXPIRES AT │ %-52s +──────────────┼───────────────────────────────────────────────────── + KEYRING ID │ %-52s +──────────────┼───────────────────────────────────────────────────── + PROTECTION │ software +──────────────┼───────────────────────────────────────────────────── + PUBLIC KEY │ -----BEGIN PUBLIC KEY----- + │ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQ... + │ -----END PUBLIC KEY----- +──────────────┼───────────────────────────────────────────────────── + PURPOSE │ wrap_asymmetric_key + +`, + testWrappingKeyID, + testTime, + testTime, + testKeyRingID), + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var buf bytes.Buffer + p.Cmd.SetOut(&buf) + if err := outputResult(p, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(buf.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/kms/wrappingkey/wrappingkey.go b/internal/cmd/beta/kms/wrappingkey/wrappingkey.go index 00184a521..168808e37 100644 --- a/internal/cmd/beta/kms/wrappingkey/wrappingkey.go +++ b/internal/cmd/beta/kms/wrappingkey/wrappingkey.go @@ -3,6 +3,7 @@ package wrappingkey import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/create" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/list" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -27,4 +28,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) }