Skip to content

Commit b3907e9

Browse files
committed
feat(vpn): basic connection commands
- implement create, describe, list, delete for vpn connection - add helpers for string based enum flags - make JoinStringPtr generic to accept string based enum slices
1 parent 9331e88 commit b3907e9

20 files changed

Lines changed: 2405 additions & 2 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ require (
3737
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.7
3838
github.com/stackitcloud/stackit-sdk-go/services/ske v1.11.0
3939
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.4.3
40+
github.com/stackitcloud/stackit-sdk-go/services/vpn v0.14.0
4041
github.com/zalando/go-keyring v0.2.6
4142
golang.org/x/mod v0.34.0
4243
golang.org/x/oauth2 v0.35.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,8 @@ github.com/stackitcloud/stackit-sdk-go/services/ske v1.11.0 h1:QoKyQPe8FqDqJLNgE
656656
github.com/stackitcloud/stackit-sdk-go/services/ske v1.11.0/go.mod h1:KhVYCR58wETqdI7Quwhe3OR3BhB2T/b7DzaMsfDnr8g=
657657
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.4.3 h1:AQrcr+qeIuZob+3TT2q1L4WOPtpsu5SEpkTnOUHDqfE=
658658
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.4.3/go.mod h1:8BBGC69WFXWWmKgzSjgE4HvsI7pEgO0RN2cASwuPJ18=
659+
github.com/stackitcloud/stackit-sdk-go/services/vpn v0.14.0 h1:LMgbzhPunuelsIsfyEj/5O/aYfNcg/eGHsnZ7AZOhYg=
660+
github.com/stackitcloud/stackit-sdk-go/services/vpn v0.14.0/go.mod h1:toIjQk1dhxdUFVyCWJJja0w/0nFpDid8MWX0ukQfvfo=
659661
github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=
660662
github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=
661663
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

internal/cmd/beta/beta.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake"
1212
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs"
1313
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex"
14+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn"
1415
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
1516
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
1617
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
@@ -47,4 +48,5 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) {
4748
cmd.AddCommand(edge.NewCmd(params))
4849
cmd.AddCommand(intake.NewCmd(params))
4950
cmd.AddCommand(cdn.NewCmd(params))
51+
cmd.AddCommand(vpn.NewCmd(params))
5052
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package connection
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/create"
6+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/delete"
7+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/describe"
8+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/list"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
12+
)
13+
14+
func NewCmd(p *types.CmdParams) *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "connection",
17+
Short: "Provides functionality for VPN connections",
18+
Long: "Provides functionality for VPN connections.",
19+
Args: args.NoArgs,
20+
Run: utils.CmdHelp,
21+
}
22+
addSubcommands(cmd, p)
23+
return cmd
24+
}
25+
26+
func addSubcommands(cmd *cobra.Command, p *types.CmdParams) {
27+
cmd.AddCommand(create.NewCmd(p))
28+
cmd.AddCommand(delete.NewCmd(p))
29+
cmd.AddCommand(describe.NewCmd(p))
30+
cmd.AddCommand(list.NewCmd(p))
31+
}

internal/cmd/beta/vpn/connection/create/create.go

