Skip to content

Commit 29bac6d

Browse files
committed
feat(alb): add template support
1 parent 4c609f4 commit 29bac6d

File tree

4 files changed

+378
-0
lines changed

4 files changed

+378
-0
lines changed

internal/cmd/beta/alb/alb.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package alb
22

33
import (
44
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/list"
5+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/template"
56
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
67
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
78

@@ -25,5 +26,6 @@ func NewCmd(p *print.Printer) *cobra.Command {
2526
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
2627
cmd.AddCommand(
2728
list.NewCmd(p),
29+
template.NewCmd(p),
2830
)
2931
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package template
2+
3+
import (
4+
"bytes"
5+
"context"
6+
_ "embed"
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"os"
11+
12+
"github.com/goccy/go-yaml"
13+
"github.com/spf13/cobra"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
19+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
20+
"github.com/stackitcloud/stackit-sdk-go/services/alb"
21+
)
22+
23+
const (
24+
formatFlag = "format"
25+
)
26+
27+
type inputModel struct {
28+
*globalflags.GlobalFlagModel
29+
Format *string
30+
}
31+
32+
//go:embed template.json
33+
var template []byte
34+
35+
func NewCmd(p *print.Printer) *cobra.Command {
36+
cmd := &cobra.Command{
37+
Use: "template",
38+
Short: "create an alb template",
39+
Long: "creates a json or yaml template file for creating/updating an application loadbalancer.",
40+
Args: args.NoArgs,
41+
Example: examples.Build(
42+
examples.NewExample(
43+
`Creat a yaml template`,
44+
`$ stackit beta alb template --format=yaml`,
45+
),
46+
examples.NewExample(
47+
`Creat a json template`,
48+
`$ stackit beta alb template --format=json`,
49+
),
50+
),
51+
RunE: func(cmd *cobra.Command, _ []string) error {
52+
ctx := context.Background()
53+
model, err := parseInput(p, cmd)
54+
if err != nil {
55+
return err
56+
}
57+
58+
var reader io.Reader
59+
if model.Format == nil || *model.Format == "json" {
60+
reader = bytes.NewReader(template)
61+
} else if *model.Format == "yaml" {
62+
var target alb.CreateLoadBalancerPayload
63+
if err := json.Unmarshal(template, &target); err != nil {
64+
return fmt.Errorf("cannot unmarshal template: %w", err)
65+
}
66+
data, err := yaml.Marshal(&target)
67+
if err != nil {
68+
return fmt.Errorf("cannot marshal template to yaml: %w", err)
69+
}
70+
reader = bytes.NewReader(data)
71+
} else {
72+
return fmt.Errorf("invalid format %q defined. Must be 'json' or 'yaml'", *model.Format)
73+
}
74+
io.Copy(os.Stdout, reader)
75+
76+
_, _ = ctx, model
77+
78+
return nil
79+
},
80+
}
81+
82+
configureFlags(cmd)
83+
return cmd
84+
}
85+
86+
func configureFlags(cmd *cobra.Command) {
87+
cmd.Flags().VarP(flags.EnumFlag(true, "json", "json", "yaml"), formatFlag, "f", "Defines the output format (yaml or json), default is json")
88+
}
89+
90+
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
91+
globalFlags := globalflags.Parse(p, cmd)
92+
if globalFlags.ProjectId == "" {
93+
return nil, &errors.ProjectIdError{}
94+
}
95+
96+
model := inputModel{
97+
GlobalFlagModel: globalFlags,
98+
Format: flags.FlagToStringPointer(p, cmd, formatFlag),
99+
}
100+
101+
if p.IsVerbosityDebug() {
102+
modelStr, err := print.BuildDebugStrFromInputModel(model)
103+
if err != nil {
104+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
105+
} else {
106+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
107+
}
108+
}
109+
110+
return &model, nil
111+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
{
2+
"externalAddress": "10.100.42.1",
3+
"listeners": [
4+
{
5+
"displayName": "listener1",
6+
"http": {},
7+
"https": {
8+
"certificateConfig": {
9+
"certificateIds": [
10+
"cert-1",
11+
"cert-2",
12+
"cert-3"
13+
]
14+
}
15+
},
16+
"port": 443,
17+
"protocol": "PROTOCOL_HTTPS",
18+
"rules": [
19+
{
20+
"host": "front.facing.host",
21+
"http": {
22+
"subRules": [
23+
{
24+
"cookiePersistence": {
25+
"name": "cookie1",
26+
"ttl": "120s"
27+
},
28+
"headers": [
29+
{
30+
"name": "testheader1",
31+
"exactMatch": "X-test-header1"
32+
},
33+
{
34+
"name": "testheader2",
35+
"exactMatch": "X-test-header2"
36+
},
37+
{
38+
"name": "testheader3",
39+
"exactMatch": "X-test-header3"
40+
}
41+
],
42+
"pathPrefix": "/foo",
43+
"queryParameters": [
44+
{
45+
"name": "query-param",
46+
"exactMatch": "q"
47+
},
48+
{
49+
"name": "region",
50+
"exactMatch": "region"
51+
}
52+
],
53+
"targetPool": "my-target-pool",
54+
"webSocket": false
55+
}
56+
]
57+
}
58+
}
59+
]
60+
}
61+
],
62+
"name": "my-load-balancer",
63+
"networks": [
64+
{
65+
"networkId": "00000000-0000-0000-0000-000000000000",
66+
"role": "ROLE_LISTENERS_AND_TARGETS"
67+
},
68+
{
69+
"networkId": "00000000-0000-0000-0000-000000000001",
70+
"role": "ROLE_LISTENERS_AND_TARGETS"
71+
}
72+
],
73+
"options": {
74+
"accessControl": {
75+
"allowedSourceRanges": [
76+
"192.168.42.0-192.168.42.10",
77+
"192.168.54.0-192.168.54.10"
78+
]
79+
},
80+
"ephemeralAddress": true,
81+
"observability": {
82+
"logs": {
83+
"credentialsRef": "my-credentials",
84+
"pushUrl": "https://my.observability.host/<observability-instance-id>/loki/api/v1/push"
85+
},
86+
"metrics": {
87+
"credentialsRef": "my-credentials",
88+
"pushUrl": "https://my.observability.host/<observability-instance-id>/<argus-instance-id>/api/v1/receive"
89+
}
90+
},
91+
"privateNetworkOnly": true
92+
},
93+
"planId": "p10",
94+
"targetPools": [
95+
{
96+
"activeHealthCheck": {
97+
"healthyThreshold": 3,
98+
"httpHealthChecks": {
99+
"okStatuses": [
100+
"200",
101+
"204"
102+
],
103+
"path": "/health"
104+
},
105+
"interval": "10s",
106+
"intervalJitter": "3s",
107+
"timeout": "5s",
108+
"unhealthyThreshold": 1
109+
},
110+
"name": "my-target-pool",
111+
"targetPort": 5732,
112+
"targets": [
113+
{
114+
"displayName": "my-target1",
115+
"ip": "192.11.2.5"
116+
}
117+
],
118+
"tlsConfig": {
119+
"customCa": "my.private.ca",
120+
"enabled": true,
121+
"skipCertificateValidation": false
122+
}
123+
}
124+
]
125+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package template
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/uuid"
12+
"github.com/stackitcloud/stackit-sdk-go/services/alb"
13+
)
14+
15+
var projectIdFlag = globalflags.ProjectIdFlag
16+
17+
type testCtxKey struct{}
18+
19+
var (
20+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
21+
testClient = &alb.APIClient{}
22+
testProjectId = uuid.NewString()
23+
testRegion = "eu01"
24+
testLimit int64 = 10
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, 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.ApiListLoadBalancersRequest)) alb.ApiListLoadBalancersRequest {
48+
request := testClient.ListLoadBalancers(context.Background(), testProjectId, 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, projectIdFlag)
77+
}),
78+
isValid: false,
79+
},
80+
{
81+
description: "project id invalid 1",
82+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
83+
flagValues[projectIdFlag] = ""
84+
}),
85+
isValid: false,
86+
},
87+
{
88+
description: "project id invalid 2",
89+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
90+
flagValues[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+

0 commit comments

Comments
 (0)