Skip to content

Commit 879b341

Browse files
committed
feat(server): implement detach security group command
1 parent c45dcbc commit 879b341

File tree

2 files changed

+339
-6
lines changed

2 files changed

+339
-6
lines changed
Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,121 @@
11
package detach
22

33
import (
4-
"github.com/spf13/cobra"
4+
"context"
5+
"fmt"
6+
57
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
11+
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
17+
iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
19+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
20+
)
21+
22+
const (
23+
serverIdArg = "SERVER_ID"
24+
25+
securityGroupIdFlag = "security-group-id"
626
)
727

28+
type inputModel struct {
29+
*globalflags.GlobalFlagModel
30+
ServerId string
31+
SecurityGroupId string
32+
}
33+
834
func NewCmd(params *types.CmdParams) *cobra.Command {
935
cmd := &cobra.Command{
10-
Use: "detach",
11-
Short: "Detach a security group from a server",
12-
Long: "Detach a security group from a server.",
13-
Run: func(cmd *cobra.Command, args []string) {
14-
params.Printer.Info("Detaching security group from server...")
36+
Use: fmt.Sprintf("detach %s", serverIdArg),
37+
Short: "Detaches a security group from a server",
38+
Long: "Detaches a security group from a server.",
39+
Args: args.SingleArg(serverIdArg, utils.ValidateUUID),
40+
Example: examples.Build(
41+
examples.NewExample(
42+
`Detach a security group with ID "xxx" from a server with ID "yyy"`,
43+
`$ stackit server security-group detach yyy --security-group-id xxx`,
44+
),
45+
),
46+
RunE: func(cmd *cobra.Command, args []string) error {
47+
ctx := context.Background()
48+
model, err := parseInput(params.Printer, cmd, args)
49+
if err != nil {
50+
return err
51+
}
52+
53+
// Configure API client
54+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
55+
if err != nil {
56+
return err
57+
}
58+
59+
serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.Region, model.ServerId)
60+
if err != nil {
61+
params.Printer.Debug(print.ErrorLevel, "get server name: %v", err)
62+
serverLabel = model.ServerId
63+
} else if serverLabel == "" {
64+
serverLabel = model.ServerId
65+
}
66+
67+
securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.Region, model.SecurityGroupId)
68+
if err != nil {
69+
params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err)
70+
securityGroupLabel = model.SecurityGroupId
71+
}
72+
73+
prompt := fmt.Sprintf("Are you sure you want to detach security group %q from server %q?", securityGroupLabel, serverLabel)
74+
err = params.Printer.PromptForConfirmation(prompt)
75+
if err != nil {
76+
return err
77+
}
78+
79+
// Call API
80+
req := buildRequest(ctx, model, apiClient)
81+
if err := req.Execute(); err != nil {
82+
return fmt.Errorf("detach security group from server: %w", err)
83+
}
84+
85+
params.Printer.Info("Detached security group %q from server %q\n", securityGroupLabel, serverLabel)
86+
87+
return nil
1588
},
1689
}
90+
configureFlags(cmd)
1791
return cmd
1892
}
93+
94+
func configureFlags(cmd *cobra.Command) {
95+
cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, "Security Group ID")
96+
97+
err := flags.MarkFlagsRequired(cmd, securityGroupIdFlag)
98+
cobra.CheckErr(err)
99+
}
100+
101+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
102+
serverId := inputArgs[0]
103+
globalFlags := globalflags.Parse(p, cmd)
104+
if globalFlags.ProjectId == "" {
105+
return nil, &cliErr.ProjectIdError{}
106+
}
107+
108+
model := inputModel{
109+
GlobalFlagModel: globalFlags,
110+
ServerId: serverId,
111+
SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag),
112+
}
113+
114+
p.DebugInputModel(model)
115+
return &model, nil
116+
}
117+
118+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRemoveSecurityGroupFromServerRequest {
119+
req := apiClient.RemoveSecurityGroupFromServer(ctx, model.ProjectId, model.Region, model.ServerId, model.SecurityGroupId)
120+
return req
121+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,231 @@
11
package detach
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
8+
9+
"github.com/google/go-cmp/cmp"
10+
"github.com/google/go-cmp/cmp/cmpopts"
11+
"github.com/google/uuid"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
14+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
15+
)
16+
17+
const (
18+
testRegion = "eu01"
19+
)
20+
21+
type testCtxKey struct{}
22+
23+
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
24+
var testClient = &iaas.APIClient{}
25+
var testProjectId = uuid.NewString()
26+
var testServerId = uuid.NewString()
27+
var testSecurityGroupId = uuid.NewString()
28+
29+
func fixtureArgValues(mods ...func(argValues []string)) []string {
30+
argValues := []string{
31+
testServerId,
32+
}
33+
for _, mod := range mods {
34+
mod(argValues)
35+
}
36+
return argValues
37+
}
38+
39+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
40+
flagValues := map[string]string{
41+
globalflags.ProjectIdFlag: testProjectId,
42+
globalflags.RegionFlag: testRegion,
43+
44+
securityGroupIdFlag: testSecurityGroupId,
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+
Verbosity: globalflags.VerbosityDefault,
56+
ProjectId: testProjectId,
57+
Region: testRegion,
58+
},
59+
ServerId: testServerId,
60+
SecurityGroupId: testSecurityGroupId,
61+
}
62+
for _, mod := range mods {
63+
mod(model)
64+
}
65+
return model
66+
}
67+
68+
func fixtureRequest(mods ...func(request *iaas.ApiRemoveSecurityGroupFromServerRequest)) iaas.ApiRemoveSecurityGroupFromServerRequest {
69+
request := testClient.RemoveSecurityGroupFromServer(testCtx, testProjectId, testRegion, testServerId, testSecurityGroupId)
70+
for _, mod := range mods {
71+
mod(&request)
72+
}
73+
return request
74+
}
75+
76+
func TestParseInput(t *testing.T) {
77+
tests := []struct {
78+
description string
79+
argValues []string
80+
flagValues map[string]string
81+
isValid bool
82+
expectedModel *inputModel
83+
}{
84+
{
85+
description: "base",
86+
argValues: fixtureArgValues(),
87+
flagValues: fixtureFlagValues(),
88+
isValid: true,
89+
expectedModel: fixtureInputModel(),
90+
},
91+
{
92+
description: "no values",
93+
flagValues: map[string]string{},
94+
isValid: false,
95+
},
96+
{
97+
description: "project id missing",
98+
argValues: fixtureArgValues(),
99+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
100+
delete(flagValues, globalflags.ProjectIdFlag)
101+
}),
102+
isValid: false,
103+
},
104+
{
105+
description: "project id invalid 1",
106+
argValues: fixtureArgValues(),
107+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
108+
flagValues[globalflags.ProjectIdFlag] = ""
109+
}),
110+
isValid: false,
111+
},
112+
{
113+
description: "project id invalid 2",
114+
argValues: fixtureArgValues(),
115+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
116+
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
117+
}),
118+
isValid: false,
119+
},
120+
{
121+
description: "security group id missing",
122+
argValues: fixtureArgValues(),
123+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
124+
delete(flagValues, securityGroupIdFlag)
125+
}),
126+
isValid: false,
127+
},
128+
{
129+
description: "security group id invalid 1",
130+
argValues: fixtureArgValues(),
131+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
132+
flagValues[securityGroupIdFlag] = ""
133+
}),
134+
isValid: false,
135+
},
136+
{
137+
description: "security group id invalid 2",
138+
argValues: fixtureArgValues(),
139+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
140+
flagValues[securityGroupIdFlag] = "invalid-uuid"
141+
}),
142+
isValid: false,
143+
},
144+
{
145+
description: "server id argument missing",
146+
argValues: []string{},
147+
isValid: false,
148+
},
149+
}
150+
151+
for _, tt := range tests {
152+
t.Run(tt.description, func(t *testing.T) {
153+
p := print.NewPrinter()
154+
cmd := NewCmd(&types.CmdParams{Printer: p})
155+
err := globalflags.Configure(cmd.Flags())
156+
if err != nil {
157+
t.Fatalf("configure global flags: %v", err)
158+
}
159+
160+
for flag, value := range tt.flagValues {
161+
err := cmd.Flags().Set(flag, value)
162+
if err != nil {
163+
if !tt.isValid {
164+
return
165+
}
166+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
167+
}
168+
}
169+
170+
err = cmd.ValidateArgs(tt.argValues)
171+
if err != nil {
172+
if !tt.isValid {
173+
return
174+
}
175+
t.Fatalf("error parsing args: %v", err)
176+
}
177+
178+
err = cmd.ValidateRequiredFlags()
179+
if err != nil {
180+
if !tt.isValid {
181+
return
182+
}
183+
t.Fatalf("error validating flags: %v", err)
184+
}
185+
186+
model, err := parseInput(p, cmd, tt.argValues)
187+
if err != nil {
188+
if !tt.isValid {
189+
return
190+
}
191+
t.Fatalf("error parsing input: %v", err)
192+
}
193+
194+
if !tt.isValid {
195+
t.Fatalf("did not fail on invalid input")
196+
}
197+
diff := cmp.Diff(model, tt.expectedModel)
198+
if diff != "" {
199+
t.Fatalf("Data does not match: %s", diff)
200+
}
201+
})
202+
}
203+
}
204+
205+
func TestBuildRequest(t *testing.T) {
206+
tests := []struct {
207+
description string
208+
model *inputModel
209+
expectedRequest iaas.ApiRemoveSecurityGroupFromServerRequest
210+
}{
211+
{
212+
description: "base",
213+
model: fixtureInputModel(),
214+
expectedRequest: fixtureRequest(),
215+
},
216+
}
217+
218+
for _, tt := range tests {
219+
t.Run(tt.description, func(t *testing.T) {
220+
request := buildRequest(testCtx, tt.model, testClient)
221+
222+
diff := cmp.Diff(request, tt.expectedRequest,
223+
cmp.AllowUnexported(tt.expectedRequest),
224+
cmpopts.EquateComparable(testCtx),
225+
)
226+
if diff != "" {
227+
t.Fatalf("Data does not match: %s", diff)
228+
}
229+
})
230+
}
231+
}

0 commit comments

Comments
 (0)