Skip to content

Commit a0f7113

Browse files
committed
add volume backup update and update tests
1 parent 2de3fa6 commit a0f7113

File tree

3 files changed

+364
-2
lines changed

3 files changed

+364
-2
lines changed

internal/cmd/volume/backup/create/create.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command {
9292
// Get source label (use ID if name not available)
9393
// sourceLabel := model.SourceID
9494

95-
// TODO: SDK needs to be updated/released to support this async operation
96-
// Wait for async operation, if async mode not enabled
9795
if !model.Async {
9896
s := spinner.New(params.Printer)
9997
s.Start("Creating backup")
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package update
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/goccy/go-yaml"
9+
"github.com/spf13/cobra"
10+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
19+
20+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
21+
)
22+
23+
const (
24+
backupIdArg = "BACKUP_ID"
25+
nameFlag = "name"
26+
labelsFlag = "labels"
27+
)
28+
29+
type inputModel struct {
30+
*globalflags.GlobalFlagModel
31+
BackupId string
32+
Name *string
33+
Labels map[string]string
34+
}
35+
36+
func NewCmd(params *params.CmdParams) *cobra.Command {
37+
cmd := &cobra.Command{
38+
Use: fmt.Sprintf("update %s", backupIdArg),
39+
Short: "Updates a backup",
40+
Long: "Updates a backup by its ID.",
41+
Args: args.SingleArg(backupIdArg, utils.ValidateUUID),
42+
Example: examples.Build(
43+
examples.NewExample(
44+
`Update a backup name`,
45+
"$ stackit volume backup update xxx-xxx-xxx --name new-name"),
46+
examples.NewExample(
47+
`Update backup labels`,
48+
"$ stackit volume backup update xxx-xxx-xxx --labels key1=value1,key2=value2"),
49+
),
50+
RunE: func(cmd *cobra.Command, args []string) error {
51+
ctx := context.Background()
52+
model, err := parseInput(params.Printer, cmd, args)
53+
if err != nil {
54+
return err
55+
}
56+
57+
// Configure API client
58+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
59+
if err != nil {
60+
return err
61+
}
62+
63+
// Call API
64+
req := buildRequest(ctx, model, apiClient)
65+
resp, err := req.Execute()
66+
if err != nil {
67+
return fmt.Errorf("update backup: %w", err)
68+
}
69+
70+
// Get backup label (use ID if name not available)
71+
backupLabel := model.BackupId
72+
if resp.Name != nil {
73+
backupLabel = *resp.Name
74+
}
75+
76+
return outputResult(params.Printer, model.OutputFormat, backupLabel, resp)
77+
},
78+
}
79+
80+
configureFlags(cmd)
81+
return cmd
82+
}
83+
84+
func configureFlags(cmd *cobra.Command) {
85+
cmd.Flags().String(nameFlag, "", "Name of the backup")
86+
cmd.Flags().StringToString(labelsFlag, nil, "Key-value string pairs as labels")
87+
}
88+
89+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
90+
backupId := inputArgs[0]
91+
92+
globalFlags := globalflags.Parse(p, cmd)
93+
if globalFlags.ProjectId == "" {
94+
return nil, &errors.ProjectIdError{}
95+
}
96+
97+
name := flags.FlagToStringPointer(p, cmd, nameFlag)
98+
labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag)
99+
if labels == nil {
100+
labels = &map[string]string{}
101+
}
102+
103+
model := inputModel{
104+
GlobalFlagModel: globalFlags,
105+
BackupId: backupId,
106+
Name: name,
107+
Labels: *labels,
108+
}
109+
110+
if p.IsVerbosityDebug() {
111+
modelStr, err := print.BuildDebugStrFromInputModel(model)
112+
if err != nil {
113+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
114+
} else {
115+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
116+
}
117+
}
118+
119+
return &model, nil
120+
}
121+
122+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateBackupRequest {
123+
req := apiClient.UpdateBackup(ctx, model.ProjectId, model.BackupId)
124+
125+
updatePayload := iaas.NewUpdateBackupPayloadWithDefaults()
126+
if model.Name != nil {
127+
updatePayload.Name = model.Name
128+
}
129+
130+
// Convert map[string]string to map[string]interface{}
131+
var labelsMap *map[string]interface{}
132+
if len(model.Labels) > 0 {
133+
labelsMap = utils.Ptr(map[string]interface{}{})
134+
for k, v := range model.Labels {
135+
(*labelsMap)[k] = v
136+
}
137+
}
138+
updatePayload.Labels = labelsMap
139+
140+
req = req.UpdateBackupPayload(*updatePayload)
141+
return req
142+
}
143+
144+
func outputResult(p *print.Printer, outputFormat, backupLabel string, backup *iaas.Backup) error {
145+
if backup == nil {
146+
return fmt.Errorf("backup response is empty")
147+
}
148+
149+
switch outputFormat {
150+
case print.JSONOutputFormat:
151+
details, err := json.MarshalIndent(backup, "", " ")
152+
if err != nil {
153+
return fmt.Errorf("marshal backup: %w", err)
154+
}
155+
p.Outputln(string(details))
156+
return nil
157+
158+
case print.YAMLOutputFormat:
159+
details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
160+
if err != nil {
161+
return fmt.Errorf("marshal backup: %w", err)
162+
}
163+
p.Outputln(string(details))
164+
return nil
165+
166+
default:
167+
p.Outputf("Updated backup %q\n", backupLabel)
168+
return nil
169+
}
170+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package update
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/google/go-cmp/cmp/cmpopts"
13+
"github.com/google/uuid"
14+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
15+
)
16+
17+
var projectIdFlag = globalflags.ProjectIdFlag
18+
19+
type testCtxKey struct{}
20+
21+
var (
22+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
23+
testClient = &iaas.APIClient{}
24+
testProjectId = uuid.NewString()
25+
testBackupId = uuid.NewString()
26+
testName = "test-backup"
27+
testLabels = map[string]string{"key1": "value1"}
28+
)
29+
30+
func fixtureArgValues(mods ...func(argValues []string)) []string {
31+
argValues := []string{
32+
testBackupId,
33+
}
34+
for _, mod := range mods {
35+
mod(argValues)
36+
}
37+
return argValues
38+
}
39+
40+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
41+
flagValues := map[string]string{
42+
projectIdFlag: testProjectId,
43+
nameFlag: testName,
44+
labelsFlag: "key1=value1",
45+
}
46+
for _, mod := range mods {
47+
mod(flagValues)
48+
}
49+
return flagValues
50+
}
51+
52+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
53+
model := &inputModel{
54+
GlobalFlagModel: &globalflags.GlobalFlagModel{
55+
ProjectId: testProjectId,
56+
Verbosity: globalflags.VerbosityDefault,
57+
},
58+
BackupId: testBackupId,
59+
Name: &testName,
60+
Labels: testLabels,
61+
}
62+
for _, mod := range mods {
63+
mod(model)
64+
}
65+
return model
66+
}
67+
68+
func fixtureRequest(mods ...func(request *iaas.ApiUpdateBackupRequest)) iaas.ApiUpdateBackupRequest {
69+
request := testClient.UpdateBackup(testCtx, testProjectId, testBackupId)
70+
payload := iaas.NewUpdateBackupPayloadWithDefaults()
71+
payload.Name = &testName
72+
73+
// Convert test labels to map[string]interface{}
74+
labelsMap := map[string]interface{}{}
75+
for k, v := range testLabels {
76+
labelsMap[k] = v
77+
}
78+
payload.Labels = &labelsMap
79+
80+
request = request.UpdateBackupPayload(*payload)
81+
for _, mod := range mods {
82+
mod(&request)
83+
}
84+
return request
85+
}
86+
87+
func TestParseInput(t *testing.T) {
88+
tests := []struct {
89+
description string
90+
argValues []string
91+
flagValues map[string]string
92+
isValid bool
93+
expectedModel *inputModel
94+
}{
95+
{
96+
description: "base",
97+
argValues: fixtureArgValues(),
98+
flagValues: fixtureFlagValues(),
99+
isValid: true,
100+
expectedModel: fixtureInputModel(),
101+
},
102+
{
103+
description: "no values",
104+
argValues: []string{},
105+
flagValues: map[string]string{},
106+
isValid: false,
107+
},
108+
{
109+
description: "no arg values",
110+
argValues: []string{},
111+
flagValues: fixtureFlagValues(),
112+
isValid: false,
113+
},
114+
{
115+
description: "no flag values",
116+
argValues: fixtureArgValues(),
117+
flagValues: map[string]string{},
118+
isValid: false,
119+
},
120+
}
121+
122+
for _, tt := range tests {
123+
t.Run(tt.description, func(t *testing.T) {
124+
p := print.NewPrinter()
125+
cmd := NewCmd(&params.CmdParams{Printer: p})
126+
err := globalflags.Configure(cmd.Flags())
127+
if err != nil {
128+
t.Fatalf("configure global flags: %v", err)
129+
}
130+
131+
for flag, value := range tt.flagValues {
132+
err := cmd.Flags().Set(flag, value)
133+
if err != nil {
134+
if !tt.isValid {
135+
return
136+
}
137+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
138+
}
139+
}
140+
141+
err = cmd.ValidateArgs(tt.argValues)
142+
if err != nil {
143+
if !tt.isValid {
144+
return
145+
}
146+
t.Fatalf("error validating args: %v", err)
147+
}
148+
149+
model, err := parseInput(p, cmd, tt.argValues)
150+
if err != nil {
151+
if !tt.isValid {
152+
return
153+
}
154+
t.Fatalf("error parsing input: %v", err)
155+
}
156+
157+
if !tt.isValid {
158+
t.Fatalf("did not fail on invalid input")
159+
}
160+
diff := cmp.Diff(model, tt.expectedModel)
161+
if diff != "" {
162+
t.Fatalf("Data does not match: %s", diff)
163+
}
164+
})
165+
}
166+
}
167+
168+
func TestBuildRequest(t *testing.T) {
169+
tests := []struct {
170+
description string
171+
model *inputModel
172+
expectedRequest iaas.ApiUpdateBackupRequest
173+
}{
174+
{
175+
description: "base",
176+
model: fixtureInputModel(),
177+
expectedRequest: fixtureRequest(),
178+
},
179+
}
180+
181+
for _, tt := range tests {
182+
t.Run(tt.description, func(t *testing.T) {
183+
request := buildRequest(testCtx, tt.model, testClient)
184+
185+
diff := cmp.Diff(request, tt.expectedRequest,
186+
cmp.AllowUnexported(tt.expectedRequest),
187+
cmpopts.EquateComparable(testCtx),
188+
)
189+
if diff != "" {
190+
t.Fatalf("Data does not match: %s", diff)
191+
}
192+
})
193+
}
194+
}

0 commit comments

Comments
 (0)