From 6a041f902d42b0953e937bf5cf7bc7176e2afd3b Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Thu, 2 Jul 2026 13:22:49 +0200 Subject: [PATCH 01/11] feat(vpn): onboard gateway relates to STACKITCLI-353 --- .../cmd/beta/vpn/gateway/create/create.go | 217 ++++++++++++++++ .../beta/vpn/gateway/create/create_test.go | 238 ++++++++++++++++++ .../cmd/beta/vpn/gateway/delete/delete.go | 120 +++++++++ .../beta/vpn/gateway/delete/delete_test.go | 174 +++++++++++++ .../cmd/beta/vpn/gateway/describe/describe.go | 125 +++++++++ .../vpn/gateway/describe/describe_test.go | 212 ++++++++++++++++ internal/cmd/beta/vpn/gateway/gateway.go | 32 +++ internal/cmd/beta/vpn/gateway/list/list.go | 136 ++++++++++ .../cmd/beta/vpn/gateway/list/list_test.go | 196 +++++++++++++++ internal/cmd/beta/vpn/plans/plans.go | 136 ++++++++++ internal/cmd/beta/vpn/plans/plans_test.go | 196 +++++++++++++++ internal/cmd/beta/vpn/quotas/quotas.go | 121 +++++++++ internal/cmd/beta/vpn/quotas/quotas_test.go | 189 ++++++++++++++ internal/cmd/beta/vpn/vpn.go | 6 + internal/cmd/config/set/set.go | 4 + internal/cmd/config/unset/unset.go | 7 + internal/cmd/config/unset/unset_test.go | 13 + internal/pkg/config/config.go | 2 + internal/pkg/services/vpn/utils/utils.go | 19 ++ internal/pkg/services/vpn/utils/utils_test.go | 87 +++++++ 20 files changed, 2230 insertions(+) create mode 100644 internal/cmd/beta/vpn/gateway/create/create.go create mode 100644 internal/cmd/beta/vpn/gateway/create/create_test.go create mode 100644 internal/cmd/beta/vpn/gateway/delete/delete.go create mode 100644 internal/cmd/beta/vpn/gateway/delete/delete_test.go create mode 100644 internal/cmd/beta/vpn/gateway/describe/describe.go create mode 100644 internal/cmd/beta/vpn/gateway/describe/describe_test.go create mode 100644 internal/cmd/beta/vpn/gateway/gateway.go create mode 100644 internal/cmd/beta/vpn/gateway/list/list.go create mode 100644 internal/cmd/beta/vpn/gateway/list/list_test.go create mode 100644 internal/cmd/beta/vpn/plans/plans.go create mode 100644 internal/cmd/beta/vpn/plans/plans_test.go create mode 100644 internal/cmd/beta/vpn/quotas/quotas.go create mode 100644 internal/cmd/beta/vpn/quotas/quotas_test.go create mode 100644 internal/pkg/services/vpn/utils/utils.go create mode 100644 internal/pkg/services/vpn/utils/utils_test.go diff --git a/internal/cmd/beta/vpn/gateway/create/create.go b/internal/cmd/beta/vpn/gateway/create/create.go new file mode 100644 index 000000000..6aa98e38e --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/create/create.go @@ -0,0 +1,217 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api/wait" +) + +const ( + availabilityZoneTunnel1Flag = "availability-zone-tunnel-1" + availabilityZoneTunnel2Flag = "availability-zone-tunnel-2" + bgpLocalAsnFlag = "bgp-local-asn" + bgpOverrideAdvertisedRoutesFlag = "bgp-override-advertised-routes" + nameFlag = "name" + labelsFlag = "labels" + planIdFlag = "plan-id" + routingTypeFlag = "routing-type" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + AvailabilityZone vpn.CreateGatewayPayloadAvailabilityZones + Bgp *vpn.BGPGatewayConfig + Name string + Labels *map[string]string + PlanId string + RoutingType vpn.RoutingType +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a vpn gateway", + Long: "Creates a vpn gateway.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a vpn gateway with name "xxx", plan "p500", policy based routing and both tunnels in availability-zone eu01-1`, + "$ 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", + ), + examples.NewExample( + `Create a vpn gateway with the labels foo=bar and x=y`, + "$ 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", + ), + examples.NewExample( + `Create a vpn gateway with bgp enabled, yyy as local asn and [aaa, bbb] as override advertised routes`, + "$ 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", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil || projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create a vpn gateway for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create vpn gateway: %w", err) + } + var gatewayId string + if resp != nil && resp.HasId() { + gatewayId = *resp.Id + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Creating vpn gateway", func() error { + _, err = wait.CreateGatewayWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, gatewayId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("waiting for vpn gateway creation: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(availabilityZoneTunnel1Flag, "", "Availability Zone of Tunnel 1") + cmd.Flags().String(availabilityZoneTunnel2Flag, "", "Availability Zone of Tunnel 2") + cmd.Flags().Int64(bgpLocalAsnFlag, 0, "ASN for private use (reserved by IANA), both 16Bit and 32Bit ranges are valid (RFC 6996)") + cmd.Flags().StringArray(bgpOverrideAdvertisedRoutesFlag, nil, "A list of IPv4 Prefixes to advertise via BGP") + cmd.Flags().String(nameFlag, "", "Gateway name") + cmd.Flags().StringToString(labelsFlag, nil, "Labels in key=value format, separated by commas") + cmd.Flags().String(planIdFlag, "", "Plan ID") + cmd.Flags().String(routingTypeFlag, "", fmt.Sprintf("Routing Type: %q", vpn.AllowedRoutingTypeEnumValues)) + // cmd.Flags().Var(flags.EnumFlag(false, "", sdkUtils.EnumSliceToStringSlice(vpn.AllowedRoutingTypeEnumValues)...), routingTypeFlag, fmt.Sprintf("Routing Type, one of: %q", vpn.AllowedRoutingTypeEnumValues)) + + err := flags.MarkFlagsRequired(cmd, availabilityZoneTunnel1Flag, availabilityZoneTunnel2Flag, nameFlag, planIdFlag, routingTypeFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + bgpLocalAsn := flags.FlagToInt64Pointer(p, cmd, bgpLocalAsnFlag) + if bgpLocalAsn != nil { + if *bgpLocalAsn < 0 { + return nil, &errors.FlagValidationError{ + Flag: bgpLocalAsnFlag, + Details: "must be a positive integer", + } + } + } + bgpOverrideAdvertisedRoutes := flags.FlagToStringArrayValue(p, cmd, bgpOverrideAdvertisedRoutesFlag) + if bgpOverrideAdvertisedRoutes != nil { + if bgpLocalAsn == nil { + return nil, &errors.DependingFlagIsMissing{ + MissingFlag: bgpLocalAsnFlag, + SetFlag: bgpOverrideAdvertisedRoutesFlag, + } + } + } + + var bgp *vpn.BGPGatewayConfig + if bgpLocalAsn != nil || bgpOverrideAdvertisedRoutes != nil { + bgp = &vpn.BGPGatewayConfig{ + LocalAsn: *bgpLocalAsn, + OverrideAdvertisedRoutes: flags.FlagToStringArrayValue(p, cmd, bgpOverrideAdvertisedRoutesFlag), + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + AvailabilityZone: vpn.CreateGatewayPayloadAvailabilityZones{ + Tunnel1: flags.FlagToStringValue(p, cmd, availabilityZoneTunnel1Flag), + Tunnel2: flags.FlagToStringValue(p, cmd, availabilityZoneTunnel2Flag), + }, + Bgp: bgp, + Name: flags.FlagToStringValue(p, cmd, nameFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + PlanId: flags.FlagToStringValue(p, cmd, planIdFlag), + RoutingType: vpn.RoutingType(flags.FlagToStringValue(p, cmd, routingTypeFlag)), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) vpn.ApiCreateGatewayRequest { + req := apiClient.DefaultAPI.CreateGateway(ctx, model.ProjectId, model.Region) + req = req.CreateGatewayPayload( + vpn.CreateGatewayPayload{ + AvailabilityZones: model.AvailabilityZone, + Bgp: model.Bgp, + DisplayName: model.Name, + Labels: model.Labels, + PlanId: model.PlanId, + RoutingType: model.RoutingType, + }, + ) + return req +} + +func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel string, item *vpn.GatewayResponse) error { + return p.OutputResult(outputFormat, item, func() error { + if item == nil { + p.Outputln("vpn gateway response is empty") + return nil + } + operation := "Created" + if async { + operation = "Triggered creation of" + } + p.Outputf( + "%s vpn gateway %q in project %q.\nGateway ID: %s\n", + operation, + item.DisplayName, + projectLabel, + utils.PtrString(item.Id), + ) + return nil + }) +} diff --git a/internal/cmd/beta/vpn/gateway/create/create_test.go b/internal/cmd/beta/vpn/gateway/create/create_test.go new file mode 100644 index 000000000..0a389c551 --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/create/create_test.go @@ -0,0 +1,238 @@ +package create + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &vpn.APIClient{DefaultAPI: &vpn.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testName = "test-name" +var testPlanId = "planId" +var testRoutingType = vpn.ROUTINGTYPE_POLICY_BASED +var testAvailabilityZoneTunnel1 = "eu01-1" +var testAvailabilityZoneTunnel2 = "eu01-2" +var testBgpLocalAsn = 64512 +var testBgpOverrideAdvertisedRoutes = "10.10.10.10/32" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + availabilityZoneTunnel1Flag: testAvailabilityZoneTunnel1, + availabilityZoneTunnel2Flag: testAvailabilityZoneTunnel2, + bgpLocalAsnFlag: strconv.Itoa(testBgpLocalAsn), + bgpOverrideAdvertisedRoutesFlag: testBgpOverrideAdvertisedRoutes, + nameFlag: testName, + labelsFlag: "foo=bar", + planIdFlag: testPlanId, + routingTypeFlag: string(testRoutingType), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Name: testName, + AvailabilityZone: vpn.CreateGatewayPayloadAvailabilityZones{ + Tunnel1: testAvailabilityZoneTunnel1, + Tunnel2: testAvailabilityZoneTunnel2, + }, + Bgp: &vpn.BGPGatewayConfig{ + LocalAsn: int64(testBgpLocalAsn), + OverrideAdvertisedRoutes: []string{ + testBgpOverrideAdvertisedRoutes, + }, + }, + Labels: &map[string]string{ + "foo": "bar", + }, + PlanId: testPlanId, + RoutingType: testRoutingType, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiCreateGatewayRequest)) vpn.ApiCreateGatewayRequest { + request := testClient.DefaultAPI.CreateGateway(testCtx, testProjectId, testRegion) + request = request.CreateGatewayPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(request *vpn.CreateGatewayPayload)) vpn.CreateGatewayPayload { + payload := vpn.CreateGatewayPayload{ + AvailabilityZones: vpn.CreateGatewayPayloadAvailabilityZones{ + Tunnel1: testAvailabilityZoneTunnel1, + Tunnel2: testAvailabilityZoneTunnel2, + }, + Bgp: &vpn.BGPGatewayConfig{ + LocalAsn: int64(testBgpLocalAsn), + OverrideAdvertisedRoutes: []string{ + testBgpOverrideAdvertisedRoutes, + }, + }, + DisplayName: testName, + Labels: &map[string]string{ + "foo": "bar", + }, + PlanId: testPlanId, + RoutingType: testRoutingType, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "required only", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelsFlag) + delete(flagValues, bgpLocalAsnFlag) + delete(flagValues, bgpOverrideAdvertisedRoutesFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Labels = nil + model.Bgp = nil + }), + }, + { + description: "multiple labels", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[labelsFlag] = "label1=foo,label2=bar" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Labels = &map[string]string{ + "label1": "foo", + "label2": "bar", + } + }), + }, + { + description: "missing required name", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest vpn.ApiCreateGatewayRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + cmp.AllowUnexported(vpn.NullableString{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + async bool + projectLabel string + item *vpn.GatewayResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + item: &vpn.GatewayResponse{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.item); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/gateway/delete/delete.go b/internal/cmd/beta/vpn/gateway/delete/delete.go new file mode 100644 index 000000000..5fdc74a6d --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/delete/delete.go @@ -0,0 +1,120 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + vpnUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + gatewayIdArg = "GATEWAY_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + GatewayId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", gatewayIdArg), + Short: "Deletes a vpn gateway", + Long: "Deletes a vpn gateway.", + Args: args.SingleArg(gatewayIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a vpn gateway with the ID "xxx"`, + "$ stackit beta vpn gateway delete xxx", + ), + ), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + gatewayLabel, err := vpnUtils.GetGatewayName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.GatewayId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get gateway name: %v", err) + gatewayLabel = model.GatewayId + } else if gatewayLabel == "" { + gatewayLabel = model.GatewayId + } + + prompt := fmt.Sprintf("Are you sure you want to delete the vpn gateway %q? (This cannot be undone)", gatewayLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete vpn gateway: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Deleting gateway", func() error { + _, err = wait.DeleteGatewayWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.GatewayId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("waiting for gateway deletion: %w", err) + } + } + + operation := "Deleted" + if model.Async { + operation = "Triggered deletion of" + } + + params.Printer.Outputf("%s gateway %q\n", operation, gatewayLabel) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + gatewayId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + GatewayId: gatewayId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) vpn.ApiDeleteGatewayRequest { + return apiClient.DefaultAPI.DeleteGateway(ctx, model.ProjectId, model.Region, model.GatewayId) +} diff --git a/internal/cmd/beta/vpn/gateway/delete/delete_test.go b/internal/cmd/beta/vpn/gateway/delete/delete_test.go new file mode 100644 index 000000000..145a49ca8 --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/delete/delete_test.go @@ -0,0 +1,174 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &vpn.APIClient{DefaultAPI: &vpn.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" +var testGatewayId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testGatewayId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + GatewayId: testGatewayId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiDeleteGatewayRequest)) vpn.ApiDeleteGatewayRequest { + request := testClient.DefaultAPI.DeleteGateway(testCtx, testProjectId, testRegion, testGatewayId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "gateway id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "gateway id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest vpn.ApiDeleteGatewayRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/gateway/describe/describe.go b/internal/cmd/beta/vpn/gateway/describe/describe.go new file mode 100644 index 000000000..874fcee27 --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/describe/describe.go @@ -0,0 +1,125 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + gatewayIdArg = "GATEWAY_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + GatewayId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", gatewayIdArg), + Short: "Shows details of a gateway", + Long: "Shows details of a gateway.", + Args: args.SingleArg(gatewayIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a gateway with ID "xxx"`, + "$ stackit beta vpn gateway describe xxx", + ), + ), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("describe vpn gateway: %w", err) + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil || projectLabel == "" { + projectLabel = model.ProjectId + } + + return outputResult(params.Printer, model.OutputFormat, model.GatewayId, projectLabel, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + gatewayId := inputArgs[0] + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + GatewayId: gatewayId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) vpn.ApiGetGatewayRequest { + return apiClient.DefaultAPI.GetGateway(ctx, model.ProjectId, model.Region, model.GatewayId) +} + +func outputResult(p *print.Printer, outputFormat, gatewayId, projectLabel string, gateway *vpn.GatewayResponse) error { + return p.OutputResult(outputFormat, gateway, func() error { + if gateway == nil { + p.Outputf("gateway %q not found in project %q\n", gatewayId, projectLabel) + return nil + } + + table := tables.NewTable() + table.SetTitle("Gateway") + + table.AddRow("ID", utils.PtrString(gateway.Id)) + table.AddSeparator() + table.AddRow("NAME", gateway.DisplayName) + table.AddSeparator() + table.AddRow("Labels", gateway.GetLabels()) + table.AddSeparator() + table.AddRow("STATE", utils.PtrString(gateway.State)) + table.AddSeparator() + table.AddRow("Plan ID", gateway.PlanId) + table.AddSeparator() + table.AddRow("Routing Type", gateway.RoutingType) + table.AddSeparator() + table.AddRow("Availability Zones Tunnel 1", gateway.AvailabilityZones.Tunnel1) + table.AddSeparator() + table.AddRow("Availability Zones Tunnel 2", gateway.AvailabilityZones.Tunnel2) + + if err := table.Display(p); err != nil { + return fmt.Errorf("render tables: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/vpn/gateway/describe/describe_test.go b/internal/cmd/beta/vpn/gateway/describe/describe_test.go new file mode 100644 index 000000000..d0ca98f5a --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/describe/describe_test.go @@ -0,0 +1,212 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &vpn.APIClient{DefaultAPI: &vpn.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testGatewayId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testGatewayId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + GatewayId: testGatewayId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiGetGatewayRequest)) vpn.ApiGetGatewayRequest { + request := testClient.DefaultAPI.GetGateway(testCtx, testProjectId, testRegion, testGatewayId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "gateway id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "gateway id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest vpn.ApiGetGatewayRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + gatewayId string + projectLabel string + gateway *vpn.GatewayResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + gateway: &vpn.GatewayResponse{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.gatewayId, tt.args.gateway); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/gateway/gateway.go b/internal/cmd/beta/vpn/gateway/gateway.go new file mode 100644 index 000000000..c86a0894a --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/gateway.go @@ -0,0 +1,32 @@ +package gateway + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/gateway/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/gateway/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/gateway/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/gateway/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "gateway", + Short: "Provides functionality for VPN gateway", + Long: "Provides functionality for VPN gateway.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/beta/vpn/gateway/list/list.go b/internal/cmd/beta/vpn/gateway/list/list.go new file mode 100644 index 000000000..a424eb241 --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/list/list.go @@ -0,0 +1,136 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all vpn gateways", + Long: "Lists all vpn gateways.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all vpn gateways`, + "$ stackit beta vpn gateway list", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list vpn gateways: %w", err) + } + + // Truncate output + items := utils.GetSliceFromPointer(&resp.Gateways) + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil || projectLabel == "" { + projectLabel = model.ProjectId + } + + return outputResult(params.Printer, model.OutputFormat, items, projectLabel) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be grater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) vpn.ApiListGatewaysRequest { + return apiClient.DefaultAPI.ListGateways(ctx, model.ProjectId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat string, gateways []vpn.GatewayResponse, projectLabel string) error { + return p.OutputResult(outputFormat, gateways, func() error { + + if len(gateways) == 0 { + p.Info("No gateways found for %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "PLAN ID", "ROUTING TYPE", "STATE") + + for _, gateway := range gateways { + table.AddRow( + utils.PtrString(gateway.Id), + gateway.DisplayName, + gateway.PlanId, + gateway.RoutingType, + utils.PtrString(gateway.State), + ) + } + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/vpn/gateway/list/list_test.go b/internal/cmd/beta/vpn/gateway/list/list_test.go new file mode 100644 index 000000000..9674587aa --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/list/list_test.go @@ -0,0 +1,196 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &vpn.APIClient{DefaultAPI: &vpn.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testLimit int64 = 10 + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + limitFlag: strconv.FormatInt(testLimit, 10), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Limit: utils.Ptr(testLimit), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiListGatewaysRequest)) vpn.ApiListGatewaysRequest { + request := testClient.DefaultAPI.ListGateways(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "invalid limit 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + { + description: "invalid limit 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-1" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest vpn.ApiListGatewaysRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + gateways []vpn.GatewayResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty gateway in gateways", + args: args{ + gateways: []vpn.GatewayResponse{{}}, + }, + wantErr: false, + }, + { + name: "set empty gateway", + args: args{ + gateways: []vpn.GatewayResponse{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.gateways, tt.args.projectLabel); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/plans/plans.go b/internal/cmd/beta/vpn/plans/plans.go new file mode 100644 index 000000000..8f94a6a30 --- /dev/null +++ b/internal/cmd/beta/vpn/plans/plans.go @@ -0,0 +1,136 @@ +package plans + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "plans", + Short: "Lists all available plans", + Long: "Lists all available plans.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all available plans`, + "$ stackit beta vpn plans", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list vpn plans: %w", err) + } + + // Truncate output + items := utils.GetSliceFromPointer(&resp.Plans) + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil || projectLabel == "" { + projectLabel = model.ProjectId + } + + return outputResult(params.Printer, model.OutputFormat, items, projectLabel) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be grater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) vpn.ApiListPlansRequest { + return apiClient.DefaultAPI.ListPlans(ctx, model.Region) +} + +func outputResult(p *print.Printer, outputFormat string, plans []vpn.Plan, projectLabel string) error { + return p.OutputResult(outputFormat, plans, func() error { + + if len(plans) == 0 { + p.Info("No plans found for %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "MAX BANDWIDTH", "MAX CONNECTIONS", "SKU") + + for _, plan := range plans { + table.AddRow( + utils.PtrString(plan.PlanId), + utils.PtrString(plan.Name), + utils.PtrString(plan.MaxBandwidth), + utils.PtrString(plan.MaxConnections), + utils.PtrString(plan.Sku), + ) + } + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/vpn/plans/plans_test.go b/internal/cmd/beta/vpn/plans/plans_test.go new file mode 100644 index 000000000..db017d48f --- /dev/null +++ b/internal/cmd/beta/vpn/plans/plans_test.go @@ -0,0 +1,196 @@ +package plans + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &vpn.APIClient{DefaultAPI: &vpn.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testLimit int64 = 10 + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + limitFlag: strconv.FormatInt(testLimit, 10), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Limit: utils.Ptr(testLimit), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiListPlansRequest)) vpn.ApiListPlansRequest { + request := testClient.DefaultAPI.ListPlans(testCtx, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "invalid limit 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + { + description: "invalid limit 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-1" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest vpn.ApiListPlansRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + plans []vpn.Plan + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty plan in plans", + args: args{ + plans: []vpn.Plan{{}}, + }, + wantErr: false, + }, + { + name: "set empty plan", + args: args{ + plans: []vpn.Plan{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.plans, tt.args.projectLabel); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/quotas/quotas.go b/internal/cmd/beta/vpn/quotas/quotas.go new file mode 100644 index 000000000..5a338fee3 --- /dev/null +++ b/internal/cmd/beta/vpn/quotas/quotas.go @@ -0,0 +1,121 @@ +package quotas + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "quotas", + Short: "Lists all vpn quotas", + Long: "Lists all vpn quotas.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all vpn quotas`, + "$ stackit beta vpn quotas", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list vpn quotas: %w", err) + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil || projectLabel == "" { + projectLabel = model.ProjectId + } + + return outputResult(params.Printer, model.OutputFormat, resp, projectLabel) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be grater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) vpn.ApiListQuotasRequest { + return apiClient.DefaultAPI.ListQuotas(ctx, model.ProjectId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat string, quota *vpn.QuotaListResponse, projectLabel string) error { + return p.OutputResult(outputFormat, quota, func() error { + + if quota == nil { + p.Info("No quotas for %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("NAME", "LIMIT", "CURRENT USAGE") + + table.AddRow("Gateway Instances", quota.Quotas.Gateways.Limit, quota.Quotas.Gateways.Usage) + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/vpn/quotas/quotas_test.go b/internal/cmd/beta/vpn/quotas/quotas_test.go new file mode 100644 index 000000000..d5579ff09 --- /dev/null +++ b/internal/cmd/beta/vpn/quotas/quotas_test.go @@ -0,0 +1,189 @@ +package quotas + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &vpn.APIClient{DefaultAPI: &vpn.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testLimit int64 = 10 + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + limitFlag: strconv.FormatInt(testLimit, 10), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Limit: utils.Ptr(testLimit), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiListQuotasRequest)) vpn.ApiListQuotasRequest { + request := testClient.DefaultAPI.ListQuotas(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "invalid limit 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + { + description: "invalid limit 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-1" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest vpn.ApiListQuotasRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + quota *vpn.QuotaListResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty quotas", + args: args{ + quota: &vpn.QuotaListResponse{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.quota, tt.args.projectLabel); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/vpn.go b/internal/cmd/beta/vpn/vpn.go index 8ee353974..499d41d45 100644 --- a/internal/cmd/beta/vpn/vpn.go +++ b/internal/cmd/beta/vpn/vpn.go @@ -4,6 +4,9 @@ import ( "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/gateway" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/plans" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/quotas" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) @@ -22,4 +25,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(connection.NewCmd(params)) + cmd.AddCommand(gateway.NewCmd(params)) + cmd.AddCommand(plans.NewCmd(params)) + cmd.AddCommand(quotas.NewCmd(params)) } diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 9bb2713b5..2895ecbe7 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -53,6 +53,7 @@ const ( logsCustomEndpointFlag = "logs-custom-endpoint" sfsCustomEndpointFlag = "sfs-custom-endpoint" cdnCustomEndpointFlag = "cdn-custom-endpoint" + vpnCustomEndpointFlag = "vpn-custom-endpoint" ) type inputModel struct { @@ -172,6 +173,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(logsCustomEndpointFlag, "", "Logs API base URL, used in calls to this API") cmd.Flags().String(sfsCustomEndpointFlag, "", "SFS API base URL, used in calls to this API") cmd.Flags().String(cdnCustomEndpointFlag, "", "CDN API base URL, used in calls to this API") + cmd.Flags().String(vpnCustomEndpointFlag, "", "VPN API base URL, used in calls to this API") err := viper.BindPFlag(config.SessionTimeLimitKey, cmd.Flags().Lookup(sessionTimeLimitFlag)) cobra.CheckErr(err) @@ -240,6 +242,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.CDNCustomEndpointKey, cmd.Flags().Lookup(cdnCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.VPNCustomEndpointKey, cmd.Flags().Lookup(vpnCustomEndpointFlag)) + cobra.CheckErr(err) } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index 580292b2e..da43bfe9e 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -56,6 +56,7 @@ const ( intakeCustomEndpointFlag = "intake-custom-endpoint" logsCustomEndpointFlag = "logs-custom-endpoint" cdnCustomEndpointFlag = "cdn-custom-endpoint" + vpnCustomEndpointFlag = "vpn-custom-endpoint" ) var ( @@ -105,6 +106,7 @@ type inputModel struct { IntakeCustomEndpoint bool LogsCustomEndpoint bool CDNCustomEndpoint bool + VpnCustomEndpoint bool } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -246,6 +248,9 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if model.CDNCustomEndpoint { viper.Set(config.CDNCustomEndpointKey, "") } + if model.VpnCustomEndpoint { + viper.Set(config.VPNCustomEndpointKey, "") + } err := config.Write() if err != nil { @@ -300,6 +305,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(logsCustomEndpointFlag, false, "Logs API base URL. If unset, uses the default base URL") cmd.Flags().Bool(sfsCustomEndpointFlag, false, "SFS API base URL. If unset, uses the default base URL") cmd.Flags().Bool(cdnCustomEndpointFlag, false, "Custom CDN endpoint URL. If unset, uses the default base URL") + cmd.Flags().Bool(vpnCustomEndpointFlag, false, "VPN API base URL. If unset, uses the default base URL") } func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { @@ -345,6 +351,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { IntakeCustomEndpoint: flags.FlagToBoolValue(p, cmd, intakeCustomEndpointFlag), LogsCustomEndpoint: flags.FlagToBoolValue(p, cmd, logsCustomEndpointFlag), CDNCustomEndpoint: flags.FlagToBoolValue(p, cmd, cdnCustomEndpointFlag), + VpnCustomEndpoint: flags.FlagToBoolValue(p, cmd, vpnCustomEndpointFlag), } p.DebugInputModel(model) diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index 17edcc9b7..feee86e38 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -48,6 +48,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool intakeCustomEndpointFlag: true, logsCustomEndpointFlag: true, cdnCustomEndpointFlag: true, + vpnCustomEndpointFlag: true, } for _, mod := range mods { mod(flagValues) @@ -94,6 +95,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { IntakeCustomEndpoint: true, LogsCustomEndpoint: true, CDNCustomEndpoint: true, + VpnCustomEndpoint: true, } for _, mod := range mods { mod(model) @@ -156,6 +158,7 @@ func TestParseInput(t *testing.T) { model.IntakeCustomEndpoint = false model.LogsCustomEndpoint = false model.CDNCustomEndpoint = false + model.VpnCustomEndpoint = false }), }, { @@ -358,6 +361,16 @@ func TestParseInput(t *testing.T) { model.CDNCustomEndpoint = false }), }, + { + description: "vpn custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[vpnCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.VpnCustomEndpoint = false + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 9e1084dac..effb68051 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -122,6 +122,7 @@ var ConfigKeys = []string{ ServiceEnablementCustomEndpointKey, SfsCustomEndpointKey, TokenCustomEndpointKey, + VPNCustomEndpointKey, } var defaultConfigFolderPath string @@ -213,6 +214,7 @@ func setConfigDefaults() { viper.SetDefault(AlbCustomEndpoint, "") viper.SetDefault(LogsCustomEndpointKey, "") viper.SetDefault(CDNCustomEndpointKey, "") + viper.SetDefault(VPNCustomEndpointKey, "") } func getConfigFilePath(configFolder string) string { diff --git a/internal/pkg/services/vpn/utils/utils.go b/internal/pkg/services/vpn/utils/utils.go new file mode 100644 index 000000000..dfba86dd9 --- /dev/null +++ b/internal/pkg/services/vpn/utils/utils.go @@ -0,0 +1,19 @@ +package utils + +import ( + "context" + "fmt" + + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" +) + +func GetGatewayName(ctx context.Context, client vpn.DefaultAPI, projectId, region, gatewayId string) (string, error) { + resp, err := client.GetGateway(ctx, projectId, region, gatewayId).Execute() + if err != nil { + return "", fmt.Errorf("get gateway: %w", err) + } + if resp != nil { + return resp.DisplayName, nil + } + return "", nil +} diff --git a/internal/pkg/services/vpn/utils/utils_test.go b/internal/pkg/services/vpn/utils/utils_test.go new file mode 100644 index 000000000..27d173ca4 --- /dev/null +++ b/internal/pkg/services/vpn/utils/utils_test.go @@ -0,0 +1,87 @@ +package utils + +import ( + "context" + "fmt" + "testing" + + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + testGatewayName = "test-gateway-01" + testRegion = "eu01" +) + +var ( + testProjectId = uuid.NewString() + testGatewayId = uuid.NewString() +) + +type mockSettings struct { + getGatewayFails bool + getGatewayResp *vpn.GatewayResponse +} + +func newAPIMock(settings *mockSettings) vpn.DefaultAPI { + return &vpn.DefaultAPIServiceMock{ + GetGatewayExecuteMock: utils.Ptr(func(_ vpn.ApiGetGatewayRequest) (*vpn.GatewayResponse, error) { + if settings.getGatewayFails { + return nil, fmt.Errorf("could not get gateway details") + } + + return settings.getGatewayResp, nil + }), + } +} + +func TestGetGatewayName(t *testing.T) { + tests := []struct { + description string + getGatewayResp *vpn.GatewayResponse + getGatewayFails bool + isValid bool + expectedOutput string + }{ + { + description: "base", + getGatewayResp: &vpn.GatewayResponse{ + DisplayName: testGatewayName, + }, + isValid: true, + expectedOutput: testGatewayName, + }, + { + description: "get gateway fails", + getGatewayFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := newAPIMock(&mockSettings{ + getGatewayFails: tt.getGatewayFails, + getGatewayResp: tt.getGatewayResp, + }) + + output, err := GetGatewayName(context.Background(), client, testProjectId, testRegion, testGatewayId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} From 60bcf0b8a64603b1425bd944c6d4fe7f6c8f1d04 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Mon, 6 Jul 2026 14:46:43 +0200 Subject: [PATCH 02/11] fixed remaining issues --- docs/stackit_beta_vpn.md | 3 ++ docs/stackit_beta_vpn_gateway.md | 37 +++++++++++++ docs/stackit_beta_vpn_gateway_create.md | 54 +++++++++++++++++++ docs/stackit_beta_vpn_gateway_delete.md | 40 ++++++++++++++ docs/stackit_beta_vpn_gateway_describe.md | 40 ++++++++++++++ docs/stackit_beta_vpn_gateway_list.md | 44 +++++++++++++++ docs/stackit_beta_vpn_plans.md | 41 ++++++++++++++ docs/stackit_beta_vpn_quotas.md | 41 ++++++++++++++ docs/stackit_config_set.md | 1 + docs/stackit_config_unset.md | 1 + .../cmd/beta/vpn/gateway/create/create.go | 21 +++++--- .../beta/vpn/gateway/create/create_test.go | 2 +- .../cmd/beta/vpn/gateway/describe/describe.go | 4 +- internal/cmd/beta/vpn/gateway/list/list.go | 8 ++- internal/cmd/beta/vpn/plans/plans.go | 19 ++----- internal/cmd/beta/vpn/plans/plans_test.go | 39 ++++---------- internal/cmd/beta/vpn/quotas/quotas.go | 4 +- 17 files changed, 341 insertions(+), 58 deletions(-) create mode 100644 docs/stackit_beta_vpn_gateway.md create mode 100644 docs/stackit_beta_vpn_gateway_create.md create mode 100644 docs/stackit_beta_vpn_gateway_delete.md create mode 100644 docs/stackit_beta_vpn_gateway_describe.md create mode 100644 docs/stackit_beta_vpn_gateway_list.md create mode 100644 docs/stackit_beta_vpn_plans.md create mode 100644 docs/stackit_beta_vpn_quotas.md diff --git a/docs/stackit_beta_vpn.md b/docs/stackit_beta_vpn.md index 861daa4dd..a0752f708 100644 --- a/docs/stackit_beta_vpn.md +++ b/docs/stackit_beta_vpn.md @@ -31,4 +31,7 @@ stackit beta vpn [flags] * [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands * [stackit beta vpn connection](./stackit_beta_vpn_connection.md) - Provides functionality for VPN connections +* [stackit beta vpn gateway](./stackit_beta_vpn_gateway.md) - Provides functionality for VPN gateway +* [stackit beta vpn plans](./stackit_beta_vpn_plans.md) - Lists all available plans +* [stackit beta vpn quotas](./stackit_beta_vpn_quotas.md) - Lists all vpn quotas diff --git a/docs/stackit_beta_vpn_gateway.md b/docs/stackit_beta_vpn_gateway.md new file mode 100644 index 000000000..10c37aebc --- /dev/null +++ b/docs/stackit_beta_vpn_gateway.md @@ -0,0 +1,37 @@ +## stackit beta vpn gateway + +Provides functionality for VPN gateway + +### Synopsis + +Provides functionality for VPN gateway. + +``` +stackit beta vpn gateway [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta vpn gateway" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn](./stackit_beta_vpn.md) - Provides functionality for VPN +* [stackit beta vpn gateway create](./stackit_beta_vpn_gateway_create.md) - Creates a vpn gateway +* [stackit beta vpn gateway delete](./stackit_beta_vpn_gateway_delete.md) - Deletes a vpn gateway +* [stackit beta vpn gateway describe](./stackit_beta_vpn_gateway_describe.md) - Shows details of a gateway +* [stackit beta vpn gateway list](./stackit_beta_vpn_gateway_list.md) - Lists all vpn gateways + diff --git a/docs/stackit_beta_vpn_gateway_create.md b/docs/stackit_beta_vpn_gateway_create.md new file mode 100644 index 000000000..e9de973cd --- /dev/null +++ b/docs/stackit_beta_vpn_gateway_create.md @@ -0,0 +1,54 @@ +## stackit beta vpn gateway create + +Creates a vpn gateway + +### Synopsis + +Creates a vpn gateway. + +``` +stackit beta vpn gateway create [flags] +``` + +### Examples + +``` + Create a vpn gateway with name "xxx", plan "p500", policy based routing and both tunnels in availability-zone eu01-1 + $ 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 + + Create a vpn gateway with the labels foo=bar and x=y + $ 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 + + Create a vpn gateway with bgp enabled, yyy as local asn and [aaa, bbb] as override advertised routes + $ 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 +``` + +### Options + +``` + --availability-zone-tunnel-1 string Availability Zone of Tunnel 1 + --availability-zone-tunnel-2 string Availability Zone of Tunnel 2 + --bgp-local-asn int ASN for private use (reserved by IANA), both 16Bit and 32Bit ranges are valid (RFC 6996) + --bgp-override-advertised-routes stringArray A list of IPv4 Prefixes to advertise via BGP + -h, --help Help for "stackit beta vpn gateway create" + --labels stringToString Labels in key=value format, separated by commas (default []) + --name string Gateway name + --plan-id string Plan ID + --routing-type string Routing Type of the VPN (one of: [POLICY_BASED, ROUTE_BASED, BGP_ROUTE_BASED]) +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn gateway](./stackit_beta_vpn_gateway.md) - Provides functionality for VPN gateway + diff --git a/docs/stackit_beta_vpn_gateway_delete.md b/docs/stackit_beta_vpn_gateway_delete.md new file mode 100644 index 000000000..986503e35 --- /dev/null +++ b/docs/stackit_beta_vpn_gateway_delete.md @@ -0,0 +1,40 @@ +## stackit beta vpn gateway delete + +Deletes a vpn gateway + +### Synopsis + +Deletes a vpn gateway. + +``` +stackit beta vpn gateway delete GATEWAY_ID [flags] +``` + +### Examples + +``` + Delete a vpn gateway with the ID "xxx" + $ stackit beta vpn gateway delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta vpn gateway delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn gateway](./stackit_beta_vpn_gateway.md) - Provides functionality for VPN gateway + diff --git a/docs/stackit_beta_vpn_gateway_describe.md b/docs/stackit_beta_vpn_gateway_describe.md new file mode 100644 index 000000000..82b8f23bf --- /dev/null +++ b/docs/stackit_beta_vpn_gateway_describe.md @@ -0,0 +1,40 @@ +## stackit beta vpn gateway describe + +Shows details of a gateway + +### Synopsis + +Shows details of a gateway. + +``` +stackit beta vpn gateway describe GATEWAY_ID [flags] +``` + +### Examples + +``` + Describe a gateway with the ID "xxx" + $ stackit beta vpn gateway describe xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta vpn gateway describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn gateway](./stackit_beta_vpn_gateway.md) - Provides functionality for VPN gateway + diff --git a/docs/stackit_beta_vpn_gateway_list.md b/docs/stackit_beta_vpn_gateway_list.md new file mode 100644 index 000000000..0c72b56b1 --- /dev/null +++ b/docs/stackit_beta_vpn_gateway_list.md @@ -0,0 +1,44 @@ +## stackit beta vpn gateway list + +Lists all vpn gateways + +### Synopsis + +Lists all vpn gateways. + +``` +stackit beta vpn gateway list [flags] +``` + +### Examples + +``` + List all vpn gateways + $ stackit beta vpn gateway list + + List up to 4 vpn gateways + $ stackit beta vpn gateway list --limit 4 +``` + +### Options + +``` + -h, --help Help for "stackit beta vpn gateway list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn gateway](./stackit_beta_vpn_gateway.md) - Provides functionality for VPN gateway + diff --git a/docs/stackit_beta_vpn_plans.md b/docs/stackit_beta_vpn_plans.md new file mode 100644 index 000000000..a56b2022f --- /dev/null +++ b/docs/stackit_beta_vpn_plans.md @@ -0,0 +1,41 @@ +## stackit beta vpn plans + +Lists all available plans + +### Synopsis + +Lists all available plans. + +``` +stackit beta vpn plans [flags] +``` + +### Examples + +``` + List all available plans + $ stackit beta vpn plans +``` + +### Options + +``` + -h, --help Help for "stackit beta vpn plans" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn](./stackit_beta_vpn.md) - Provides functionality for VPN + diff --git a/docs/stackit_beta_vpn_quotas.md b/docs/stackit_beta_vpn_quotas.md new file mode 100644 index 000000000..8ca5f68f2 --- /dev/null +++ b/docs/stackit_beta_vpn_quotas.md @@ -0,0 +1,41 @@ +## stackit beta vpn quotas + +Lists all vpn quotas + +### Synopsis + +Lists all vpn quotas. + +``` +stackit beta vpn quotas [flags] +``` + +### Examples + +``` + List all vpn quotas + $ stackit beta vpn quotas +``` + +### Options + +``` + -h, --help Help for "stackit beta vpn quotas" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn](./stackit_beta_vpn.md) - Provides functionality for VPN + diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index 2a49e8ea6..ab3d79834 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -63,6 +63,7 @@ stackit config set [flags] --ske-custom-endpoint string SKE API base URL, used in calls to this API --sqlserverflex-custom-endpoint string SQLServer Flex API base URL, used in calls to this API --token-custom-endpoint string Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication. + --vpn-custom-endpoint string VPN API base URL, used in calls to this API ``` ### Options inherited from parent commands diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index d76f3068c..ea816220f 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -66,6 +66,7 @@ stackit config unset [flags] --sqlserverflex-custom-endpoint SQLServer Flex API base URL. If unset, uses the default base URL --token-custom-endpoint Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication. --verbosity Verbosity of the CLI + --vpn-custom-endpoint VPN API base URL. If unset, uses the default base URL ``` ### SEE ALSO diff --git a/internal/cmd/beta/vpn/gateway/create/create.go b/internal/cmd/beta/vpn/gateway/create/create.go index 6aa98e38e..245a0d1ea 100644 --- a/internal/cmd/beta/vpn/gateway/create/create.go +++ b/internal/cmd/beta/vpn/gateway/create/create.go @@ -6,6 +6,9 @@ import ( "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +20,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" - "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api/wait" ) const ( @@ -29,7 +30,14 @@ const ( nameFlag = "name" labelsFlag = "labels" planIdFlag = "plan-id" - routingTypeFlag = "routing-type" +) + +var ( + routingTypeFlag = flags.StringEnumFlag( + "routing-type", + vpn.AllowedRoutingTypeEnumValues, + "Routing Type of the VPN", + ) ) type inputModel struct { @@ -123,10 +131,9 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(nameFlag, "", "Gateway name") cmd.Flags().StringToString(labelsFlag, nil, "Labels in key=value format, separated by commas") cmd.Flags().String(planIdFlag, "", "Plan ID") - cmd.Flags().String(routingTypeFlag, "", fmt.Sprintf("Routing Type: %q", vpn.AllowedRoutingTypeEnumValues)) - // cmd.Flags().Var(flags.EnumFlag(false, "", sdkUtils.EnumSliceToStringSlice(vpn.AllowedRoutingTypeEnumValues)...), routingTypeFlag, fmt.Sprintf("Routing Type, one of: %q", vpn.AllowedRoutingTypeEnumValues)) + routingTypeFlag.Register(cmd.Flags()) - err := flags.MarkFlagsRequired(cmd, availabilityZoneTunnel1Flag, availabilityZoneTunnel2Flag, nameFlag, planIdFlag, routingTypeFlag) + err := flags.MarkFlagsRequired(cmd, availabilityZoneTunnel1Flag, availabilityZoneTunnel2Flag, nameFlag, planIdFlag, routingTypeFlag.Name()) cobra.CheckErr(err) } @@ -173,7 +180,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, Name: flags.FlagToStringValue(p, cmd, nameFlag), Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), PlanId: flags.FlagToStringValue(p, cmd, planIdFlag), - RoutingType: vpn.RoutingType(flags.FlagToStringValue(p, cmd, routingTypeFlag)), + RoutingType: vpn.RoutingType(routingTypeFlag.Get()), } p.DebugInputModel(model) diff --git a/internal/cmd/beta/vpn/gateway/create/create_test.go b/internal/cmd/beta/vpn/gateway/create/create_test.go index 0a389c551..09f9b2bee 100644 --- a/internal/cmd/beta/vpn/gateway/create/create_test.go +++ b/internal/cmd/beta/vpn/gateway/create/create_test.go @@ -46,7 +46,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st nameFlag: testName, labelsFlag: "foo=bar", planIdFlag: testPlanId, - routingTypeFlag: string(testRoutingType), + routingTypeFlag.Name(): string(testRoutingType), } for _, mod := range mods { mod(flagValues) diff --git a/internal/cmd/beta/vpn/gateway/describe/describe.go b/internal/cmd/beta/vpn/gateway/describe/describe.go index 874fcee27..357fb81de 100644 --- a/internal/cmd/beta/vpn/gateway/describe/describe.go +++ b/internal/cmd/beta/vpn/gateway/describe/describe.go @@ -36,7 +36,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Args: args.SingleArg(gatewayIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( - `Describe a gateway with ID "xxx"`, + `Describe a gateway with the ID "xxx"`, "$ stackit beta vpn gateway describe xxx", ), ), @@ -105,7 +105,7 @@ func outputResult(p *print.Printer, outputFormat, gatewayId, projectLabel string table.AddSeparator() table.AddRow("NAME", gateway.DisplayName) table.AddSeparator() - table.AddRow("Labels", gateway.GetLabels()) + table.AddRow("Labels", utils.JoinStringMap(gateway.GetLabels(), ": ", "\n")) table.AddSeparator() table.AddRow("STATE", utils.PtrString(gateway.State)) table.AddSeparator() diff --git a/internal/cmd/beta/vpn/gateway/list/list.go b/internal/cmd/beta/vpn/gateway/list/list.go index a424eb241..42e465c8e 100644 --- a/internal/cmd/beta/vpn/gateway/list/list.go +++ b/internal/cmd/beta/vpn/gateway/list/list.go @@ -6,6 +6,8 @@ import ( "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" ) const ( @@ -40,6 +41,10 @@ func NewCmd(params *types.CmdParams) *cobra.Command { `List all vpn gateways`, "$ stackit beta vpn gateway list", ), + examples.NewExample( + `List up to 4 vpn gateways`, + "$ stackit beta vpn gateway list --limit 4", + ), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -112,7 +117,6 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClie func outputResult(p *print.Printer, outputFormat string, gateways []vpn.GatewayResponse, projectLabel string) error { return p.OutputResult(outputFormat, gateways, func() error { - if len(gateways) == 0 { p.Info("No gateways found for %q\n", projectLabel) return nil diff --git a/internal/cmd/beta/vpn/plans/plans.go b/internal/cmd/beta/vpn/plans/plans.go index 8f94a6a30..d18fd73ab 100644 --- a/internal/cmd/beta/vpn/plans/plans.go +++ b/internal/cmd/beta/vpn/plans/plans.go @@ -6,18 +6,18 @@ import ( "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" ) const ( @@ -67,12 +67,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { items = items[:*model.Limit] } - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil || projectLabel == "" { - projectLabel = model.ProjectId - } - - return outputResult(params.Printer, model.OutputFormat, items, projectLabel) + return outputResult(params.Printer, model.OutputFormat, items, model.Region) }, } configureFlags(cmd) @@ -85,9 +80,6 @@ func configureFlags(cmd *cobra.Command) { func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) - if globalFlags.ProjectId == "" { - return nil, &errors.ProjectIdError{} - } limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) if limit != nil && *limit < 1 { @@ -110,11 +102,10 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClie return apiClient.DefaultAPI.ListPlans(ctx, model.Region) } -func outputResult(p *print.Printer, outputFormat string, plans []vpn.Plan, projectLabel string) error { +func outputResult(p *print.Printer, outputFormat string, plans []vpn.Plan, region string) error { return p.OutputResult(outputFormat, plans, func() error { - if len(plans) == 0 { - p.Info("No plans found for %q\n", projectLabel) + p.Info("No plans found for Region %q\n", region) return nil } diff --git a/internal/cmd/beta/vpn/plans/plans_test.go b/internal/cmd/beta/vpn/plans/plans_test.go index db017d48f..7513ab482 100644 --- a/internal/cmd/beta/vpn/plans/plans_test.go +++ b/internal/cmd/beta/vpn/plans/plans_test.go @@ -7,7 +7,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/google/uuid" vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -16,7 +15,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -var projectIdFlag = globalflags.ProjectIdFlag var regionFlag = globalflags.RegionFlag type testCtxKey struct{} @@ -24,16 +22,14 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &vpn.APIClient{DefaultAPI: &vpn.DefaultAPIService{}} -var testProjectId = uuid.NewString() var testRegion = "eu01" - var testLimit int64 = 10 func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, - limitFlag: strconv.FormatInt(testLimit, 10), + // Project ID is not necessary for this request + regionFlag: testRegion, + limitFlag: strconv.FormatInt(testLimit, 10), } for _, mod := range mods { mod(flagValues) @@ -44,7 +40,6 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ - ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, Region: testRegion, }, @@ -81,28 +76,12 @@ func TestParseInput(t *testing.T) { { description: "no flag values", flagValues: map[string]string{}, - isValid: false, - }, - { - description: "project id missing", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) - }), - isValid: false, - }, - { - description: "project id invalid 1", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" - }), - isValid: false, - }, - { - description: "project id invalid 2", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" - }), - isValid: false, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + }, + isValid: true, }, { description: "invalid limit 1", diff --git a/internal/cmd/beta/vpn/quotas/quotas.go b/internal/cmd/beta/vpn/quotas/quotas.go index 5a338fee3..48d1067ec 100644 --- a/internal/cmd/beta/vpn/quotas/quotas.go +++ b/internal/cmd/beta/vpn/quotas/quotas.go @@ -6,6 +6,8 @@ import ( "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/types" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" ) const ( @@ -105,7 +106,6 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClie func outputResult(p *print.Printer, outputFormat string, quota *vpn.QuotaListResponse, projectLabel string) error { return p.OutputResult(outputFormat, quota, func() error { - if quota == nil { p.Info("No quotas for %q\n", projectLabel) return nil From 8ffb1116f4457cfd3f3f8755689b11b76eb9e073 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Tue, 7 Jul 2026 15:23:17 +0200 Subject: [PATCH 03/11] add status endpoint --- docs/stackit_beta_vpn_gateway.md | 1 + docs/stackit_beta_vpn_gateway_delete.md | 2 +- docs/stackit_beta_vpn_gateway_describe.md | 2 +- docs/stackit_beta_vpn_gateway_status.md | 40 ++++ .../cmd/beta/vpn/gateway/delete/delete.go | 2 +- .../cmd/beta/vpn/gateway/describe/describe.go | 2 +- internal/cmd/beta/vpn/gateway/gateway.go | 2 + .../cmd/beta/vpn/gateway/status/status.go | 181 +++++++++++++++ .../beta/vpn/gateway/status/status_test.go | 212 ++++++++++++++++++ 9 files changed, 440 insertions(+), 4 deletions(-) create mode 100644 docs/stackit_beta_vpn_gateway_status.md create mode 100644 internal/cmd/beta/vpn/gateway/status/status.go create mode 100644 internal/cmd/beta/vpn/gateway/status/status_test.go diff --git a/docs/stackit_beta_vpn_gateway.md b/docs/stackit_beta_vpn_gateway.md index 10c37aebc..17ea3a627 100644 --- a/docs/stackit_beta_vpn_gateway.md +++ b/docs/stackit_beta_vpn_gateway.md @@ -34,4 +34,5 @@ stackit beta vpn gateway [flags] * [stackit beta vpn gateway delete](./stackit_beta_vpn_gateway_delete.md) - Deletes a vpn gateway * [stackit beta vpn gateway describe](./stackit_beta_vpn_gateway_describe.md) - Shows details of a gateway * [stackit beta vpn gateway list](./stackit_beta_vpn_gateway_list.md) - Lists all vpn gateways +* [stackit beta vpn gateway status](./stackit_beta_vpn_gateway_status.md) - Shows the status of a gateway diff --git a/docs/stackit_beta_vpn_gateway_delete.md b/docs/stackit_beta_vpn_gateway_delete.md index 986503e35..65cc5feed 100644 --- a/docs/stackit_beta_vpn_gateway_delete.md +++ b/docs/stackit_beta_vpn_gateway_delete.md @@ -13,7 +13,7 @@ stackit beta vpn gateway delete GATEWAY_ID [flags] ### Examples ``` - Delete a vpn gateway with the ID "xxx" + Delete the vpn gateway with the ID "xxx" $ stackit beta vpn gateway delete xxx ``` diff --git a/docs/stackit_beta_vpn_gateway_describe.md b/docs/stackit_beta_vpn_gateway_describe.md index 82b8f23bf..0c029c81a 100644 --- a/docs/stackit_beta_vpn_gateway_describe.md +++ b/docs/stackit_beta_vpn_gateway_describe.md @@ -13,7 +13,7 @@ stackit beta vpn gateway describe GATEWAY_ID [flags] ### Examples ``` - Describe a gateway with the ID "xxx" + Describe the gateway with the ID "xxx" $ stackit beta vpn gateway describe xxx ``` diff --git a/docs/stackit_beta_vpn_gateway_status.md b/docs/stackit_beta_vpn_gateway_status.md new file mode 100644 index 000000000..412462183 --- /dev/null +++ b/docs/stackit_beta_vpn_gateway_status.md @@ -0,0 +1,40 @@ +## stackit beta vpn gateway status + +Shows the status of a gateway + +### Synopsis + +Shows the status of a gateway. + +``` +stackit beta vpn gateway status GATEWAY_ID [flags] +``` + +### Examples + +``` + Show the status of the gateway with the ID "xxx" + $ stackit beta vpn gateway describe xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta vpn gateway status" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn gateway](./stackit_beta_vpn_gateway.md) - Provides functionality for VPN gateway + diff --git a/internal/cmd/beta/vpn/gateway/delete/delete.go b/internal/cmd/beta/vpn/gateway/delete/delete.go index 5fdc74a6d..d5785dd43 100644 --- a/internal/cmd/beta/vpn/gateway/delete/delete.go +++ b/internal/cmd/beta/vpn/gateway/delete/delete.go @@ -37,7 +37,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Args: args.SingleArg(gatewayIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( - `Delete a vpn gateway with the ID "xxx"`, + `Delete the vpn gateway with the ID "xxx"`, "$ stackit beta vpn gateway delete xxx", ), ), diff --git a/internal/cmd/beta/vpn/gateway/describe/describe.go b/internal/cmd/beta/vpn/gateway/describe/describe.go index 357fb81de..2549f4661 100644 --- a/internal/cmd/beta/vpn/gateway/describe/describe.go +++ b/internal/cmd/beta/vpn/gateway/describe/describe.go @@ -36,7 +36,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Args: args.SingleArg(gatewayIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( - `Describe a gateway with the ID "xxx"`, + `Describe the gateway with the ID "xxx"`, "$ stackit beta vpn gateway describe xxx", ), ), diff --git a/internal/cmd/beta/vpn/gateway/gateway.go b/internal/cmd/beta/vpn/gateway/gateway.go index c86a0894a..0038106c6 100644 --- a/internal/cmd/beta/vpn/gateway/gateway.go +++ b/internal/cmd/beta/vpn/gateway/gateway.go @@ -5,6 +5,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/gateway/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/gateway/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/gateway/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/gateway/status" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -29,4 +30,5 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(status.NewCmd(params)) } diff --git a/internal/cmd/beta/vpn/gateway/status/status.go b/internal/cmd/beta/vpn/gateway/status/status.go new file mode 100644 index 000000000..b9ff5ceb2 --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/status/status.go @@ -0,0 +1,181 @@ +package status + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + gatewayIdArg = "GATEWAY_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + GatewayId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("status %s", gatewayIdArg), + Short: "Shows the status of a gateway", + Long: "Shows the status of a gateway.", + Args: args.SingleArg(gatewayIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Show the status of the gateway with the ID "xxx"`, + "$ stackit beta vpn gateway describe xxx", + ), + ), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("describe vpn gateway: %w", err) + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil || projectLabel == "" { + projectLabel = model.ProjectId + } + + return outputResult(params.Printer, model.OutputFormat, model.GatewayId, projectLabel, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + gatewayId := inputArgs[0] + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + GatewayId: gatewayId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) vpn.ApiGetGatewayStatusRequest { + return apiClient.DefaultAPI.GetGatewayStatus(ctx, model.ProjectId, model.Region, model.GatewayId) +} + +func outputResult(p *print.Printer, outputFormat, gatewayId, projectLabel string, gateway *vpn.GatewayStatusResponse) error { + return p.OutputResult(outputFormat, gateway, func() error { + if gateway == nil { + p.Outputf("gateway %q not found in project %q\n", gatewayId, projectLabel) + return nil + } + + mainTable := tables.NewTable() + mainTable.SetTitle("Gateway Status") + + mainTable.AddRow("ID", gateway.GetId()) + mainTable.AddSeparator() + mainTable.AddRow("NAME", gateway.GetDisplayName()) + mainTable.AddSeparator() + mainTable.AddRow("STATUS", gateway.GetGatewayStatus()) + if gateway.ErrorMessage != nil { + mainTable.AddSeparator() + mainTable.AddRow("ERROR MESSAGE", *gateway.ErrorMessage) + } + + ts := []tables.Table{ + mainTable, + } + for _, tunnel := range gateway.Tunnels { + ts = append(ts, tunnelTable(tunnel)) + } + + return tables.DisplayTables(p, ts) + }) +} + +func tunnelTable(tunnel vpn.VPNTunnels) tables.Table { + title := "Tunnel" + if tunnel.Name != nil { + title = string(*tunnel.Name) + } + + table := tables.NewTable() + table.SetTitle(title) + + table.AddSeparator() + table.AddRow("PUBLIC IP", tunnel.GetPublicIP()) + table.AddSeparator() + table.AddRow("INTERNAL NEXT HOP IP", tunnel.GetInternalNextHopIP()) + table.AddSeparator() + table.AddRow("STATE", tunnel.GetInstanceState()) + + if tunnel.BgpStatus.IsSet() { + table.AddSeparator() + routeString := "" + for _, route := range tunnel.BgpStatus.Get().Routes { + if route.Network != "" { + routeString += fmt.Sprintf("Network: %s; ", route.Network) + } + if route.Origin != "" { + routeString += fmt.Sprintf("Origin: %s; ", route.Origin) + } + if route.Path != "" { + routeString += fmt.Sprintf("Path: %s; ", route.Path) + } + if route.PeerId != "" { + routeString += fmt.Sprintf("PeerId: %s; ", route.PeerId) + } + routeString += fmt.Sprintf("Weight: %d\n", route.Weight) + } + table.AddRow("BGP Routes", routeString) + table.AddSeparator() + bgpPeers := "" + for _, peer := range tunnel.BgpStatus.Get().Peers { + if peer.PeerUptime != "" { + bgpPeers += fmt.Sprintf("PeerUptime: %s; ", peer.PeerUptime) + } + if peer.RemoteIP != "" { + bgpPeers += fmt.Sprintf("RemoteIP: %s; ", peer.RemoteIP) + } + if peer.State != "" { + bgpPeers += fmt.Sprintf("State: %s; ", peer.State) + } + bgpPeers += fmt.Sprintf("LocalAsn: %d; ", peer.LocalAs) + bgpPeers += fmt.Sprintf("PfxRcd: %d; ", peer.PfxRcd) + bgpPeers += fmt.Sprintf("PfxSnt: %d; ", peer.PfxSnt) + bgpPeers += fmt.Sprintf("RemoteAs: %d\n", peer.RemoteAs) + } + table.AddRow("BGP Peers", bgpPeers) + } + + return table +} diff --git a/internal/cmd/beta/vpn/gateway/status/status_test.go b/internal/cmd/beta/vpn/gateway/status/status_test.go new file mode 100644 index 000000000..4552144e8 --- /dev/null +++ b/internal/cmd/beta/vpn/gateway/status/status_test.go @@ -0,0 +1,212 @@ +package status + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &vpn.APIClient{DefaultAPI: &vpn.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testGatewayId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testGatewayId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + GatewayId: testGatewayId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiGetGatewayStatusRequest)) vpn.ApiGetGatewayStatusRequest { + request := testClient.DefaultAPI.GetGatewayStatus(testCtx, testProjectId, testRegion, testGatewayId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "gateway id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "gateway id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest vpn.ApiGetGatewayStatusRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + gatewayId string + projectLabel string + gateway *vpn.GatewayStatusResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + gateway: &vpn.GatewayStatusResponse{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.gatewayId, tt.args.gateway); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From 23c0028b47d9f152fa553a2d5f8579669baef803 Mon Sep 17 00:00:00 2001 From: Manuel Vaas <34416897+Manuelvaas@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:45:38 +0200 Subject: [PATCH 04/11] Update internal/cmd/beta/vpn/gateway/list/list.go Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- internal/cmd/beta/vpn/gateway/list/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/vpn/gateway/list/list.go b/internal/cmd/beta/vpn/gateway/list/list.go index 42e465c8e..cef3a33a6 100644 --- a/internal/cmd/beta/vpn/gateway/list/list.go +++ b/internal/cmd/beta/vpn/gateway/list/list.go @@ -98,7 +98,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, if limit != nil && *limit < 1 { return nil, &errors.FlagValidationError{ Flag: limitFlag, - Details: "must be grater than 0", + Details: "must be greater than 0", } } From fb43c9b97989883cf935011fe2bdd6a436816381 Mon Sep 17 00:00:00 2001 From: Manuel Vaas <34416897+Manuelvaas@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:46:46 +0200 Subject: [PATCH 05/11] Update internal/cmd/beta/vpn/gateway/status/status.go Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- internal/cmd/beta/vpn/gateway/status/status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/vpn/gateway/status/status.go b/internal/cmd/beta/vpn/gateway/status/status.go index b9ff5ceb2..89356cbfd 100644 --- a/internal/cmd/beta/vpn/gateway/status/status.go +++ b/internal/cmd/beta/vpn/gateway/status/status.go @@ -37,7 +37,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Example: examples.Build( examples.NewExample( `Show the status of the gateway with the ID "xxx"`, - "$ stackit beta vpn gateway describe xxx", + "$ stackit beta vpn gateway status xxx", ), ), RunE: func(cmd *cobra.Command, inputArgs []string) error { From f33674e05fe626b8587e33c01c4dbb2dbaf41932 Mon Sep 17 00:00:00 2001 From: Manuel Vaas <34416897+Manuelvaas@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:46:55 +0200 Subject: [PATCH 06/11] Update internal/cmd/beta/vpn/plans/plans.go Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- internal/cmd/beta/vpn/plans/plans.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/vpn/plans/plans.go b/internal/cmd/beta/vpn/plans/plans.go index d18fd73ab..2c9f75ebe 100644 --- a/internal/cmd/beta/vpn/plans/plans.go +++ b/internal/cmd/beta/vpn/plans/plans.go @@ -85,7 +85,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, if limit != nil && *limit < 1 { return nil, &errors.FlagValidationError{ Flag: limitFlag, - Details: "must be grater than 0", + Details: "must be greater than 0", } } From 8075a01b11fc2557a049c0f878b2ccca3c13a094 Mon Sep 17 00:00:00 2001 From: Manuel Vaas <34416897+Manuelvaas@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:47:07 +0200 Subject: [PATCH 07/11] Update internal/cmd/beta/vpn/quotas/quotas.go Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- internal/cmd/beta/vpn/quotas/quotas.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/vpn/quotas/quotas.go b/internal/cmd/beta/vpn/quotas/quotas.go index 48d1067ec..4fdefb36d 100644 --- a/internal/cmd/beta/vpn/quotas/quotas.go +++ b/internal/cmd/beta/vpn/quotas/quotas.go @@ -87,7 +87,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, if limit != nil && *limit < 1 { return nil, &errors.FlagValidationError{ Flag: limitFlag, - Details: "must be grater than 0", + Details: "must be greater than 0", } } From f46c11141f284b871d3d18894b59b931086ce1d5 Mon Sep 17 00:00:00 2001 From: Manuel Vaas <34416897+Manuelvaas@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:48:32 +0200 Subject: [PATCH 08/11] Update docs/stackit_beta_vpn_gateway_status.md Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- docs/stackit_beta_vpn_gateway_status.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stackit_beta_vpn_gateway_status.md b/docs/stackit_beta_vpn_gateway_status.md index 412462183..5da1d3b6e 100644 --- a/docs/stackit_beta_vpn_gateway_status.md +++ b/docs/stackit_beta_vpn_gateway_status.md @@ -14,7 +14,7 @@ stackit beta vpn gateway status GATEWAY_ID [flags] ``` Show the status of the gateway with the ID "xxx" - $ stackit beta vpn gateway describe xxx + $ stackit beta vpn gateway status xxx ``` ### Options From 10243c530d1179d8ec937285fb49df65ae9b7daa Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Wed, 15 Jul 2026 09:48:02 +0200 Subject: [PATCH 09/11] fixed typo --- internal/cmd/beta/sfs/share/list/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/sfs/share/list/list.go b/internal/cmd/beta/sfs/share/list/list.go index 5816a9185..8cbd7666c 100644 --- a/internal/cmd/beta/sfs/share/list/list.go +++ b/internal/cmd/beta/sfs/share/list/list.go @@ -106,7 +106,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, if limit != nil && *limit < 1 { return nil, &errors.FlagValidationError{ Flag: limitFlag, - Details: "must be grater than 0", + Details: "must be greater than 0", } } From 6b204c04c6d1194fd2ff54547527f4dcf5fba35e Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Wed, 15 Jul 2026 09:54:47 +0200 Subject: [PATCH 10/11] reduced variables in tests --- .../cmd/beta/vpn/gateway/create/create_test.go | 7 ++----- .../cmd/beta/vpn/gateway/delete/delete_test.go | 13 +++++-------- .../beta/vpn/gateway/describe/describe_test.go | 13 +++++-------- internal/cmd/beta/vpn/gateway/list/list_test.go | 15 ++++++--------- .../cmd/beta/vpn/gateway/status/status_test.go | 13 +++++-------- internal/cmd/beta/vpn/plans/plans_test.go | 6 ++---- internal/cmd/beta/vpn/quotas/quotas_test.go | 15 ++++++--------- 7 files changed, 31 insertions(+), 51 deletions(-) diff --git a/internal/cmd/beta/vpn/gateway/create/create_test.go b/internal/cmd/beta/vpn/gateway/create/create_test.go index 09f9b2bee..0116c74cf 100644 --- a/internal/cmd/beta/vpn/gateway/create/create_test.go +++ b/internal/cmd/beta/vpn/gateway/create/create_test.go @@ -15,9 +15,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -36,8 +33,8 @@ var testBgpOverrideAdvertisedRoutes = "10.10.10.10/32" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, availabilityZoneTunnel1Flag: testAvailabilityZoneTunnel1, availabilityZoneTunnel2Flag: testAvailabilityZoneTunnel2, diff --git a/internal/cmd/beta/vpn/gateway/delete/delete_test.go b/internal/cmd/beta/vpn/gateway/delete/delete_test.go index 145a49ca8..8f4497054 100644 --- a/internal/cmd/beta/vpn/gateway/delete/delete_test.go +++ b/internal/cmd/beta/vpn/gateway/delete/delete_test.go @@ -13,9 +13,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -27,8 +24,8 @@ var testGatewayId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -106,7 +103,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -114,7 +111,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -122,7 +119,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, diff --git a/internal/cmd/beta/vpn/gateway/describe/describe_test.go b/internal/cmd/beta/vpn/gateway/describe/describe_test.go index d0ca98f5a..ccddd49eb 100644 --- a/internal/cmd/beta/vpn/gateway/describe/describe_test.go +++ b/internal/cmd/beta/vpn/gateway/describe/describe_test.go @@ -14,9 +14,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -29,8 +26,8 @@ var testGatewayId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -108,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -116,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -124,7 +121,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, diff --git a/internal/cmd/beta/vpn/gateway/list/list_test.go b/internal/cmd/beta/vpn/gateway/list/list_test.go index 9674587aa..1cd6f9b06 100644 --- a/internal/cmd/beta/vpn/gateway/list/list_test.go +++ b/internal/cmd/beta/vpn/gateway/list/list_test.go @@ -16,9 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -31,9 +28,9 @@ var testLimit int64 = 10 func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, - limitFlag: strconv.FormatInt(testLimit, 10), + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: strconv.FormatInt(testLimit, 10), } for _, mod := range mods { mod(flagValues) @@ -86,21 +83,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, diff --git a/internal/cmd/beta/vpn/gateway/status/status_test.go b/internal/cmd/beta/vpn/gateway/status/status_test.go index 4552144e8..554344c1b 100644 --- a/internal/cmd/beta/vpn/gateway/status/status_test.go +++ b/internal/cmd/beta/vpn/gateway/status/status_test.go @@ -14,9 +14,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -29,8 +26,8 @@ var testGatewayId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -108,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -116,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -124,7 +121,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, diff --git a/internal/cmd/beta/vpn/plans/plans_test.go b/internal/cmd/beta/vpn/plans/plans_test.go index 7513ab482..d063e83e9 100644 --- a/internal/cmd/beta/vpn/plans/plans_test.go +++ b/internal/cmd/beta/vpn/plans/plans_test.go @@ -15,8 +15,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -28,8 +26,8 @@ var testLimit int64 = 10 func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ // Project ID is not necessary for this request - regionFlag: testRegion, - limitFlag: strconv.FormatInt(testLimit, 10), + globalflags.RegionFlag: testRegion, + limitFlag: strconv.FormatInt(testLimit, 10), } for _, mod := range mods { mod(flagValues) diff --git a/internal/cmd/beta/vpn/quotas/quotas_test.go b/internal/cmd/beta/vpn/quotas/quotas_test.go index d5579ff09..f84219f89 100644 --- a/internal/cmd/beta/vpn/quotas/quotas_test.go +++ b/internal/cmd/beta/vpn/quotas/quotas_test.go @@ -16,9 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -31,9 +28,9 @@ var testLimit int64 = 10 func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, - limitFlag: strconv.FormatInt(testLimit, 10), + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: strconv.FormatInt(testLimit, 10), } for _, mod := range mods { mod(flagValues) @@ -86,21 +83,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, From 3215197f973e09c3b84561b73cfc0eb8fae9bfe2 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Wed, 15 Jul 2026 10:14:51 +0200 Subject: [PATCH 11/11] removed utils.getSliceFromPointer(&var) pattern --- internal/cmd/beta/sfs/export-policy/list/list.go | 2 +- internal/cmd/beta/sfs/performance-class/list/list.go | 2 +- internal/cmd/beta/sfs/resource-pool/list/list.go | 2 +- internal/cmd/beta/sfs/share/list/list.go | 2 +- internal/cmd/beta/sfs/snapshot-policy/list/list.go | 2 +- internal/cmd/beta/sfs/snapshot/list/list.go | 2 +- internal/cmd/beta/vpn/gateway/list/list.go | 7 +++---- internal/cmd/beta/vpn/plans/plans.go | 7 +++---- 8 files changed, 12 insertions(+), 14 deletions(-) diff --git a/internal/cmd/beta/sfs/export-policy/list/list.go b/internal/cmd/beta/sfs/export-policy/list/list.go index d3fd00883..a05242958 100644 --- a/internal/cmd/beta/sfs/export-policy/list/list.go +++ b/internal/cmd/beta/sfs/export-policy/list/list.go @@ -76,7 +76,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } // Truncate output - items := utils.GetSliceFromPointer(&resp.ShareExportPolicies) + items := resp.ShareExportPolicies if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } diff --git a/internal/cmd/beta/sfs/performance-class/list/list.go b/internal/cmd/beta/sfs/performance-class/list/list.go index 2352c7411..3543fd523 100644 --- a/internal/cmd/beta/sfs/performance-class/list/list.go +++ b/internal/cmd/beta/sfs/performance-class/list/list.go @@ -63,7 +63,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - performanceClasses := utils.GetSliceFromPointer(&resp.PerformanceClasses) + performanceClasses := resp.PerformanceClasses return outputResult(params.Printer, model.OutputFormat, projectLabel, performanceClasses) }, diff --git a/internal/cmd/beta/sfs/resource-pool/list/list.go b/internal/cmd/beta/sfs/resource-pool/list/list.go index aae15972b..f2be8e746 100644 --- a/internal/cmd/beta/sfs/resource-pool/list/list.go +++ b/internal/cmd/beta/sfs/resource-pool/list/list.go @@ -66,7 +66,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list SFS resource pools: %w", err) } - resourcePools := utils.GetSliceFromPointer(&resp.ResourcePools) + resourcePools := resp.ResourcePools // Truncate output if model.Limit != nil && len(resourcePools) > int(*model.Limit) { diff --git a/internal/cmd/beta/sfs/share/list/list.go b/internal/cmd/beta/sfs/share/list/list.go index 8cbd7666c..9db657fb3 100644 --- a/internal/cmd/beta/sfs/share/list/list.go +++ b/internal/cmd/beta/sfs/share/list/list.go @@ -76,7 +76,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } // Truncate output - items := utils.GetSliceFromPointer(&resp.Shares) + items := resp.Shares if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } diff --git a/internal/cmd/beta/sfs/snapshot-policy/list/list.go b/internal/cmd/beta/sfs/snapshot-policy/list/list.go index d343216f2..b97b7a740 100644 --- a/internal/cmd/beta/sfs/snapshot-policy/list/list.go +++ b/internal/cmd/beta/sfs/snapshot-policy/list/list.go @@ -93,7 +93,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } // Truncate output - items := utils.GetSliceFromPointer(&resp.SnapshotPolicies) + items := resp.SnapshotPolicies if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } diff --git a/internal/cmd/beta/sfs/snapshot/list/list.go b/internal/cmd/beta/sfs/snapshot/list/list.go index 418434846..9689e9bbd 100644 --- a/internal/cmd/beta/sfs/snapshot/list/list.go +++ b/internal/cmd/beta/sfs/snapshot/list/list.go @@ -63,7 +63,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } // Truncate output - items := utils.GetSliceFromPointer(&resp.ResourcePoolSnapshots) + items := resp.ResourcePoolSnapshots if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } diff --git a/internal/cmd/beta/vpn/gateway/list/list.go b/internal/cmd/beta/vpn/gateway/list/list.go index cef3a33a6..a14d2279c 100644 --- a/internal/cmd/beta/vpn/gateway/list/list.go +++ b/internal/cmd/beta/vpn/gateway/list/list.go @@ -67,9 +67,8 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } // Truncate output - items := utils.GetSliceFromPointer(&resp.Gateways) - if model.Limit != nil && len(items) > int(*model.Limit) { - items = items[:*model.Limit] + if model.Limit != nil && len(resp.Gateways) > int(*model.Limit) { + resp.Gateways = resp.Gateways[:*model.Limit] } projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) @@ -77,7 +76,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - return outputResult(params.Printer, model.OutputFormat, items, projectLabel) + return outputResult(params.Printer, model.OutputFormat, resp.Gateways, projectLabel) }, } configureFlags(cmd) diff --git a/internal/cmd/beta/vpn/plans/plans.go b/internal/cmd/beta/vpn/plans/plans.go index 2c9f75ebe..ee0617c10 100644 --- a/internal/cmd/beta/vpn/plans/plans.go +++ b/internal/cmd/beta/vpn/plans/plans.go @@ -62,12 +62,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } // Truncate output - items := utils.GetSliceFromPointer(&resp.Plans) - if model.Limit != nil && len(items) > int(*model.Limit) { - items = items[:*model.Limit] + if model.Limit != nil && len(resp.Plans) > int(*model.Limit) { + resp.Plans = resp.Plans[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items, model.Region) + return outputResult(params.Printer, model.OutputFormat, resp.Plans, model.Region) }, } configureFlags(cmd)