You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli

Package Overview
Dependencies
Versions
173
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli - go Package Compare versions

Comparing version
v0.34.3
to
v0.34.4
+50
docs/stackit_volume_backup_create.md
## 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(&params.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(&params.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(&params.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(&params.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(&params.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(&params.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(&params.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(&params.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
}
}
+33
-2
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
}

@@ -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