Skip to content

Commit 1561c90

Browse files
committed
add snapshots delete subcommand
1 parent 94e43e5 commit 1561c90

File tree

2 files changed

+328
-0
lines changed

2 files changed

+328
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package delete
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
17+
18+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
19+
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
20+
)
21+
22+
const (
23+
snapshotIdArg = "SNAPSHOT_ID"
24+
)
25+
26+
type inputModel struct {
27+
*globalflags.GlobalFlagModel
28+
SnapshotId string
29+
}
30+
31+
func NewCmd(params *params.CmdParams) *cobra.Command {
32+
cmd := &cobra.Command{
33+
Use: fmt.Sprintf("delete %s", snapshotIdArg),
34+
Short: "Deletes a snapshot",
35+
Long: "Deletes a snapshot by its ID.",
36+
Args: args.SingleArg(snapshotIdArg, utils.ValidateUUID),
37+
Example: examples.Build(
38+
examples.NewExample(
39+
`Delete a snapshot`,
40+
"$ stackit volume snapshot delete xxx-xxx-xxx"),
41+
examples.NewExample(
42+
`Delete a snapshot and wait for deletion to be completed`,
43+
"$ stackit volume snapshot delete xxx-xxx-xxx --async=false"),
44+
),
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
ctx := context.Background()
47+
model, err := parseInput(params.Printer, cmd, args)
48+
if err != nil {
49+
return err
50+
}
51+
52+
// Configure API client
53+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
54+
if err != nil {
55+
return err
56+
}
57+
58+
// Get snapshot name for label
59+
snapshotLabel := model.SnapshotId
60+
snapshot, err := apiClient.GetSnapshot(ctx, model.ProjectId, model.SnapshotId).Execute()
61+
if err != nil {
62+
params.Printer.Debug(print.ErrorLevel, "get snapshot name: %v", err)
63+
} else if snapshot != nil && snapshot.Name != nil {
64+
snapshotLabel = *snapshot.Name
65+
}
66+
67+
if !model.AssumeYes {
68+
prompt := fmt.Sprintf("Are you sure you want to delete snapshot %q? (This cannot be undone)", snapshotLabel)
69+
err = params.Printer.PromptForConfirmation(prompt)
70+
if err != nil {
71+
return err
72+
}
73+
}
74+
75+
// Call API
76+
req := buildRequest(ctx, model, apiClient)
77+
err = req.Execute()
78+
if err != nil {
79+
return fmt.Errorf("delete snapshot: %w", err)
80+
}
81+
82+
// Wait for async operation, if async mode not enabled
83+
if !model.Async {
84+
s := spinner.New(params.Printer)
85+
s.Start("Deleting snapshot")
86+
_, err = wait.DeleteSnapshotWaitHandler(ctx, apiClient, model.ProjectId, model.SnapshotId).WaitWithContext(ctx)
87+
if err != nil {
88+
return fmt.Errorf("wait for snapshot deletion: %w", err)
89+
}
90+
s.Stop()
91+
}
92+
93+
if model.Async {
94+
params.Printer.Info("Triggered deletion of snapshot %q\n", snapshotLabel)
95+
} else {
96+
params.Printer.Info("Deleted snapshot %q\n", snapshotLabel)
97+
}
98+
return nil
99+
},
100+
}
101+
return cmd
102+
}
103+
104+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
105+
snapshotId := inputArgs[0]
106+
107+
globalFlags := globalflags.Parse(p, cmd)
108+
if globalFlags.ProjectId == "" {
109+
return nil, &errors.ProjectIdError{}
110+
}
111+
112+
model := inputModel{
113+
GlobalFlagModel: globalFlags,
114+
SnapshotId: snapshotId,
115+
}
116+
117+
if p.IsVerbosityDebug() {
118+
modelStr, err := print.BuildDebugStrFromInputModel(model)
119+
if err != nil {
120+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
121+
} else {
122+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
123+
}
124+
}
125+
126+
return &model, nil
127+
}
128+
129+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteSnapshotRequest {
130+
return apiClient.DeleteSnapshot(ctx, model.ProjectId, model.SnapshotId)
131+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package delete
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+
type testCtxKey struct{}
18+
19+
var (
20+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
21+
testClient = &iaas.APIClient{}
22+
testProjectId = uuid.NewString()
23+
testSnapshotId = uuid.NewString()
24+
)
25+
26+
func fixtureArgValues(mods ...func(argValues []string)) []string {
27+
argValues := []string{
28+
testSnapshotId,
29+
}
30+
for _, mod := range mods {
31+
mod(argValues)
32+
}
33+
return argValues
34+
}
35+
36+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
37+
flagValues := map[string]string{
38+
globalflags.ProjectIdFlag: testProjectId,
39+
}
40+
for _, mod := range mods {
41+
mod(flagValues)
42+
}
43+
return flagValues
44+
}
45+
46+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
47+
model := &inputModel{
48+
GlobalFlagModel: &globalflags.GlobalFlagModel{
49+
ProjectId: testProjectId,
50+
Verbosity: globalflags.VerbosityDefault,
51+
},
52+
SnapshotId: testSnapshotId,
53+
}
54+
for _, mod := range mods {
55+
mod(model)
56+
}
57+
return model
58+
}
59+
60+
func fixtureRequest(mods ...func(request *iaas.ApiDeleteSnapshotRequest)) iaas.ApiDeleteSnapshotRequest {
61+
request := testClient.DeleteSnapshot(testCtx, testProjectId, testSnapshotId)
62+
for _, mod := range mods {
63+
mod(&request)
64+
}
65+
return request
66+
}
67+
68+
func TestParseInput(t *testing.T) {
69+
tests := []struct {
70+
description string
71+
argValues []string
72+
flagValues map[string]string
73+
isValid bool
74+
expectedModel *inputModel
75+
}{
76+
{
77+
description: "base",
78+
argValues: fixtureArgValues(),
79+
flagValues: fixtureFlagValues(),
80+
isValid: true,
81+
expectedModel: fixtureInputModel(),
82+
},
83+
{
84+
description: "no values",
85+
argValues: []string{},
86+
flagValues: map[string]string{},
87+
isValid: false,
88+
},
89+
{
90+
description: "no arg values",
91+
argValues: []string{},
92+
flagValues: fixtureFlagValues(),
93+
isValid: false,
94+
},
95+
{
96+
description: "no flag values",
97+
argValues: fixtureArgValues(),
98+
flagValues: map[string]string{},
99+
isValid: false,
100+
},
101+
{
102+
description: "project id missing",
103+
argValues: fixtureArgValues(),
104+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
105+
delete(flagValues, globalflags.ProjectIdFlag)
106+
}),
107+
isValid: false,
108+
},
109+
{
110+
description: "project id invalid",
111+
argValues: fixtureArgValues(),
112+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
113+
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
114+
}),
115+
isValid: false,
116+
},
117+
{
118+
description: "snapshot id invalid",
119+
argValues: []string{"invalid-uuid"},
120+
flagValues: fixtureFlagValues(),
121+
isValid: false,
122+
},
123+
}
124+
125+
for _, tt := range tests {
126+
t.Run(tt.description, func(t *testing.T) {
127+
p := print.NewPrinter()
128+
cmd := NewCmd(&params.CmdParams{Printer: p})
129+
err := globalflags.Configure(cmd.Flags())
130+
if err != nil {
131+
t.Fatalf("configure global flags: %v", err)
132+
}
133+
134+
for flag, value := range tt.flagValues {
135+
err := cmd.Flags().Set(flag, value)
136+
if err != nil {
137+
if !tt.isValid {
138+
return
139+
}
140+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
141+
}
142+
}
143+
144+
err = cmd.ValidateArgs(tt.argValues)
145+
if err != nil {
146+
if !tt.isValid {
147+
return
148+
}
149+
t.Fatalf("error validating args: %v", err)
150+
}
151+
152+
model, err := parseInput(p, cmd, tt.argValues)
153+
if err != nil {
154+
if !tt.isValid {
155+
return
156+
}
157+
t.Fatalf("error parsing input: %v", err)
158+
}
159+
160+
if !tt.isValid {
161+
t.Fatalf("did not fail on invalid input")
162+
}
163+
diff := cmp.Diff(model, tt.expectedModel)
164+
if diff != "" {
165+
t.Fatalf("Data does not match: %s", diff)
166+
}
167+
})
168+
}
169+
}
170+
171+
func TestBuildRequest(t *testing.T) {
172+
tests := []struct {
173+
description string
174+
model *inputModel
175+
expectedRequest iaas.ApiDeleteSnapshotRequest
176+
}{
177+
{
178+
description: "base",
179+
model: fixtureInputModel(),
180+
expectedRequest: fixtureRequest(),
181+
},
182+
}
183+
184+
for _, tt := range tests {
185+
t.Run(tt.description, func(t *testing.T) {
186+
request := buildRequest(testCtx, tt.model, testClient)
187+
188+
diff := cmp.Diff(request, tt.expectedRequest,
189+
cmp.AllowUnexported(tt.expectedRequest),
190+
cmpopts.EquateComparable(testCtx),
191+
)
192+
if diff != "" {
193+
t.Fatalf("Data does not match: %s", diff)
194+
}
195+
})
196+
}
197+
}

0 commit comments

Comments
 (0)