Skip to content

Commit 41a3819

Browse files
committed
feat(alb project): support for quota and plans
1 parent 05c82eb commit 41a3819

File tree

7 files changed

+743
-0
lines changed

7 files changed

+743
-0
lines changed

internal/cmd/beta/alb/alb.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/delete"
77
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/describe"
88
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/list"
9+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/plans"
910
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/pool"
11+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/quotas"
1012
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/template"
1113
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/update"
1214
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
@@ -39,5 +41,7 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) {
3941
describe.NewCmd(p),
4042
delete.NewCmd(p),
4143
pool.NewCmd(p),
44+
plans.NewCmd(p),
45+
quotas.NewCmd(p),
4246
)
4347
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package plans
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/goccy/go-yaml"
9+
"github.com/spf13/cobra"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
11+
"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/globalflags"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/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/alb"
20+
)
21+
22+
type inputModel struct {
23+
*globalflags.GlobalFlagModel
24+
}
25+
26+
func NewCmd(p *print.Printer) *cobra.Command {
27+
cmd := &cobra.Command{
28+
Use: "plans",
29+
Short: "Lists the alb plans",
30+
Long: "Lists the available application loadbalancer plans.",
31+
Args: args.NoArgs,
32+
Example: examples.Build(
33+
examples.NewExample(
34+
`List all application loadbalancer plans`,
35+
`$ stackit beta alb plans`,
36+
),
37+
),
38+
RunE: func(cmd *cobra.Command, _ []string) error {
39+
ctx := context.Background()
40+
model, err := parseInput(p, cmd)
41+
if err != nil {
42+
return err
43+
}
44+
45+
// Configure API client
46+
apiClient, err := client.ConfigureClient(p)
47+
if err != nil {
48+
return err
49+
}
50+
51+
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
52+
if err != nil {
53+
p.Debug(print.ErrorLevel, "get project name: %v", err)
54+
projectLabel = model.ProjectId
55+
} else if projectLabel == "" {
56+
projectLabel = model.ProjectId
57+
}
58+
59+
// Call API
60+
request := buildRequest(ctx, model, apiClient)
61+
62+
response, err := request.Execute()
63+
if err != nil {
64+
return fmt.Errorf("list plans: %w", err)
65+
}
66+
67+
if items := response.ValidPlans; items == nil || len(*items) == 0 {
68+
p.Info("No plans found for project %q", projectLabel)
69+
} else {
70+
if err := outputResult(p, model.OutputFormat, *items); err != nil {
71+
return fmt.Errorf("output plans: %w", err)
72+
}
73+
}
74+
75+
return nil
76+
},
77+
}
78+
79+
return cmd
80+
}
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+
92+
if p.IsVerbosityDebug() {
93+
modelStr, err := print.BuildDebugStrFromInputModel(model)
94+
if err != nil {
95+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
96+
} else {
97+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
98+
}
99+
}
100+
101+
return &model, nil
102+
}
103+
104+
func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListPlansRequest {
105+
request := apiClient.ListPlans(ctx, model.Region)
106+
107+
return request
108+
}
109+
110+
func outputResult(p *print.Printer, outputFormat string, items []alb.PlanDetails) error {
111+
switch outputFormat {
112+
case print.JSONOutputFormat:
113+
details, err := json.MarshalIndent(items, "", " ")
114+
if err != nil {
115+
return fmt.Errorf("marshal plans: %w", err)
116+
}
117+
p.Outputln(string(details))
118+
119+
return nil
120+
case print.YAMLOutputFormat:
121+
details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
122+
if err != nil {
123+
return fmt.Errorf("marshal plans: %w", err)
124+
}
125+
p.Outputln(string(details))
126+
127+
return nil
128+
default:
129+
table := tables.NewTable()
130+
table.SetHeader("PLAN ID", "NAME", "FLAVOR", "MAX CONNS", "DESCRIPTION")
131+
for _, item := range items {
132+
table.AddRow(utils.PtrString(item.PlanId),
133+
utils.PtrString(item.Name),
134+
utils.PtrString(item.FlavorName),
135+
utils.PtrString(item.MaxConnections),
136+
utils.Truncate(item.Description, 70),
137+
)
138+
}
139+
err := table.Display(p)
140+
if err != nil {
141+
return fmt.Errorf("render table: %w", err)
142+
}
143+
144+
return nil
145+
}
146+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package plans
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{}, "foo")
20+
testClient = &alb.APIClient{}
21+
testProjectId = uuid.NewString()
22+
testRegion = "eu01"
23+
)
24+
25+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
26+
flagValues := map[string]string{
27+
globalflags.ProjectIdFlag: testProjectId,
28+
globalflags.RegionFlag: testRegion,
29+
}
30+
for _, mod := range mods {
31+
mod(flagValues)
32+
}
33+
return flagValues
34+
}
35+
36+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
37+
model := &inputModel{
38+
GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault},
39+
}
40+
for _, mod := range mods {
41+
mod(model)
42+
}
43+
return model
44+
}
45+
46+
func fixtureRequest(mods ...func(request *alb.ApiListPlansRequest)) alb.ApiListPlansRequest {
47+
request := testClient.ListPlans(testCtx, testRegion)
48+
for _, mod := range mods {
49+
mod(&request)
50+
}
51+
return request
52+
}
53+
54+
func TestParseInput(t *testing.T) {
55+
tests := []struct {
56+
description string
57+
flagValues map[string]string
58+
isValid bool
59+
expectedModel *inputModel
60+
}{
61+
{
62+
description: "base",
63+
flagValues: fixtureFlagValues(),
64+
isValid: true,
65+
expectedModel: fixtureInputModel(),
66+
},
67+
{
68+
description: "no values",
69+
flagValues: map[string]string{},
70+
isValid: false,
71+
},
72+
{
73+
description: "project id missing",
74+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
75+
delete(flagValues, globalflags.ProjectIdFlag)
76+
}),
77+
isValid: false,
78+
},
79+
{
80+
description: "project id invalid 1",
81+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
82+
flagValues[globalflags.ProjectIdFlag] = ""
83+
}),
84+
isValid: false,
85+
},
86+
{
87+
description: "project id invalid 2",
88+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
89+
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
90+
}),
91+
isValid: false,
92+
},
93+
}
94+
95+
for _, tt := range tests {
96+
t.Run(tt.description, func(t *testing.T) {
97+
p := print.NewPrinter()
98+
cmd := NewCmd(p)
99+
if err := globalflags.Configure(cmd.Flags()); err != nil {
100+
t.Errorf("cannot configure global flags: %v", err)
101+
}
102+
103+
for flag, value := range tt.flagValues {
104+
err := cmd.Flags().Set(flag, value)
105+
if err != nil {
106+
if !tt.isValid {
107+
return
108+
}
109+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
110+
}
111+
}
112+
113+
if err := cmd.ValidateRequiredFlags(); err != nil {
114+
if !tt.isValid {
115+
return
116+
}
117+
t.Fatalf("error validating flags: %v", err)
118+
}
119+
120+
model, err := parseInput(p, cmd)
121+
if err != nil {
122+
if !tt.isValid {
123+
return
124+
}
125+
t.Fatalf("error parsing flags: %v", err)
126+
}
127+
128+
if !tt.isValid {
129+
t.Fatalf("did not fail on invalid input")
130+
}
131+
diff := cmp.Diff(model, tt.expectedModel)
132+
if diff != "" {
133+
t.Fatalf("Data does not match: %s", diff)
134+
}
135+
})
136+
}
137+
}
138+
139+
func TestBuildRequest(t *testing.T) {
140+
tests := []struct {
141+
description string
142+
model *inputModel
143+
expectedRequest alb.ApiListPlansRequest
144+
}{
145+
{
146+
description: "base",
147+
model: fixtureInputModel(),
148+
expectedRequest: fixtureRequest(),
149+
},
150+
}
151+
152+
for _, tt := range tests {
153+
t.Run(tt.description, func(t *testing.T) {
154+
request := buildRequest(testCtx, tt.model, testClient)
155+
diff := cmp.Diff(request, tt.expectedRequest,
156+
cmp.AllowUnexported(tt.expectedRequest),
157+
cmpopts.EquateComparable(testCtx),
158+
cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"),
159+
)
160+
if diff != "" {
161+
t.Fatalf("Data does not match: %s", diff)
162+
}
163+
})
164+
}
165+
}
166+
167+
func Test_outputResult(t *testing.T) {
168+
type args struct {
169+
outputFormat string
170+
items []alb.PlanDetails
171+
}
172+
tests := []struct {
173+
name string
174+
args args
175+
wantErr bool
176+
}{
177+
{
178+
name: "empty",
179+
args: args{
180+
outputFormat: "",
181+
items: []alb.PlanDetails{},
182+
},
183+
wantErr: false,
184+
},
185+
{
186+
name: "output format json",
187+
args: args{
188+
outputFormat: print.JSONOutputFormat,
189+
items: []alb.PlanDetails{},
190+
},
191+
wantErr: false,
192+
},
193+
}
194+
p := print.NewPrinter()
195+
p.Cmd = NewCmd(p)
196+
for _, tt := range tests {
197+
t.Run(tt.name, func(t *testing.T) {
198+
if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr {
199+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
200+
}
201+
})
202+
}
203+
}

0 commit comments

Comments
 (0)