Lines changed: 426 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
package create
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/google/go-cmp/cmp"
9+
"github.com/google/go-cmp/cmp/cmpopts"
10+
"github.com/google/uuid"
11+
vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api"
12+
13+
"github.com/spf13/cobra"
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/testparams"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
19+
sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
20+
)
21+
22+
type testCtxKey struct{}
23+
24+
var (
25+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "test")
26+
testProjectId = uuid.NewString()
27+
testGatewayID = uuid.NewString()
28+
testClient, _ = vpn.NewAPIClient(
29+
sdkConfig.WithoutAuthentication(),
30+
)
31+
)
32+
33+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
34+
flagValues := map[string]string{
35+
globalflags.ProjectIdFlag: testProjectId,
36+
gatewayIdFlag: testGatewayID,
37+
displayNameFlag: "test-connection",
38+
tunnel1RemoteAddressFlag: "1.2.3.4",
39+
tunnel1PreSharedKeyFlag: "test-psk-1",
40+
tunnel1Phase1EncryptionAlgorithmsFlag.Name(): "aes256",
41+
tunnel1Phase1IntegrityAlgorithmsFlag.Name(): "sha2_256",
42+
tunnel1Phase2EncryptionAlgorithmsFlag.Name(): "aes256",
43+
tunnel1Phase2IntegrityAlgorithmsFlag.Name(): "sha2_256",
44+
tunnel2RemoteAddressFlag: "5.6.7.8",
45+
tunnel2PreSharedKeyFlag: "test-psk-2",
46+
tunnel2Phase1EncryptionAlgorithmsFlag.Name(): "aes256",
47+
tunnel2Phase1IntegrityAlgorithmsFlag.Name(): "sha2_256",
48+
tunnel2Phase2EncryptionAlgorithmsFlag.Name(): "aes256",
49+
tunnel2Phase2IntegrityAlgorithmsFlag.Name(): "sha2_256",
50+
}
51+
for _, m := range mods {
52+
m(flagValues)
53+
}
54+
return flagValues
55+
}
56+
57+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
58+
model := &inputModel{
59+
GlobalFlagModel: &globalflags.GlobalFlagModel{
60+
Verbosity: globalflags.VerbosityDefault,
61+
ProjectId: testProjectId,
62+
},
63+
GatewayId: testGatewayID,
64+
DisplayName: "test-connection",
65+
Enabled: nil,
66+
Tunnel1RemoteAddress: "1.2.3.4",
67+
Tunnel1PreSharedKey: "test-psk-1",
68+
Tunnel1Phase1EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
69+
Tunnel1Phase1IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
70+
Tunnel1Phase2EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
71+
Tunnel1Phase2IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
72+
Tunnel2RemoteAddress: "5.6.7.8",
73+
Tunnel2PreSharedKey: "test-psk-2",
74+
Tunnel2Phase1EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
75+
Tunnel2Phase1IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
76+
Tunnel2Phase2EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
77+
Tunnel2Phase2IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
78+
}
79+
for _, mod := range mods {
80+
mod(model)
81+
}
82+
return model
83+
}
84+
85+
func fixtureRequest(mods ...func(request *vpn.ApiCreateGatewayConnectionRequest)) vpn.ApiCreateGatewayConnectionRequest {
86+
request := testClient.DefaultAPI.CreateGatewayConnection(testCtx, testProjectId, "", testGatewayID)
87+
payload := vpn.CreateGatewayConnectionPayload{
88+
DisplayName: "test-connection",
89+
Enabled: nil,
90+
Tunnel1: vpn.TunnelConfiguration{
91+
RemoteAddress: "1.2.3.4",
92+
PreSharedKey: utils.Ptr("test-psk-1"),
93+
Phase1: vpn.TunnelConfigurationPhase1{
94+
EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
95+
IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
96+
},
97+
Phase2: vpn.TunnelConfigurationPhase2{
98+
EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
99+
IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
100+
},
101+
},
102+
Tunnel2: vpn.TunnelConfiguration{
103+
RemoteAddress: "5.6.7.8",
104+
PreSharedKey: utils.Ptr("test-psk-2"),
105+
Phase1: vpn.TunnelConfigurationPhase1{
106+
EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
107+
IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
108+
},
109+
Phase2: vpn.TunnelConfigurationPhase2{
110+
EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
111+
IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
112+
},
113+
},
114+
}
115+
request = request.CreateGatewayConnectionPayload(payload)
116+
for _, mod := range mods {
117+
mod(&request)
118+
}
119+
return request
120+
}
121+
122+
func TestParseInput(t *testing.T) {
123+
tests := []struct {
124+
description string
125+
argValues []string
126+
flagValues map[string]string
127+
isValid bool
128+
expectedModel *inputModel
129+
}{
130+
{
131+
description: "base",
132+
argValues: []string{},
133+
flagValues: fixtureFlagValues(),
134+
isValid: true,
135+
expectedModel: fixtureInputModel(),
136+
},
137+
{
138+
description: "no flags",
139+
argValues: []string{},
140+
flagValues: map[string]string{},
141+
isValid: false,
142+
},
143+
{
144+
description: "missing project id",
145+
argValues: []string{},
146+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
147+
delete(flagValues, globalflags.ProjectIdFlag)
148+
}),
149+
isValid: false,
150+
},
151+
{
152+
description: "missing gateway id",
153+
argValues: []string{},
154+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
155+
delete(flagValues, gatewayIdFlag)
156+
}),
157+
isValid: false,
158+
},
159+
{
160+
description: "missing display name",
161+
argValues: []string{},
162+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
163+
delete(flagValues, displayNameFlag)
164+
}),
165+
isValid: false,
166+
},
167+
{
168+
description: "missing tunnel1 remote address",
169+
argValues: []string{},
170+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
171+
delete(flagValues, tunnel1RemoteAddressFlag)
172+
}),
173+
isValid: false,
174+
},
175+
}
176+
177+
for _, tt := range tests {
178+
t.Run(tt.description, func(t *testing.T) {
179+
testutils.TestParseInput(t, NewCmd, func(printer *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
180+
return parseInput(printer, cmd)
181+
}, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
182+
})
183+
}
184+
}
185+
186+
func TestBuildRequest(t *testing.T) {
187+
tests := []struct {
188+
description string
189+
model *inputModel
190+
expectedResult vpn.ApiCreateGatewayConnectionRequest
191+
}{
192+
{
193+
description: "base",
194+
model: fixtureInputModel(),
195+
expectedResult: fixtureRequest(),
196+
},
197+
{
198+
description: "with optional fields",
199+
model: fixtureInputModel(func(model *inputModel) {
200+
model.Labels = &map[string]string{"env": "prod"}
201+
model.LocalSubnets = []string{"10.0.0.0/24"}
202+
model.RemoteSubnets = []string{"192.168.0.0/24"}
203+
model.StaticRoutes = []string{"10.1.0.0/24"}
204+
model.Tunnel1BgpRemoteAsn = utils.Ptr(int64(65000))
205+
model.Tunnel1PeeringLocalAddress = utils.Ptr("169.254.0.1")
206+
model.Tunnel1PeeringRemoteAddress = utils.Ptr("169.254.0.2")
207+
model.Tunnel1Phase1DhGroups = []vpn.PhaseDhGroupsInner{"14"}
208+
model.Tunnel1Phase1RekeyTime = utils.Ptr(int32(3600))
209+
model.Tunnel1Phase2DhGroups = []vpn.PhaseDhGroupsInner{"14"}
210+
model.Tunnel1Phase2RekeyTime = utils.Ptr(int32(3600))
211+
model.Tunnel1Phase2DpdAction = utils.Ptr(vpn.TunnelConfigurationPhase2AllOfDpdAction("restart"))
212+
model.Tunnel1Phase2StartAction = utils.Ptr(vpn.TunnelConfigurationPhase2AllOfStartAction("start"))
213+
}),
214+
expectedResult: fixtureRequest(func(request *vpn.ApiCreateGatewayConnectionRequest) {
215+
payload := vpn.CreateGatewayConnectionPayload{
216+
DisplayName: "test-connection",
217+
Enabled: nil,
218+
Labels: &map[string]string{"env": "prod"},
219+
LocalSubnets: []string{"10.0.0.0/24"},
220+
RemoteSubnets: []string{"192.168.0.0/24"},
221+
StaticRoutes: []string{"10.1.0.0/24"},
222+
Tunnel1: vpn.TunnelConfiguration{
223+
RemoteAddress: "1.2.3.4",
224+
PreSharedKey: utils.Ptr("test-psk-1"),
225+
Bgp: &vpn.BGPTunnelConfig{
226+
RemoteAsn: 65000,
227+
},
228+
Peering: &vpn.PeeringConfig{
229+
LocalAddress: utils.Ptr("169.254.0.1"),
230+
RemoteAddress: utils.Ptr("169.254.0.2"),
231+
},
232+
Phase1: vpn.TunnelConfigurationPhase1{
233+
DhGroups: []vpn.PhaseDhGroupsInner{"14"},
234+
EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
235+
IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
236+
RekeyTime: utils.Ptr(int32(3600)),
237+
},
238+
Phase2: vpn.TunnelConfigurationPhase2{
239+
DhGroups: []vpn.PhaseDhGroupsInner{"14"},
240+
EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
241+
IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
242+
RekeyTime: utils.Ptr(int32(3600)),
243+
DpdAction: utils.Ptr(vpn.TunnelConfigurationPhase2AllOfDpdAction("restart")),
244+
StartAction: utils.Ptr(vpn.TunnelConfigurationPhase2AllOfStartAction("start")),
245+
},
246+
},
247+
Tunnel2: vpn.TunnelConfiguration{
248+
RemoteAddress: "5.6.7.8",
249+
PreSharedKey: utils.Ptr("test-psk-2"),
250+
Phase1: vpn.TunnelConfigurationPhase1{
251+
EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
252+
IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
253+
},
254+
Phase2: vpn.TunnelConfigurationPhase2{
255+
EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
256+
IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
257+
},
258+
},
259+
}
260+
*request = request.CreateGatewayConnectionPayload(payload)
261+
}),
262+
},
263+
}
264+
265+
for _, tt := range tests {
266+
t.Run(tt.description, func(t *testing.T) {
267+
request, err := buildRequest(testCtx, tt.model, testClient)
268+
if err != nil {
269+
t.Fatalf("unexpected error: %v", err)
270+
}
271+
272+
diff := cmp.Diff(request, tt.expectedResult,
273+
cmp.AllowUnexported(tt.expectedResult),
274+
cmpopts.IgnoreUnexported(vpn.DefaultAPIService{}),
275+
cmpopts.EquateComparable(testCtx),
276+
)
277+
if diff != "" {
278+
t.Fatalf("data does not match: %s", diff)
279+
}
280+
})
281+
}
282+
}
283+
284+
func TestOutputResult(t *testing.T) {
285+
tests := []struct {
286+
description string
287+
model *inputModel
288+
resp *vpn.ConnectionResponse
289+
expected string
290+
wantErr bool
291+
}{
292+
{
293+
description: "nil response",
294+
model: fixtureInputModel(),
295+
resp: nil,
296+
wantErr: true,
297+
expected: "",
298+
},
299+
{
300+
description: "success",
301+
model: fixtureInputModel(),
302+
resp: &vpn.ConnectionResponse{
303+
Id: utils.Ptr("conn-1234"),
304+
},
305+
expected: fmt.Sprintf("Created VPN connection \"conn-1234\" for gateway %q in project %q.\n", testGatewayID, testProjectId),
306+
},
307+
}
308+
309+
for _, tt := range tests {
310+
t.Run(tt.description, func(t *testing.T) {
311+
params := testparams.NewTestParams()
312+
err := outputResult(params.Printer, tt.model, testProjectId, tt.resp)
313+
if (err != nil) != tt.wantErr {
314+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
315+
}
316+
if !tt.wantErr && params.Out.String() != tt.expected {
317+
t.Errorf("want:\n%s\ngot:\n%s", tt.expected, params.Out.String())
318+
}
319+
})
320+
}
321+
}

0 commit comments

Comments
 (0)