Skip to content

Commit a6f5129

Browse files
committed
feature: implemented describe command
1 parent 18cf848 commit a6f5129

File tree

2 files changed

+330
-4
lines changed

2 files changed

+330
-4
lines changed
Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,171 @@
11
package describe
22

33
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/goccy/go-yaml"
410
"github.com/spf13/cobra"
511
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
613
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
715
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
19+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
820
)
921

22+
type inputModel struct {
23+
*globalflags.GlobalFlagModel
24+
SecurityGroupId string
25+
}
26+
27+
const argNameGroupId = "argGroupId"
28+
1029
func NewCmd(p *print.Printer) *cobra.Command {
1130
cmd := &cobra.Command{
1231
Use: "describe",
1332
Short: "describe security groups",
1433
Long: "describe security groups",
15-
Args: args.NoArgs,
34+
Args: args.SingleArg(argNameGroupId, utils.ValidateUUID),
1635
Example: examples.Build(
17-
examples.NewExample(`example 1`, `foo bar baz`),
18-
examples.NewExample(`example 2`, `foo bar baz`),
36+
examples.NewExample(`describe an existing group`, `$ stackit beta security-group describe 9e9c44fe-eb9a-4d45-bf08-365e961845d1`),
1937
),
2038
RunE: func(cmd *cobra.Command, args []string) error {
2139
return executeDescribe(cmd, p, args)
2240
},
2341
}
24-
cmd.Flags().String("dummy", "foo", "fooify")
42+
configureFlags(cmd)
43+
2544
return cmd
2645
}
2746

