Skip to content

Commit 9e84aed

Browse files
committed
feature: add list command
1 parent ee4ff8a commit 9e84aed

File tree

2 files changed

+296
-4
lines changed

2 files changed

+296
-4
lines changed
Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,112 @@
11
package list
22

33
import (
4+
"context"
5+
"fmt"
6+
47
"github.com/spf13/cobra"
58
"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/projectname"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
16+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
817
)
918

19+
type inputModel struct {
20+
*globalflags.GlobalFlagModel
21+
Labels string
22+
}
23+
1024
func NewCmd(p *print.Printer) *cobra.Command {
1125
cmd := &cobra.Command{
1226
Use: "list",
1327
Short: "list security groups",
1428
Long: "list security groups",
1529
Args: args.NoArgs,
1630
Example: examples.Build(
17-
examples.NewExample(`example 1`, `foo bar baz`),
18-
examples.NewExample(`example 2`, `foo bar baz`),
31+
examples.NewExample(`list all groups`, `$ stackit beta security-group list`),
32+
examples.NewExample(`list groups with labels`, `$ stackit beta security-group list --labels label1=value1,label2=value2`),
1933
),
2034
RunE: func(cmd *cobra.Command, args []string) error {
2135
return executeList(cmd, p, args)
2236
},
2337
}
24-
cmd.Flags().String("dummy", "foo", "fooify")
38+
39+
configureFlags(cmd)
2540
return cmd
2641
}
2742

