Skip to content

Commit a742d27

Browse files
committed
feat: implemented delete command
1 parent 0258829 commit a742d27

File tree

5 files changed

+361
-1
lines changed

5 files changed

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

internal/cmd/beta/image/image.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package security_group
22

33
import (
44
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/image/create"
5-
list "github.com/stackitcloud/stackit-cli/internal/cmd/beta/image/image"
5+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/image/list"
6+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/image/delete"
67
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
78
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
89

@@ -27,5 +28,6 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) {
2728
cmd.AddCommand(
2829
create.NewCmd(p),
2930
list.NewCmd(p),
31+
delete.NewCmd(p),
3032
)
3133
}

internal/pkg/services/iaas/utils/utils.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type IaaSClient interface {
1717
GetNetworkAreaExecute(ctx context.Context, organizationId, areaId string) (*iaas.NetworkArea, error)
1818
ListNetworkAreaProjectsExecute(ctx context.Context, organizationId, areaId string) (*iaas.ProjectListResponse, error)
1919
GetNetworkAreaRangeExecute(ctx context.Context, organizationId, areaId, networkRangeId string) (*iaas.NetworkRange, error)
20+
GetImageExecute(ctx context.Context, projectId string, imageId string) (*iaas.Image, error)
2021
}
2122

2223
func GetSecurityGroupRuleName(ctx context.Context, apiClient IaaSClient, projectId, securityGroupRuleId, securityGroupId string) (string, error) {
@@ -117,3 +118,14 @@ func GetNetworkRangeFromAPIResponse(prefix string, networkRanges *[]iaas.Network
117118
}
118119
return iaas.NetworkRange{}, fmt.Errorf("new network range not found in API response")
119120
}
121+
122+
func GetImageName(ctx context.Context, apiClient IaaSClient, projectId, imageId string) (string, error) {
123+
resp, err := apiClient.GetImageExecute(ctx, projectId, imageId)
124+
if err != nil {
125+
return "", fmt.Errorf("get image: %w", err)
126+
}
127+
if resp.Name == nil {
128+
return "", nil
129+
}
130+
return *resp.Name, nil
131+
}

0 commit comments

Comments
 (0)