Skip to content

Commit e175b9d

Browse files
committed
feat(alb): add describe command
1 parent f6bd9ed commit e175b9d

File tree

2 files changed

+368
-0
lines changed

2 files changed

+368
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package describe
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
9+
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
16+
17+
"github.com/goccy/go-yaml"
18+
"github.com/spf13/cobra"
19+
"github.com/stackitcloud/stackit-sdk-go/services/alb"
20+
)
21+
22+
const (
23+
loadbalancerNameArg = "LOADBALANCER_NAME_ARG"
24+
)
25+
26+
type inputModel struct {
27+
*globalflags.GlobalFlagModel
28+
Name string
29+
}
30+
31+
func NewCmd(p *print.Printer) *cobra.Command {
32+
cmd := &cobra.Command{
33+
Use: fmt.Sprintf("describe %s", loadbalancerNameArg),
34+
Short: "Describes an application loadbalancer",
35+
Long: "Describes an application loadbalancer.",
36+
Args: args.SingleArg(loadbalancerNameArg, nil),
37+
Example: examples.Build(
38+
examples.NewExample(
39+
`Get details about an application loadbalancer with name "my-load-balancer"`,
40+
"$ stackit beta alb describe my-load-balancer",
41+
),
42+
),
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
ctx := context.Background()
45+
model, err := parseInput(p, cmd, args)
46+
if err != nil {
47+
return err
48+
}
49+
50+
// Configure API client
51+
apiClient, err := client.ConfigureClient(p)
52+
if err != nil {
53+
return err
54+
}
55+
56+
// Call API
57+
req := buildRequest(ctx, model, apiClient)
58+
resp, err := req.Execute()
59+
if err != nil {
60+
return fmt.Errorf("read loadbalancer: %w", err)
61+
}
62+
63+
if loadbalancer := resp; loadbalancer != nil {
64+
return outputResult(p, model.OutputFormat, *loadbalancer)
65+
}
66+
p.Outputln("No load balancer found.")
67+
return nil
68+
},
69+
}
70+
configureFlags(cmd)
71+
return cmd
72+
}
73+
74+
func configureFlags(cmd *cobra.Command) {
75+
}
76+
77+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
78+
globalFlags := globalflags.Parse(p, cmd)
79+
80+
loadbalancerName := inputArgs[0]
81+
model := inputModel{
82+
GlobalFlagModel: globalFlags,
83+
Name: loadbalancerName,
84+
}
85+
86+
if p.IsVerbosityDebug() {
87+
modelStr, err := print.BuildDebugStrFromInputModel(model)
88+
if err != nil {
89+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
90+
} else {
91+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
92+
}
93+
}
94+
95+
return &model, nil
96+
}
97+
98+
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetLoadBalancerRequest {
99+
return apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, model.Name)
100+
}
101+
102+
func outputResult(p *print.Printer, outputFormat string, response alb.LoadBalancer) error {
103+
switch outputFormat {
104+
case print.JSONOutputFormat:
105+
details, err := json.MarshalIndent(response, "", " ")
106+
107+
if err != nil {
108+
return fmt.Errorf("marshal loadbalancer: %w", err)
109+
}
110+
p.Outputln(string(details))
111+
112+
return nil
113+
case print.YAMLOutputFormat:
114+
details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
115+
116+
if err != nil {
117+
return fmt.Errorf("marshal loadbalancer: %w", err)
118+
}
119+
p.Outputln(string(details))
120+
121+
return nil
122+
default:
123+
table := tables.NewTable()
124+
table.AddRow("EXTERNAL ADDRESS", utils.PtrString(response.ExternalAddress))
125+
table.AddSeparator()
126+
var numErrors int
127+
if response.Errors != nil {
128+
numErrors = len(*response.Errors)
129+
}
130+
table.AddRow("NUMBER OF ERRORS", numErrors)
131+
table.AddSeparator()
132+
table.AddRow("PLAN ID", utils.PtrString(response.PlanId))
133+
table.AddSeparator()
134+
table.AddRow("REGION", utils.PtrString(response.Region))
135+
table.AddSeparator()
136+
table.AddRow("STATUS", utils.PtrString(response.Status))
137+
table.AddSeparator()
138+
table.AddRow("VERSION", utils.PtrString(response.Version))
139+
140+
p.Outputln(table.Render())
141+
}
142+
143+
return nil
144+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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/alb"
14+
)
15+
16+
type testCtxKey struct{}
17+
18+
var (
19+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "test")
20+
testProjectId = uuid.NewString()
21+
testRegion = "eu01"
22+
testClient = &alb.APIClient{}
23+
testLoadBalancerName = "my-test-loadbalancer"
24+
)
25+
26+
func fixtureArgValues(mods ...func(argVales []string)) []string {
27+
argVales := []string{
28+
testLoadBalancerName,
29+
}
30+
for _, m := range mods {
31+
m(argVales)
32+
}
33+
return argVales
34+
}
35+
36+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
37+
flagValues := map[string]string{
38+
globalflags.ProjectIdFlag: testProjectId,
39+
globalflags.RegionFlag: testRegion,
40+
}
41+
for _, m := range mods {
42+
m(flagValues)
43+
}
44+
return flagValues
45+
}
46+
47+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
48+
model := &inputModel{
49+
GlobalFlagModel: &globalflags.GlobalFlagModel{
50+
Verbosity: globalflags.VerbosityDefault,
51+
ProjectId: testProjectId,
52+
Region: testRegion,
53+
},
54+
Name: testLoadBalancerName,
55+
}
56+
for _, mod := range mods {
57+
mod(model)
58+
}
59+
return model
60+
}
61+
62+
func fixtureRequest(mods ...func(request *alb.ApiGetLoadBalancerRequest)) alb.ApiGetLoadBalancerRequest {
63+
request := testClient.GetLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName)
64+
for _, mod := range mods {
65+
mod(&request)
66+
}
67+
return request
68+
}
69+
70+
func TestParseInput(t *testing.T) {
71+
tests := []struct {
72+
description string
73+
argsValues []string
74+
flagValues map[string]string
75+
isValid bool
76+
expectedModel *inputModel
77+
}{
78+
{
79+
description: "base",
80+
argsValues: fixtureArgValues(),
81+
flagValues: fixtureFlagValues(),
82+
isValid: true,
83+
expectedModel: fixtureInputModel(),
84+
},
85+
{
86+
description: "no values",
87+
argsValues: []string{},
88+
flagValues: map[string]string{
89+
globalflags.ProjectIdFlag: testProjectId,
90+
globalflags.RegionFlag: testRegion,
91+
},
92+
isValid: false,
93+
},
94+
{
95+
description: "no arg values",
96+
argsValues: []string{},
97+
flagValues: fixtureFlagValues(),
98+
isValid: false,
99+
},
100+
{
101+
description: "no flag values",
102+
argsValues: fixtureArgValues(),
103+
flagValues: map[string]string{
104+
globalflags.ProjectIdFlag: testProjectId,
105+
globalflags.RegionFlag: testRegion,
106+
},
107+
isValid: true,
108+
expectedModel: fixtureInputModel(),
109+
},
110+
}
111+
112+
for _, tt := range tests {
113+
t.Run(tt.description, func(t *testing.T) {
114+
p := print.NewPrinter()
115+
cmd := NewCmd(p)
116+
err := globalflags.Configure(cmd.Flags())
117+
if err != nil {
118+
t.Fatalf("configure global flags: %v", err)
119+
}
120+
121+
for flag, value := range tt.flagValues {
122+
err = cmd.Flags().Set(flag, value)
123+
if err != nil {
124+
if !tt.isValid {
125+
return
126+
}
127+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
128+
}
129+
}
130+
131+
err = cmd.ValidateArgs(tt.argsValues)
132+
if err != nil {
133+
if !tt.isValid {
134+
return
135+
}
136+
t.Fatalf("error validating args: %v", err)
137+
}
138+
139+
err = cmd.ValidateRequiredFlags()
140+
if err != nil {
141+
if !tt.isValid {
142+
return
143+
}
144+
t.Fatalf("error validating flags: %v", err)
145+
}
146+
147+
model, err := parseInput(p, cmd, tt.argsValues)
148+
if err != nil {
149+
if !tt.isValid {
150+
return
151+
}
152+
t.Fatalf("error parsing input: %v", err)
153+
}
154+
155+
if !tt.isValid {
156+
t.Fatalf("did not fail on invalid input")
157+
}
158+
diff := cmp.Diff(model, tt.expectedModel)
159+
if diff != "" {
160+
t.Fatalf("Data does not match: %s", diff)
161+
}
162+
})
163+
}
164+
}
165+
166+
func TestBuildRequest(t *testing.T) {
167+
tests := []struct {
168+
description string
169+
model *inputModel
170+
expectedResult alb.ApiGetLoadBalancerRequest
171+
}{
172+
{
173+
description: "base",
174+
model: fixtureInputModel(),
175+
expectedResult: fixtureRequest(),
176+
},
177+
}
178+
179+
for _, tt := range tests {
180+
t.Run(tt.description, func(t *testing.T) {
181+
request := buildRequest(testCtx, tt.model, testClient)
182+
183+
diff := cmp.Diff(request, tt.expectedResult,
184+
cmp.AllowUnexported(tt.expectedResult),
185+
cmpopts.EquateComparable(testCtx),
186+
)
187+
if diff != "" {
188+
t.Fatalf("data does not match: %s", diff)
189+
}
190+
})
191+
}
192+
}
193+
194+
func Test_outputResult(t *testing.T) {
195+
type args struct {
196+
outputFormat string
197+
showOnlyPublicKey bool
198+
response alb.LoadBalancer
199+
}
200+
tests := []struct {
201+
name string
202+
args args
203+
wantErr bool
204+
}{
205+
{
206+
name: "base",
207+
args: args{
208+
outputFormat: "",
209+
showOnlyPublicKey: false,
210+
response: alb.LoadBalancer{},
211+
},
212+
wantErr: false,
213+
},
214+
}
215+
p := print.NewPrinter()
216+
p.Cmd = NewCmd(p)
217+
for _, tt := range tests {
218+
t.Run(tt.name, func(t *testing.T) {
219+
if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr {
220+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
221+
}
222+
})
223+
}
224+
}

0 commit comments

Comments
 (0)