From 892faf50daf025ed72414da1d4c3a0833eb1cb9d Mon Sep 17 00:00:00 2001 From: GokceGK Date: Thu, 7 May 2026 21:22:51 +0200 Subject: [PATCH] feat(sfs): onboard snapshot policy commands relates to STACKITCLI-395 --- docs/stackit_beta_sfs.md | 1 + docs/stackit_beta_sfs_snapshot-policy.md | 35 +++ ...ackit_beta_sfs_snapshot-policy_describe.md | 40 ++++ docs/stackit_beta_sfs_snapshot-policy_list.md | 48 ++++ internal/cmd/beta/sfs/sfs.go | 2 + .../sfs/snapshot-policy/describe/describe.go | 149 ++++++++++++ .../snapshot-policy/describe/describe_test.go | 221 ++++++++++++++++++ .../cmd/beta/sfs/snapshot-policy/list/list.go | 161 +++++++++++++ .../sfs/snapshot-policy/list/list_test.go | 193 +++++++++++++++ .../sfs/snapshot-policy/snapshot-policy.go | 27 +++ 10 files changed, 877 insertions(+) create mode 100644 docs/stackit_beta_sfs_snapshot-policy.md create mode 100644 docs/stackit_beta_sfs_snapshot-policy_describe.md create mode 100644 docs/stackit_beta_sfs_snapshot-policy_list.md create mode 100644 internal/cmd/beta/sfs/snapshot-policy/describe/describe.go create mode 100644 internal/cmd/beta/sfs/snapshot-policy/describe/describe_test.go create mode 100644 internal/cmd/beta/sfs/snapshot-policy/list/list.go create mode 100644 internal/cmd/beta/sfs/snapshot-policy/list/list_test.go create mode 100644 internal/cmd/beta/sfs/snapshot-policy/snapshot-policy.go diff --git a/docs/stackit_beta_sfs.md b/docs/stackit_beta_sfs.md index 7067bb52b..ef4878d50 100644 --- a/docs/stackit_beta_sfs.md +++ b/docs/stackit_beta_sfs.md @@ -35,4 +35,5 @@ stackit beta sfs [flags] * [stackit beta sfs resource-pool](./stackit_beta_sfs_resource-pool.md) - Provides functionality for SFS resource pools * [stackit beta sfs share](./stackit_beta_sfs_share.md) - Provides functionality for SFS shares * [stackit beta sfs snapshot](./stackit_beta_sfs_snapshot.md) - Provides functionality for SFS snapshots +* [stackit beta sfs snapshot-policy](./stackit_beta_sfs_snapshot-policy.md) - Provides functionality for SFS snapshot policies diff --git a/docs/stackit_beta_sfs_snapshot-policy.md b/docs/stackit_beta_sfs_snapshot-policy.md new file mode 100644 index 000000000..19709c056 --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot-policy.md @@ -0,0 +1,35 @@ +## stackit beta sfs snapshot-policy + +Provides functionality for SFS snapshot policies + +### Synopsis + +Provides functionality for SFS snapshot policies. + +``` +stackit beta sfs snapshot-policy [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot-policy" +``` + +### 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 sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (stackit file storage) +* [stackit beta sfs snapshot-policy describe](./stackit_beta_sfs_snapshot-policy_describe.md) - Shows details of a snapshot policy +* [stackit beta sfs snapshot-policy list](./stackit_beta_sfs_snapshot-policy_list.md) - Lists all snapshot policies of a project + diff --git a/docs/stackit_beta_sfs_snapshot-policy_describe.md b/docs/stackit_beta_sfs_snapshot-policy_describe.md new file mode 100644 index 000000000..c2a751fcc --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot-policy_describe.md @@ -0,0 +1,40 @@ +## stackit beta sfs snapshot-policy describe + +Shows details of a snapshot policy + +### Synopsis + +Shows details of a snapshot policy. + +``` +stackit beta sfs snapshot-policy describe SNAPSHOT_POLICY_ID [flags] +``` + +### Examples + +``` + Describe a snapshot policy with ID "xxx" + $ stackit beta sfs snapshot-policy describe xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot-policy 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 sfs snapshot-policy](./stackit_beta_sfs_snapshot-policy.md) - Provides functionality for SFS snapshot policies + diff --git a/docs/stackit_beta_sfs_snapshot-policy_list.md b/docs/stackit_beta_sfs_snapshot-policy_list.md new file mode 100644 index 000000000..a7d8a2d7e --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot-policy_list.md @@ -0,0 +1,48 @@ +## stackit beta sfs snapshot-policy list + +Lists all snapshot policies of a project + +### Synopsis + +Lists all snapshot policies of a project. + +``` +stackit beta sfs snapshot-policy list [flags] +``` + +### Examples + +``` + List all snapshot policies + $ stackit beta sfs snapshot-policy list + + List all immutable snapshot policies + $ stackit beta sfs snapshot-policy list --immutable + + List up to 10 snapshot policies + $ stackit beta sfs snapshot-policy list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot-policy list" + --immutable Immutable snapshot policy + --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" "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 sfs snapshot-policy](./stackit_beta_sfs_snapshot-policy.md) - Provides functionality for SFS snapshot policies + diff --git a/internal/cmd/beta/sfs/sfs.go b/internal/cmd/beta/sfs/sfs.go index 2477e4843..a1c972f3e 100644 --- a/internal/cmd/beta/sfs/sfs.go +++ b/internal/cmd/beta/sfs/sfs.go @@ -6,6 +6,7 @@ import ( resourcepool "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/resource-pool" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/share" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot" + snapshotpolicy "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot-policy" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -30,5 +31,6 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(share.NewCmd(params)) cmd.AddCommand(exportpolicy.NewCmd(params)) cmd.AddCommand(snapshot.NewCmd(params)) + cmd.AddCommand(snapshotpolicy.NewCmd(params)) cmd.AddCommand(performanceclass.NewCmd(params)) } diff --git a/internal/cmd/beta/sfs/snapshot-policy/describe/describe.go b/internal/cmd/beta/sfs/snapshot-policy/describe/describe.go new file mode 100644 index 000000000..2123ac0c6 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/describe/describe.go @@ -0,0 +1,149 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "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/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const snapshotPolicyIdArg = "SNAPSHOT_POLICY_ID" + +type inputModel struct { + *globalflags.GlobalFlagModel + SnapshotPolicyId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", snapshotPolicyIdArg), + Short: "Shows details of a snapshot policy", + Long: "Shows details of a snapshot policy.", + Args: args.SingleArg(snapshotPolicyIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a snapshot policy with ID "xxx"`, + "$ stackit beta sfs snapshot-policy describe xxx", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read snapshot policy: %w", err) + } + + // Get projectLabel + 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 + } + + return outputResult(params.Printer, model.OutputFormat, model.SnapshotPolicyId, projectLabel, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + snapshotPolicyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + SnapshotPolicyId: snapshotPolicyId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiGetSnapshotPolicyRequest { + return apiClient.DefaultAPI.GetSnapshotPolicy(ctx, model.ProjectId, model.SnapshotPolicyId) +} + +func outputResult(p *print.Printer, outputFormat, snapshotPolicyId, projectLabel string, snapshotPolicy *sfs.GetSnapshotPolicyResponse) error { + return p.OutputResult(outputFormat, snapshotPolicy, func() error { + if snapshotPolicy == nil || snapshotPolicy.SnapshotPolicy == nil { + p.Outputf("Snapshot policy %q not found in project %q", snapshotPolicyId, projectLabel) + return nil + } + + var content []tables.Table + + table := tables.NewTable() + table.SetTitle("Snapshot Policy") + policy := snapshotPolicy.SnapshotPolicy + + table.AddRow("ID", utils.PtrString(policy.Id)) + table.AddSeparator() + table.AddRow("NAME", utils.PtrString(policy.Name)) + table.AddSeparator() + table.AddRow("ENABLED", utils.PtrString(policy.Enabled)) + table.AddSeparator() + table.AddRow("COMMENT", utils.PtrString(policy.Comment)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(policy.CreatedAt)) + + content = append(content, table) + + if len(policy.SnapshotSchedules) > 0 { + snapshotSchedulesTable := tables.NewTable() + snapshotSchedulesTable.SetTitle("Snapshot Schedules") + + snapshotSchedulesTable.SetHeader("ID", "NAME", "INTERVAL", "PREFIX", "RETENTION COUNT", "RETENTION PERIOD", "CREATED AT") + + for _, snapshotSchedule := range policy.SnapshotSchedules { + snapshotSchedulesTable.AddRow( + utils.PtrString(snapshotSchedule.Id), + utils.PtrString(snapshotSchedule.Name), + utils.PtrString(snapshotSchedule.Interval), + utils.PtrString(snapshotSchedule.Prefix), + utils.PtrString(snapshotSchedule.RetentionCount), + utils.PtrString(snapshotSchedule.RetentionPeriod), + utils.ConvertTimePToDateTimeString(snapshotSchedule.CreatedAt), + ) + snapshotSchedulesTable.AddSeparator() + } + + content = append(content, snapshotSchedulesTable) + } + + if err := tables.DisplayTables(p, content); err != nil { + return fmt.Errorf("render tables: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/sfs/snapshot-policy/describe/describe_test.go b/internal/cmd/beta/sfs/snapshot-policy/describe/describe_test.go new file mode 100644 index 000000000..10e69067a --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/describe/describe_test.go @@ -0,0 +1,221 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" +var testSnapshotPolicyId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testSnapshotPolicyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + SnapshotPolicyId: testSnapshotPolicyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiGetSnapshotPolicyRequest)) sfs.ApiGetSnapshotPolicyRequest { + request := testClient.DefaultAPI.GetSnapshotPolicy(testCtx, testProjectId, testSnapshotPolicyId) + 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: "snapshot policy id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "snapshot policy id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + 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) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiGetSnapshotPolicyRequest + }{ + { + 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, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + snapshotPolicyId string + projectLabel string + snapshotPolicy *sfs.GetSnapshotPolicyResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty snapshot policy", + args: args{ + snapshotPolicy: &sfs.GetSnapshotPolicyResponse{}, + }, + wantErr: false, + }, + { + name: "set empty snapshot policy", + args: args{ + snapshotPolicy: &sfs.GetSnapshotPolicyResponse{ + SnapshotPolicy: &sfs.SnapshotPolicy{}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.snapshotPolicyId, tt.args.projectLabel, tt.args.snapshotPolicy); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/snapshot-policy/list/list.go b/internal/cmd/beta/sfs/snapshot-policy/list/list.go new file mode 100644 index 000000000..60a0859fd --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/list/list.go @@ -0,0 +1,161 @@ +package list + +import ( + "context" + "fmt" + "strconv" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "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/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + limitFlag = "limit" + immutableFlag = "immutable" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 + Immutable bool +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all snapshot policies of a project", + Long: "Lists all snapshot policies of a project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all snapshot policies`, + "$ stackit beta sfs snapshot-policy list", + ), + examples.NewExample( + `List all immutable snapshot policies`, + "$ stackit beta sfs snapshot-policy list --immutable", + ), + examples.NewExample( + `List up to 10 snapshot policies`, + "$ stackit beta sfs snapshot-policy list --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list snapshot policies: %w", err) + } + + // Get projectLabel + 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 + } + + // Truncate output + items := utils.GetSliceFromPointer(&resp.SnapshotPolicies) + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().Bool(immutableFlag, false, "Immutable snapshot policy") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + Immutable: flags.FlagToBoolValue(p, cmd, immutableFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiListSnapshotPoliciesRequest { + req := apiClient.DefaultAPI.ListSnapshotPolicies(ctx, model.ProjectId) + if model.Immutable { + req = req.Immutable(true) + } + return req +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, snapshotPolicies []sfs.SnapshotPolicy) error { + return p.OutputResult(outputFormat, snapshotPolicies, func() error { + if len(snapshotPolicies) == 0 { + p.Outputf("No snapshot policies found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "COMMENT", "ENABLED", "AMOUNT OF SNAPSHOT SCHEDULES", "CREATED AT") + + for _, snapshotPolicy := range snapshotPolicies { + amountSnapshotSchedules := "-" + if snapshotPolicy.SnapshotSchedules != nil { + amountSnapshotSchedules = strconv.Itoa(len(snapshotPolicy.SnapshotSchedules)) + } + table.AddRow( + utils.PtrString(snapshotPolicy.Id), + utils.PtrString(snapshotPolicy.Name), + utils.PtrString(snapshotPolicy.Comment), + utils.PtrString(snapshotPolicy.Enabled), + amountSnapshotSchedules, + utils.ConvertTimePToDateTimeString(snapshotPolicy.CreatedAt), + ) + } + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/snapshot-policy/list/list_test.go b/internal/cmd/beta/sfs/snapshot-policy/list/list_test.go new file mode 100644 index 000000000..ddbb887c7 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/list/list_test.go @@ -0,0 +1,193 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + limitFlag: strconv.Itoa(10), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiListSnapshotPoliciesRequest)) sfs.ApiListSnapshotPoliciesRequest { + request := testClient.DefaultAPI.ListSnapshotPolicies(testCtx, testProjectId) + 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", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "immutable snapshot policies", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[immutableFlag] = "true" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Immutable = true + }), + }, + } + 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) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiListSnapshotPoliciesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "immutable snapshot policies", + model: fixtureInputModel(func(model *inputModel) { + model.Immutable = true + }), + expectedRequest: fixtureRequest().Immutable(true), + }, + } + + 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, sfs.DefaultAPIService{}), + 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 + snapshotPolicies []sfs.SnapshotPolicy + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty snapshot policy", + args: args{ + snapshotPolicies: []sfs.SnapshotPolicy{ + {}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.snapshotPolicies); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/snapshot-policy/snapshot-policy.go b/internal/cmd/beta/sfs/snapshot-policy/snapshot-policy.go new file mode 100644 index 000000000..f45eaa1a9 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/snapshot-policy.go @@ -0,0 +1,27 @@ +package snapshotpolicy + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot-policy/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot-policy/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "snapshot-policy", + Short: "Provides functionality for SFS snapshot policies", + Long: "Provides functionality for SFS snapshot policies.", + Args: cobra.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) +}