Skip to content

Commit 8958c51

Browse files
committed
feature: completed delete command
1 parent 7888897 commit 8958c51

File tree

2 files changed

+284
-8
lines changed

2 files changed

+284
-8
lines changed
Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,135 @@
11
package delete
22

33
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
48
"github.com/spf13/cobra"
5-
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
610
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
713
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
15+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
816
)
917

18+
type inputModel struct {
19+
*globalflags.GlobalFlagModel
20+
Id string
21+
}
22+
1023
func NewCmd(p *print.Printer) *cobra.Command {
1124
cmd := &cobra.Command{
1225
Use: "delete",
13-
Short: "delete security groups",
14-
Long: "delete security groups",
15-
Args: args.NoArgs,
26+
Short: "delete a security group",
27+
Long: "delete a security group by its internal id",
28+
Args: cobra.ExactArgs(1),
1629
Example: examples.Build(
17-
examples.NewExample(`example 1`, `foo bar baz`),
18-
examples.NewExample(`example 2`, `foo bar baz`),
30+
examples.NewExample(`delete a named group`, `$ stackit beta security-group delete 43ad419a-c68b-4911-87cd-e05752ac1e31`),
1931
),
2032
RunE: func(cmd *cobra.Command, args []string) error {
2133
return executeDelete(cmd, p, args)
2234
},
2335
}
24-
cmd.Flags().String("dummy", "foo", "fooify")
36+
2537
return cmd
2638
}
2739

