mygithub.libinneed.workers.dev/stackitcloud/stackit-cli
Advanced tools
| ## 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 | ||
| Create a backup from a snapshot with a name | ||
| $ 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 | ||
| ``` | ||
| ### 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, one of ["volume" "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 | ||
| ## 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 with ID "xxx" | ||
| $ stackit volume backup delete xxx | ||
| ``` | ||
| ### 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 | ||
| ## 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 with ID "xxx" | ||
| $ stackit volume backup describe xxx | ||
| Get details of a backup with ID "xxx" in JSON format | ||
| $ stackit volume backup describe 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 | ||
| ## 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 | ||
| ## 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 with ID "xxx" | ||
| $ stackit volume backup restore xxx | ||
| ``` | ||
| ### 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 | ||
| ## stackit volume backup update | ||
| Updates a backup | ||
| ### Synopsis | ||
| Updates a backup by its ID. | ||
| ``` | ||
| stackit volume backup update BACKUP_ID [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Update the name of a backup with ID "xxx" | ||
| $ stackit volume backup update xxx --name new-name | ||
| Update the labels of a backup with ID "xxx" | ||
| $ stackit volume backup update 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 | ||
| ## 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 | ||
| 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)) | ||
| } |
| 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/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/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| type testCtxKey struct{} | ||
| const ( | ||
| testName = "my-backup" | ||
| 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: testSourceType, | ||
| 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: testSourceType, | ||
| Name: utils.Ptr(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) | ||
| createPayload := iaas.NewCreateBackupPayloadWithDefaults() | ||
| createPayload.Name = utils.Ptr(testName) | ||
| createPayload.Labels = &map[string]interface{}{ | ||
| "key1": "value1", | ||
| } | ||
| createPayload.Source = &iaas.BackupSource{ | ||
| Id: &testSourceId, | ||
| Type: utils.Ptr(testSourceType), | ||
| } | ||
| request = request.CreateBackupPayload(*createPayload) | ||
| 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, globalflags.ProjectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.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(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) { | ||
| 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: "backup is nil", | ||
| args: args{ | ||
| backup: nil, | ||
| }, | ||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| 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" | ||
| 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" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" | ||
| ) | ||
| const ( | ||
| sourceIdFlag = "source-id" | ||
| sourceTypeFlag = "source-type" | ||
| nameFlag = "name" | ||
| labelsFlag = "labels" | ||
| ) | ||
| var sourceTypeFlagOptions = []string{"volume", "snapshot"} | ||
| 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"), | ||
| examples.NewExample( | ||
| `Create a backup from a snapshot with a name`, | ||
| "$ 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"), | ||
| ), | ||
| 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 | ||
| } | ||
| 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 | ||
| } | ||
| // Get source name for label (use ID if name not available) | ||
| sourceLabel := model.SourceID | ||
| if model.SourceType == "volume" { | ||
| 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 name != "" { | ||
| sourceLabel = name | ||
| } | ||
| } else if model.SourceType == "snapshot" { | ||
| 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 name != "" { | ||
| sourceLabel = name | ||
| } | ||
| } | ||
| if !model.AssumeYes { | ||
| 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 | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("create volume backup: %w", err) | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(params.Printer) | ||
| s.Start("Creating backup") | ||
| resp, err = wait.CreateBackupWaitHandler(ctx, apiClient, model.ProjectId, *resp.Id).WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for backup creation: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| return outputResult(params.Printer, model.OutputFormat, model.Async, sourceLabel, 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().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") | ||
| 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) | ||
| 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, | ||
| 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.ApiCreateBackupRequest { | ||
| req := apiClient.CreateBackup(ctx, model.ProjectId) | ||
| 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(payload) | ||
| } | ||
| 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") | ||
| } | ||
| 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, utils.PtrString(resp.Id)) | ||
| } else { | ||
| p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, utils.PtrString(resp.Id)) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| 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" | ||
| ) | ||
| 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{ | ||
| globalflags.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, globalflags.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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| 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" | ||
| 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" | ||
| ) | ||
| 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 with ID "xxx"`, "$ stackit volume backup delete xxx"), | ||
| ), | ||
| 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 | ||
| } | ||
| 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 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.Outputf("Triggered deletion of backup %q\n", backupLabel) | ||
| } else { | ||
| params.Printer.Outputf("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 | ||
| } |
| 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" | ||
| ) | ||
| 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{ | ||
| globalflags.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, globalflags.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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package describe | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "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 with ID "xxx"`, | ||
| "$ stackit volume backup describe xxx"), | ||
| examples.NewExample( | ||
| `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() | ||
| 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.AddSeparator() | ||
| table.AddRow("NAME", utils.PtrString(backup.Name)) | ||
| table.AddSeparator() | ||
| table.AddRow("SIZE", utils.PtrByteSizeDefault(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() | ||
| 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)) | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } |
| 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" | ||
| ) | ||
| 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{ | ||
| globalflags.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, globalflags.ProjectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| 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: "empty backup in slice", | ||
| args: args{ | ||
| backups: []iaas.Backup{{}}, | ||
| }, | ||
| wantErr: false, | ||
| }, | ||
| { | ||
| name: "empty slice", | ||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "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 backups`, | ||
| "$ stackit volume backup list"), | ||
| examples.NewExample( | ||
| `List all backups in JSON format`, | ||
| "$ stackit volume backup list --output-format json"), | ||
| examples.NewExample( | ||
| `List up to 10 backups`, | ||
| "$ stackit volume backup list --limit 10"), | ||
| examples.NewExample( | ||
| `List 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 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") | ||
| } | ||
| 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 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 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 _, 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(backup.Size, ""), | ||
| utils.PtrString(backup.Status), | ||
| utils.PtrString(backup.SnapshotId), | ||
| utils.PtrString(backup.VolumeId), | ||
| utils.PtrString(backup.AvailabilityZone), | ||
| labelsString, | ||
| utils.ConvertTimePToDateTimeString(backup.CreatedAt), | ||
| utils.ConvertTimePToDateTimeString(backup.UpdatedAt), | ||
| ) | ||
| table.AddSeparator() | ||
| } | ||
| p.Outputln(table.Render()) | ||
| return nil | ||
| } | ||
| } |
| 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" | ||
| ) | ||
| 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{ | ||
| globalflags.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, globalflags.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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| 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" | ||
| 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" | ||
| ) | ||
| 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 with ID "xxx"`, "$ stackit volume backup restore xxx"), | ||
| ), | ||
| 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 | ||
| } | ||
| backupLabel, err := iaasutils.GetBackupName(ctx, apiClient, model.ProjectId, model.BackupId) | ||
| if err != nil { | ||
| params.Printer.Debug(print.ErrorLevel, "get backup details: %v", err) | ||
| } | ||
| // Get source details for labels | ||
| var sourceLabel string | ||
| backup, err := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId).Execute() | ||
| if err == nil && backup != nil && backup.VolumeId != nil { | ||
| sourceLabel = *backup.VolumeId | ||
| 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 name != "" { | ||
| sourceLabel = name | ||
| } | ||
| } | ||
| 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() | ||
| } | ||
| if model.Async { | ||
| params.Printer.Outputf("Triggered restore of %q with %q in %q\n", sourceLabel, backupLabel, model.ProjectId) | ||
| } else { | ||
| params.Printer.Outputf("Restored %q with %q in %q\n", sourceLabel, backupLabel, model.ProjectId) | ||
| } | ||
| 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 | ||
| } |
| 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" | ||
| ) | ||
| 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{ | ||
| globalflags.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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| 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" | ||
| 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" | ||
| ) | ||
| 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 the name of a backup with ID "xxx"`, | ||
| "$ stackit volume backup update xxx --name new-name"), | ||
| examples.NewExample( | ||
| `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() | ||
| 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 | ||
| } | ||
| 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) | ||
| 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) | ||
| } | ||
| 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) | ||
| payload := iaas.UpdateBackupPayload{ | ||
| Name: model.Name, | ||
| Labels: utils.ConvertStringMapToInterfaceMap(utils.Ptr(model.Labels)), | ||
| } | ||
| req = req.UpdateBackupPayload(payload) | ||
| 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 | ||
| } | ||
| } |
| name: CI | ||
| on: [pull_request, workflow_dispatch] | ||
| on: | ||
| pull_request: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: | ||
| - main | ||
| env: | ||
| CODE_COVERAGE_FILE_NAME: "coverage.out" # must be the same as in Makefile | ||
| CODE_COVERAGE_ARTIFACT_NAME: "code-coverage" | ||
| jobs: | ||
@@ -29,4 +38,11 @@ main: | ||
| - name: Archive code coverage results | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: ${{ env.CODE_COVERAGE_ARTIFACT_NAME }} | ||
| path: ${{ env.CODE_COVERAGE_FILE_NAME }} | ||
| config: | ||
| name: Check GoReleaser config | ||
| if: github.event_name == 'pull_request' | ||
| runs-on: ubuntu-latest | ||
@@ -40,2 +56,17 @@ steps: | ||
| with: | ||
| args: check | ||
| args: check | ||
| code_coverage: | ||
| name: "Code coverage report" | ||
| if: github.event_name == 'pull_request' # Do not run when workflow is triggered by push to main branch | ||
| runs-on: ubuntu-latest | ||
| needs: main | ||
| permissions: | ||
| contents: read | ||
| actions: read # to download code coverage results from "main" job | ||
| pull-requests: write # write permission needed to comment on PR | ||
| steps: | ||
| - uses: fgrosse/go-coverage-report@v1.1.1 | ||
| with: | ||
| coverage-artifact-name: ${{ env.CODE_COVERAGE_ARTIFACT_NAME }} | ||
| coverage-file-name: ${{ env.CODE_COVERAGE_FILE_NAME }} |
@@ -51,2 +51,4 @@ # STACKIT CLI release workflow. | ||
| security unlock-keychain -p "${{ secrets.TEMP_KEYCHAIN }}" $KEYCHAIN_PATH | ||
| # the keychain gets locked automatically after 300s, so we have to extend this interval to e.g. 900 seconds | ||
| security set-keychain-settings -lut 900 | ||
| security import ./ApplicationID.p12 -P "${{ secrets.APPLICATION_ID }}" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH | ||
@@ -53,0 +55,0 @@ security list-keychain -d user -s $KEYCHAIN_PATH |
@@ -33,2 +33,3 @@ ## stackit volume | ||
| * [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 | ||
@@ -35,0 +36,0 @@ * [stackit volume delete](./stackit_volume_delete.md) - Deletes a volume |
+4
-4
@@ -21,7 +21,7 @@ module github.com/stackitcloud/stackit-cli | ||
| 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 | ||
@@ -28,0 +28,0 @@ github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.16.0 |
@@ -177,11 +177,2 @@ package create | ||
| 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{ | ||
@@ -191,3 +182,3 @@ AvailabilityZone: model.AvailabilityZone, | ||
| Description: model.Description, | ||
| Labels: labelsMap, | ||
| Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), | ||
| PerformanceClass: model.PerformanceClass, | ||
@@ -194,0 +185,0 @@ Size: model.Size, |
@@ -138,15 +138,6 @@ package update | ||
| 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), | ||
| } | ||
@@ -153,0 +144,0 @@ |
@@ -5,2 +5,3 @@ package volume | ||
| "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" | ||
@@ -39,2 +40,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/volume/delete" | ||
| cmd.AddCommand(performanceclass.NewCmd(params)) | ||
| cmd.AddCommand(backup.NewCmd(params)) | ||
| } |
@@ -36,2 +36,6 @@ package utils | ||
| GetAffinityGroupResp *iaas.AffinityGroup | ||
| GetBackupFails bool | ||
| GetBackupResp *iaas.Backup | ||
| GetSnapshotFails bool | ||
| GetSnapshotResp *iaas.Snapshot | ||
| } | ||
@@ -116,2 +120,15 @@ | ||
| 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) { | ||
@@ -118,0 +135,0 @@ type args struct { |
@@ -29,2 +29,4 @@ package utils | ||
| 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) | ||
| } | ||
@@ -174,1 +176,20 @@ | ||
| } | ||
| 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 | ||
| } | ||
| 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 | ||
| } |
+1
-1
@@ -28,3 +28,3 @@ ROOT_DIR ?= $(shell git rev-parse --show-toplevel) | ||
| @echo ">> Running tests for the CLI application" | ||
| @go test ./... -count=1 | ||
| @go test ./... -count=1 -coverprofile=coverage.out | ||
@@ -31,0 +31,0 @@ # Test coverage |
Sorry, the diff of this file is too big to display