From c3e660242b5a379938ba2c125cc8621c8ebb8fe1 Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Wed, 25 Mar 2026 12:35:58 +0100 Subject: [PATCH 1/2] feat(objectstorage): onboard compliance lock cmd relates to STACKITCLI-341 --- docs/stackit_object-storage.md | 1 + .../stackit_object-storage_compliance-lock.md | 36 ++++ ...object-storage_compliance-lock_describe.md | 40 ++++ ...kit_object-storage_compliance-lock_lock.md | 40 ++++ ...t_object-storage_compliance-lock_unlock.md | 40 ++++ .../compliance-lock/compliance-lock.go | 30 +++ .../compliance-lock/describe/describe.go | 111 +++++++++++ .../compliance-lock/describe/describe_test.go | 179 +++++++++++++++++ .../compliance-lock/lock/lock.go | 116 +++++++++++ .../compliance-lock/lock/lock_test.go | 180 ++++++++++++++++++ .../compliance-lock/unlock/unlock.go | 106 +++++++++++ .../compliance-lock/unlock/unlock_test.go | 128 +++++++++++++ internal/cmd/object-storage/object_storage.go | 2 + 13 files changed, 1009 insertions(+) create mode 100644 docs/stackit_object-storage_compliance-lock.md create mode 100644 docs/stackit_object-storage_compliance-lock_describe.md create mode 100644 docs/stackit_object-storage_compliance-lock_lock.md create mode 100644 docs/stackit_object-storage_compliance-lock_unlock.md create mode 100644 internal/cmd/object-storage/compliance-lock/compliance-lock.go create mode 100644 internal/cmd/object-storage/compliance-lock/describe/describe.go create mode 100644 internal/cmd/object-storage/compliance-lock/describe/describe_test.go create mode 100644 internal/cmd/object-storage/compliance-lock/lock/lock.go create mode 100644 internal/cmd/object-storage/compliance-lock/lock/lock_test.go create mode 100644 internal/cmd/object-storage/compliance-lock/unlock/unlock.go create mode 100644 internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go diff --git a/docs/stackit_object-storage.md b/docs/stackit_object-storage.md index 5caa02380..bae7c2496 100644 --- a/docs/stackit_object-storage.md +++ b/docs/stackit_object-storage.md @@ -31,6 +31,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 compliance-lock](./stackit_object-storage_compliance-lock.md) - Provides functionality to manage Object Storage compliance lock * [stackit object-storage credentials](./stackit_object-storage_credentials.md) - Provides functionality for Object Storage credentials * [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 diff --git a/docs/stackit_object-storage_compliance-lock.md b/docs/stackit_object-storage_compliance-lock.md new file mode 100644 index 000000000..435807d29 --- /dev/null +++ b/docs/stackit_object-storage_compliance-lock.md @@ -0,0 +1,36 @@ +## stackit object-storage compliance-lock + +Provides functionality to manage Object Storage compliance lock + +### Synopsis + +Provides functionality to manage Object Storage compliance lock. + +``` +stackit object-storage compliance-lock [flags] +``` + +### Options + +``` + -h, --help Help for "stackit object-storage compliance-lock" +``` + +### 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 object-storage](./stackit_object-storage.md) - Provides functionality for Object Storage +* [stackit object-storage compliance-lock describe](./stackit_object-storage_compliance-lock_describe.md) - Describe object storage compliance lock +* [stackit object-storage compliance-lock lock](./stackit_object-storage_compliance-lock_lock.md) - Create object storage compliance lock +* [stackit object-storage compliance-lock unlock](./stackit_object-storage_compliance-lock_unlock.md) - Delete object storage compliance lock + diff --git a/docs/stackit_object-storage_compliance-lock_describe.md b/docs/stackit_object-storage_compliance-lock_describe.md new file mode 100644 index 000000000..393b034da --- /dev/null +++ b/docs/stackit_object-storage_compliance-lock_describe.md @@ -0,0 +1,40 @@ +## stackit object-storage compliance-lock describe + +Describe object storage compliance lock + +### Synopsis + +Describe object storage compliance lock. + +``` +stackit object-storage compliance-lock describe [flags] +``` + +### Examples + +``` + Describe object storage compliance lock + $ stackit object-storage compliance-lock describe +``` + +### Options + +``` + -h, --help Help for "stackit object-storage compliance-lock 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 object-storage compliance-lock](./stackit_object-storage_compliance-lock.md) - Provides functionality to manage Object Storage compliance lock + diff --git a/docs/stackit_object-storage_compliance-lock_lock.md b/docs/stackit_object-storage_compliance-lock_lock.md new file mode 100644 index 000000000..98a55265d --- /dev/null +++ b/docs/stackit_object-storage_compliance-lock_lock.md @@ -0,0 +1,40 @@ +## stackit object-storage compliance-lock lock + +Create object storage compliance lock + +### Synopsis + +Create object storage compliance lock. + +``` +stackit object-storage compliance-lock lock [flags] +``` + +### Examples + +``` + Create object storage compliance lock + $ stackit object-storage compliance-lock lock +``` + +### Options + +``` + -h, --help Help for "stackit object-storage compliance-lock lock" +``` + +### 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 object-storage compliance-lock](./stackit_object-storage_compliance-lock.md) - Provides functionality to manage Object Storage compliance lock + diff --git a/docs/stackit_object-storage_compliance-lock_unlock.md b/docs/stackit_object-storage_compliance-lock_unlock.md new file mode 100644 index 000000000..5666c3a40 --- /dev/null +++ b/docs/stackit_object-storage_compliance-lock_unlock.md @@ -0,0 +1,40 @@ +## stackit object-storage compliance-lock unlock + +Delete object storage compliance lock + +### Synopsis + +Delete object storage compliance lock. + +``` +stackit object-storage compliance-lock unlock [flags] +``` + +### Examples + +``` + Delete object storage compliance lock + $ stackit object-storage compliance-lock unlock +``` + +### Options + +``` + -h, --help Help for "stackit object-storage compliance-lock unlock" +``` + +### 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 object-storage compliance-lock](./stackit_object-storage_compliance-lock.md) - Provides functionality to manage Object Storage compliance lock + diff --git a/internal/cmd/object-storage/compliance-lock/compliance-lock.go b/internal/cmd/object-storage/compliance-lock/compliance-lock.go new file mode 100644 index 000000000..49df4e178 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/compliance-lock.go @@ -0,0 +1,30 @@ +package compliancelock + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/compliance-lock/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/compliance-lock/lock" + "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/compliance-lock/unlock" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "compliance-lock", + Short: "Provides functionality to manage Object Storage compliance lock", + Long: "Provides functionality to manage Object Storage compliance lock.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(lock.NewCmd(params)) + cmd.AddCommand(unlock.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/object-storage/compliance-lock/describe/describe.go b/internal/cmd/object-storage/compliance-lock/describe/describe.go new file mode 100644 index 000000000..2aa3ea34a --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/describe/describe.go @@ -0,0 +1,111 @@ +package describe + +import ( + "context" + "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/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" + objectStorageUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Describe object storage compliance lock", + Long: "Describe object storage compliance lock.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Describe object storage compliance lock`, + "$ stackit object-storage compliance-lock describe"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Check if the project is enabled before trying to describe + enabled, err := objectStorageUtils.ProjectEnabled(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) + if err != nil { + return fmt.Errorf("check if Object Storage is enabled: %w", err) + } + if !enabled { + return &errors.ServiceDisabledError{ + Service: "object-storage", + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get object storage compliance lock: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiGetComplianceLockRequest { + req := apiClient.DefaultAPI.GetComplianceLock(ctx, model.ProjectId, model.Region) + return req +} + +func outputResult(p *print.Printer, outputFormat string, resp *objectstorage.ComplianceLockResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("response is empty") + } + + table := tables.NewTable() + table.AddRow("PROJECT ID", resp.Project) + table.AddSeparator() + table.AddRow("MAX RETENTION DAYS", resp.MaxRetentionDays) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/object-storage/compliance-lock/describe/describe_test.go b/internal/cmd/object-storage/compliance-lock/describe/describe_test.go new file mode 100644 index 000000000..6f838d607 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/describe/describe_test.go @@ -0,0 +1,179 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +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, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *objectstorage.ApiGetComplianceLockRequest)) objectstorage.ApiGetComplianceLockRequest { + request := testClient.DefaultAPI.GetComplianceLock(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + 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, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest objectstorage.ApiGetComplianceLockRequest + }{ + { + 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) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + complianceLock *objectstorage.ComplianceLockResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{ + outputFormat: print.PrettyOutputFormat, + }, + wantErr: true, + }, + { + name: "set empty compliance lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + complianceLock: &objectstorage.ComplianceLockResponse{}, + }, + wantErr: false, + }, + { + name: "set filled lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + complianceLock: &objectstorage.ComplianceLockResponse{ + Project: uuid.New().String(), + MaxRetentionDays: int32(42), + }, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(&types.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.complianceLock); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/object-storage/compliance-lock/lock/lock.go b/internal/cmd/object-storage/compliance-lock/lock/lock.go new file mode 100644 index 000000000..e179f7c26 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/lock/lock.go @@ -0,0 +1,116 @@ +package lock + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "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/object-storage/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" + + "github.com/spf13/cobra" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "lock", + Short: "Create object storage compliance lock", + Long: "Create object storage compliance lock.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create object storage compliance lock`, + "$ stackit object-storage compliance-lock lock"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create object storage compliance-lock for project %s?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Check if the project is enabled before trying to create + enabled, err := utils.ProjectEnabled(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) + if err != nil { + return fmt.Errorf("check if Object Storage is enabled: %w", err) + } + if !enabled { + return &errors.ServiceDisabledError{ + Service: "object-storage", + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create object storage compliance lock: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiCreateComplianceLockRequest { + req := apiClient.DefaultAPI.CreateComplianceLock(ctx, model.ProjectId, model.Region) + return req +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *objectstorage.ComplianceLockResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("create compliance lock response is empty") + } + + p.Outputf("Created object storage compliance lock for project \"%s\" with maximum retention period of %d days.\n", projectLabel, resp.MaxRetentionDays) + return nil + }) +} diff --git a/internal/cmd/object-storage/compliance-lock/lock/lock_test.go b/internal/cmd/object-storage/compliance-lock/lock/lock_test.go new file mode 100644 index 000000000..46ae3bc39 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/lock/lock_test.go @@ -0,0 +1,180 @@ +package lock + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +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, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *objectstorage.ApiCreateComplianceLockRequest)) objectstorage.ApiCreateComplianceLockRequest { + request := testClient.DefaultAPI.CreateComplianceLock(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + 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, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest objectstorage.ApiCreateComplianceLockRequest + }{ + { + 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) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + complianceLock *objectstorage.ComplianceLockResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{ + outputFormat: print.PrettyOutputFormat, + }, + wantErr: true, + }, + { + name: "set empty compliance lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + complianceLock: &objectstorage.ComplianceLockResponse{}, + }, + wantErr: false, + }, + { + name: "set filled lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + complianceLock: &objectstorage.ComplianceLockResponse{ + Project: uuid.New().String(), + MaxRetentionDays: int32(42), + }, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(&types.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.complianceLock); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/object-storage/compliance-lock/unlock/unlock.go b/internal/cmd/object-storage/compliance-lock/unlock/unlock.go new file mode 100644 index 000000000..ad9320c2a --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/unlock/unlock.go @@ -0,0 +1,106 @@ +package unlock + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "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/object-storage/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" + + "github.com/spf13/cobra" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "unlock", + Short: "Delete object storage compliance lock", + Long: "Delete object storage compliance lock.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete object storage compliance lock`, + "$ stackit object-storage compliance-lock unlock"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to delete object storage compliance-lock for project %s?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Check if the project is enabled before trying to create + enabled, err := utils.ProjectEnabled(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) + if err != nil { + return fmt.Errorf("check if Object Storage is enabled: %w", err) + } + if !enabled { + return &errors.ServiceDisabledError{ + Service: "object-storage", + } + } + + // Call API + _, err = buildRequest(ctx, model, apiClient).Execute() + if err != nil { + return fmt.Errorf("delete object storage compliance lock: %w", err) + } + + params.Printer.Outputf("Deleted object storage compliance lock for project \"%s\".\n", projectLabel) + + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiDeleteComplianceLockRequest { + req := apiClient.DefaultAPI.DeleteComplianceLock(ctx, model.ProjectId, model.Region) + return req +} diff --git a/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go b/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go new file mode 100644 index 000000000..25c4748d7 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go @@ -0,0 +1,128 @@ +package unlock + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +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, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *objectstorage.ApiDeleteComplianceLockRequest)) objectstorage.ApiDeleteComplianceLockRequest { + request := testClient.DefaultAPI.DeleteComplianceLock(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + 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, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest objectstorage.ApiDeleteComplianceLockRequest + }{ + { + 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/object-storage/object_storage.go b/internal/cmd/object-storage/object_storage.go index 88358e0d8..1f4c11799 100644 --- a/internal/cmd/object-storage/object_storage.go +++ b/internal/cmd/object-storage/object_storage.go @@ -2,6 +2,7 @@ package objectstorage import ( "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/bucket" + complianceLock "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/compliance-lock" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials" credentialsGroup "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials-group" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/disable" @@ -31,4 +32,5 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(enable.NewCmd(params)) cmd.AddCommand(credentialsGroup.NewCmd(params)) cmd.AddCommand(credentials.NewCmd(params)) + cmd.AddCommand(complianceLock.NewCmd(params)) } From 186e7124a2401e6aa25233353fa5db0c847c6fe6 Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Thu, 26 Mar 2026 17:47:31 +0100 Subject: [PATCH 2/2] fix tests --- .../object-storage/compliance-lock/describe/describe_test.go | 2 +- internal/cmd/object-storage/compliance-lock/lock/lock_test.go | 2 +- .../cmd/object-storage/compliance-lock/unlock/unlock_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/cmd/object-storage/compliance-lock/describe/describe_test.go b/internal/cmd/object-storage/compliance-lock/describe/describe_test.go index 6f838d607..1c92d7cdf 100644 --- a/internal/cmd/object-storage/compliance-lock/describe/describe_test.go +++ b/internal/cmd/object-storage/compliance-lock/describe/describe_test.go @@ -120,7 +120,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/object-storage/compliance-lock/lock/lock_test.go b/internal/cmd/object-storage/compliance-lock/lock/lock_test.go index 46ae3bc39..48e39cb90 100644 --- a/internal/cmd/object-storage/compliance-lock/lock/lock_test.go +++ b/internal/cmd/object-storage/compliance-lock/lock/lock_test.go @@ -120,7 +120,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go b/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go index 25c4748d7..1df9eb3a9 100644 --- a/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go +++ b/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go @@ -117,7 +117,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" {