Skip to content

Commit 92ea8c2

Browse files
committed
feat(iaas): onboard vpc commands
relates to STACKITCLI-420
1 parent fa38f73 commit 92ea8c2

19 files changed

Lines changed: 1781 additions & 3 deletions

File tree

‎internal/cmd/alpha/alpha.go‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package alpha
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
8+
"github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
12+
)
13+
14+
func NewCmd(params *types.CmdParams) *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "alpha",
17+
Short: "Contains alpha STACKIT CLI commands",
18+
Long: fmt.Sprintf("%s\n%s",
19+
"Contains alpha STACKIT CLI commands.",
20+
"The commands under this group are still in a beta state, and functionality may be incomplete or have breaking changes."),
21+
Args: args.NoArgs,
22+
Example: examples.Build(
23+
examples.NewExample(
24+
"See the currently available alpha commands",
25+
"$ stackit alpha --help"),
26+
examples.NewExample(
27+
"Execute an alpha command",
28+
"$ stackit alpha MY_COMMAND"),
29+
),
30+
}
31+
addSubcommands(cmd, params)
32+
return cmd
33+
}
34+
35+
func addSubcommands(cmd *cobra.Command, params *types.CmdParams) {
36+
cmd.AddCommand(vpc.NewCmd(params))
37+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package create
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api"
9+
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/flags"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaasalpha/client"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
19+
)
20+
21+
const (
22+
nameFlag = "name"
23+
descriptionFlag = "description"
24+
labelsFlag = "labels"
25+
)
26+
27+
type inputModel struct {
28+
*globalflags.GlobalFlagModel
29+
Name string
30+
Description *string
31+
Labels map[string]any
32+
}
33+
34+
func NewCmd(params *types.CmdParams) *cobra.Command {
35+
cmd := &cobra.Command{
36+
Use: "create",
37+
Short: "Creates a VPC",
38+
Long: fmt.Sprintf("%s\n%s",
39+
"Creates a VPC.",
40+
"VPC can only be used in projects with enabled VPC."),
41+
Args: args.NoArgs,
42+
Example: examples.Build(
43+
examples.NewExample(
44+
`Create a vpc with name "my-vpc"`,
45+
"$ stackit alpha vpc create --name my-vpc",
46+
),
47+
),
48+
RunE: func(cmd *cobra.Command, args []string) error {
49+
ctx := context.Background()
50+
model, err := parseInput(params.Printer, cmd, args)
51+
if err != nil {
52+
return err
53+
}
54+
55+
// Configure API client
56+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
57+
if err != nil {
58+
return err
59+
}
60+
61+
projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd)
62+
if err != nil {
63+
params.Printer.Debug(print.ErrorLevel, "get project name: %v", err)
64+
projectLabel = model.ProjectId
65+
}
66+
67+
prompt := fmt.Sprintf("Are you sure you want to create a vpc for project %q?", projectLabel)
68+
err = params.Printer.PromptForConfirmation(prompt)
69+
if err != nil {
70+
return err
71+
}
72+
73+
// Call API
74+
req := buildRequest(ctx, model, apiClient)
75+
resp, err := req.Execute()
76+
if err != nil {
77+
return fmt.Errorf("create vpc: %w", err)
78+
}
79+
80+
return outputResult(params.Printer, model.OutputFormat, projectLabel, resp)
81+
},
82+
}
83+
configureFlags(cmd)
84+
return cmd
85+
}
86+
87+
func configureFlags(cmd *cobra.Command) {
88+
cmd.Flags().String(nameFlag, "", "Name of the VPC")
89+
cmd.Flags().String(descriptionFlag, "", "Description of the VPC")
90+
cmd.Flags().StringToString(labelsFlag, nil, "Comma separated list of labels of the VPC")
91+
92+
err := cmd.MarkFlagRequired(nameFlag)
93+
cobra.CheckErr(err)
94+
}
95+
96+
func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
97+
globalFlags := globalflags.Parse(p, cmd)
98+
if globalFlags.ProjectId == "" {
99+
return nil, &errors.ProjectIdError{}
100+
}
101+
102+
model := inputModel{
103+
GlobalFlagModel: globalFlags,
104+
Name: flags.FlagToStringValue(p, cmd, nameFlag),
105+
Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
106+
Labels: flags.FlagToStringToAny(p, cmd, labelsFlag),
107+
}
108+
109+
p.DebugInputModel(model)
110+
return &model, nil
111+
}
112+
113+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateVPCRequest {
114+
payload := iaas.CreateVPCPayload{
115+
Name: model.Name,
116+
Description: model.Description,
117+
Labels: model.Labels,
118+
}
119+
return apiClient.DefaultAPI.CreateVPC(ctx, model.ProjectId).CreateVPCPayload(payload)
120+
}
121+
122+
func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *iaas.VPC) error {
123+
if resp == nil {
124+
return fmt.Errorf("create vpc response is nil")
125+
}
126+
return p.OutputResult(outputFormat, resp, func() error {
127+
p.Outputf("Created vpc for %q. VPC ID: %s\n", projectLabel, resp.Id)
128+
return nil
129+
})
130+
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package create
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/google/go-cmp/cmp/cmpopts"
9+
"github.com/google/uuid"
10+
iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api"
11+
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/testparams"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
15+
)
16+
17+
type testCtxKey struct{}
18+
19+
var (
20+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
21+
testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}}
22+
23+
testProjectId = uuid.NewString()
24+
testName = "test-vpc"
25+
testDescription = "test-vpc-description"
26+
testLabels = "environment=dev,endpoint=alpha"
27+
)
28+
29+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
30+
flagValues := map[string]string{
31+
globalflags.ProjectIdFlag: testProjectId,
32+
nameFlag: testName,
33+
descriptionFlag: testDescription,
34+
labelsFlag: testLabels,
35+
}
36+
for _, mod := range mods {
37+
mod(flagValues)
38+
}
39+
return flagValues
40+
}
41+
42+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
43+
model := &inputModel{
44+
GlobalFlagModel: &globalflags.GlobalFlagModel{
45+
ProjectId: testProjectId,
46+
Verbosity: globalflags.VerbosityDefault,
47+
},
48+
Name: testName,
49+
Description: new(testDescription),
50+
Labels: map[string]any{
51+
"environment": "dev",
52+
"endpoint": "alpha",
53+
},
54+
}
55+
for _, mod := range mods {
56+
mod(model)
57+
}
58+
return model
59+
}
60+
61+
func fixturePayload(mods ...func(payload *iaas.CreateVPCPayload)) (payload iaas.CreateVPCPayload) {
62+
payload = iaas.CreateVPCPayload{
63+
Name: testName,
64+
Description: new(testDescription),
65+
Labels: map[string]any{"environment": "dev", "endpoint": "alpha"},
66+
}
67+
for _, f := range mods {
68+
f(&payload)
69+
}
70+
return payload
71+
}
72+
73+
func fixtureRequest(mods ...func(request *iaas.ApiCreateVPCRequest)) iaas.ApiCreateVPCRequest {
74+
request := testClient.DefaultAPI.CreateVPC(testCtx, testProjectId)
75+
76+
request = request.CreateVPCPayload(fixturePayload())
77+
for _, mod := range mods {
78+
mod(&request)
79+
}
80+
return request
81+
}
82+
83+
func TestParseInput(t *testing.T) {
84+
tests := []struct {
85+
description string
86+
argValues []string
87+
flagValues map[string]string
88+
isValid bool
89+
expectedModel *inputModel
90+
}{
91+
{
92+
description: "base",
93+
flagValues: fixtureFlagValues(),
94+
isValid: true,
95+
expectedModel: fixtureInputModel(),
96+
},
97+
{
98+
description: "no values",
99+
flagValues: map[string]string{},
100+
isValid: false,
101+
},
102+
{
103+
description: "required fields only",
104+
flagValues: map[string]string{
105+
globalflags.ProjectIdFlag: testProjectId,
106+
nameFlag: testName,
107+
},
108+
isValid: true,
109+
expectedModel: &inputModel{
110+
GlobalFlagModel: &globalflags.GlobalFlagModel{
111+
ProjectId: testProjectId,
112+
Verbosity: globalflags.VerbosityDefault,
113+
},
114+
Name: testName,
115+
},
116+
},
117+
{
118+
description: "project id missing",
119+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
120+
delete(flagValues, globalflags.ProjectIdFlag)
121+
}),
122+
isValid: false,
123+
},
124+
{
125+
description: "project id invalid 1",
126+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
127+
flagValues[globalflags.ProjectIdFlag] = ""
128+
}),
129+
isValid: false,
130+
},
131+
{
132+
description: "project id invalid 2",
133+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
134+
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
135+
}),
136+
isValid: false,
137+
},
138+
{
139+
description: "name missing",
140+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
141+
delete(flagValues, nameFlag)
142+
}),
143+
isValid: false,
144+
},
145+
}
146+
147+
for _, tt := range tests {
148+
t.Run(tt.description, func(t *testing.T) {
149+
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
150+
})
151+
}
152+
}
153+
154+
func TestBuildRequest(t *testing.T) {
155+
tests := []struct {
156+
description string
157+
model *inputModel
158+
expectedRequest iaas.ApiCreateVPCRequest
159+
}{
160+
{
161+
description: "base",
162+
model: fixtureInputModel(),
163+
expectedRequest: fixtureRequest(),
164+
},
165+
{
166+
description: "required fields only",
167+
model: &inputModel{
168+
GlobalFlagModel: &globalflags.GlobalFlagModel{
169+
ProjectId: testProjectId,
170+
Verbosity: globalflags.VerbosityDefault,
171+
},
172+
Name: testName,
173+
},
174+
expectedRequest: fixtureRequest(func(request *iaas.ApiCreateVPCRequest) {
175+
*request = request.CreateVPCPayload(iaas.CreateVPCPayload{
176+
Name: testName,
177+
})
178+
}),
179+
},
180+
}
181+
182+
for _, tt := range tests {
183+
t.Run(tt.description, func(t *testing.T) {
184+
request := buildRequest(testCtx, tt.model, testClient)
185+
186+
diff := cmp.Diff(request, tt.expectedRequest,
187+
cmp.AllowUnexported(tt.expectedRequest, iaas.DefaultAPIService{}),
188+
cmpopts.EquateComparable(testCtx),
189+
)
190+
if diff != "" {
191+
t.Fatalf("Data does not match: %s", diff)
192+
}
193+
})
194+
}
195+
}
196+
197+
func TestOutputResult(t *testing.T) {
198+
type args struct {
199+
outputFormat string
200+
projectLabel string
201+
resp *iaas.VPC
202+
}
203+
tests := []struct {
204+
name string
205+
args args
206+
wantErr bool
207+
}{
208+
{
209+
name: "empty",
210+
args: args{},
211+
wantErr: true,
212+
},
213+
{
214+
name: "empty vpc response",
215+
args: args{
216+
projectLabel: testProjectId,
217+
resp: &iaas.VPC{},
218+
},
219+
},
220+
}
221+
params := testparams.NewTestParams()
222+
for _, tt := range tests {
223+
t.Run(tt.name, func(t *testing.T) {
224+
if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr {
225+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
226+
}
227+
})
228+
}
229+
}

0 commit comments

Comments
 (0)