Skip to content

Commit 09c5932

Browse files
committed
feat(vpn): onboard gateway
relates to STACKITCLI-353
1 parent ca535dd commit 09c5932

20 files changed

Lines changed: 2230 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package create
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
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/vpn/client"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
19+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
20+
vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api"
21+
"github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api/wait"
22+
)
23+
24+
const (
25+
availabilityZoneTunnel1Flag = "availability-zone-tunnel-1"
26+
availabilityZoneTunnel2Flag = "availability-zone-tunnel-2"
27+
bgpLocalAsnFlag = "bgp-local-asn"
28+
bgpOverrideAdvertisedRoutesFlag = "bgp-override-advertised-routes"
29+
nameFlag = "name"
30+
labelsFlag = "labels"
31+
planIdFlag = "plan-id"
32+
routingTypeFlag = "routing-type"
33+
)
34+
35+
type inputModel struct {
36+
*globalflags.GlobalFlagModel
37+
AvailabilityZone vpn.CreateGatewayPayloadAvailabilityZones
38+
Bgp *vpn.BGPGatewayConfig
39+
Name string
40+
Labels *map[string]string
41+
PlanId string
42+
RoutingType vpn.RoutingType
43+
}
44+
45+
func NewCmd(params *types.CmdParams) *cobra.Command {
46+
cmd := &cobra.Command{
47+
Use: "create",
48+
Short: "Creates a vpn gateway",
49+
Long: "Creates a vpn gateway.",
50+
Args: args.NoArgs,
51+
Example: examples.Build(
52+
examples.NewExample(
53+
`Create a vpn gateway with name "xxx", plan "p500", policy based routing and both tunnels in availability-zone eu01-1`,
54+
"$ stackit beta vpn gateway create --name xxx --plan-id p500 --routing-type POLICY_BASED --availability-zone-tunnel-1 eu01-1 --availability-zone-tunnel-2 eu01-1",
55+
),
56+
examples.NewExample(
57+
`Create a vpn gateway with the labels foo=bar and x=y`,
58+
"$ stackit beta vpn gateway create --name xxx --plan-id p500 --routing-type POLICY_BASED --availability-zone-tunnel-1 eu01-1 --availability-zone-tunnel-2 eu01-1 --label foo=bar,x=y",
59+
),
60+
examples.NewExample(
61+
`Create a vpn gateway with bgp enabled, yyy as local asn and [aaa, bbb] as override advertised routes`,
62+
"$ stackit beta vpn gateway create --name xxx --plan-id p500 --routing-type POLICY_BASED --availability-zone-tunnel-1 eu01-1 --availability-zone-tunnel-2 eu01-1 --bgp-local-asn yyy --bgp-override-advertised-routes aaa,bbb",
63+
),
64+
),
65+
RunE: func(cmd *cobra.Command, args []string) error {
66+
ctx := context.Background()
67+
model, err := parseInput(params.Printer, cmd, args)
68+
if err != nil {
69+
return fmt.Errorf("unable to parse input: %w", err)
70+
}
71+
72+
// Configure API client
73+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
74+
if err != nil {
75+
return err
76+
}
77+
78+
projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd)
79+
if err != nil || projectLabel == "" {
80+
projectLabel = model.ProjectId
81+
}
82+
83+
prompt := fmt.Sprintf("Are you sure you want to create a vpn gateway for project %q?", projectLabel)
84+
err = params.Printer.PromptForConfirmation(prompt)
85+
if err != nil {
86+
return err
87+
}
88+
89+
// Call API
90+
req := buildRequest(ctx, model, apiClient)
91+
resp, err := req.Execute()
92+
if err != nil {
93+
return fmt.Errorf("create vpn gateway: %w", err)
94+
}
95+
var gatewayId string
96+
if resp != nil && resp.HasId() {
97+
gatewayId = *resp.Id
98+
}
99+
100+
// Wait for async operation, if async mode not enabled
101+
if !model.Async {
102+
err := spinner.Run(params.Printer, "Creating vpn gateway", func() error {
103+
_, err = wait.CreateGatewayWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, gatewayId).WaitWithContext(ctx)
104+
return err
105+
})
106+
if err != nil {
107+
return fmt.Errorf("waiting for vpn gateway creation: %w", err)
108+
}
109+
}
110+
111+
return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp)
112+
},
113+
}
114+
configureFlags(cmd)
115+
return cmd
116+
}
117+
118+
func configureFlags(cmd *cobra.Command) {
119+
cmd.Flags().String(availabilityZoneTunnel1Flag, "", "Availability Zone of Tunnel 1")
120+
cmd.Flags().String(availabilityZoneTunnel2Flag, "", "Availability Zone of Tunnel 2")
121+
cmd.Flags().Int64(bgpLocalAsnFlag, 0, "ASN for private use (reserved by IANA), both 16Bit and 32Bit ranges are valid (RFC 6996)")
122+
cmd.Flags().StringArray(bgpOverrideAdvertisedRoutesFlag, nil, "A list of IPv4 Prefixes to advertise via BGP")
123+
cmd.Flags().String(nameFlag, "", "Gateway name")
124+
cmd.Flags().StringToString(labelsFlag, nil, "Labels in key=value format, separated by commas")
125+
cmd.Flags().String(planIdFlag, "", "Plan ID")
126+
cmd.Flags().String(routingTypeFlag, "", fmt.Sprintf("Routing Type: %q", vpn.AllowedRoutingTypeEnumValues))
127+
// cmd.Flags().Var(flags.EnumFlag(false, "", sdkUtils.EnumSliceToStringSlice(vpn.AllowedRoutingTypeEnumValues)...), routingTypeFlag, fmt.Sprintf("Routing Type, one of: %q", vpn.AllowedRoutingTypeEnumValues))
128+
129+
err := flags.MarkFlagsRequired(cmd, availabilityZoneTunnel1Flag, availabilityZoneTunnel2Flag, nameFlag, planIdFlag, routingTypeFlag)
130+
cobra.CheckErr(err)
131+
}
132+
133+
func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
134+
globalFlags := globalflags.Parse(p, cmd)
135+
if globalFlags.ProjectId == "" {
136+
return nil, &errors.ProjectIdError{}
137+
}
138+
139+
bgpLocalAsn := flags.FlagToInt64Pointer(p, cmd, bgpLocalAsnFlag)
140+
if bgpLocalAsn != nil {
141+
if *bgpLocalAsn < 0 {
142+
return nil, &errors.FlagValidationError{
143+
Flag: bgpLocalAsnFlag,
144+
Details: "must be a positive integer",
145+
}
146+
}
147+
}
148+
bgpOverrideAdvertisedRoutes := flags.FlagToStringArrayValue(p, cmd, bgpOverrideAdvertisedRoutesFlag)
149+
if bgpOverrideAdvertisedRoutes != nil {
150+
if bgpLocalAsn == nil {
151+
return nil, &errors.DependingFlagIsMissing{
152+
MissingFlag: bgpLocalAsnFlag,
153+
SetFlag: bgpOverrideAdvertisedRoutesFlag,
154+
}
155+
}
156+
}
157+
158+
var bgp *vpn.BGPGatewayConfig
159+
if bgpLocalAsn != nil || bgpOverrideAdvertisedRoutes != nil {
160+
bgp = &vpn.BGPGatewayConfig{
161+
LocalAsn: *bgpLocalAsn,
162+
OverrideAdvertisedRoutes: flags.FlagToStringArrayValue(p, cmd, bgpOverrideAdvertisedRoutesFlag),
163+
}
164+
}
165+
166+
model := inputModel{
167+
GlobalFlagModel: globalFlags,
168+
AvailabilityZone: vpn.CreateGatewayPayloadAvailabilityZones{
169+
Tunnel1: flags.FlagToStringValue(p, cmd, availabilityZoneTunnel1Flag),
170+
Tunnel2: flags.FlagToStringValue(p, cmd, availabilityZoneTunnel2Flag),
171+
},
172+
Bgp: bgp,
173+
Name: flags.FlagToStringValue(p, cmd, nameFlag),
174+
Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag),
175+
PlanId: flags.FlagToStringValue(p, cmd, planIdFlag),
176+
RoutingType: vpn.RoutingType(flags.FlagToStringValue(p, cmd, routingTypeFlag)),
177+
}
178+
179+
p.DebugInputModel(model)
180+
return &model, nil
181+
}
182+
183+
func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) vpn.ApiCreateGatewayRequest {
184+
req := apiClient.DefaultAPI.CreateGateway(ctx, model.ProjectId, model.Region)
185+
req = req.CreateGatewayPayload(
186+
vpn.CreateGatewayPayload{
187+
AvailabilityZones: model.AvailabilityZone,
188+
Bgp: model.Bgp,
189+
DisplayName: model.Name,
190+
Labels: model.Labels,
191+
PlanId: model.PlanId,
192+
RoutingType: model.RoutingType,
193+
},
194+
)
195+
return req
196+
}
197+
198+
func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel string, item *vpn.GatewayResponse) error {
199+
return p.OutputResult(outputFormat, item, func() error {
200+
if item == nil {
201+
p.Outputln("vpn gateway response is empty")
202+
return nil
203+
}
204+
operation := "Created"
205+
if async {
206+
operation = "Triggered creation of"
207+
}
208+
p.Outputf(
209+
"%s vpn gateway %q in project %q.\nGateway ID: %s\n",
210+
operation,
211+
item.DisplayName,
212+
projectLabel,
213+
utils.PtrString(item.Id),
214+
)
215+
return nil
216+
})
217+
}

0 commit comments

Comments
 (0)