Skip to content

Commit ad9521d

Browse files
committed
feat(alb project): support for quota and plans
1 parent c433739 commit ad9521d

File tree

7 files changed

+754
-0
lines changed

7 files changed

+754
-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: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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+
testLimit int64 = 10
24+
)
25+
26+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
27+
flagValues := map[string]string{
28+
globalflags.ProjectIdFlag: testProjectId,
29+
globalflags.RegionFlag: testRegion,
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, Region: testRegion, Verbosity: globalflags.VerbosityDefault},
40+
}
41+
for _, mod := range mods {
42+
mod(model)
43+
}
44+
return model
45+
}
46+
47+
func fixtureRequest(mods ...func(request *alb.ApiListPlansRequest)) alb.ApiListPlansRequest {
48+
request := testClient.ListPlans(testCtx, testRegion)
49+
for _, mod := range mods {
50+
mod(&request)
51+
}
52+
return request
53+
}
54+
55+
func TestParseInput(t *testing.T) {
56+
tests := []struct {
57+
description string
58+
flagValues map[string]string
59+
isValid bool
60+
expectedModel *inputModel
61+
}{
62+
{
63+
description: "base",
64+
flagValues: fixtureFlagValues(),
65+
isValid: true,
66+
expectedModel: fixtureInputModel(),
67+
},
68+
{
69+
description: "no values",
70+
flagValues: map[string]string{},
71+
isValid: false,
72+
},
73+
{
74+
description: "project id missing",
75+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
76+
delete(flagValues, globalflags.ProjectIdFlag)
77+
}),
78+
isValid: false,
79+
},
80+
{
81+
description: "project id invalid 1",
82+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
83+
flagValues[globalflags.ProjectIdFlag] = ""
84+
}),
85+
isValid: false,
86+
},
87+
{
88+
description: "project id invalid 2",
89+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
90+
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
91+
}),
92+
isValid: false,
93+
},
94+
}
95+
96+
for _, tt := range tests {
97+
t.Run(tt.description, func(t *testing.T) {
98+
p := print.NewPrinter()
99+
cmd := NewCmd(p)
100+
if err := globalflags.Configure(cmd.Flags()); err != nil {
101+
t.Errorf("cannot configure global flags: %v", err)
102+
}
103+
104+
for flag, value := range tt.flagValues {
105+
err := cmd.Flags().Set(flag, value)
106+
if err != nil {
107+
if !tt.isValid {
108+
return
109+
}
110+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
111+
}
112+
}
113+
114+
if err := cmd.ValidateRequiredFlags(); err != nil {
115+
if !tt.isValid {
116+
return
117+
}
118+
t.Fatalf("error validating flags: %v", err)
119+
}
120+
121+
model, err := parseInput(p, cmd)
122+
if err != nil {
123+
if !tt.isValid {
124+
return
125+
}
126+
t.Fatalf("error parsing flags: %v", err)
127+
}
128+
129+
if !tt.isValid {
130+
t.Fatalf("did not fail on invalid input")
131+
}
132+
diff := cmp.Diff(model, tt.expectedModel)
133+
if diff != "" {
134+
t.Fatalf("Data does not match: %s", diff)
135+
}
136+
})
137+
}
138+
}
139+
140+
func TestBuildRequest(t *testing.T) {
141+
tests := []struct {
142+
description string
143+
model *inputModel
144+
expectedRequest alb.ApiListPlansRequest
145+
}{
146+
{
147+
description: "base",
148+
model: fixtureInputModel(),
149+
expectedRequest: fixtureRequest(),
150+
},
151+
}
152+
153+
for _, tt := range tests {
154+
t.Run(tt.description, func(t *testing.T) {
155+
request := buildRequest(testCtx, tt.model, testClient)
156+
diff := cmp.Diff(request, tt.expectedRequest,
157+
cmp.AllowUnexported(tt.expectedRequest),
158+
cmpopts.EquateComparable(testCtx),
159+
cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"),
160+
)
161+
if diff != "" {
162+
t.Fatalf("Data does not match: %s", diff)
163+
}
164+
})
165+
}
166+
}
167+
168+
func Test_outputResult(t *testing.T) {
169+
type args struct {
170+
outputFormat string
171+
items []alb.PlanDetails
172+
}
173+
tests := []struct {
174+
name string
175+
args args
176+
wantErr bool
177+
}{
178+
{
179+
name: "empty",
180+
args: args{
181+
outputFormat: "",
182+
items: []alb.PlanDetails{},
183+
},
184+
wantErr: false,
185+
},
186+
{
187+
name: "output format json",
188+
args: args{
189+
outputFormat: print.JSONOutputFormat,
190+
items: []alb.PlanDetails{},
191+
},
192+
wantErr: false,
193+
},
194+
}
195+
p := print.NewPrinter()
196+
p.Cmd = NewCmd(p)
197+
for _, tt := range tests {
198+
t.Run(tt.name, func(t *testing.T) {
199+
if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr {
200+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
201+
}
202+
})
203+
}
204+
}

0 commit comments

Comments
 (0)