2847
func executeDescribe(cmd *cobra.Command, p *print.Printer, args []string) error {
2948
p.Info("executing describe command")
49+
ctx := context.Background()
50+
model, err := parseInput(p, cmd, args)
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+
// Call API
62+
request := buildRequest(ctx, model, apiClient)
63+
64+
p.Info("security group %q for %q\n", model.SecurityGroupId, model.ProjectId)
65+
66+
group, err := request.Execute()
67+
if err != nil {
68+
return fmt.Errorf("get security group: %w", err)
69+
}
70+
if err := outputResult(p, model, group); err != nil {
71+
return err
72+
}
73+
3074
return nil
3175
}
76+
77+
func configureFlags(cmd *cobra.Command) {
78+
globalflags.Configure(cmd.Flags())
79+
}
80+
81+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetSecurityGroupRequest {
82+
request := apiClient.GetSecurityGroup(ctx, model.ProjectId, model.SecurityGroupId)
83+
return request
84+
85+
}
86+
87+
func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) {
88+
globalFlags := globalflags.Parse(p, cmd)
89+
if globalFlags.ProjectId == "" {
90+
return nil, &errors.ProjectIdError{}
91+
}
92+
if err := cmd.ValidateArgs(args); err != nil {
93+
return nil, &errors.ArgValidationError{
94+
Arg: argNameGroupId,
95+
Details: fmt.Sprintf("argument validation failed: %v", err),
96+
}
97+
}
98+
99+
model := inputModel{
100+
GlobalFlagModel: globalFlags,
101+
SecurityGroupId: args[0],
102+
}
103+
104+
if p.IsVerbosityDebug() {
105+
modelStr, err := print.BuildDebugStrFromInputModel(model)
106+
if err != nil {
107+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
108+
} else {
109+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
110+
}
111+
}
112+
113+
return &model, nil
114+
}
115+
116+
func outputResult(p *print.Printer, model *inputModel, resp *iaas.SecurityGroup) error {
117+
switch model.OutputFormat {
118+
case print.JSONOutputFormat:
119+
details, err := json.MarshalIndent(resp, "", " ")
120+
if err != nil {
121+
return fmt.Errorf("marshal security group: %w", err)
122+
}
123+
p.Outputln(string(details))
124+
125+
return nil
126+
case print.YAMLOutputFormat:
127+
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true))
128+
if err != nil {
129+
return fmt.Errorf("marshal security group: %w", err)
130+
}
131+
p.Outputln(string(details))
132+
133+
return nil
134+
default:
135+
table := tables.NewTable()
136+
if id := resp.Id; id != nil {
137+
table.AddRow("ID", *id)
138+
}
139+
table.AddSeparator()
140+
141+
if name := resp.Name; name != nil {
142+
table.AddRow("NAME", name)
143+
table.AddSeparator()
144+
}
145+
146+
if description := resp.Description; description != nil {
147+
table.AddRow("DESCRIPTION", description)
148+
table.AddSeparator()
149+
}
150+
151+
if labels := resp.Labels; labels != nil {
152+
var builder strings.Builder
153+
for k, v := range *labels {
154+
builder.WriteString(fmt.Sprintf("%s=%s ", k, v))
155+
}
156+
table.AddRow("LABELS", builder.String())
157+
table.AddSeparator()
158+
}
159+
160+
if stateful := resp.Stateful; stateful != nil {
161+
table.AddRow("STATEFUL", stateful)
162+
table.AddSeparator()
163+
}
164+
165+
if err := table.Display(p); err != nil {
166+
return fmt.Errorf("render table: %w", err)
167+
}
168+
169+
return nil
170+
}
171+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package describe
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+
testSecurityGroupId = []string{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+
SecurityGroupId: testSecurityGroupId[0],
41+
}
42+
for _, mod := range mods {
43+
mod(model)
44+
}
45+
return model
46+
}
47+
48+
func fixtureRequest(mods ...func(request *iaas.ApiGetSecurityGroupRequest)) iaas.ApiGetSecurityGroupRequest {
49+
request := testClient.GetSecurityGroup(testCtx, testProjectId, testSecurityGroupId[0])
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+
isValid bool
61+
args []string
62+
expectedModel *inputModel
63+
}{
64+
{
65+
description: "base",
66+
flagValues: fixtureFlagValues(),
67+
expectedModel: fixtureInputModel(),
68+
args: testSecurityGroupId,
69+
isValid: true,
70+
},
71+
{
72+
description: "no values",
73+
flagValues: map[string]string{},
74+
args: testSecurityGroupId,
75+
isValid: false,
76+
},
77+
{
78+
description: "project id missing",
79+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
80+
delete(flagValues, projectIdFlag)
81+
}),
82+
args: testSecurityGroupId,
83+
isValid: false,
84+
},
85+
{
86+
description: "project id invalid 1",
87+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
88+
flagValues[projectIdFlag] = ""
89+
}),
90+
args: testSecurityGroupId,
91+
isValid: false,
92+
},
93+
{
94+
description: "project id invalid 2",
95+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
96+
flagValues[projectIdFlag] = "invalid-uuid"
97+
}),
98+
args: testSecurityGroupId,
99+
isValid: false,
100+
},
101+
{
102+
description: "no group id passed",
103+
flagValues: fixtureFlagValues(),
104+
args: nil,
105+
isValid: false,
106+
},
107+
{
108+
description: "multiple group ids passed",
109+
flagValues: fixtureFlagValues(),
110+
args: []string{uuid.NewString(), uuid.NewString()},
111+
isValid: false,
112+
},
113+
{
114+
description: "invalid group id passed",
115+
flagValues: fixtureFlagValues(),
116+
args: []string{"foobar"},
117+
},
118+
}
119+
120+
for _, tt := range tests {
121+
t.Run(tt.description, func(t *testing.T) {
122+
p := print.NewPrinter()
123+
cmd := NewCmd(p)
124+
125+
for flag, value := range tt.flagValues {
126+
err := cmd.Flags().Set(flag, value)
127+
if err != nil {
128+
if !tt.isValid {
129+
return
130+
}
131+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
132+
}
133+
}
134+
135+
if err := cmd.ValidateRequiredFlags(); err != nil {
136+
if !tt.isValid {
137+
return
138+
}
139+
t.Fatalf("error validating flags: %v", err)
140+
}
141+
142+
model, err := parseInput(p, cmd, tt.args)
143+
if err != nil {
144+
if !tt.isValid {
145+
return
146+
}
147+
t.Fatalf("error parsing flags: %v", err)
148+
}
149+
150+
if !tt.isValid {
151+
t.Fatalf("did not fail on invalid input")
152+
}
153+
diff := cmp.Diff(model, tt.expectedModel)
154+
if diff != "" {
155+
t.Fatalf("Data does not match: %s", diff)
156+
}
157+
})
158+
}
159+
}
160+
161+
func TestBuildRequest(t *testing.T) {
162+
tests := []struct {
163+
description string
164+
model *inputModel
165+
expectedRequest iaas.ApiGetSecurityGroupRequest
166+
}{
167+
{
168+
description: "base",
169+
model: fixtureInputModel(),
170+
expectedRequest: fixtureRequest(),
171+
},
172+
}
173+
174+
for _, tt := range tests {
175+
t.Run(tt.description, func(t *testing.T) {
176+
request := buildRequest(testCtx, tt.model, testClient)
177+
diff := cmp.Diff(request, tt.expectedRequest,
178+
cmp.AllowUnexported(tt.expectedRequest),
179+
cmpopts.EquateComparable(testCtx),
180+
)
181+
if diff != "" {
182+
t.Fatalf("Data does not match: %s", diff)
183+
}
184+
})
185+
}
186+
}

0 commit comments

Comments
 (0)