28-
func executeList(cmd *cobra.Command, p *print.Printer, args []string) error {
43+
func configureFlags(cmd *cobra.Command) {
44+
cmd.Flags().String("labels", "", "a list of labels in the form <key>=<value>")
45+
}
46+
47+
func executeList(cmd *cobra.Command, p *print.Printer, _ []string) error {
2948
p.Info("executing list command")
49+
ctx := context.Background()
50+
model, err := parseInput(p, cmd)
51+
if err != nil {
52+
return err
53+
}
54+
55+
// Configure API client
56+
apiClient, err := client.ConfigureClient(p)
57+
if err != nil {
58+
return err
59+
}
60+
61+
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
62+
if err != nil {
63+
p.Debug(print.ErrorLevel, "get project name: %v", err)
64+
projectLabel = model.ProjectId
65+
}
66+
67+
// Call API
68+
req := buildRequest(ctx, model, apiClient)
69+
_, err = req.Execute()
70+
if err != nil {
71+
return fmt.Errorf("list security group: %w", err)
72+
}
73+
74+
operationState := "Enabled"
75+
if model.Async {
76+
operationState = "Triggered enablement of"
77+
}
78+
p.Info("%s security group for %q\n", operationState, projectLabel)
3079
return nil
3180
}
81+
82+
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
83+
globalFlags := globalflags.Parse(p, cmd)
84+
if globalFlags.ProjectId == "" {
85+
return nil, &errors.ProjectIdError{}
86+
}
87+
88+
model := inputModel{
89+
GlobalFlagModel: globalFlags,
90+
91+
Labels: flags.FlagToStringValue(p, cmd, "labels"),
92+
}
93+
94+
if p.IsVerbosityDebug() {
95+
modelStr, err := print.BuildDebugStrFromInputModel(model)
96+
if err != nil {
97+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
98+
} else {
99+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
100+
}
101+
}
102+
103+
return &model, nil
104+
}
105+
106+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListSecurityGroupsRequest {
107+
request := apiClient.ListSecurityGroups(ctx, model.ProjectId)
108+
request = request.LabelSelector(model.Labels)
109+
110+
return request
111+
112+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package list
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+
testLabels = "fooKey=fooValue,barKey=barValue,bazKey=bazValue"
26+
testStateful = true
27+
)
28+
29+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
30+
flagValues := map[string]string{
31+
projectIdFlag: testProjectId,
32+
"labels": testLabels,
33+
}
34+
for _, mod := range mods {
35+
mod(flagValues)
36+
}
37+
return flagValues
38+
}
39+
40+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
41+
model := &inputModel{
42+
GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault},
43+
Labels: testLabels,
44+
}
45+
for _, mod := range mods {
46+
mod(model)
47+
}
48+
return model
49+
}
50+
51+
func fixtureRequest(mods ...func(request *iaas.ApiListSecurityGroupsRequest)) iaas.ApiListSecurityGroupsRequest {
52+
request := testClient.ListSecurityGroups(testCtx, testProjectId)
53+
request = request.LabelSelector(testLabels)
54+
for _, mod := range mods {
55+
mod(&request)
56+
}
57+
return request
58+
}
59+
60+
func TestParseInput(t *testing.T) {
61+
tests := []struct {
62+
description string
63+
flagValues map[string]string
64+
isValid bool
65+
expectedModel *inputModel
66+
}{
67+
{
68+
description: "base",
69+
flagValues: fixtureFlagValues(),
70+
isValid: true,
71+
expectedModel: fixtureInputModel(),
72+
},
73+
{
74+
description: "no values",
75+
flagValues: map[string]string{},
76+
isValid: false,
77+
},
78+
{
79+
description: "project id missing",
80+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
81+
delete(flagValues, projectIdFlag)
82+
}),
83+
isValid: false,
84+
},
85+
{
86+
description: "project id invalid 1",
87+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
88+
flagValues[projectIdFlag] = ""
89+
}),
90+
isValid: false,
91+
},
92+
{
93+
description: "project id invalid 2",
94+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
95+
flagValues[projectIdFlag] = "invalid-uuid"
96+
}),
97+
isValid: false,
98+
},
99+
{
100+
description: "no labels",
101+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
102+
delete(flagValues, "labels")
103+
}),
104+
isValid: true,
105+
expectedModel: fixtureInputModel(func(model *inputModel) {
106+
model.Labels = ""
107+
}),
108+
},
109+
{
110+
description: "single label",
111+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
112+
flagValues["labels"] = "foo=bar"
113+
}),
114+
isValid: true,
115+
expectedModel: fixtureInputModel(func(model *inputModel) {
116+
model.Labels = "foo=bar"
117+
}),
118+
},
119+
}
120+
121+
for _, tt := range tests {
122+
t.Run(tt.description, func(t *testing.T) {
123+
cmd := &cobra.Command{}
124+
configureFlags(cmd)
125+
err := globalflags.Configure(cmd.Flags())
126+
if err != nil {
127+
t.Fatalf("configure global flags: %v", err)
128+
}
129+
130+
for flag, value := range tt.flagValues {
131+
err := cmd.Flags().Set(flag, value)
132+
if err != nil {
133+
if !tt.isValid {
134+
return
135+
}
136+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
137+
}
138+
}
139+
140+
err = cmd.ValidateRequiredFlags()
141+
if err != nil {
142+
if !tt.isValid {
143+
return
144+
}
145+
t.Fatalf("error validating flags: %v", err)
146+
}
147+
148+
p := print.NewPrinter()
149+
model, err := parseInput(p, cmd)
150+
if err != nil {
151+
if !tt.isValid {
152+
return
153+
}
154+
t.Fatalf("error parsing flags: %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.ApiListSecurityGroupsRequest
173+
}{
174+
{
175+
description: "base",
176+
model: fixtureInputModel(),
177+
expectedRequest: fixtureRequest(),
178+
},
179+
{
180+
description: "no labels",
181+
model: fixtureInputModel(func(model *inputModel) {
182+
model.Labels = ""
183+
}),
184+
expectedRequest: fixtureRequest(func(request *iaas.ApiListSecurityGroupsRequest) {
185+
*request = request.LabelSelector("")
186+
}),
187+
},
188+
{
189+
description: "single label",
190+
model: fixtureInputModel(func(model *inputModel) {
191+
model.Labels = "foo=bar"
192+
}),
193+
expectedRequest: fixtureRequest(func(request *iaas.ApiListSecurityGroupsRequest) {
194+
*request = request.LabelSelector("foo=bar")
195+
}),
196+
},
197+
}
198+
199+
for _, tt := range tests {
200+
t.Run(tt.description, func(t *testing.T) {
201+
request := buildRequest(testCtx, tt.model, testClient)
202+
diff := cmp.Diff(request, tt.expectedRequest,
203+
cmp.AllowUnexported(tt.expectedRequest),
204+
cmpopts.EquateComparable(testCtx),
205+
)
206+
if diff != "" {
207+
t.Fatalf("Data does not match: %s", diff)
208+
}
209+
})
210+
}
211+
}

0 commit comments

Comments
 (0)