From 5da9ab6cc0562281938fa10852870de9387989be Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 20 May 2025 21:09:27 +0200 Subject: [PATCH 01/38] add volume backup create --- internal/cmd/volume/backup/backup.go | 36 ++++ internal/cmd/volume/backup/create/create.go | 212 ++++++++++++++++++++ internal/cmd/volume/volume.go | 2 + 3 files changed, 250 insertions(+) create mode 100644 internal/cmd/volume/backup/backup.go create mode 100644 internal/cmd/volume/backup/create/create.go diff --git a/internal/cmd/volume/backup/backup.go b/internal/cmd/volume/backup/backup.go new file mode 100644 index 000000000..b7cf8b37b --- /dev/null +++ b/internal/cmd/volume/backup/backup.go @@ -0,0 +1,36 @@ +package backup + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/restore" + "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "backup", + Short: "Provides functionality for volume backups", + Long: "Provides functionality for volume backups.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(restore.NewCmd(params)) +} diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go new file mode 100644 index 000000000..350a53e9b --- /dev/null +++ b/internal/cmd/volume/backup/create/create.go @@ -0,0 +1,212 @@ +package create + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "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/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + sourceIdFlag = "source-id" + sourceTypeFlag = "source-type" + nameFlag = "name" + labelsFlag = "labels" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + SourceID string + SourceType string + Name *string + Labels map[string]string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a backup from a specific source", + Long: "Creates a backup from a specific source (volume or snapshot).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a backup from a volume`, + "$ stackit volume backup create --source-id xxx --source-type volume --project-id xxx"), + examples.NewExample( + `Create a backup from a snapshot with a name`, + "$ stackit volume backup create --source-id xxx --source-type snapshot --name my-backup --project-id xxx"), + examples.NewExample( + `Create a backup with labels`, + "$ stackit volume backup create --source-id xxx --source-type volume --labels key1=value1,key2=value2 --project-id xxx"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + 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 + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create backup from %s? (This cannot be undone)", model.SourceID) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // TODO: necessary? utils for each service seperately? + // Check if the project is enabled before trying to create + // enabled, err := utils.ProjectEnabled(ctx, apiClient, model.ProjectId) + // if err != nil { + // return fmt.Errorf("check if IaaS is enabled: %w", err) + // } + // if !enabled { + // return &errors.ServiceDisabledError{ + // Service: "iaas", + // } + // } + + // Call API + req := buildRequest(model, apiClient, ctx) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create volume backup: %w", err) + } + + // TODO: How to check source-name exists? + // Get source label (use ID if name not available) + // sourceLabel := model.SourceID + + // TODO: SDK needs to be updated/released to support this async operation + // Wait for async operation, if async mode not enabled + // if !model.Async { + // s := spinner.New(params.Printer) + // s.Start("Creating backup") + // _, err = wait.CreateBackupWaitHandler(ctx, apiClient, model.ProjectId, model.SourceID).WaitWithContext(ctx) + // if err != nil { + // return fmt.Errorf("wait for volume backup creation: %w", err) + // } + // s.Stop() + // } + + return outputResult(params.Printer, model.OutputFormat, model.Async, model.SourceID, projectLabel, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(sourceIdFlag, "", "ID of the source from which a backup should be created") + cmd.Flags().String(sourceTypeFlag, "", "Source type of the backup (volume or snapshot)") + cmd.Flags().String(nameFlag, "", "Name of the backup") + cmd.Flags().StringToString(labelsFlag, nil, "Key-value string pairs as labels") + + err := flags.MarkFlagsRequired(cmd, sourceIdFlag, sourceTypeFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + sourceID := flags.FlagToStringValue(p, cmd, sourceIdFlag) + if sourceID == "" { + return nil, fmt.Errorf("source-id is required") + } + + sourceType := flags.FlagToStringValue(p, cmd, sourceTypeFlag) + if sourceType != "volume" && sourceType != "snapshot" { + return nil, fmt.Errorf("source-type must be either 'volume' or 'snapshot'") + } + + name := flags.FlagToStringValue(p, cmd, nameFlag) + labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag) + + model := inputModel{ + GlobalFlagModel: globalFlags, + SourceID: sourceID, + SourceType: sourceType, + Name: &name, + Labels: *labels, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +// TODO: Enough? +func buildRequest(model *inputModel, apiClient *iaas.APIClient, ctx context.Context) iaas.ApiCreateBackupRequest { + // TODO: doc says if createeBackup func provides snapshot-id but isnt in the func-signature? + req := apiClient.CreateBackup(ctx, model.ProjectId) + return req +} + +// TODO: create(volume)BackupResponse needs to be created? +func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel, projectLabel string, resp *iaas.CreateVolumeBackupResponse) error { + if resp == nil { + return fmt.Errorf("create backup response is empty") + } + + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return fmt.Errorf("marshal backup: %w", err) + } + p.Outputln(string(details)) + return nil + + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal backup: %w", err) + } + p.Outputln(string(details)) + return nil + + default: + if async { + p.Outputf("Triggered backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Backup.Id) + } else { + p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Backup.Id) + } + return nil + } +} diff --git a/internal/cmd/volume/volume.go b/internal/cmd/volume/volume.go index 1e876e85b..d5cad1614 100644 --- a/internal/cmd/volume/volume.go +++ b/internal/cmd/volume/volume.go @@ -2,6 +2,7 @@ package volume import ( "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/create" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/describe" @@ -35,4 +36,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(update.NewCmd(params)) cmd.AddCommand(resize.NewCmd(params)) cmd.AddCommand(performanceclass.NewCmd(params)) + cmd.AddCommand(backup.NewCmd(params)) } From ce3e135b50c73f911dc35ab65fe39390a2e4ae2b Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Fri, 23 May 2025 10:53:34 +0200 Subject: [PATCH 02/38] wip --- go.mod | 4 + internal/cmd/volume/backup/create/create.go | 32 +-- internal/cmd/volume/backup/list/list.go | 203 ++++++++++++++++++++ 3 files changed, 224 insertions(+), 15 deletions(-) create mode 100644 internal/cmd/volume/backup/list/list.go diff --git a/go.mod b/go.mod index 29ef475e5..aa26329a5 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,10 @@ module github.com/stackitcloud/stackit-cli go 1.24 +replace ( + github.com/stackitcloud/stackit-sdk-go/services/iaas => /Users/bfuertsch/work/StackIT/stackit-sdk-go/services/iaas +) + require ( github.com/fatih/color v1.18.0 github.com/goccy/go-yaml v1.18.0 diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index 350a53e9b..e89046fc4 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -15,9 +15,11 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" ) const ( @@ -79,7 +81,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - // TODO: necessary? utils for each service seperately? + // TODO: why not necessary here? utils for each service seperately? // Check if the project is enabled before trying to create // enabled, err := utils.ProjectEnabled(ctx, apiClient, model.ProjectId) // if err != nil { @@ -98,21 +100,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("create volume backup: %w", err) } - // TODO: How to check source-name exists? + // TODO: How to check if "source-name" exists? // Get source label (use ID if name not available) // sourceLabel := model.SourceID // TODO: SDK needs to be updated/released to support this async operation // Wait for async operation, if async mode not enabled - // if !model.Async { - // s := spinner.New(params.Printer) - // s.Start("Creating backup") - // _, err = wait.CreateBackupWaitHandler(ctx, apiClient, model.ProjectId, model.SourceID).WaitWithContext(ctx) - // if err != nil { - // return fmt.Errorf("wait for volume backup creation: %w", err) - // } - // s.Stop() - // } + if !model.Async { + s := spinner.New(params.Printer) + s.Start("Creating backup") + _, err = wait.CreateBackupWaitHandler(ctx, apiClient, model.ProjectId, model.SourceID).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for volume backup creation: %w", err) + } + s.Stop() + } return outputResult(params.Printer, model.OutputFormat, model.Async, model.SourceID, projectLabel, resp) }, @@ -178,8 +180,8 @@ func buildRequest(model *inputModel, apiClient *iaas.APIClient, ctx context.Cont return req } -// TODO: create(volume)BackupResponse needs to be created? -func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel, projectLabel string, resp *iaas.CreateVolumeBackupResponse) error { +// TODO: create(volume)BackupResponse or createBackupResponse needs to be created +func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel, projectLabel string, resp *iaas.Backup) error { if resp == nil { return fmt.Errorf("create backup response is empty") } @@ -203,9 +205,9 @@ func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel default: if async { - p.Outputf("Triggered backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Backup.Id) + p.Outputf("Triggered backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id) } else { - p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Backup.Id) + p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id) } return nil } diff --git a/internal/cmd/volume/backup/list/list.go b/internal/cmd/volume/backup/list/list.go new file mode 100644 index 000000000..56a3e44c0 --- /dev/null +++ b/internal/cmd/volume/backup/list/list.go @@ -0,0 +1,203 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "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/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + limitFlag = "limit" + labelSelectorFlag = "label-selector" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 + LabelSelector *string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all backups", + Long: "Lists all backups in a project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all volume backups`, + "$ stackit volume backup list"), + examples.NewExample( + `List all volume backups in JSON format`, + "$ stackit volume backup list --output-format json"), + examples.NewExample( + `List up to 10 volume backups`, + "$ stackit volume backup list --limit 10"), + examples.NewExample( + `List volume backups with specific labels`, + "$ stackit volume backup list --label-selector key1=value1,key2=value2"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return 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("get volume backups: %w", err) + } + if resp.Items == nil || len(*resp.Items) == 0 { + 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 + } + params.Printer.Info("No backups found for project %s\n", projectLabel) + return nil + } + backups := *resp.Items + + // Truncate output + if model.Limit != nil && len(backups) > int(*model.Limit) { + backups = backups[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, backups) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().String(labelSelectorFlag, "", "Filter backups by labels (comma-separated key=value pairs)") +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*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", + } + } + + labelSelector := flags.FlagToStringPointer(p, cmd, labelSelectorFlag) + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + LabelSelector: labelSelector, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListBackupsRequest { + req := apiClient.ListBackups(ctx, model.ProjectId) + + if model.LabelSelector != nil { + req = req.LabelSelector(*model.LabelSelector) + } + + return req +} + +func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) error { + if backups == nil { + return fmt.Errorf("backups is empty") + } + + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(backups, "", " ") + if err != nil { + return fmt.Errorf("marshal volume backup list: %w", err) + } + p.Outputln(string(details)) + return nil + + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(backups, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal volume backup list: %w", err) + } + p.Outputln(string(details)) + return nil + + default: + table := tables.NewTable() + table.SetHeader("ID", "NAME", "SIZE", "STATUS", "SNAPSHOT ID", "VOLUME ID", "AVAILABILITY ZONE", "LABELS", "CREATED AT", "UPDATED AT") + + for i := range backups { + backup := backups[i] + + // Format labels as a string + labelsStr := "" + if backup.Labels != nil { + labelsStr = utils.FormatLabelsAsString(*backup.Labels) + } + + table.AddRow( + utils.PtrString(backup.Id), + utils.PtrString(backup.Name), + utils.FormatSize(backup.Size), + utils.PtrString(backup.Status), + utils.PtrString(backup.SnapshotId), + utils.PtrString(backup.VolumeId), + utils.PtrString(backup.AvailabilityZone), + labelsStr, + utils.FormatTimestamp(backup.CreatedAt), + utils.FormatTimestamp(backup.UpdatedAt), + ) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} From 4a441bcb0812665412bad44fb5b638a5b35c4e0c Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Mon, 26 May 2025 09:28:18 +0200 Subject: [PATCH 03/38] add volume backup create tests --- internal/cmd/volume/backup/create/create.go | 19 +- .../cmd/volume/backup/create/create_test.go | 287 ++++++++++++++++++ 2 files changed, 292 insertions(+), 14 deletions(-) create mode 100644 internal/cmd/volume/backup/create/create_test.go diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index e89046fc4..391f572c1 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -81,18 +81,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - // TODO: why not necessary here? utils for each service seperately? - // Check if the project is enabled before trying to create - // enabled, err := utils.ProjectEnabled(ctx, apiClient, model.ProjectId) - // if err != nil { - // return fmt.Errorf("check if IaaS is enabled: %w", err) - // } - // if !enabled { - // return &errors.ServiceDisabledError{ - // Service: "iaas", - // } - // } - // Call API req := buildRequest(model, apiClient, ctx) resp, err := req.Execute() @@ -150,14 +138,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { return nil, fmt.Errorf("source-type must be either 'volume' or 'snapshot'") } - name := flags.FlagToStringValue(p, cmd, nameFlag) + name := flags.FlagToStringPointer(p, cmd, nameFlag) labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag) + if labels == nil { + labels = &map[string]string{} + } model := inputModel{ GlobalFlagModel: globalFlags, SourceID: sourceID, SourceType: sourceType, - Name: &name, + Name: name, Labels: *labels, } diff --git a/internal/cmd/volume/backup/create/create_test.go b/internal/cmd/volume/backup/create/create_test.go new file mode 100644 index 000000000..dc3a3bd8b --- /dev/null +++ b/internal/cmd/volume/backup/create/create_test.go @@ -0,0 +1,287 @@ +package create + +import ( + "context" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{} + testProjectId = uuid.NewString() + testSourceId = uuid.NewString() + testName = "my-backup" + testLabels = map[string]string{"key1": "value1"} +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + sourceIdFlag: testSourceId, + sourceTypeFlag: "volume", + nameFlag: testName, + labelsFlag: "key1=value1", + } + 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, + }, + SourceID: testSourceId, + SourceType: "volume", + Name: &testName, + Labels: testLabels, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiCreateBackupRequest)) iaas.ApiCreateBackupRequest { + request := testClient.CreateBackup(testCtx, testProjectId) + 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: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no source id", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, sourceIdFlag) + }), + isValid: false, + }, + { + description: "no source type", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, sourceTypeFlag) + }), + isValid: false, + }, + { + description: "invalid source type", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[sourceTypeFlag] = "invalid" + }), + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "only required flags", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + delete(flagValues, labelsFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Name = nil + model.Labels = make(map[string]string) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiCreateBackupRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(tt.model, testClient, testCtx) + + 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) { + backupId := "test-backup-id" + + type args struct { + outputFormat string + async bool + sourceLabel string + projectLabel string + backup *iaas.Backup + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty backup", + args: args{}, + wantErr: true, + }, + { + name: "minimal backup", + args: args{ + backup: &iaas.Backup{ + Id: &backupId, + }, + sourceLabel: "test-source", + projectLabel: "test-project", + }, + wantErr: false, + }, + { + name: "async mode", + args: args{ + backup: &iaas.Backup{ + Id: &backupId, + }, + sourceLabel: "test-source", + projectLabel: "test-project", + async: true, + }, + wantErr: false, + }, + { + name: "json output", + args: args{ + backup: &iaas.Backup{ + Id: &backupId, + }, + outputFormat: print.JSONOutputFormat, + }, + wantErr: false, + }, + { + name: "yaml output", + args: args{ + backup: &iaas.Backup{ + Id: &backupId, + }, + outputFormat: print.YAMLOutputFormat, + }, + wantErr: false, + }, + } + + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + p.Cmd = cmd + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.sourceLabel, tt.args.projectLabel, tt.args.backup); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From f4fc4ddca110ef86b6b0edb8027bb32e2b1caca2 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 27 May 2025 13:58:24 +0200 Subject: [PATCH 04/38] wip list --- internal/cmd/volume/backup/list/list.go | 30 +-- internal/cmd/volume/backup/list/list_test.go | 228 +++++++++++++++++++ 2 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 internal/cmd/volume/backup/list/list_test.go diff --git a/internal/cmd/volume/backup/list/list.go b/internal/cmd/volume/backup/list/list.go index 56a3e44c0..4fa4a16f4 100644 --- a/internal/cmd/volume/backup/list/list.go +++ b/internal/cmd/volume/backup/list/list.go @@ -40,16 +40,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `List all volume backups`, + `List all backups`, "$ stackit volume backup list"), examples.NewExample( - `List all volume backups in JSON format`, + `List all backups in JSON format`, "$ stackit volume backup list --output-format json"), examples.NewExample( - `List up to 10 volume backups`, + `List up to 10 backups`, "$ stackit volume backup list --limit 10"), examples.NewExample( - `List volume backups with specific labels`, + `List backups with specific labels`, "$ stackit volume backup list --label-selector key1=value1,key2=value2"), ), RunE: func(cmd *cobra.Command, _ []string) error { @@ -69,7 +69,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { req := buildRequest(ctx, model, apiClient) resp, err := req.Execute() if err != nil { - return fmt.Errorf("get volume backups: %w", err) + return fmt.Errorf("get backups: %w", err) } if resp.Items == nil || len(*resp.Items) == 0 { projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) @@ -97,7 +97,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") - cmd.Flags().String(labelSelectorFlag, "", "Filter backups by labels (comma-separated key=value pairs)") + cmd.Flags().String(labelSelectorFlag, "", "Filter backups by labels") } func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { @@ -153,7 +153,7 @@ func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) case print.JSONOutputFormat: details, err := json.MarshalIndent(backups, "", " ") if err != nil { - return fmt.Errorf("marshal volume backup list: %w", err) + return fmt.Errorf("marshal backup list: %w", err) } p.Outputln(string(details)) return nil @@ -161,7 +161,7 @@ func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) case print.YAMLOutputFormat: details, err := yaml.MarshalWithOptions(backups, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) if err != nil { - return fmt.Errorf("marshal volume backup list: %w", err) + return fmt.Errorf("marshal backup list: %w", err) } p.Outputln(string(details)) return nil @@ -173,23 +173,17 @@ func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) for i := range backups { backup := backups[i] - // Format labels as a string - labelsStr := "" - if backup.Labels != nil { - labelsStr = utils.FormatLabelsAsString(*backup.Labels) - } - table.AddRow( utils.PtrString(backup.Id), utils.PtrString(backup.Name), - utils.FormatSize(backup.Size), + utils.PtrByteSizeDefault((*int64)(backup.Size), ""), utils.PtrString(backup.Status), utils.PtrString(backup.SnapshotId), utils.PtrString(backup.VolumeId), utils.PtrString(backup.AvailabilityZone), - labelsStr, - utils.FormatTimestamp(backup.CreatedAt), - utils.FormatTimestamp(backup.UpdatedAt), + utils.PtrStringDefault(backup.Labels, ""), + utils.ConvertTimePToDateTimeString(backup.CreatedAt), + utils.ConvertTimePToDateTimeString(backup.UpdatedAt), ) } diff --git a/internal/cmd/volume/backup/list/list_test.go b/internal/cmd/volume/backup/list/list_test.go new file mode 100644 index 000000000..71876c17f --- /dev/null +++ b/internal/cmd/volume/backup/list/list_test.go @@ -0,0 +1,228 @@ +package list + +import ( + "context" + "testing" + + "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/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{} + testProjectId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + limitFlag: "10", + labelSelectorFlag: "key1=value1", + } + 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, + }, + Limit: utils.Ptr(int64(10)), + LabelSelector: utils.Ptr("key1=value1"), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiListBackupsRequest)) iaas.ApiListBackupsRequest { + request := testClient.ListBackups(testCtx, testProjectId) + request = request.LabelSelector("key1=value1") + 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: "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: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + p := print.NewPrinter() + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiListBackupsRequest + }{ + { + 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 + backups []iaas.Backup + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "set empty create backup response", + args: args{ + backups: []iaas.Backup{}, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.backups); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From 43b6bd5bda7c29773269edfb00948d38fde58f00 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Sun, 1 Jun 2025 19:54:42 +0200 Subject: [PATCH 05/38] add volume backup update and update tests --- internal/cmd/volume/backup/create/create.go | 2 - internal/cmd/volume/backup/update/update.go | 170 +++++++++++++++ .../cmd/volume/backup/update/update_test.go | 194 ++++++++++++++++++ 3 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 internal/cmd/volume/backup/update/update.go create mode 100644 internal/cmd/volume/backup/update/update_test.go diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index 391f572c1..ac3eecd57 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -92,8 +92,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Get source label (use ID if name not available) // sourceLabel := model.SourceID - // TODO: SDK needs to be updated/released to support this async operation - // Wait for async operation, if async mode not enabled if !model.Async { s := spinner.New(params.Printer) s.Start("Creating backup") diff --git a/internal/cmd/volume/backup/update/update.go b/internal/cmd/volume/backup/update/update.go new file mode 100644 index 000000000..f1985b103 --- /dev/null +++ b/internal/cmd/volume/backup/update/update.go @@ -0,0 +1,170 @@ +package update + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "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/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + backupIdArg = "BACKUP_ID" + nameFlag = "name" + labelsFlag = "labels" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + BackupId string + Name *string + Labels map[string]string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", backupIdArg), + Short: "Updates a backup", + Long: "Updates a backup by its ID.", + Args: args.SingleArg(backupIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update a backup name`, + "$ stackit volume backup update xxx-xxx-xxx --name new-name"), + examples.NewExample( + `Update backup labels`, + "$ stackit volume backup update xxx-xxx-xxx --labels key1=value1,key2=value2"), + ), + 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 + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update backup: %w", err) + } + + // Get backup label (use ID if name not available) + backupLabel := model.BackupId + if resp.Name != nil { + backupLabel = *resp.Name + } + + return outputResult(params.Printer, model.OutputFormat, backupLabel, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(nameFlag, "", "Name of the backup") + cmd.Flags().StringToString(labelsFlag, nil, "Key-value string pairs as labels") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + backupId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + name := flags.FlagToStringPointer(p, cmd, nameFlag) + labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag) + if labels == nil { + labels = &map[string]string{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + BackupId: backupId, + Name: name, + Labels: *labels, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateBackupRequest { + req := apiClient.UpdateBackup(ctx, model.ProjectId, model.BackupId) + + updatePayload := iaas.NewUpdateBackupPayloadWithDefaults() + if model.Name != nil { + updatePayload.Name = model.Name + } + + // Convert map[string]string to map[string]interface{} + var labelsMap *map[string]interface{} + if len(model.Labels) > 0 { + labelsMap = utils.Ptr(map[string]interface{}{}) + for k, v := range model.Labels { + (*labelsMap)[k] = v + } + } + updatePayload.Labels = labelsMap + + req = req.UpdateBackupPayload(*updatePayload) + return req +} + +func outputResult(p *print.Printer, outputFormat, backupLabel string, backup *iaas.Backup) error { + if backup == nil { + return fmt.Errorf("backup response is empty") + } + + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(backup, "", " ") + if err != nil { + return fmt.Errorf("marshal backup: %w", err) + } + p.Outputln(string(details)) + return nil + + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal backup: %w", err) + } + p.Outputln(string(details)) + return nil + + default: + p.Outputf("Updated backup %q\n", backupLabel) + return nil + } +} diff --git a/internal/cmd/volume/backup/update/update_test.go b/internal/cmd/volume/backup/update/update_test.go new file mode 100644 index 000000000..d7c1fb8de --- /dev/null +++ b/internal/cmd/volume/backup/update/update_test.go @@ -0,0 +1,194 @@ +package update + +import ( + "context" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{} + testProjectId = uuid.NewString() + testBackupId = uuid.NewString() + testName = "test-backup" + testLabels = map[string]string{"key1": "value1"} +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testBackupId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + nameFlag: testName, + labelsFlag: "key1=value1", + } + 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, + }, + BackupId: testBackupId, + Name: &testName, + Labels: testLabels, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiUpdateBackupRequest)) iaas.ApiUpdateBackupRequest { + request := testClient.UpdateBackup(testCtx, testProjectId, testBackupId) + payload := iaas.NewUpdateBackupPayloadWithDefaults() + payload.Name = &testName + + // Convert test labels to map[string]interface{} + labelsMap := map[string]interface{}{} + for k, v := range testLabels { + labelsMap[k] = v + } + payload.Labels = &labelsMap + + request = request.UpdateBackupPayload(*payload) + 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, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiUpdateBackupRequest + }{ + { + 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) + } + }) + } +} From c64994799615bf62cf101046d2cc27f1353b4bcd Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Sun, 1 Jun 2025 20:09:54 +0200 Subject: [PATCH 06/38] add volume backup delete and delete tests --- internal/cmd/volume/backup/delete/delete.go | 132 +++++++++++++ .../cmd/volume/backup/delete/delete_test.go | 185 ++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 internal/cmd/volume/backup/delete/delete.go create mode 100644 internal/cmd/volume/backup/delete/delete_test.go diff --git a/internal/cmd/volume/backup/delete/delete.go b/internal/cmd/volume/backup/delete/delete.go new file mode 100644 index 000000000..eb7377257 --- /dev/null +++ b/internal/cmd/volume/backup/delete/delete.go @@ -0,0 +1,132 @@ +package delete + +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/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" +) + +const ( + backupIdArg = "BACKUP_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + BackupId string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", backupIdArg), + Short: "Deletes a backup", + Long: "Deletes a backup by its ID.", + Args: args.SingleArg(backupIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a backup`, + "$ stackit volume backup delete xxx-xxx-xxx"), + examples.NewExample( + `Delete a backup and wait for deletion to be completed`, + "$ stackit volume backup delete xxx-xxx-xxx --async=false"), + ), + 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 + } + + backup, err := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get backup name: %v", err) + } + backupLabel := model.BackupId + if backup != nil && backup.Name != nil { + backupLabel = *backup.Name + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete backup %q? (This cannot be undone)", backupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete backup: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + s := spinner.New(params.Printer) + s.Start("Deleting backup") + _, err = wait.DeleteBackupWaitHandler(ctx, apiClient, model.ProjectId, model.BackupId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for backup deletion: %w", err) + } + s.Stop() + } + + if model.Async { + params.Printer.Info("Triggered deletion of backup %q\n", backupLabel) + } else { + params.Printer.Info("Deleted backup %q\n", backupLabel) + } + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + backupId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + BackupId: backupId, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteBackupRequest { + req := apiClient.DeleteBackup(ctx, model.ProjectId, model.BackupId) + return req +} diff --git a/internal/cmd/volume/backup/delete/delete_test.go b/internal/cmd/volume/backup/delete/delete_test.go new file mode 100644 index 000000000..1dc7618e1 --- /dev/null +++ b/internal/cmd/volume/backup/delete/delete_test.go @@ -0,0 +1,185 @@ +package delete + +import ( + "context" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{} + testProjectId = uuid.NewString() + testBackupId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testBackupId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + BackupId: testBackupId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiDeleteBackupRequest)) iaas.ApiDeleteBackupRequest { + request := testClient.DeleteBackup(testCtx, testProjectId, testBackupId) + 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, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiDeleteBackupRequest + }{ + { + 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) + } + }) + } +} From 909323f8814db11bf9af1ab2f292b9f78be08588 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Sun, 1 Jun 2025 20:23:22 +0200 Subject: [PATCH 07/38] add volume backup describe and describe tests --- .../cmd/volume/backup/describe/describe.go | 174 ++++++++++++++ .../volume/backup/describe/describe_test.go | 219 ++++++++++++++++++ 2 files changed, 393 insertions(+) create mode 100644 internal/cmd/volume/backup/describe/describe.go create mode 100644 internal/cmd/volume/backup/describe/describe_test.go diff --git a/internal/cmd/volume/backup/describe/describe.go b/internal/cmd/volume/backup/describe/describe.go new file mode 100644 index 000000000..faefe239c --- /dev/null +++ b/internal/cmd/volume/backup/describe/describe.go @@ -0,0 +1,174 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "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/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + backupIdArg = "BACKUP_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + BackupId string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", backupIdArg), + Short: "Describes a backup", + Long: "Describes a backup by its ID.", + Args: args.SingleArg(backupIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of a backup`, + "$ stackit volume backup describe xxx-xxx-xxx"), + examples.NewExample( + `Get details of a backup in JSON format`, + "$ stackit volume backup describe xxx-xxx-xxx --output-format json"), + ), + 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 + } + + // Call API + req := buildRequest(ctx, model, apiClient) + backup, err := req.Execute() + if err != nil { + return fmt.Errorf("get backup details: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, backup) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + backupId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + BackupId: backupId, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetBackupRequest { + req := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, backup *iaas.Backup) error { + if backup == nil { + return fmt.Errorf("backup response is empty") + } + + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(backup, "", " ") + if err != nil { + return fmt.Errorf("marshal backup: %w", err) + } + p.Outputln(string(details)) + return nil + + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal backup: %w", err) + } + p.Outputln(string(details)) + return nil + + default: + table := tables.NewTable() + table.AddRow( + "ID", + utils.PtrString(backup.Id), + ) + table.AddRow( + "NAME", + utils.PtrString(backup.Name), + ) + table.AddRow( + "SIZE", + utils.PtrByteSizeDefault((*int64)(backup.Size), ""), + ) + table.AddRow( + "STATUS", + utils.PtrString(backup.Status), + ) + table.AddRow( + "SNAPSHOT ID", + utils.PtrString(backup.SnapshotId), + ) + table.AddRow( + "VOLUME ID", + utils.PtrString(backup.VolumeId), + ) + table.AddRow( + "AVAILABILITY ZONE", + utils.PtrString(backup.AvailabilityZone), + ) + table.AddRow( + "LABELS", + utils.PtrStringDefault(backup.Labels, ""), + ) + table.AddRow( + "CREATED AT", + utils.ConvertTimePToDateTimeString(backup.CreatedAt), + ) + table.AddRow( + "UPDATED AT", + utils.ConvertTimePToDateTimeString(backup.UpdatedAt), + ) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/volume/backup/describe/describe_test.go b/internal/cmd/volume/backup/describe/describe_test.go new file mode 100644 index 000000000..492e76bd8 --- /dev/null +++ b/internal/cmd/volume/backup/describe/describe_test.go @@ -0,0 +1,219 @@ +package describe + +import ( + "context" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{} + testProjectId = uuid.NewString() + testBackupId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testBackupId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + BackupId: testBackupId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiGetBackupRequest)) iaas.ApiGetBackupRequest { + request := testClient.GetBackup(testCtx, testProjectId, testBackupId) + 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, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiGetBackupRequest + }{ + { + 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 + backup *iaas.Backup + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "backup as argument", + args: args{ + backup: &iaas.Backup{}, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.backup); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From db1ff240f05ba823d67924519e771616d9506154 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Sun, 1 Jun 2025 21:21:31 +0200 Subject: [PATCH 08/38] add volume backup restore and restore tests --- internal/cmd/volume/backup/restore/restore.go | 149 ++++++++++++++ .../cmd/volume/backup/restore/restore_test.go | 185 ++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 internal/cmd/volume/backup/restore/restore.go create mode 100644 internal/cmd/volume/backup/restore/restore_test.go diff --git a/internal/cmd/volume/backup/restore/restore.go b/internal/cmd/volume/backup/restore/restore.go new file mode 100644 index 000000000..aa8568759 --- /dev/null +++ b/internal/cmd/volume/backup/restore/restore.go @@ -0,0 +1,149 @@ +package restore + +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/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" +) + +const ( + backupIdArg = "BACKUP_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + BackupId string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("restore %s", backupIdArg), + Short: "Restores a backup", + Long: "Restores a backup by its ID.", + Args: args.SingleArg(backupIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Restore a backup`, + "$ stackit volume backup restore xxx-xxx-xxx"), + examples.NewExample( + `Restore a backup and wait for restore to be completed`, + "$ stackit volume backup restore xxx-xxx-xxx --async=false"), + ), + 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 + } + + // Get backup details for labels + backup, err := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get backup details: %v", err) + } + backupLabel := model.BackupId + if backup != nil && backup.Name != nil { + backupLabel = *backup.Name + } + + // Get source details for labels + var sourceLabel string + if backup != nil && backup.VolumeId != nil { + volume, err := apiClient.GetVolume(ctx, model.ProjectId, *backup.VolumeId).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get volume details: %v", err) + sourceLabel = *backup.VolumeId + } else if volume.Name != nil { + sourceLabel = *volume.Name + } else { + sourceLabel = *backup.VolumeId + } + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to restore %q with backup %q? (This cannot be undone)", sourceLabel, backupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("restore backup: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + s := spinner.New(params.Printer) + s.Start("Restoring backup") + _, err = wait.RestoreBackupWaitHandler(ctx, apiClient, model.ProjectId, model.BackupId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for backup restore: %w", err) + } + s.Stop() + } + + projectLabel := model.ProjectId + + if model.Async { + params.Printer.Info("Triggered restore of %q with %q in %q\n", sourceLabel, backupLabel, projectLabel) + } else { + params.Printer.Info("Restored %q with %q in %q\n", sourceLabel, backupLabel, projectLabel) + } + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + backupId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + BackupId: backupId, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRestoreBackupRequest { + req := apiClient.RestoreBackup(ctx, model.ProjectId, model.BackupId) + return req +} diff --git a/internal/cmd/volume/backup/restore/restore_test.go b/internal/cmd/volume/backup/restore/restore_test.go new file mode 100644 index 000000000..d86996f15 --- /dev/null +++ b/internal/cmd/volume/backup/restore/restore_test.go @@ -0,0 +1,185 @@ +package restore + +import ( + "context" + "testing" + + "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/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{} + testProjectId = uuid.NewString() + testBackupId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testBackupId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + BackupId: testBackupId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiRestoreBackupRequest)) iaas.ApiRestoreBackupRequest { + request := testClient.RestoreBackup(testCtx, testProjectId, testBackupId) + 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, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(¶ms.CmdParams{Printer: p}) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiRestoreBackupRequest + }{ + { + 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) + } + }) + } +} From aa28b9f99a7c0660534aa182f7c38d4026e77a72 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Sun, 1 Jun 2025 21:33:53 +0200 Subject: [PATCH 09/38] generate docs for volume backup api --- docs/stackit_volume.md | 1 + docs/stackit_volume_backup.md | 39 ++++++++++++++++++++ docs/stackit_volume_backup_create.md | 50 +++++++++++++++++++++++++ docs/stackit_volume_backup_delete.md | 43 ++++++++++++++++++++++ docs/stackit_volume_backup_describe.md | 43 ++++++++++++++++++++++ docs/stackit_volume_backup_list.md | 51 ++++++++++++++++++++++++++ docs/stackit_volume_backup_restore.md | 43 ++++++++++++++++++++++ docs/stackit_volume_backup_update.md | 45 +++++++++++++++++++++++ 8 files changed, 315 insertions(+) create mode 100644 docs/stackit_volume_backup.md create mode 100644 docs/stackit_volume_backup_create.md create mode 100644 docs/stackit_volume_backup_delete.md create mode 100644 docs/stackit_volume_backup_describe.md create mode 100644 docs/stackit_volume_backup_list.md create mode 100644 docs/stackit_volume_backup_restore.md create mode 100644 docs/stackit_volume_backup_update.md diff --git a/docs/stackit_volume.md b/docs/stackit_volume.md index c83878554..4955b6299 100644 --- a/docs/stackit_volume.md +++ b/docs/stackit_volume.md @@ -30,6 +30,7 @@ stackit volume [flags] ### SEE ALSO * [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit volume backup](./stackit_volume_backup.md) - Provides functionality for volume backups * [stackit volume create](./stackit_volume_create.md) - Creates a volume * [stackit volume delete](./stackit_volume_delete.md) - Deletes a volume * [stackit volume describe](./stackit_volume_describe.md) - Shows details of a volume diff --git a/docs/stackit_volume_backup.md b/docs/stackit_volume_backup.md new file mode 100644 index 000000000..f6390f385 --- /dev/null +++ b/docs/stackit_volume_backup.md @@ -0,0 +1,39 @@ +## stackit volume backup + +Provides functionality for volume backups + +### Synopsis + +Provides functionality for volume backups. + +``` +stackit volume backup [flags] +``` + +### Options + +``` + -h, --help Help for "stackit volume backup" +``` + +### 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 volume](./stackit_volume.md) - Provides functionality for volumes +* [stackit volume backup create](./stackit_volume_backup_create.md) - Creates a backup from a specific source +* [stackit volume backup delete](./stackit_volume_backup_delete.md) - Deletes a backup +* [stackit volume backup describe](./stackit_volume_backup_describe.md) - Describes a backup +* [stackit volume backup list](./stackit_volume_backup_list.md) - Lists all backups +* [stackit volume backup restore](./stackit_volume_backup_restore.md) - Restores a backup +* [stackit volume backup update](./stackit_volume_backup_update.md) - Updates a backup + diff --git a/docs/stackit_volume_backup_create.md b/docs/stackit_volume_backup_create.md new file mode 100644 index 000000000..75351edcf --- /dev/null +++ b/docs/stackit_volume_backup_create.md @@ -0,0 +1,50 @@ +## stackit volume backup create + +Creates a backup from a specific source + +### Synopsis + +Creates a backup from a specific source (volume or snapshot). + +``` +stackit volume backup create [flags] +``` + +### Examples + +``` + Create a backup from a volume + $ stackit volume backup create --source-id xxx --source-type volume --project-id xxx + + Create a backup from a snapshot with a name + $ stackit volume backup create --source-id xxx --source-type snapshot --name my-backup --project-id xxx + + Create a backup with labels + $ stackit volume backup create --source-id xxx --source-type volume --labels key1=value1,key2=value2 --project-id xxx +``` + +### Options + +``` + -h, --help Help for "stackit volume backup create" + --labels stringToString Key-value string pairs as labels (default []) + --name string Name of the backup + --source-id string ID of the source from which a backup should be created + --source-type string Source type of the backup (volume or snapshot) +``` + +### 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 volume backup](./stackit_volume_backup.md) - Provides functionality for volume backups + diff --git a/docs/stackit_volume_backup_delete.md b/docs/stackit_volume_backup_delete.md new file mode 100644 index 000000000..5295ff9d2 --- /dev/null +++ b/docs/stackit_volume_backup_delete.md @@ -0,0 +1,43 @@ +## stackit volume backup delete + +Deletes a backup + +### Synopsis + +Deletes a backup by its ID. + +``` +stackit volume backup delete BACKUP_ID [flags] +``` + +### Examples + +``` + Delete a backup + $ stackit volume backup delete xxx-xxx-xxx + + Delete a backup and wait for deletion to be completed + $ stackit volume backup delete xxx-xxx-xxx --async=false +``` + +### Options + +``` + -h, --help Help for "stackit volume backup delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "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 volume backup](./stackit_volume_backup.md) - Provides functionality for volume backups + diff --git a/docs/stackit_volume_backup_describe.md b/docs/stackit_volume_backup_describe.md new file mode 100644 index 000000000..ce98eebb4 --- /dev/null +++ b/docs/stackit_volume_backup_describe.md @@ -0,0 +1,43 @@ +## stackit volume backup describe + +Describes a backup + +### Synopsis + +Describes a backup by its ID. + +``` +stackit volume backup describe BACKUP_ID [flags] +``` + +### Examples + +``` + Get details of a backup + $ stackit volume backup describe xxx-xxx-xxx + + Get details of a backup in JSON format + $ stackit volume backup describe xxx-xxx-xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit volume backup 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 volume backup](./stackit_volume_backup.md) - Provides functionality for volume backups + diff --git a/docs/stackit_volume_backup_list.md b/docs/stackit_volume_backup_list.md new file mode 100644 index 000000000..91f3ca99a --- /dev/null +++ b/docs/stackit_volume_backup_list.md @@ -0,0 +1,51 @@ +## stackit volume backup list + +Lists all backups + +### Synopsis + +Lists all backups in a project. + +``` +stackit volume backup list [flags] +``` + +### Examples + +``` + List all backups + $ stackit volume backup list + + List all backups in JSON format + $ stackit volume backup list --output-format json + + List up to 10 backups + $ stackit volume backup list --limit 10 + + List backups with specific labels + $ stackit volume backup list --label-selector key1=value1,key2=value2 +``` + +### Options + +``` + -h, --help Help for "stackit volume backup list" + --label-selector string Filter backups by labels + --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 volume backup](./stackit_volume_backup.md) - Provides functionality for volume backups + diff --git a/docs/stackit_volume_backup_restore.md b/docs/stackit_volume_backup_restore.md new file mode 100644 index 000000000..1a2df0563 --- /dev/null +++ b/docs/stackit_volume_backup_restore.md @@ -0,0 +1,43 @@ +## stackit volume backup restore + +Restores a backup + +### Synopsis + +Restores a backup by its ID. + +``` +stackit volume backup restore BACKUP_ID [flags] +``` + +### Examples + +``` + Restore a backup + $ stackit volume backup restore xxx-xxx-xxx + + Restore a backup and wait for restore to be completed + $ stackit volume backup restore xxx-xxx-xxx --async=false +``` + +### Options + +``` + -h, --help Help for "stackit volume backup restore" +``` + +### 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 volume backup](./stackit_volume_backup.md) - Provides functionality for volume backups + diff --git a/docs/stackit_volume_backup_update.md b/docs/stackit_volume_backup_update.md new file mode 100644 index 000000000..184c1abf0 --- /dev/null +++ b/docs/stackit_volume_backup_update.md @@ -0,0 +1,45 @@ +## stackit volume backup update + +Updates a backup + +### Synopsis + +Updates a backup by its ID. + +``` +stackit volume backup update BACKUP_ID [flags] +``` + +### Examples + +``` + Update a backup name + $ stackit volume backup update xxx-xxx-xxx --name new-name + + Update backup labels + $ stackit volume backup update xxx-xxx-xxx --labels key1=value1,key2=value2 +``` + +### Options + +``` + -h, --help Help for "stackit volume backup update" + --labels stringToString Key-value string pairs as labels (default []) + --name string Name of the backup +``` + +### 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 volume backup](./stackit_volume_backup.md) - Provides functionality for volume backups + From 41ea880c9846e6925ad563990318bf10df7d5a67 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Sun, 1 Jun 2025 21:46:23 +0200 Subject: [PATCH 10/38] volume backup create sourcelabel fix --- internal/cmd/volume/backup/create/create.go | 36 ++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index ac3eecd57..18bb35d46 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -73,8 +73,26 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } + // Get source name for label (use ID if name not available) + sourceLabel := model.SourceID + if model.SourceType == "volume" { + volume, err := apiClient.GetVolume(ctx, model.ProjectId, model.SourceID).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) + } else if volume != nil && volume.Name != nil { + sourceLabel = *volume.Name + } + } else if model.SourceType == "snapshot" { + snapshot, err := apiClient.GetSnapshot(ctx, model.ProjectId, model.SourceID).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get snapshot name: %v", err) + } else if snapshot != nil && snapshot.Name != nil { + sourceLabel = *snapshot.Name + } + } + if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create backup from %s? (This cannot be undone)", model.SourceID) + prompt := fmt.Sprintf("Are you sure you want to create backup from %s? (This cannot be undone)", sourceLabel) err = params.Printer.PromptForConfirmation(prompt) if err != nil { return err @@ -88,21 +106,23 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("create volume backup: %w", err) } - // TODO: How to check if "source-name" exists? - // Get source label (use ID if name not available) - // sourceLabel := model.SourceID - + // Wait for async operation, if async mode not enabled if !model.Async { s := spinner.New(params.Printer) s.Start("Creating backup") - _, err = wait.CreateBackupWaitHandler(ctx, apiClient, model.ProjectId, model.SourceID).WaitWithContext(ctx) + resp, err = wait.CreateBackupWaitHandler(ctx, apiClient, model.ProjectId, *resp.Id).WaitWithContext(ctx) if err != nil { - return fmt.Errorf("wait for volume backup creation: %w", err) + return fmt.Errorf("wait for backup creation: %w", err) } s.Stop() } - return outputResult(params.Printer, model.OutputFormat, model.Async, model.SourceID, projectLabel, resp) + if model.Async { + params.Printer.Info("Triggered backup of %q in %q. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id) + } else { + params.Printer.Info("Created backup of %q in %q. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id) + } + return nil }, } From b9b172da6d8c6bcf6c5381fc569864333fc13edd Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Mon, 2 Jun 2025 18:39:12 +0200 Subject: [PATCH 11/38] fix according to review request --- internal/cmd/volume/backup/create/create.go | 3 --- .../cmd/volume/backup/create/create_test.go | 16 +++++++--------- .../cmd/volume/backup/delete/delete_test.go | 6 ++---- .../volume/backup/describe/describe_test.go | 6 ++---- internal/cmd/volume/backup/list/list_test.go | 18 ++++++++---------- .../cmd/volume/backup/restore/restore_test.go | 6 ++---- .../cmd/volume/backup/update/update_test.go | 8 +++----- 7 files changed, 24 insertions(+), 39 deletions(-) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index 18bb35d46..a86ef34aa 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -182,14 +182,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { return &model, nil } -// TODO: Enough? func buildRequest(model *inputModel, apiClient *iaas.APIClient, ctx context.Context) iaas.ApiCreateBackupRequest { - // TODO: doc says if createeBackup func provides snapshot-id but isnt in the func-signature? req := apiClient.CreateBackup(ctx, model.ProjectId) return req } -// TODO: create(volume)BackupResponse or createBackupResponse needs to be created func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel, projectLabel string, resp *iaas.Backup) error { if resp == nil { return fmt.Errorf("create backup response is empty") diff --git a/internal/cmd/volume/backup/create/create_test.go b/internal/cmd/volume/backup/create/create_test.go index dc3a3bd8b..6d1b2a2dd 100644 --- a/internal/cmd/volume/backup/create/create_test.go +++ b/internal/cmd/volume/backup/create/create_test.go @@ -14,8 +14,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var ( @@ -29,11 +27,11 @@ var ( func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - sourceIdFlag: testSourceId, - sourceTypeFlag: "volume", - nameFlag: testName, - labelsFlag: "key1=value1", + globalflags.ProjectIdFlag: testProjectId, + sourceIdFlag: testSourceId, + sourceTypeFlag: "volume", + nameFlag: testName, + labelsFlag: "key1=value1", } for _, mod := range mods { mod(flagValues) @@ -108,14 +106,14 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, diff --git a/internal/cmd/volume/backup/delete/delete_test.go b/internal/cmd/volume/backup/delete/delete_test.go index 1dc7618e1..8425e9c98 100644 --- a/internal/cmd/volume/backup/delete/delete_test.go +++ b/internal/cmd/volume/backup/delete/delete_test.go @@ -14,8 +14,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var ( @@ -37,7 +35,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, } for _, mod := range mods { mod(flagValues) @@ -104,7 +102,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, diff --git a/internal/cmd/volume/backup/describe/describe_test.go b/internal/cmd/volume/backup/describe/describe_test.go index 492e76bd8..3dcac1e56 100644 --- a/internal/cmd/volume/backup/describe/describe_test.go +++ b/internal/cmd/volume/backup/describe/describe_test.go @@ -14,8 +14,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var ( @@ -37,7 +35,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, } for _, mod := range mods { mod(flagValues) @@ -104,7 +102,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, diff --git a/internal/cmd/volume/backup/list/list_test.go b/internal/cmd/volume/backup/list/list_test.go index 71876c17f..b66d54f53 100644 --- a/internal/cmd/volume/backup/list/list_test.go +++ b/internal/cmd/volume/backup/list/list_test.go @@ -16,8 +16,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var ( @@ -28,9 +26,9 @@ var ( func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", - labelSelectorFlag: "key1=value1", + globalflags.ProjectIdFlag: testProjectId, + limitFlag: "10", + labelSelectorFlag: "key1=value1", } for _, mod := range mods { mod(flagValues) @@ -83,21 +81,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -209,9 +207,9 @@ func TestOutputResult(t *testing.T) { wantErr: true, }, { - name: "set empty create backup response", + name: "empty backup in slice", args: args{ - backups: []iaas.Backup{}, + backups: []iaas.Backup{{}}, }, wantErr: false, }, diff --git a/internal/cmd/volume/backup/restore/restore_test.go b/internal/cmd/volume/backup/restore/restore_test.go index d86996f15..217300720 100644 --- a/internal/cmd/volume/backup/restore/restore_test.go +++ b/internal/cmd/volume/backup/restore/restore_test.go @@ -14,8 +14,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var ( @@ -37,7 +35,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, } for _, mod := range mods { mod(flagValues) @@ -104,7 +102,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, diff --git a/internal/cmd/volume/backup/update/update_test.go b/internal/cmd/volume/backup/update/update_test.go index d7c1fb8de..b0826ae65 100644 --- a/internal/cmd/volume/backup/update/update_test.go +++ b/internal/cmd/volume/backup/update/update_test.go @@ -14,8 +14,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var ( @@ -39,9 +37,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - nameFlag: testName, - labelsFlag: "key1=value1", + globalflags.ProjectIdFlag: testProjectId, + nameFlag: testName, + labelsFlag: "key1=value1", } for _, mod := range mods { mod(flagValues) From 804d6130b3fc1f17194876f157767fb9292bf18a Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Mon, 2 Jun 2025 18:54:06 +0200 Subject: [PATCH 12/38] fix linting errors --- internal/cmd/volume/backup/create/create.go | 6 +++--- internal/cmd/volume/backup/create/create_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index a86ef34aa..efb39d307 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -54,7 +54,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a backup with labels`, "$ stackit volume backup create --source-id xxx --source-type volume --labels key1=value1,key2=value2 --project-id xxx"), ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { ctx := context.Background() model, err := parseInput(params.Printer, cmd) if err != nil { @@ -100,7 +100,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - req := buildRequest(model, apiClient, ctx) + req := buildRequest(ctx, model, apiClient) resp, err := req.Execute() if err != nil { return fmt.Errorf("create volume backup: %w", err) @@ -182,7 +182,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { return &model, nil } -func buildRequest(model *inputModel, apiClient *iaas.APIClient, ctx context.Context) iaas.ApiCreateBackupRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateBackupRequest { req := apiClient.CreateBackup(ctx, model.ProjectId) return req } diff --git a/internal/cmd/volume/backup/create/create_test.go b/internal/cmd/volume/backup/create/create_test.go index 6d1b2a2dd..25d35a3a5 100644 --- a/internal/cmd/volume/backup/create/create_test.go +++ b/internal/cmd/volume/backup/create/create_test.go @@ -192,7 +192,7 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(tt.model, testClient, testCtx) + request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), From 3f440368c8ffad7d9ae1d8cbb49caa0f282c7873 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Thu, 5 Jun 2025 20:02:29 +0200 Subject: [PATCH 13/38] add required backup payload --- internal/cmd/volume/backup/create/create.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index efb39d307..326d9168b 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -16,6 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/iaas" @@ -184,6 +185,25 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateBackupRequest { req := apiClient.CreateBackup(ctx, model.ProjectId) + + // Convert map[string]string to map[string]interface{} + var labelsMap *map[string]interface{} + if len(model.Labels) > 0 { + labelsMap = utils.Ptr(map[string]interface{}{}) + for k, v := range model.Labels { + (*labelsMap)[k] = v + } + } + + createPayload := iaas.NewCreateBackupPayloadWithDefaults() + createPayload.Name = model.Name + createPayload.Labels = labelsMap + createPayload.Source = &iaas.BackupSource{ + Id: &model.SourceID, + Type: &model.SourceType, + } + + req = req.CreateBackupPayload(*createPayload) return req } From 7c5d094424d570f256cc7ab7c59ad4921d972887 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Thu, 5 Jun 2025 21:17:13 +0200 Subject: [PATCH 14/38] fix describe visual presentation --- .../cmd/volume/backup/describe/describe.go | 59 ++++++------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/internal/cmd/volume/backup/describe/describe.go b/internal/cmd/volume/backup/describe/describe.go index faefe239c..fe7b4f87d 100644 --- a/internal/cmd/volume/backup/describe/describe.go +++ b/internal/cmd/volume/backup/describe/describe.go @@ -123,46 +123,25 @@ func outputResult(p *print.Printer, outputFormat string, backup *iaas.Backup) er default: table := tables.NewTable() - table.AddRow( - "ID", - utils.PtrString(backup.Id), - ) - table.AddRow( - "NAME", - utils.PtrString(backup.Name), - ) - table.AddRow( - "SIZE", - utils.PtrByteSizeDefault((*int64)(backup.Size), ""), - ) - table.AddRow( - "STATUS", - utils.PtrString(backup.Status), - ) - table.AddRow( - "SNAPSHOT ID", - utils.PtrString(backup.SnapshotId), - ) - table.AddRow( - "VOLUME ID", - utils.PtrString(backup.VolumeId), - ) - table.AddRow( - "AVAILABILITY ZONE", - utils.PtrString(backup.AvailabilityZone), - ) - table.AddRow( - "LABELS", - utils.PtrStringDefault(backup.Labels, ""), - ) - table.AddRow( - "CREATED AT", - utils.ConvertTimePToDateTimeString(backup.CreatedAt), - ) - table.AddRow( - "UPDATED AT", - utils.ConvertTimePToDateTimeString(backup.UpdatedAt), - ) + table.AddRow("ID", utils.PtrString(backup.Id)) + table.AddSeparator() + table.AddRow("NAME", utils.PtrString(backup.Name)) + table.AddSeparator() + table.AddRow("SIZE", utils.PtrByteSizeDefault((*int64)(backup.Size), "")) + table.AddSeparator() + table.AddRow("STATUS", utils.PtrString(backup.Status)) + table.AddSeparator() + table.AddRow("SNAPSHOT ID", utils.PtrString(backup.SnapshotId)) + table.AddSeparator() + table.AddRow("VOLUME ID", utils.PtrString(backup.VolumeId)) + table.AddSeparator() + table.AddRow("AVAILABILITY ZONE", utils.PtrString(backup.AvailabilityZone)) + table.AddSeparator() + table.AddRow("LABELS", utils.PtrStringDefault(backup.Labels, "")) + table.AddSeparator() + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(backup.CreatedAt)) + table.AddSeparator() + table.AddRow("UPDATED AT", utils.ConvertTimePToDateTimeString(backup.UpdatedAt)) err := table.Display(p) if err != nil { From 30b99a8448f63e9132d308cc0f23d73b58222ffc Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Thu, 5 Jun 2025 21:17:44 +0200 Subject: [PATCH 15/38] fix list visual presentation --- internal/cmd/volume/backup/list/list.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/internal/cmd/volume/backup/list/list.go b/internal/cmd/volume/backup/list/list.go index 4fa4a16f4..a4109531e 100644 --- a/internal/cmd/volume/backup/list/list.go +++ b/internal/cmd/volume/backup/list/list.go @@ -185,13 +185,10 @@ func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) utils.ConvertTimePToDateTimeString(backup.CreatedAt), utils.ConvertTimePToDateTimeString(backup.UpdatedAt), ) + table.AddSeparator() } - err := table.Display(p) - if err != nil { - return fmt.Errorf("render table: %w", err) - } - + p.Outputln(table.Render()) return nil } } From df449ee6185e45b213fe2ad9b626b59d1111e424 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Thu, 5 Jun 2025 21:53:15 +0200 Subject: [PATCH 16/38] add newest version of iaas service --- go.mod | 8 ++++---- go.sum | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index aa26329a5..0bbfbf9fd 100644 --- a/go.mod +++ b/go.mod @@ -22,11 +22,11 @@ require ( github.com/stackitcloud/stackit-sdk-go/core v0.17.2 github.com/stackitcloud/stackit-sdk-go/services/alb v0.5.0 github.com/stackitcloud/stackit-sdk-go/services/authorization v0.7.0 - github.com/stackitcloud/stackit-sdk-go/services/dns v0.15.1 - github.com/stackitcloud/stackit-sdk-go/services/git v0.5.1 + github.com/stackitcloud/stackit-sdk-go/services/dns v0.15.0 + github.com/stackitcloud/stackit-sdk-go/services/git v0.5.0 github.com/stackitcloud/stackit-sdk-go/services/iaas v0.24.0 - github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.2.1 - github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.23.1 + github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.2.0 + github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.23.0 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.1.0 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.16.0 github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.2.1 diff --git a/go.sum b/go.sum index 3560b9d7b..2948bc975 100644 --- a/go.sum +++ b/go.sum @@ -568,20 +568,20 @@ github.com/stackitcloud/stackit-sdk-go/services/alb v0.5.0 h1:7UKUi7Od7KpuVjV4I/ github.com/stackitcloud/stackit-sdk-go/services/alb v0.5.0/go.mod h1:RBLBx00zF9MoA/mcLoWwYaACFE0xrWp/EHlzo5S7nhA= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.7.0 h1:VpONplkdlEh7Pf22+cNnnHH4bx+S9QI+z55XYRE74JY= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.7.0/go.mod h1:dJ19ZwFjp2bfC5ZobXV3vUdSpE3quUw3GuoFSKLpHIo= -github.com/stackitcloud/stackit-sdk-go/services/dns v0.15.1 h1:+7iIR5r2epGNkAcuERX4kEg1H8hz2yR9+/zLYqZR7Xk= -github.com/stackitcloud/stackit-sdk-go/services/dns v0.15.1/go.mod h1:xhAdw016dY/hVsLerlExSMocqCc872+S0y1CdV3jAjU= -github.com/stackitcloud/stackit-sdk-go/services/git v0.5.1 h1:xs8CMY7t8nULQvZr5+XZRs8yWw8YMVw+HfjcuMhieR4= -github.com/stackitcloud/stackit-sdk-go/services/git v0.5.1/go.mod h1:agI7SONeLR/IZL3TOgn1tDzfS63O2rWKQE8+huRjEzU= +github.com/stackitcloud/stackit-sdk-go/services/dns v0.15.0 h1:GQAA9gqhKN0ZRc1vRYURHeVjSghh+iF+5DK0HdeuakI= +github.com/stackitcloud/stackit-sdk-go/services/dns v0.15.0/go.mod h1:PMHoavoIaRZpkI9BA0nsnRjGoHASVSBon45XB3QyhMA= +github.com/stackitcloud/stackit-sdk-go/services/git v0.5.0 h1:s0A2EPBrnBxfKStKA/B1izbyYHw/0m2RdqN3Inkv9hI= +github.com/stackitcloud/stackit-sdk-go/services/git v0.5.0/go.mod h1:XhXHJpOVC9Rpwyf1G+EpMbprBafH9aZb8vWBdR+z0WM= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.24.0 h1:aLlZmcsDHqqc7KPsevvs+W6EPZFT51u/dx5TcVQsE6g= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.24.0/go.mod h1:TaMx7kukGpRm0BkNCmS7u2x12q1pgfbD55DAnLIjOIQ= -github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0 h1:Ef4SyTBjIkfwaws4mssa6AoK+OokHFtr7ZIflUpoXVE= -github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0/go.mod h1:FiVhDlw9+yuTiUmnyGLn2qpsLW26w9OC4TS1y78czvg= -github.com/stackitcloud/stackit-sdk-go/services/logme v0.24.1 h1:hfnILDJGBwwqUIs4xt/7Jj4LBe+JsSdHy+Md2ynUg4Y= -github.com/stackitcloud/stackit-sdk-go/services/logme v0.24.1/go.mod h1:XjDMHhAQogFXsVR+o138CPYG1FOe0/Nl2Vm+fAgzx2A= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.24.1 h1:7nN7ZCuWSbJMy5KqoOqSbp5JKIOvyuDqVRtxVvT1iyE= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.24.1/go.mod h1:Pb8IEV5/jP8k75dVcN5cn3kP7PHTy/4KXXKpG76oj4U= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.2.1 h1:TWz7qJ4Mg5pquDXODSZ1dzhS95ZYn3w1aKjuRU2VqCg= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.2.1/go.mod h1:U1Zf/S9IuDvRJq1tRKFT/bsJd4qxYzwtukqX3TL++Mw= +github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.2.0 h1:gaTgjmEIvq7Nmji5YHh1haFvA/8dWyOgCg3lw6drjL4= +github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.2.0/go.mod h1:h3oM6cS23Yfynp8Df1hNr0FxtY5Alii/2g8Wqi5SIVE= +github.com/stackitcloud/stackit-sdk-go/services/logme v0.24.0 h1:5J2TH9ig5cp+5pCHugrsDJuFsRnIOQHQUqsxlweRXL0= +github.com/stackitcloud/stackit-sdk-go/services/logme v0.24.0/go.mod h1:+3jizYma6Dq3XVn6EMMdSBF9eIm0w6hCJvrStB3AIL0= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.24.0 h1:t/Ten9AuoWFmrDq5gAI3kVZShF3i8zEAaeBsYYqiaao= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.24.0/go.mod h1:qgvi3qiAzB1wKpMJ5CPnEaUToeiwgnQxGvlkjdisaLU= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.2.0 h1:U/IhjLOz0vG6zuxTqGhBd8f609s6JB+X9PaL6x/VM58= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.2.0/go.mod h1:+JSnz5/AvGN5ek/iH008frRc/NgjSr1EVOTIbyLwAuQ= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.2.0 h1:+dKIPVz9ydKbX3x6+1NvYk++OA378w74p+N6SjDmzBQ= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.2.0/go.mod h1:iCOYS9yICXQPyMAIdUGMqJDLY8HXKiVAdiMzO/mPvtA= github.com/stackitcloud/stackit-sdk-go/services/observability v0.7.1 h1:6OObzh2zk7wg75zYstcj0kjOjaxWc4joqA6qdeo8DP4= From db7d9a254b2b323aec95305815bea12b4e0011f7 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Thu, 5 Jun 2025 22:24:07 +0200 Subject: [PATCH 17/38] fix backup create test --- internal/cmd/volume/backup/create/create.go | 10 ++------ .../cmd/volume/backup/create/create_test.go | 25 ++++++++++++++----- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index 326d9168b..828809023 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -118,12 +118,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { s.Stop() } - if model.Async { - params.Printer.Info("Triggered backup of %q in %q. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id) - } else { - params.Printer.Info("Created backup of %q in %q. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id) - } - return nil + return outputResult(params.Printer, model.OutputFormat, model.Async, sourceLabel, projectLabel, resp) }, } @@ -203,8 +198,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli Type: &model.SourceType, } - req = req.CreateBackupPayload(*createPayload) - return req + return req.CreateBackupPayload(*createPayload) } func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel, projectLabel string, resp *iaas.Backup) error { diff --git a/internal/cmd/volume/backup/create/create_test.go b/internal/cmd/volume/backup/create/create_test.go index 25d35a3a5..022c46fa3 100644 --- a/internal/cmd/volume/backup/create/create_test.go +++ b/internal/cmd/volume/backup/create/create_test.go @@ -17,12 +17,13 @@ import ( type testCtxKey struct{} var ( - testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} - testProjectId = uuid.NewString() - testSourceId = uuid.NewString() - testName = "my-backup" - testLabels = map[string]string{"key1": "value1"} + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{} + testProjectId = uuid.NewString() + testSourceId = uuid.NewString() + testName = "my-backup" + testLabels = map[string]string{"key1": "value1"} + testSourceType = "volume" ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -58,6 +59,18 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureRequest(mods ...func(request *iaas.ApiCreateBackupRequest)) iaas.ApiCreateBackupRequest { request := testClient.CreateBackup(testCtx, testProjectId) + + createPayload := iaas.NewCreateBackupPayloadWithDefaults() + createPayload.Name = &testName + createPayload.Labels = &map[string]interface{}{ + "key1": "value1", + } + createPayload.Source = &iaas.BackupSource{ + Id: &testSourceId, + Type: &testSourceType, + } + + request = request.CreateBackupPayload(*createPayload) for _, mod := range mods { mod(&request) } From ec43946e65ce0ac2510f4e40e54ba349079d6efe Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 20:46:05 +0200 Subject: [PATCH 18/38] add testcase for an empty backup slice --- internal/cmd/volume/backup/list/list_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/cmd/volume/backup/list/list_test.go b/internal/cmd/volume/backup/list/list_test.go index b66d54f53..7a24783c2 100644 --- a/internal/cmd/volume/backup/list/list_test.go +++ b/internal/cmd/volume/backup/list/list_test.go @@ -213,6 +213,13 @@ func TestOutputResult(t *testing.T) { }, wantErr: false, }, + { + name: "empty slice", + args: args{ + backups: []iaas.Backup{}, + }, + wantErr: false, + }, } p := print.NewPrinter() p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) From 1270bb27e8417a0852cf57f71438056f104c06ff Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 21:19:22 +0200 Subject: [PATCH 19/38] add create backup is nil testcaste --- internal/cmd/volume/backup/create/create_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/cmd/volume/backup/create/create_test.go b/internal/cmd/volume/backup/create/create_test.go index 022c46fa3..a9ccddc21 100644 --- a/internal/cmd/volume/backup/create/create_test.go +++ b/internal/cmd/volume/backup/create/create_test.go @@ -239,6 +239,13 @@ func TestOutputResult(t *testing.T) { args: args{}, wantErr: true, }, + { + name: "backup is nil", + args: args{ + backup: nil, + }, + wantErr: true, + }, { name: "minimal backup", args: args{ From 0eab9e4ea7124706f1aeeca064da0f87a51480d7 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 21:25:02 +0200 Subject: [PATCH 20/38] add handle for possible nil pointer --- internal/cmd/volume/backup/create/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index 828809023..d1f34b33c 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -225,9 +225,9 @@ func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel default: if async { - p.Outputf("Triggered backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id) + p.Outputf("Triggered backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, utils.PtrString(resp.Id)) } else { - p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id) + p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, utils.PtrString(resp.Id)) } return nil } From 8119d2d304d1e1c635ad0c60335daef717eb7b20 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 21:28:08 +0200 Subject: [PATCH 21/38] refactor loop --- internal/cmd/volume/backup/list/list.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/cmd/volume/backup/list/list.go b/internal/cmd/volume/backup/list/list.go index a4109531e..b2d072e05 100644 --- a/internal/cmd/volume/backup/list/list.go +++ b/internal/cmd/volume/backup/list/list.go @@ -170,9 +170,7 @@ func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) table := tables.NewTable() table.SetHeader("ID", "NAME", "SIZE", "STATUS", "SNAPSHOT ID", "VOLUME ID", "AVAILABILITY ZONE", "LABELS", "CREATED AT", "UPDATED AT") - for i := range backups { - backup := backups[i] - + for _, backup := range backups { table.AddRow( utils.PtrString(backup.Id), utils.PtrString(backup.Name), From 9257fe50f08af44eeac5f899f7e04d8525a21e31 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 21:31:13 +0200 Subject: [PATCH 22/38] refactor branching --- internal/cmd/volume/backup/restore/restore.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/cmd/volume/backup/restore/restore.go b/internal/cmd/volume/backup/restore/restore.go index aa8568759..9d3ba7e33 100644 --- a/internal/cmd/volume/backup/restore/restore.go +++ b/internal/cmd/volume/backup/restore/restore.go @@ -68,14 +68,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Get source details for labels var sourceLabel string if backup != nil && backup.VolumeId != nil { + sourceLabel = *backup.VolumeId volume, err := apiClient.GetVolume(ctx, model.ProjectId, *backup.VolumeId).Execute() if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume details: %v", err) - sourceLabel = *backup.VolumeId } else if volume.Name != nil { sourceLabel = *volume.Name - } else { - sourceLabel = *backup.VolumeId } } From d0bc7ecd770006b2148fd85c46102724a49e7758 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 21:32:25 +0200 Subject: [PATCH 23/38] refactor reduce variable --- internal/cmd/volume/backup/restore/restore.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/cmd/volume/backup/restore/restore.go b/internal/cmd/volume/backup/restore/restore.go index 9d3ba7e33..acf0093db 100644 --- a/internal/cmd/volume/backup/restore/restore.go +++ b/internal/cmd/volume/backup/restore/restore.go @@ -103,12 +103,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { s.Stop() } - projectLabel := model.ProjectId - if model.Async { - params.Printer.Info("Triggered restore of %q with %q in %q\n", sourceLabel, backupLabel, projectLabel) + params.Printer.Info("Triggered restore of %q with %q in %q\n", sourceLabel, backupLabel, model.ProjectId) } else { - params.Printer.Info("Restored %q with %q in %q\n", sourceLabel, backupLabel, projectLabel) + params.Printer.Info("Restored %q with %q in %q\n", sourceLabel, backupLabel, model.ProjectId) } return nil }, From 726da7f455e537f2f4cc1f785041b7f703bc3c56 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 21:39:52 +0200 Subject: [PATCH 24/38] refactor examples --- internal/cmd/volume/backup/create/create.go | 6 +++--- internal/cmd/volume/backup/delete/delete.go | 6 +----- internal/cmd/volume/backup/describe/describe.go | 8 ++++---- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index d1f34b33c..3f7655227 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -47,13 +47,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Example: examples.Build( examples.NewExample( `Create a backup from a volume`, - "$ stackit volume backup create --source-id xxx --source-type volume --project-id xxx"), + "$ stackit volume backup create --source-id xxx --source-type volume"), examples.NewExample( `Create a backup from a snapshot with a name`, - "$ stackit volume backup create --source-id xxx --source-type snapshot --name my-backup --project-id xxx"), + "$ stackit volume backup create --source-id xxx --source-type snapshot --name my-backup"), examples.NewExample( `Create a backup with labels`, - "$ stackit volume backup create --source-id xxx --source-type volume --labels key1=value1,key2=value2 --project-id xxx"), + "$ stackit volume backup create --source-id xxx --source-type volume --labels key1=value1,key2=value2"), ), RunE: func(cmd *cobra.Command, _ []string) error { ctx := context.Background() diff --git a/internal/cmd/volume/backup/delete/delete.go b/internal/cmd/volume/backup/delete/delete.go index eb7377257..3a9bcfc7b 100644 --- a/internal/cmd/volume/backup/delete/delete.go +++ b/internal/cmd/volume/backup/delete/delete.go @@ -36,11 +36,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.SingleArg(backupIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( - `Delete a backup`, - "$ stackit volume backup delete xxx-xxx-xxx"), - examples.NewExample( - `Delete a backup and wait for deletion to be completed`, - "$ stackit volume backup delete xxx-xxx-xxx --async=false"), + `Delete a backup with ID "xxx"`, "$ stackit volume backup delete xxx"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() diff --git a/internal/cmd/volume/backup/describe/describe.go b/internal/cmd/volume/backup/describe/describe.go index fe7b4f87d..34b593216 100644 --- a/internal/cmd/volume/backup/describe/describe.go +++ b/internal/cmd/volume/backup/describe/describe.go @@ -37,11 +37,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.SingleArg(backupIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( - `Get details of a backup`, - "$ stackit volume backup describe xxx-xxx-xxx"), + `Get details of a backup with ID "xxx"`, + "$ stackit volume backup describe xxx"), examples.NewExample( - `Get details of a backup in JSON format`, - "$ stackit volume backup describe xxx-xxx-xxx --output-format json"), + `Get details of a backup with ID "xxx" in JSON format`, + "$ stackit volume backup describe xxx --output-format json"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() From ced4edf53e6a7acf56bc5e8b9dd5624ede225222 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 21:45:43 +0200 Subject: [PATCH 25/38] changed labels format --- internal/cmd/volume/backup/describe/describe.go | 15 ++++++++++++--- internal/cmd/volume/backup/list/list.go | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/internal/cmd/volume/backup/describe/describe.go b/internal/cmd/volume/backup/describe/describe.go index 34b593216..f181195d9 100644 --- a/internal/cmd/volume/backup/describe/describe.go +++ b/internal/cmd/volume/backup/describe/describe.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/goccy/go-yaml" "github.com/spf13/cobra" @@ -127,7 +128,7 @@ func outputResult(p *print.Printer, outputFormat string, backup *iaas.Backup) er table.AddSeparator() table.AddRow("NAME", utils.PtrString(backup.Name)) table.AddSeparator() - table.AddRow("SIZE", utils.PtrByteSizeDefault((*int64)(backup.Size), "")) + table.AddRow("SIZE", utils.PtrByteSizeDefault(backup.Size, "")) table.AddSeparator() table.AddRow("STATUS", utils.PtrString(backup.Status)) table.AddSeparator() @@ -137,8 +138,16 @@ func outputResult(p *print.Printer, outputFormat string, backup *iaas.Backup) er table.AddSeparator() table.AddRow("AVAILABILITY ZONE", utils.PtrString(backup.AvailabilityZone)) table.AddSeparator() - table.AddRow("LABELS", utils.PtrStringDefault(backup.Labels, "")) - table.AddSeparator() + + if backup.Labels != nil && len(*backup.Labels) > 0 { + var labels []string + for key, value := range *backup.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + table.AddRow("LABELS", strings.Join(labels, "\n")) + table.AddSeparator() + } + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(backup.CreatedAt)) table.AddSeparator() table.AddRow("UPDATED AT", utils.ConvertTimePToDateTimeString(backup.UpdatedAt)) diff --git a/internal/cmd/volume/backup/list/list.go b/internal/cmd/volume/backup/list/list.go index b2d072e05..540c00480 100644 --- a/internal/cmd/volume/backup/list/list.go +++ b/internal/cmd/volume/backup/list/list.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/goccy/go-yaml" "github.com/spf13/cobra" @@ -171,15 +172,24 @@ func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) table.SetHeader("ID", "NAME", "SIZE", "STATUS", "SNAPSHOT ID", "VOLUME ID", "AVAILABILITY ZONE", "LABELS", "CREATED AT", "UPDATED AT") for _, backup := range backups { + var labelsString string + if backup.Labels != nil { + var labels []string + for key, value := range *backup.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + labelsString = strings.Join(labels, ", ") + } + table.AddRow( utils.PtrString(backup.Id), utils.PtrString(backup.Name), - utils.PtrByteSizeDefault((*int64)(backup.Size), ""), + utils.PtrByteSizeDefault(backup.Size, ""), utils.PtrString(backup.Status), utils.PtrString(backup.SnapshotId), utils.PtrString(backup.VolumeId), utils.PtrString(backup.AvailabilityZone), - utils.PtrStringDefault(backup.Labels, ""), + labelsString, utils.ConvertTimePToDateTimeString(backup.CreatedAt), utils.ConvertTimePToDateTimeString(backup.UpdatedAt), ) From 0e51999911c3615b711332d6df0ef11390d424c8 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 21:49:15 +0200 Subject: [PATCH 26/38] update examples --- internal/cmd/volume/backup/restore/restore.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/cmd/volume/backup/restore/restore.go b/internal/cmd/volume/backup/restore/restore.go index acf0093db..6cf0b2962 100644 --- a/internal/cmd/volume/backup/restore/restore.go +++ b/internal/cmd/volume/backup/restore/restore.go @@ -36,11 +36,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.SingleArg(backupIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( - `Restore a backup`, - "$ stackit volume backup restore xxx-xxx-xxx"), - examples.NewExample( - `Restore a backup and wait for restore to be completed`, - "$ stackit volume backup restore xxx-xxx-xxx --async=false"), + `Restore a backup with ID "xxx"`, "$ stackit volume backup restore xxx"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() From 74aa4895ec8d60335a0dd22a6ff40983a4a228c0 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 22:03:19 +0200 Subject: [PATCH 27/38] update backup examples --- internal/cmd/volume/backup/update/update.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cmd/volume/backup/update/update.go b/internal/cmd/volume/backup/update/update.go index f1985b103..181218f9e 100644 --- a/internal/cmd/volume/backup/update/update.go +++ b/internal/cmd/volume/backup/update/update.go @@ -41,11 +41,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.SingleArg(backupIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( - `Update a backup name`, - "$ stackit volume backup update xxx-xxx-xxx --name new-name"), + `Update the name of a backup with ID "xxx"`, + "$ stackit volume backup update xxx --name new-name"), examples.NewExample( - `Update backup labels`, - "$ stackit volume backup update xxx-xxx-xxx --labels key1=value1,key2=value2"), + `Update the labels of a backup with ID "xxx"`, + "$ stackit volume backup update xxx --labels key1=value1,key2=value2"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() From be8816d1ced7d0a87eafb69fa5bc449f501d0551 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 22:34:04 +0200 Subject: [PATCH 28/38] update docs --- docs/stackit_volume_backup_create.md | 6 +++--- docs/stackit_volume_backup_delete.md | 7 ++----- docs/stackit_volume_backup_describe.md | 8 ++++---- docs/stackit_volume_backup_restore.md | 7 ++----- docs/stackit_volume_backup_update.md | 8 ++++---- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/docs/stackit_volume_backup_create.md b/docs/stackit_volume_backup_create.md index 75351edcf..ef5013918 100644 --- a/docs/stackit_volume_backup_create.md +++ b/docs/stackit_volume_backup_create.md @@ -14,13 +14,13 @@ stackit volume backup create [flags] ``` Create a backup from a volume - $ stackit volume backup create --source-id xxx --source-type volume --project-id xxx + $ stackit volume backup create --source-id xxx --source-type volume Create a backup from a snapshot with a name - $ stackit volume backup create --source-id xxx --source-type snapshot --name my-backup --project-id xxx + $ stackit volume backup create --source-id xxx --source-type snapshot --name my-backup Create a backup with labels - $ stackit volume backup create --source-id xxx --source-type volume --labels key1=value1,key2=value2 --project-id xxx + $ stackit volume backup create --source-id xxx --source-type volume --labels key1=value1,key2=value2 ``` ### Options diff --git a/docs/stackit_volume_backup_delete.md b/docs/stackit_volume_backup_delete.md index 5295ff9d2..5300f7854 100644 --- a/docs/stackit_volume_backup_delete.md +++ b/docs/stackit_volume_backup_delete.md @@ -13,11 +13,8 @@ stackit volume backup delete BACKUP_ID [flags] ### Examples ``` - Delete a backup - $ stackit volume backup delete xxx-xxx-xxx - - Delete a backup and wait for deletion to be completed - $ stackit volume backup delete xxx-xxx-xxx --async=false + Delete a backup with ID "xxx" + $ stackit volume backup delete xxx ``` ### Options diff --git a/docs/stackit_volume_backup_describe.md b/docs/stackit_volume_backup_describe.md index ce98eebb4..dbff5e4dc 100644 --- a/docs/stackit_volume_backup_describe.md +++ b/docs/stackit_volume_backup_describe.md @@ -13,11 +13,11 @@ stackit volume backup describe BACKUP_ID [flags] ### Examples ``` - Get details of a backup - $ stackit volume backup describe xxx-xxx-xxx + Get details of a backup with ID "xxx" + $ stackit volume backup describe xxx - Get details of a backup in JSON format - $ stackit volume backup describe xxx-xxx-xxx --output-format json + Get details of a backup with ID "xxx" in JSON format + $ stackit volume backup describe xxx --output-format json ``` ### Options diff --git a/docs/stackit_volume_backup_restore.md b/docs/stackit_volume_backup_restore.md index 1a2df0563..80dc563db 100644 --- a/docs/stackit_volume_backup_restore.md +++ b/docs/stackit_volume_backup_restore.md @@ -13,11 +13,8 @@ stackit volume backup restore BACKUP_ID [flags] ### Examples ``` - Restore a backup - $ stackit volume backup restore xxx-xxx-xxx - - Restore a backup and wait for restore to be completed - $ stackit volume backup restore xxx-xxx-xxx --async=false + Restore a backup with ID "xxx" + $ stackit volume backup restore xxx ``` ### Options diff --git a/docs/stackit_volume_backup_update.md b/docs/stackit_volume_backup_update.md index 184c1abf0..02f86f4e8 100644 --- a/docs/stackit_volume_backup_update.md +++ b/docs/stackit_volume_backup_update.md @@ -13,11 +13,11 @@ stackit volume backup update BACKUP_ID [flags] ### Examples ``` - Update a backup name - $ stackit volume backup update xxx-xxx-xxx --name new-name + Update the name of a backup with ID "xxx" + $ stackit volume backup update xxx --name new-name - Update backup labels - $ stackit volume backup update xxx-xxx-xxx --labels key1=value1,key2=value2 + Update the labels of a backup with ID "xxx" + $ stackit volume backup update xxx --labels key1=value1,key2=value2 ``` ### Options From bbbdd3bae2579aae8df83744c76f9a1aa4d4eac5 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 10 Jun 2025 22:49:32 +0200 Subject: [PATCH 29/38] refactor configuration of flagOptions --- internal/cmd/volume/backup/create/create.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index 3f7655227..5848a972a 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -30,6 +30,8 @@ const ( labelsFlag = "labels" ) +var sourceTypeFlagOptions = []string{"volume", "snapshot"} + type inputModel struct { *globalflags.GlobalFlagModel SourceID string @@ -128,7 +130,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(sourceIdFlag, "", "ID of the source from which a backup should be created") - cmd.Flags().String(sourceTypeFlag, "", "Source type of the backup (volume or snapshot)") + cmd.Flags().Var(flags.EnumFlag(false, "", sourceTypeFlagOptions...), sourceTypeFlag, fmt.Sprintf("Source type of the backup, one of %q", sourceTypeFlagOptions)) cmd.Flags().String(nameFlag, "", "Name of the backup") cmd.Flags().StringToString(labelsFlag, nil, "Key-value string pairs as labels") @@ -148,9 +150,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { } sourceType := flags.FlagToStringValue(p, cmd, sourceTypeFlag) - if sourceType != "volume" && sourceType != "snapshot" { - return nil, fmt.Errorf("source-type must be either 'volume' or 'snapshot'") - } name := flags.FlagToStringPointer(p, cmd, nameFlag) labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag) From f7512396a2e96c1304c497b53ad42e0a931cf498 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Wed, 11 Jun 2025 19:59:05 +0200 Subject: [PATCH 30/38] refactor create backup test vars --- .../cmd/volume/backup/create/create_test.go | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/internal/cmd/volume/backup/create/create_test.go b/internal/cmd/volume/backup/create/create_test.go index a9ccddc21..3c4980cc6 100644 --- a/internal/cmd/volume/backup/create/create_test.go +++ b/internal/cmd/volume/backup/create/create_test.go @@ -7,6 +7,7 @@ import ( "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/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -16,21 +17,24 @@ import ( type testCtxKey struct{} -var ( - testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} - testProjectId = uuid.NewString() - testSourceId = uuid.NewString() +const ( testName = "my-backup" - testLabels = map[string]string{"key1": "value1"} testSourceType = "volume" ) +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{} + testProjectId = uuid.NewString() + testSourceId = uuid.NewString() + testLabels = map[string]string{"key1": "value1"} +) + func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, sourceIdFlag: testSourceId, - sourceTypeFlag: "volume", + sourceTypeFlag: testSourceType, nameFlag: testName, labelsFlag: "key1=value1", } @@ -47,8 +51,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Verbosity: globalflags.VerbosityDefault, }, SourceID: testSourceId, - SourceType: "volume", - Name: &testName, + SourceType: testSourceType, + Name: utils.Ptr(testName), Labels: testLabels, } for _, mod := range mods { @@ -61,13 +65,13 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateBackupRequest)) iaas.Api request := testClient.CreateBackup(testCtx, testProjectId) createPayload := iaas.NewCreateBackupPayloadWithDefaults() - createPayload.Name = &testName + createPayload.Name = utils.Ptr(testName) createPayload.Labels = &map[string]interface{}{ "key1": "value1", } createPayload.Source = &iaas.BackupSource{ Id: &testSourceId, - Type: &testSourceType, + Type: utils.Ptr(testSourceType), } request = request.CreateBackupPayload(*createPayload) From bc1451d11ec79e5b5b1dc913f1a726e8b468f5b7 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 17 Jun 2025 09:41:41 +0200 Subject: [PATCH 31/38] add assume check to update volumen backup --- internal/cmd/volume/backup/update/update.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/cmd/volume/backup/update/update.go b/internal/cmd/volume/backup/update/update.go index 181218f9e..70b5d4c2b 100644 --- a/internal/cmd/volume/backup/update/update.go +++ b/internal/cmd/volume/backup/update/update.go @@ -54,6 +54,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update backup %q? (This cannot be undone)", model.BackupId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + // Configure API client apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { From 575fbb8aacf33ae34c8e82fd787155578be08ebc Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 17 Jun 2025 09:50:48 +0200 Subject: [PATCH 32/38] add util function, refactor --- internal/cmd/volume/backup/create/create.go | 13 +++++++------ internal/pkg/services/iaas/utils/utils.go | 9 +++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index 5848a972a..3bb02a8b9 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -15,6 +15,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasutils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -79,18 +80,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Get source name for label (use ID if name not available) sourceLabel := model.SourceID if model.SourceType == "volume" { - volume, err := apiClient.GetVolume(ctx, model.ProjectId, model.SourceID).Execute() + name, err := iaasutils.GetVolumeName(ctx, apiClient, model.ProjectId, model.SourceID) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) - } else if volume != nil && volume.Name != nil { - sourceLabel = *volume.Name + } else if name != "" { + sourceLabel = name } } else if model.SourceType == "snapshot" { - snapshot, err := apiClient.GetSnapshot(ctx, model.ProjectId, model.SourceID).Execute() + name, err := iaasutils.GetSnapshotName(ctx, apiClient, model.ProjectId, model.SourceID) if err != nil { params.Printer.Debug(print.ErrorLevel, "get snapshot name: %v", err) - } else if snapshot != nil && snapshot.Name != nil { - sourceLabel = *snapshot.Name + } else if name != "" { + sourceLabel = name } } diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go index f2616a01a..ff7ab3bc6 100644 --- a/internal/pkg/services/iaas/utils/utils.go +++ b/internal/pkg/services/iaas/utils/utils.go @@ -26,6 +26,7 @@ type IaaSClient interface { GetNetworkAreaRangeExecute(ctx context.Context, organizationId, areaId, networkRangeId string) (*iaas.NetworkRange, error) GetImageExecute(ctx context.Context, projectId string, imageId string) (*iaas.Image, error) GetAffinityGroupExecute(ctx context.Context, projectId string, affinityGroupId string) (*iaas.AffinityGroup, error) + GetSnapshotExecute(ctx context.Context, projectId, snapshotId string) (*iaas.Snapshot, error) } func GetSecurityGroupRuleName(ctx context.Context, apiClient IaaSClient, projectId, securityGroupRuleId, securityGroupId string) (string, error) { @@ -170,3 +171,11 @@ func GetAffinityGroupName(ctx context.Context, apiClient IaaSClient, projectId, } return *resp.Name, nil } + +func GetSnapshotName(ctx context.Context, apiClient IaaSClient, projectId, snapshotId string) (string, error) { + resp, err := apiClient.GetSnapshotExecute(ctx, projectId, snapshotId) + if err != nil { + return "", fmt.Errorf("get snapshot: %w", err) + } + return *resp.Name, nil +} From ac271a8bab0d3ca471142c1872629e2c682996d9 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 17 Jun 2025 10:24:50 +0200 Subject: [PATCH 33/38] refactoring --- internal/cmd/volume/backup/delete/delete.go | 7 ++---- internal/cmd/volume/backup/restore/restore.go | 17 ++++++------- internal/cmd/volume/backup/update/update.go | 24 +++++++++---------- internal/pkg/services/iaas/utils/utils.go | 12 ++++++++++ 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/internal/cmd/volume/backup/delete/delete.go b/internal/cmd/volume/backup/delete/delete.go index 3a9bcfc7b..c6ead8bd0 100644 --- a/internal/cmd/volume/backup/delete/delete.go +++ b/internal/cmd/volume/backup/delete/delete.go @@ -15,6 +15,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + iaasutils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" ) @@ -51,14 +52,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - backup, err := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId).Execute() + backupLabel, err := iaasutils.GetBackupName(ctx, apiClient, model.ProjectId, model.BackupId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get backup name: %v", err) } - backupLabel := model.BackupId - if backup != nil && backup.Name != nil { - backupLabel = *backup.Name - } if !model.AssumeYes { prompt := fmt.Sprintf("Are you sure you want to delete backup %q? (This cannot be undone)", backupLabel) diff --git a/internal/cmd/volume/backup/restore/restore.go b/internal/cmd/volume/backup/restore/restore.go index 6cf0b2962..b1e63f073 100644 --- a/internal/cmd/volume/backup/restore/restore.go +++ b/internal/cmd/volume/backup/restore/restore.go @@ -15,6 +15,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + iaasutils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" ) @@ -51,25 +52,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - // Get backup details for labels - backup, err := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId).Execute() + backupLabel, err := iaasutils.GetBackupName(ctx, apiClient, model.ProjectId, model.BackupId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get backup details: %v", err) } - backupLabel := model.BackupId - if backup != nil && backup.Name != nil { - backupLabel = *backup.Name - } // Get source details for labels var sourceLabel string - if backup != nil && backup.VolumeId != nil { + backup, err := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId).Execute() + if err == nil && backup != nil && backup.VolumeId != nil { sourceLabel = *backup.VolumeId - volume, err := apiClient.GetVolume(ctx, model.ProjectId, *backup.VolumeId).Execute() + name, err := iaasutils.GetVolumeName(ctx, apiClient, model.ProjectId, *backup.VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume details: %v", err) - } else if volume.Name != nil { - sourceLabel = *volume.Name + } else if name != "" { + sourceLabel = name } } diff --git a/internal/cmd/volume/backup/update/update.go b/internal/cmd/volume/backup/update/update.go index 70b5d4c2b..29a48f341 100644 --- a/internal/cmd/volume/backup/update/update.go +++ b/internal/cmd/volume/backup/update/update.go @@ -15,6 +15,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasutils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" @@ -54,6 +55,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + backupLabel, err := iaasutils.GetBackupName(ctx, apiClient, model.ProjectId, model.BackupId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get backup name: %v", err) + } + if !model.AssumeYes { prompt := fmt.Sprintf("Are you sure you want to update backup %q? (This cannot be undone)", model.BackupId) err = params.Printer.PromptForConfirmation(prompt) @@ -62,12 +74,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - // 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() @@ -75,12 +81,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("update backup: %w", err) } - // Get backup label (use ID if name not available) - backupLabel := model.BackupId - if resp.Name != nil { - backupLabel = *resp.Name - } - return outputResult(params.Printer, model.OutputFormat, backupLabel, resp) }, } diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go index ff7ab3bc6..f3d3571d7 100644 --- a/internal/pkg/services/iaas/utils/utils.go +++ b/internal/pkg/services/iaas/utils/utils.go @@ -27,6 +27,7 @@ type IaaSClient interface { GetImageExecute(ctx context.Context, projectId string, imageId string) (*iaas.Image, error) GetAffinityGroupExecute(ctx context.Context, projectId string, affinityGroupId string) (*iaas.AffinityGroup, error) GetSnapshotExecute(ctx context.Context, projectId, snapshotId string) (*iaas.Snapshot, error) + GetBackupExecute(ctx context.Context, projectId, backupId string) (*iaas.Backup, error) } func GetSecurityGroupRuleName(ctx context.Context, apiClient IaaSClient, projectId, securityGroupRuleId, securityGroupId string) (string, error) { @@ -179,3 +180,14 @@ func GetSnapshotName(ctx context.Context, apiClient IaaSClient, projectId, snaps } return *resp.Name, nil } + +func GetBackupName(ctx context.Context, apiClient IaaSClient, projectId, backupId string) (string, error) { + resp, err := apiClient.GetBackupExecute(ctx, projectId, backupId) + if err != nil { + return backupId, fmt.Errorf("get backup: %w", err) + } + if resp != nil && resp.Name != nil { + return *resp.Name, nil + } + return backupId, nil +} From 35723819d10b7f6242f2781c67a2c63ef186a820 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 17 Jun 2025 10:27:15 +0200 Subject: [PATCH 34/38] update docs --- docs/stackit_volume_backup_create.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stackit_volume_backup_create.md b/docs/stackit_volume_backup_create.md index ef5013918..5a322f34a 100644 --- a/docs/stackit_volume_backup_create.md +++ b/docs/stackit_volume_backup_create.md @@ -30,7 +30,7 @@ stackit volume backup create [flags] --labels stringToString Key-value string pairs as labels (default []) --name string Name of the backup --source-id string ID of the source from which a backup should be created - --source-type string Source type of the backup (volume or snapshot) + --source-type string Source type of the backup, one of ["volume" "snapshot"] ``` ### Options inherited from parent commands From 3f92f4ee09b0e511e522cd5da4c58f562f1db0a7 Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 17 Jun 2025 10:37:08 +0200 Subject: [PATCH 35/38] fix utils test --- internal/pkg/services/iaas/utils/utils_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/pkg/services/iaas/utils/utils_test.go b/internal/pkg/services/iaas/utils/utils_test.go index f4375251d..e2ccdc469 100644 --- a/internal/pkg/services/iaas/utils/utils_test.go +++ b/internal/pkg/services/iaas/utils/utils_test.go @@ -33,6 +33,10 @@ type IaaSClientMocked struct { GetImageResp *iaas.Image GetAffinityGroupsFails bool GetAffinityGroupResp *iaas.AffinityGroup + GetBackupFails bool + GetBackupResp *iaas.Backup + GetSnapshotFails bool + GetSnapshotResp *iaas.Snapshot } func (m *IaaSClientMocked) GetAffinityGroupExecute(_ context.Context, _, _ string) (*iaas.AffinityGroup, error) { @@ -112,6 +116,19 @@ func (m *IaaSClientMocked) GetImageExecute(_ context.Context, _, _ string) (*iaa return m.GetImageResp, nil } +func (m *IaaSClientMocked) GetBackupExecute(_ context.Context, _, _ string) (*iaas.Backup, error) { + if m.GetBackupFails { + return nil, fmt.Errorf("could not get backup") + } + return m.GetBackupResp, nil +} + +func (m *IaaSClientMocked) GetSnapshotExecute(_ context.Context, _, _ string) (*iaas.Snapshot, error) { + if m.GetSnapshotFails { + return nil, fmt.Errorf("could not get snapshot") + } + return m.GetSnapshotResp, nil +} func TestGetSecurityGroupRuleName(t *testing.T) { type args struct { getInstanceFails bool From 5e1d3fd521dc3191df33053476cbea9a9e3c51eb Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 17 Jun 2025 10:55:09 +0200 Subject: [PATCH 36/38] refactor duplicate code with util func ConvertStringMapToInterfaceMap --- internal/cmd/volume/backup/create/create.go | 24 +++++++-------------- internal/cmd/volume/backup/update/update.go | 18 ++++------------ internal/cmd/volume/create/create.go | 11 +--------- internal/cmd/volume/update/update.go | 11 +--------- 4 files changed, 14 insertions(+), 50 deletions(-) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index 3bb02a8b9..447a3ea16 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -181,24 +181,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateBackupRequest { req := apiClient.CreateBackup(ctx, model.ProjectId) - // Convert map[string]string to map[string]interface{} - var labelsMap *map[string]interface{} - if len(model.Labels) > 0 { - labelsMap = utils.Ptr(map[string]interface{}{}) - for k, v := range model.Labels { - (*labelsMap)[k] = v - } - } - - createPayload := iaas.NewCreateBackupPayloadWithDefaults() - createPayload.Name = model.Name - createPayload.Labels = labelsMap - createPayload.Source = &iaas.BackupSource{ - Id: &model.SourceID, - Type: &model.SourceType, + payload := iaas.CreateBackupPayload{ + Name: model.Name, + Labels: utils.ConvertStringMapToInterfaceMap(utils.Ptr(model.Labels)), + Source: &iaas.BackupSource{ + Id: &model.SourceID, + Type: &model.SourceType, + }, } - return req.CreateBackupPayload(*createPayload) + return req.CreateBackupPayload(payload) } func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel, projectLabel string, resp *iaas.Backup) error { diff --git a/internal/cmd/volume/backup/update/update.go b/internal/cmd/volume/backup/update/update.go index 29a48f341..f23bb5108 100644 --- a/internal/cmd/volume/backup/update/update.go +++ b/internal/cmd/volume/backup/update/update.go @@ -130,22 +130,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateBackupRequest { req := apiClient.UpdateBackup(ctx, model.ProjectId, model.BackupId) - updatePayload := iaas.NewUpdateBackupPayloadWithDefaults() - if model.Name != nil { - updatePayload.Name = model.Name + payload := iaas.UpdateBackupPayload{ + Name: model.Name, + Labels: utils.ConvertStringMapToInterfaceMap(utils.Ptr(model.Labels)), } - // Convert map[string]string to map[string]interface{} - var labelsMap *map[string]interface{} - if len(model.Labels) > 0 { - labelsMap = utils.Ptr(map[string]interface{}{}) - for k, v := range model.Labels { - (*labelsMap)[k] = v - } - } - updatePayload.Labels = labelsMap - - req = req.UpdateBackupPayload(*updatePayload) + req = req.UpdateBackupPayload(payload) return req } diff --git a/internal/cmd/volume/create/create.go b/internal/cmd/volume/create/create.go index 258e0752a..5519bd5d8 100644 --- a/internal/cmd/volume/create/create.go +++ b/internal/cmd/volume/create/create.go @@ -174,20 +174,11 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli Type: model.SourceType, } - var labelsMap *map[string]interface{} - if model.Labels != nil && len(*model.Labels) > 0 { - // convert map[string]string to map[string]interface{} - labelsMap = utils.Ptr(map[string]interface{}{}) - for k, v := range *model.Labels { - (*labelsMap)[k] = v - } - } - payload := iaas.CreateVolumePayload{ AvailabilityZone: model.AvailabilityZone, Name: model.Name, Description: model.Description, - Labels: labelsMap, + Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), PerformanceClass: model.PerformanceClass, Size: model.Size, } diff --git a/internal/cmd/volume/update/update.go b/internal/cmd/volume/update/update.go index 80e137987..263f333af 100644 --- a/internal/cmd/volume/update/update.go +++ b/internal/cmd/volume/update/update.go @@ -135,19 +135,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateVolumeRequest { req := apiClient.UpdateVolume(ctx, model.ProjectId, model.VolumeId) - var labelsMap *map[string]interface{} - if model.Labels != nil && len(*model.Labels) > 0 { - // convert map[string]string to map[string]interface{} - labelsMap = utils.Ptr(map[string]interface{}{}) - for k, v := range *model.Labels { - (*labelsMap)[k] = v - } - } - payload := iaas.UpdateVolumePayload{ Name: model.Name, Description: model.Description, - Labels: labelsMap, + Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), } return req.UpdateVolumePayload(payload) From 667d6cf08da7bebe7f5a43a76c52f26407d234bb Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 17 Jun 2025 11:00:22 +0200 Subject: [PATCH 37/38] fix go mod,sum --- go.mod | 4 ---- go.sum | 16 ++++++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 0bbfbf9fd..c3bb00c1f 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,6 @@ module github.com/stackitcloud/stackit-cli go 1.24 -replace ( - github.com/stackitcloud/stackit-sdk-go/services/iaas => /Users/bfuertsch/work/StackIT/stackit-sdk-go/services/iaas -) - require ( github.com/fatih/color v1.18.0 github.com/goccy/go-yaml v1.18.0 diff --git a/go.sum b/go.sum index 2948bc975..6bd663516 100644 --- a/go.sum +++ b/go.sum @@ -574,20 +574,20 @@ github.com/stackitcloud/stackit-sdk-go/services/git v0.5.0 h1:s0A2EPBrnBxfKStKA/ github.com/stackitcloud/stackit-sdk-go/services/git v0.5.0/go.mod h1:XhXHJpOVC9Rpwyf1G+EpMbprBafH9aZb8vWBdR+z0WM= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.24.0 h1:aLlZmcsDHqqc7KPsevvs+W6EPZFT51u/dx5TcVQsE6g= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.24.0/go.mod h1:TaMx7kukGpRm0BkNCmS7u2x12q1pgfbD55DAnLIjOIQ= -github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.2.0 h1:gaTgjmEIvq7Nmji5YHh1haFvA/8dWyOgCg3lw6drjL4= -github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.2.0/go.mod h1:h3oM6cS23Yfynp8Df1hNr0FxtY5Alii/2g8Wqi5SIVE= -github.com/stackitcloud/stackit-sdk-go/services/logme v0.24.0 h1:5J2TH9ig5cp+5pCHugrsDJuFsRnIOQHQUqsxlweRXL0= -github.com/stackitcloud/stackit-sdk-go/services/logme v0.24.0/go.mod h1:+3jizYma6Dq3XVn6EMMdSBF9eIm0w6hCJvrStB3AIL0= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.24.0 h1:t/Ten9AuoWFmrDq5gAI3kVZShF3i8zEAaeBsYYqiaao= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.24.0/go.mod h1:qgvi3qiAzB1wKpMJ5CPnEaUToeiwgnQxGvlkjdisaLU= +github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0 h1:Ef4SyTBjIkfwaws4mssa6AoK+OokHFtr7ZIflUpoXVE= +github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0/go.mod h1:FiVhDlw9+yuTiUmnyGLn2qpsLW26w9OC4TS1y78czvg= +github.com/stackitcloud/stackit-sdk-go/services/logme v0.24.1 h1:hfnILDJGBwwqUIs4xt/7Jj4LBe+JsSdHy+Md2ynUg4Y= +github.com/stackitcloud/stackit-sdk-go/services/logme v0.24.1/go.mod h1:XjDMHhAQogFXsVR+o138CPYG1FOe0/Nl2Vm+fAgzx2A= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.24.1 h1:7nN7ZCuWSbJMy5KqoOqSbp5JKIOvyuDqVRtxVvT1iyE= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.24.1/go.mod h1:Pb8IEV5/jP8k75dVcN5cn3kP7PHTy/4KXXKpG76oj4U= github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.2.0 h1:U/IhjLOz0vG6zuxTqGhBd8f609s6JB+X9PaL6x/VM58= github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.2.0/go.mod h1:+JSnz5/AvGN5ek/iH008frRc/NgjSr1EVOTIbyLwAuQ= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.2.0 h1:+dKIPVz9ydKbX3x6+1NvYk++OA378w74p+N6SjDmzBQ= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.2.0/go.mod h1:iCOYS9yICXQPyMAIdUGMqJDLY8HXKiVAdiMzO/mPvtA= github.com/stackitcloud/stackit-sdk-go/services/observability v0.7.1 h1:6OObzh2zk7wg75zYstcj0kjOjaxWc4joqA6qdeo8DP4= github.com/stackitcloud/stackit-sdk-go/services/observability v0.7.1/go.mod h1:+eNo7SEeVRuW7hgujSabSketScSUKGuC88UznPS+UTE= -github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.23.1 h1:E6vCqPn1NiPHnbnvqLNQNz6a/cmeyRb5iA9cDUPtP58= -github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.23.1/go.mod h1:ifKKKCWL1U435fXGQ375SPX+burtfg1I7EGZ58COzRA= +github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.23.0 h1:/OaZCyrD8LFa4W6a2Vu2QSVMJwLLBr8ZdBKzX00MV1Q= +github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.23.0/go.mod h1:c30J6f/fXtbzcHkH3ZcabZUek3wfy5CRnEkcW5e5yXg= github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.1.0 h1:r29a9GoBLVw2VZSzdPftlIsE5t7shdxobwoT6NVUIjU= github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.1.0/go.mod h1:4g/L5PHfz1xk3udEhvPy2nXiH4UgRO5Cj6iwUa7k5VQ= github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.24.1 h1:2rDFwJtZOFYFUiJqJ9uIwM+mu+BbtuVaUHARRJtrZPU= From b15a2eac5fe964237678358ed7133b56ab406f5a Mon Sep 17 00:00:00 2001 From: Benjosh95 Date: Tue, 17 Jun 2025 11:42:44 +0200 Subject: [PATCH 38/38] fix printing behavior --- internal/cmd/volume/backup/delete/delete.go | 4 ++-- internal/cmd/volume/backup/restore/restore.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cmd/volume/backup/delete/delete.go b/internal/cmd/volume/backup/delete/delete.go index c6ead8bd0..b40d93ea7 100644 --- a/internal/cmd/volume/backup/delete/delete.go +++ b/internal/cmd/volume/backup/delete/delete.go @@ -84,9 +84,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } if model.Async { - params.Printer.Info("Triggered deletion of backup %q\n", backupLabel) + params.Printer.Outputf("Triggered deletion of backup %q\n", backupLabel) } else { - params.Printer.Info("Deleted backup %q\n", backupLabel) + params.Printer.Outputf("Deleted backup %q\n", backupLabel) } return nil }, diff --git a/internal/cmd/volume/backup/restore/restore.go b/internal/cmd/volume/backup/restore/restore.go index b1e63f073..3249f0560 100644 --- a/internal/cmd/volume/backup/restore/restore.go +++ b/internal/cmd/volume/backup/restore/restore.go @@ -97,9 +97,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } if model.Async { - params.Printer.Info("Triggered restore of %q with %q in %q\n", sourceLabel, backupLabel, model.ProjectId) + params.Printer.Outputf("Triggered restore of %q with %q in %q\n", sourceLabel, backupLabel, model.ProjectId) } else { - params.Printer.Info("Restored %q with %q in %q\n", sourceLabel, backupLabel, model.ProjectId) + params.Printer.Outputf("Restored %q with %q in %q\n", sourceLabel, backupLabel, model.ProjectId) } return nil },