2840
func executeDelete(cmd *cobra.Command, p *print.Printer, args []string) error {
29-
p.Info("executing create command")
41+
p.Info("executing delete command")
42+
ctx := context.Background()
43+
model, err := parseInput(p, cmd, args)
44+
if err != nil {
45+
return err
46+
}
47+
48+
// Configure API client
49+
apiClient, err := client.ConfigureClient(p)
50+
if err != nil {
51+
return err
52+
}
53+
54+
if !model.AssumeYes {
55+
prompt := fmt.Sprintf("Are you sure you want to delete the security group %q?", model.Id)
56+
err = p.PromptForConfirmation(prompt)
57+
if err != nil {
58+
return err
59+
}
60+
}
61+
62+
// Call API
63+
request := buildRequest(ctx, model, apiClient)
64+
65+
operationState := "Enabled"
66+
if model.Async {
67+
operationState = "Triggered security group deletion"
68+
}
69+
p.Info("%s security group %q for %q\n", operationState, model.Id, model.ProjectId)
70+
71+
if err := request.Execute(); err != nil {
72+
return fmt.Errorf("delete security group: %w", err)
73+
}
74+
3075
return nil
3176
}
77+
78+
func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) {
79+
globalFlags := globalflags.Parse(p, cmd)
80+
if globalFlags.ProjectId == "" {
81+
return nil, &errors.ProjectIdError{}
82+
}
83+
84+
if len(args) != 1 {
85+
return nil,&errors.ArgValidationError{}
86+
}
87+
88+
name := flags.FlagToStringValue(p, cmd, "name")
89+
if len(name) >= 64 {
90+
return nil, &errors.ArgValidationError{
91+
Arg: "invalid name",
92+
Details: "name exceeds 63 characters in length",
93+
}
94+
}
95+
96+
labels := make(map[string]any)
97+
for _, label := range flags.FlagToStringSliceValue(p, cmd, "labels") {
98+
parts := strings.Split(label, "=")
99+
if len(parts) != 2 {
100+
return nil, &errors.ArgValidationError{
101+
Arg: "labels",
102+
Details: "invalid label declaration. Must be in the form <key>=<value>",
103+
}
104+
}
105+
labels[parts[0]] = parts[1]
106+
107+
}
108+
description := flags.FlagToStringValue(p, cmd, "description")
109+
if len(description) >= 128 {
110+
return nil, &errors.ArgValidationError{
111+
Arg: "invalid description",
112+
Details: "description exceeds 127 characters in length",
113+
}
114+
}
115+
model := inputModel{
116+
GlobalFlagModel: globalFlags,
117+
Id: args[0],
118+
}
119+
120+
if p.IsVerbosityDebug() {
121+
modelStr, err := print.BuildDebugStrFromInputModel(model)
122+
if err != nil {
123+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
124+
} else {
125+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
126+
}
127+
}
128+
129+
return &model, nil
130+
}
131+
132+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteSecurityGroupRequest {
133+
request := apiClient.DeleteSecurityGroup(ctx, model.ProjectId, model.Id)
134+
return request
135+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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/spf13/cobra"
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+
testGroupId = uuid.NewString()
26+
)
27+
28+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
29+
flagValues := map[string]string{
30+
projectIdFlag: testProjectId,
31+
}
32+
for _, mod := range mods {
33+
mod(flagValues)
34+
}
35+
return flagValues
36+
}
37+
38+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
39+
model := &inputModel{
40+
GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault},
41+
Id: testGroupId,
42+
}
43+
for _, mod := range mods {
44+
mod(model)
45+
}
46+
return model
47+
}
48+
49+
func fixtureRequest(mods ...func(request *iaas.ApiDeleteSecurityGroupRequest)) iaas.ApiDeleteSecurityGroupRequest {
50+
request := testClient.DeleteSecurityGroup(testCtx, testProjectId, testGroupId)
51+
for _, mod := range mods {
52+
mod(&request)
53+
}
54+
return request
55+
}
56+
57+
func TestParseInput(t *testing.T) {
58+
tests := []struct {
59+
description string
60+
flagValues map[string]string
61+
args []string
62+
isValid bool
63+
expectedModel *inputModel
64+
}{
65+
{
66+
description: "base",
67+
flagValues: fixtureFlagValues(),
68+
args: []string{testGroupId},
69+
isValid: true,
70+
expectedModel: fixtureInputModel(),
71+
},
72+
{
73+
description: "project id invalid 1",
74+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
75+
flagValues[projectIdFlag] = ""
76+
}),
77+
isValid: false,
78+
},
79+
{
80+
description: "project id invalid 2",
81+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
82+
flagValues[projectIdFlag] = "invalid-uuid"
83+
}),
84+
isValid: false,
85+
},
86+
{
87+
description: "no arguments",
88+
flagValues: fixtureFlagValues(),
89+
args: nil,
90+
isValid: false,
91+
},
92+
{
93+
description: "multiple arguments",
94+
flagValues: fixtureFlagValues(),
95+
args: []string{"foo","bar"},
96+
isValid: false,
97+
},
98+
}
99+
100+
for _, tt := range tests {
101+
t.Run(tt.description, func(t *testing.T) {
102+
cmd := &cobra.Command{}
103+
err := globalflags.Configure(cmd.Flags())
104+
if err != nil {
105+
t.Fatalf("configure global flags: %v", err)
106+
}
107+
cmd.SetArgs(tt.args)
108+
109+
for flag, value := range tt.flagValues {
110+
err := cmd.Flags().Set(flag, value)
111+
if err != nil {
112+
if !tt.isValid {
113+
return
114+
}
115+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
116+
}
117+
}
118+
119+
err = cmd.ValidateRequiredFlags()
120+
if err != nil {
121+
if !tt.isValid {
122+
return
123+
}
124+
t.Fatalf("error validating flags: %v", err)
125+
}
126+
127+
p := print.NewPrinter()
128+
model, err := parseInput(p, cmd, tt.args)
129+
if err != nil {
130+
if !tt.isValid {
131+
return
132+
}
133+
t.Fatalf("error parsing flags: %v", err)
134+
}
135+
136+
if !tt.isValid {
137+
t.Fatalf("did not fail on invalid input")
138+
}
139+
diff := cmp.Diff(model, tt.expectedModel)
140+
if diff != "" {
141+
t.Fatalf("Data does not match: %s", diff)
142+
}
143+
})
144+
}
145+
}
146+
147+
func TestBuildRequest(t *testing.T) {
148+
tests := []struct {
149+
description string
150+
model *inputModel
151+
expectedRequest iaas.ApiDeleteSecurityGroupRequest
152+
}{
153+
{
154+
description: "base",
155+
model: fixtureInputModel(),
156+
expectedRequest: fixtureRequest(),
157+
},
158+
}
159+
160+
for _, tt := range tests {
161+
t.Run(tt.description, func(t *testing.T) {
162+
request := buildRequest(testCtx, tt.model, testClient)
163+
diff := cmp.Diff(request, tt.expectedRequest,
164+
cmp.AllowUnexported(tt.expectedRequest),
165+
cmpopts.EquateComparable(testCtx),
166+
)
167+
if diff != "" {
168+
t.Fatalf("Data does not match: %s", diff)
169+
}
170+
})
171+
}
172+
}

0 commit comments

Comments
 (0)