Skip to content

Commit 39f5822

Browse files
committed
feat(vpn): implement vpn connection status command
1 parent 4cf05e8 commit 39f5822

3 files changed

Lines changed: 385 additions & 0 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/delete"
88
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/describe"
99
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/list"
10+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/status"
1011
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
1112
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
1213
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
@@ -29,4 +30,5 @@ func addSubcommands(cmd *cobra.Command, p *types.CmdParams) {
2930
cmd.AddCommand(delete.NewCmd(p))
3031
cmd.AddCommand(describe.NewCmd(p))
3132
cmd.AddCommand(list.NewCmd(p))
33+
cmd.AddCommand(status.NewCmd(p))
3234
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package status
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api"
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/services/vpn/client"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
19+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
20+
)
21+
22+
const (
23+
connectionIdArg = "CONNECTION_ID"
24+
25+
gatewayIdFlag = "gateway-id"
26+
)
27+
28+
type inputModel struct {
29+
*globalflags.GlobalFlagModel
30+
GatewayId *string
31+
ConnectionId string
32+
}
33+
34+
func NewCmd(p *types.CmdParams) *cobra.Command {
35+
cmd := &cobra.Command{
36+
Use: fmt.Sprintf("status %s", connectionIdArg),
37+
Short: "Shows the status of a VPN connection",
38+
Long: "Shows the status of a VPN connection.",
39+
Args: args.SingleArg(connectionIdArg, nil),
40+
Example: examples.Build(
41+
examples.NewExample(
42+
`Show status of a VPN connection`,
43+
"$ stackit beta vpn connection status xxx --gateway-id yyy"),
44+
),
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
ctx := context.Background()
47+
model, err := parseInput(p.Printer, cmd, args)
48+
if err != nil {
49+
return err
50+
}
51+
52+
// Configure API client
53+
apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion)
54+
if err != nil {
55+
return err
56+
}
57+
58+
// Call API
59+
req, err := buildRequest(ctx, model, apiClient)
60+
if err != nil {
61+
return err
62+
}
63+
resp, err := req.Execute()
64+
if err != nil {
65+
return fmt.Errorf("get VPN connection status: %w", err)
66+
}
67+
68+
return outputResult(p.Printer, model, resp)
69+
},
70+
}
71+
configureFlags(cmd)
72+
return cmd
73+
}
74+
75+
func configureFlags(cmd *cobra.Command) {
76+
cmd.Flags().Var(flags.UUIDFlag(), gatewayIdFlag, "Gateway ID")
77+
78+
err := flags.MarkFlagsRequired(cmd, gatewayIdFlag)
79+
cobra.CheckErr(err)
80+
}
81+
82+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
83+
connectionId := inputArgs[0]
84+
85+
globalFlags := globalflags.Parse(p, cmd)
86+
if globalFlags.ProjectId == "" {
87+
return nil, &errors.ProjectIdError{}
88+
}
89+
90+
model := inputModel{
91+
GlobalFlagModel: globalFlags,
92+
GatewayId: flags.FlagToStringPointer(p, cmd, gatewayIdFlag),
93+
ConnectionId: connectionId,
94+
}
95+
96+
p.DebugInputModel(model)
97+
return &model, nil
98+
}
99+
100+
func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) (vpn.ApiGetGatewayConnectionStatusRequest, error) {
101+
req := apiClient.DefaultAPI.GetGatewayConnectionStatus(ctx, model.ProjectId, model.Region, *model.GatewayId, model.ConnectionId)
102+
return req, nil
103+
}
104+
105+
func outputResult(p *print.Printer, model *inputModel, resp *vpn.ConnectionStatusResponse) error {
106+
if resp == nil {
107+
return fmt.Errorf("status response is empty")
108+
}
109+
110+
return p.OutputResult(model.OutputFormat, resp, func() error {
111+
mainTable := tables.NewTable()
112+
mainTable.AddRow("ID", utils.PtrString(resp.Id))
113+
mainTable.AddRow("Name", utils.PtrString(resp.DisplayName))
114+
mainTable.AddRow("Enabled", utils.PtrString(resp.Enabled))
115+
116+
ts := []tables.Table{
117+
mainTable,
118+
}
119+
for _, tunnel := range resp.Tunnels {
120+
ts = append(ts, tunnelTables(&tunnel)...)
121+
}
122+
123+
return tables.DisplayTables(p, ts)
124+
})
125+
}
126+
127+
func tunnelTables(tunnel *vpn.TunnelStatus) []tables.Table {
128+
title := "Tunnel"
129+
if tunnel.Name != nil {
130+
title = string(*tunnel.Name)
131+
}
132+
133+
table := tables.NewTable()
134+
table.SetTitle(title)
135+
table.AddRow("Established", utils.PtrString(tunnel.Established))
136+
137+
res := []tables.Table{table}
138+
139+
if tunnel.Phase1 != nil {
140+
phase1Table := tables.NewTable()
141+
phase1Table.SetTitle(fmt.Sprintf("%s Phase 1", title))
142+
phase1Table.AddRow("State", utils.PtrString(tunnel.Phase1.State))
143+
phase1Table.AddRow("DH Group", utils.PtrString(tunnel.Phase1.DhGroup))
144+
phase1Table.AddRow("Encryption Algo", utils.PtrString(tunnel.Phase1.EncryptionAlgorithm))
145+
phase1Table.AddRow("Integrity Algo", utils.PtrString(tunnel.Phase1.IntegrityAlgorithm))
146+
res = append(res, phase1Table)
147+
}
148+
149+
if tunnel.Phase2 != nil {
150+
phase2Table := tables.NewTable()
151+
phase2Table.SetTitle(fmt.Sprintf("%s Phase 2", title))
152+
phase2Table.AddRow("State", utils.PtrString(tunnel.Phase2.State))
153+
phase2Table.AddRow("Protocol", utils.PtrString(tunnel.Phase2.Protocol))
154+
phase2Table.AddRow("DH Group", utils.PtrString(tunnel.Phase2.DhGroup))
155+
phase2Table.AddRow("Encryption Algo", utils.PtrString(tunnel.Phase2.EncryptionAlgorithm))
156+
phase2Table.AddRow("Integrity Algo", utils.PtrString(tunnel.Phase2.IntegrityAlgorithm))
157+
phase2Table.AddRow("Encap", utils.PtrString(tunnel.Phase2.Encap))
158+
phase2Table.AddRow("Bytes In/Out", fmt.Sprintf("%s / %s", utils.PtrString(tunnel.Phase2.BytesIn), utils.PtrString(tunnel.Phase2.BytesOut)))
159+
phase2Table.AddRow("Packets In/Out", fmt.Sprintf("%s / %s", utils.PtrString(tunnel.Phase2.PacketsIn), utils.PtrString(tunnel.Phase2.PacketsOut)))
160+
res = append(res, phase2Table)
161+
}
162+
163+
return res
164+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package status
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+
vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api"
11+
12+
sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
13+
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/testparams"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
18+
)
19+
20+
type testCtxKey struct{}
21+
22+
var (
23+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "test")
24+
testProjectId = uuid.NewString()
25+
testGatewayID = uuid.NewString()
26+
testConnectionID = uuid.NewString()
27+
testClient, _ = vpn.NewAPIClient(
28+
sdkConfig.WithoutAuthentication(),
29+
)
30+
)
31+
32+
func fixtureArgValues(mods ...func(argValues []string)) []string {
33+
argValues := []string{
34+
testConnectionID,
35+
}
36+
for _, m := range mods {
37+
m(argValues)
38+
}
39+
return argValues
40+
}
41+
42+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
43+
flagValues := map[string]string{
44+
globalflags.ProjectIdFlag: testProjectId,
45+
gatewayIdFlag: testGatewayID,
46+
}
47+
for _, m := range mods {
48+
m(flagValues)
49+
}
50+
return flagValues
51+
}
52+
53+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
54+
model := &inputModel{
55+
GlobalFlagModel: &globalflags.GlobalFlagModel{
56+
Verbosity: globalflags.VerbosityDefault,
57+
ProjectId: testProjectId,
58+
},
59+
GatewayId: utils.Ptr(testGatewayID),
60+
ConnectionId: testConnectionID,
61+
}
62+
for _, mod := range mods {
63+
mod(model)
64+
}
65+
return model
66+
}
67+
68+
func fixtureRequest(mods ...func(request *vpn.ApiGetGatewayConnectionStatusRequest)) vpn.ApiGetGatewayConnectionStatusRequest {
69+
request := testClient.DefaultAPI.GetGatewayConnectionStatus(testCtx, testProjectId, "", testGatewayID, testConnectionID)
70+
for _, mod := range mods {
71+
mod(&request)
72+
}
73+
return request
74+
}
75+
76+
func fixtureResponse(mods ...func(resp *vpn.ConnectionStatusResponse)) *vpn.ConnectionStatusResponse {
77+
resp := &vpn.ConnectionStatusResponse{
78+
Id: utils.Ptr(testConnectionID),
79+
DisplayName: utils.Ptr("test-connection"),
80+
Enabled: utils.Ptr(true),
81+
Tunnels: []vpn.TunnelStatus{
82+
{
83+
Name: utils.Ptr(vpn.TunnelStatusName("tunnel1")),
84+
Established: utils.Ptr(true),
85+
Phase1: &vpn.Phase1Status{
86+
DhGroup: utils.Ptr("MODP2048"),
87+
EncryptionAlgorithm: utils.Ptr("AES_GCM_16"),
88+
IntegrityAlgorithm: utils.Ptr("SHA_256"),
89+
State: utils.Ptr("INSTALLED"),
90+
},
91+
Phase2: &vpn.Phase2Status{
92+
BytesIn: utils.Ptr("453533"),
93+
BytesOut: utils.Ptr("46459064"),
94+
DhGroup: utils.Ptr("MODP2048"),
95+
Encap: utils.Ptr("yes"),
96+
EncryptionAlgorithm: utils.Ptr("AES_GCM_16"),
97+
IntegrityAlgorithm: utils.Ptr("SHA_256"),
98+
PacketsIn: utils.Ptr("1534134"),
99+
PacketsOut: utils.Ptr("65847343"),
100+
Protocol: utils.Ptr("ESP"),
101+
State: utils.Ptr("ESTABLISHED"),
102+
},
103+
},
104+
},
105+
}
106+
for _, mod := range mods {
107+
mod(resp)
108+
}
109+
return resp
110+
}
111+
112+
func TestParseInput(t *testing.T) {
113+
tests := []struct {
114+
description string
115+
argValues []string
116+
flagValues map[string]string
117+
isValid bool
118+
expectedModel *inputModel
119+
}{
120+
{
121+
description: "base",
122+
argValues: fixtureArgValues(),
123+
flagValues: fixtureFlagValues(),
124+
isValid: true,
125+
expectedModel: fixtureInputModel(),
126+
},
127+
{
128+
description: "no args",
129+
argValues: []string{},
130+
flagValues: fixtureFlagValues(),
131+
isValid: false,
132+
},
133+
{
134+
description: "no gateway id",
135+
argValues: fixtureArgValues(),
136+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
137+
delete(flagValues, gatewayIdFlag)
138+
}),
139+
isValid: false,
140+
},
141+
{
142+
description: "no project id",
143+
argValues: fixtureArgValues(),
144+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
145+
delete(flagValues, globalflags.ProjectIdFlag)
146+
}),
147+
isValid: false,
148+
},
149+
}
150+
151+
for _, tt := range tests {
152+
t.Run(tt.description, func(t *testing.T) {
153+
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
154+
})
155+
}
156+
}
157+
158+
func TestBuildRequest(t *testing.T) {
159+
tests := []struct {
160+
name string
161+
model *inputModel
162+
expected vpn.ApiGetGatewayConnectionStatusRequest
163+
}{
164+
{
165+
name: "base",
166+
model: fixtureInputModel(),
167+
expected: fixtureRequest(),
168+
},
169+
}
170+
171+
for _, tt := range tests {
172+
t.Run(tt.name, func(t *testing.T) {
173+
request, err := buildRequest(testCtx, tt.model, testClient)
174+
if err != nil {
175+
t.Fatalf("unexpected error: %v", err)
176+
}
177+
178+
diff := cmp.Diff(request, tt.expected,
179+
cmp.AllowUnexported(tt.expected),
180+
cmpopts.IgnoreUnexported(vpn.DefaultAPIService{}),
181+
cmpopts.EquateComparable(testCtx),
182+
)
183+
if diff != "" {
184+
t.Fatalf("data does not match: %s", diff)
185+
}
186+
})
187+
}
188+
}
189+
190+
func TestOutputResult(t *testing.T) {
191+
tests := []struct {
192+
description string
193+
model *inputModel
194+
resp *vpn.ConnectionStatusResponse
195+
wantErr bool
196+
}{
197+
{
198+
description: "nil response",
199+
model: fixtureInputModel(),
200+
resp: nil,
201+
wantErr: true,
202+
},
203+
{
204+
description: "full response",
205+
model: fixtureInputModel(),
206+
resp: fixtureResponse(),
207+
},
208+
}
209+
210+
for _, tt := range tests {
211+
t.Run(tt.description, func(t *testing.T) {
212+
params := testparams.NewTestParams()
213+
err := outputResult(params.Printer, tt.model, tt.resp)
214+
if (err != nil) != tt.wantErr {
215+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
216+
}
217+
})
218+
}
219+
}

0 commit comments

Comments
 (0)