From 641f535c52df0cad59b05b7868db0bd14e0f2653 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Fri, 9 Oct 2026 15:27:56 +0200 Subject: [PATCH 1/3] feat(iaas): add vpc region commands --- docs/stackit_alpha_vpc.md | 1 + docs/stackit_alpha_vpc_region.md | 38 ++++ docs/stackit_alpha_vpc_region_create.md | 45 ++++ docs/stackit_alpha_vpc_region_delete.md | 44 ++++ docs/stackit_alpha_vpc_region_describe.md | 44 ++++ docs/stackit_alpha_vpc_region_list.md | 44 ++++ docs/stackit_alpha_vpc_region_update.md | 45 ++++ .../cmd/alpha/vpc/region/create/create.go | 151 +++++++++++++ .../alpha/vpc/region/create/create_test.go | 200 ++++++++++++++++++ .../cmd/alpha/vpc/region/delete/delete.go | 128 +++++++++++ .../alpha/vpc/region/delete/delete_test.go | 116 ++++++++++ .../cmd/alpha/vpc/region/describe/describe.go | 135 ++++++++++++ .../vpc/region/describe/describe_test.go | 183 ++++++++++++++++ internal/cmd/alpha/vpc/region/list/list.go | 144 +++++++++++++ .../cmd/alpha/vpc/region/list/list_test.go | 186 ++++++++++++++++ internal/cmd/alpha/vpc/region/region.go | 35 +++ .../cmd/alpha/vpc/region/update/update.go | 134 ++++++++++++ .../alpha/vpc/region/update/update_test.go | 194 +++++++++++++++++ internal/cmd/alpha/vpc/vpc.go | 2 + 19 files changed, 1869 insertions(+) create mode 100644 docs/stackit_alpha_vpc_region.md create mode 100644 docs/stackit_alpha_vpc_region_create.md create mode 100644 docs/stackit_alpha_vpc_region_delete.md create mode 100644 docs/stackit_alpha_vpc_region_describe.md create mode 100644 docs/stackit_alpha_vpc_region_list.md create mode 100644 docs/stackit_alpha_vpc_region_update.md create mode 100644 internal/cmd/alpha/vpc/region/create/create.go create mode 100644 internal/cmd/alpha/vpc/region/create/create_test.go create mode 100644 internal/cmd/alpha/vpc/region/delete/delete.go create mode 100644 internal/cmd/alpha/vpc/region/delete/delete_test.go create mode 100644 internal/cmd/alpha/vpc/region/describe/describe.go create mode 100644 internal/cmd/alpha/vpc/region/describe/describe_test.go create mode 100644 internal/cmd/alpha/vpc/region/list/list.go create mode 100644 internal/cmd/alpha/vpc/region/list/list_test.go create mode 100644 internal/cmd/alpha/vpc/region/region.go create mode 100644 internal/cmd/alpha/vpc/region/update/update.go create mode 100644 internal/cmd/alpha/vpc/region/update/update_test.go diff --git a/docs/stackit_alpha_vpc.md b/docs/stackit_alpha_vpc.md index 7df257803..bd8aa7965 100644 --- a/docs/stackit_alpha_vpc.md +++ b/docs/stackit_alpha_vpc.md @@ -34,5 +34,6 @@ stackit alpha vpc [flags] * [stackit alpha vpc delete](./stackit_alpha_vpc_delete.md) - Deletes a VPC * [stackit alpha vpc describe](./stackit_alpha_vpc_describe.md) - Shows details of a VPC * [stackit alpha vpc list](./stackit_alpha_vpc_list.md) - Lists all VPC of a project +* [stackit alpha vpc region](./stackit_alpha_vpc_region.md) - Manages regional configurations of a VPC * [stackit alpha vpc update](./stackit_alpha_vpc_update.md) - Updates a VPC diff --git a/docs/stackit_alpha_vpc_region.md b/docs/stackit_alpha_vpc_region.md new file mode 100644 index 000000000..2e29c7ae9 --- /dev/null +++ b/docs/stackit_alpha_vpc_region.md @@ -0,0 +1,38 @@ +## stackit alpha vpc region + +Manages regional configurations of a VPC + +### Synopsis + +Manages regional configurations of a VPC. + +``` +stackit alpha vpc region [flags] +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc region" +``` + +### 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 alpha vpc](./stackit_alpha_vpc.md) - Manages vpcs +* [stackit alpha vpc region create](./stackit_alpha_vpc_region_create.md) - Creates a regional configuration for a VPC +* [stackit alpha vpc region delete](./stackit_alpha_vpc_region_delete.md) - Deletes a regional configuration for a VPC +* [stackit alpha vpc region describe](./stackit_alpha_vpc_region_describe.md) - Describes a regional configuration for a VPC +* [stackit alpha vpc region list](./stackit_alpha_vpc_region_list.md) - Lists all configured regions for a VPC +* [stackit alpha vpc region update](./stackit_alpha_vpc_region_update.md) - Updates a regional configuration for a VPC + diff --git a/docs/stackit_alpha_vpc_region_create.md b/docs/stackit_alpha_vpc_region_create.md new file mode 100644 index 000000000..d4ea33c57 --- /dev/null +++ b/docs/stackit_alpha_vpc_region_create.md @@ -0,0 +1,45 @@ +## stackit alpha vpc region create + +Creates a regional configuration for a VPC + +### Synopsis + +Creates a regional configuration for a VPC. VPC can only be used in projects with enabled VPC. + +``` +stackit alpha vpc region create [flags] +``` + +### Examples + +``` + Create a regional configuration "eu02" for a VPC with ID "xxx" + $ stackit alpha vpc region create --vpc-id xxx --region eu02 + + Create a regional configuration with default DNS name servers, using the configured project and region + $ stackit alpha vpc region create --vpc-id xxx --ipv4-default-nameservers 8.8.8.8,8.8.4.4 +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc region create" + --ipv4-default-nameservers strings List of default DNS name server IPs + --vpc-id string VPC ID +``` + +### 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 alpha vpc region](./stackit_alpha_vpc_region.md) - Manages regional configurations of a VPC + diff --git a/docs/stackit_alpha_vpc_region_delete.md b/docs/stackit_alpha_vpc_region_delete.md new file mode 100644 index 000000000..03bb57ba0 --- /dev/null +++ b/docs/stackit_alpha_vpc_region_delete.md @@ -0,0 +1,44 @@ +## stackit alpha vpc region delete + +Deletes a regional configuration for a VPC + +### Synopsis + +Deletes a regional configuration for a VPC. + +``` +stackit alpha vpc region delete [flags] +``` + +### Examples + +``` + Delete the regional configuration "eu02" for a VPC with ID "xxx" + $ stackit alpha vpc region delete --vpc-id xxx --region eu02 + + Delete a regional configuration using the configured project and region + $ stackit alpha vpc region delete --vpc-id xxx +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc region delete" + --vpc-id string VPC ID +``` + +### 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 alpha vpc region](./stackit_alpha_vpc_region.md) - Manages regional configurations of a VPC + diff --git a/docs/stackit_alpha_vpc_region_describe.md b/docs/stackit_alpha_vpc_region_describe.md new file mode 100644 index 000000000..ec69dc583 --- /dev/null +++ b/docs/stackit_alpha_vpc_region_describe.md @@ -0,0 +1,44 @@ +## stackit alpha vpc region describe + +Describes a regional configuration for a VPC + +### Synopsis + +Describes a regional configuration for a VPC. + +``` +stackit alpha vpc region describe [flags] +``` + +### Examples + +``` + Describe the regional configuration "eu02" for a VPC with ID "xxx" + $ stackit alpha vpc region describe --vpc-id xxx --region eu02 + + Describe a regional configuration in JSON format, using the configured project and region + $ stackit alpha vpc region describe --vpc-id xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc region describe" + --vpc-id string VPC ID +``` + +### 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 alpha vpc region](./stackit_alpha_vpc_region.md) - Manages regional configurations of a VPC + diff --git a/docs/stackit_alpha_vpc_region_list.md b/docs/stackit_alpha_vpc_region_list.md new file mode 100644 index 000000000..92c6f36fa --- /dev/null +++ b/docs/stackit_alpha_vpc_region_list.md @@ -0,0 +1,44 @@ +## stackit alpha vpc region list + +Lists all configured regions for a VPC + +### Synopsis + +Lists all configured regions for a VPC. + +``` +stackit alpha vpc region list [flags] +``` + +### Examples + +``` + List all configured regions for a VPC with ID "xxx" + $ stackit alpha vpc region list --vpc-id xxx + + List all configured regions for a VPC in JSON format + $ stackit alpha vpc region list --vpc-id xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc region list" + --vpc-id string VPC ID +``` + +### 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 alpha vpc region](./stackit_alpha_vpc_region.md) - Manages regional configurations of a VPC + diff --git a/docs/stackit_alpha_vpc_region_update.md b/docs/stackit_alpha_vpc_region_update.md new file mode 100644 index 000000000..3a60fc40c --- /dev/null +++ b/docs/stackit_alpha_vpc_region_update.md @@ -0,0 +1,45 @@ +## stackit alpha vpc region update + +Updates a regional configuration for a VPC + +### Synopsis + +Updates a regional configuration for a VPC. + +``` +stackit alpha vpc region update [flags] +``` + +### Examples + +``` + Update the default DNS name servers for a VPC with ID "xxx" in region "eu02" + $ stackit alpha vpc region update --vpc-id xxx --region eu02 --ipv4-default-nameservers 8.8.8.8,8.8.4.4 + + Clear the default DNS name servers, using the configured project and region + $ stackit alpha vpc region update --vpc-id xxx --ipv4-default-nameservers "" +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc region update" + --ipv4-default-nameservers strings List of default DNS name server IPs + --vpc-id string VPC ID +``` + +### 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 alpha vpc region](./stackit_alpha_vpc_region.md) - Manages regional configurations of a VPC + diff --git a/internal/cmd/alpha/vpc/region/create/create.go b/internal/cmd/alpha/vpc/region/create/create.go new file mode 100644 index 000000000..eb75ff3b4 --- /dev/null +++ b/internal/cmd/alpha/vpc/region/create/create.go @@ -0,0 +1,151 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api/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/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaasalpha/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaasalpha/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +const ( + vpcIdFlag = "vpc-id" + ipv4DefaultNameserversFlag = "ipv4-default-nameservers" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + VpcId string + IPv4DefaultNameservers []string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a regional configuration for a VPC", + Long: "Creates a regional configuration for a VPC. VPC can only be used in projects with enabled VPC.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a regional configuration "eu02" for a VPC with ID "xxx"`, + `$ stackit alpha vpc region create --vpc-id xxx --region eu02`, + ), + examples.NewExample( + `Create a regional configuration with default DNS name servers, using the configured project and region`, + `$ stackit alpha vpc region create --vpc-id xxx --ipv4-default-nameservers 8.8.8.8,8.8.4.4`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + vpcLabel, err := iaasUtils.GetVPCName(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc name: %v", err) + } + if vpcLabel == "" { + vpcLabel = model.VpcId + } + + prompt := fmt.Sprintf("Are you sure you want to create the regional configuration %q for VPC %q?", model.Region, vpcLabel) + if err := params.Printer.PromptForConfirmation(prompt); err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create vpc region: %w", err) + } + if !model.Async { + err := spinner.Run(params.Printer, "Create vpc region", func() error { + resp, err = wait.CreateVPCRegionWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId, model.Region).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for vpc region creation: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, model.Region, vpcLabel, resp) + }, + } + configureFlags(cmd) + + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), vpcIdFlag, "VPC ID") + cmd.Flags().StringSlice(ipv4DefaultNameserversFlag, nil, "List of default DNS name server IPs") + + cobra.CheckErr(flags.MarkFlagsRequired(cmd, vpcIdFlag)) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := &inputModel{ + GlobalFlagModel: globalFlags, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + IPv4DefaultNameservers: flags.FlagToStringSliceValue(p, cmd, ipv4DefaultNameserversFlag), + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateVPCRegionRequest { + payload := iaas.CreateVPCRegionPayload{} + if model.IPv4DefaultNameservers != nil { + payload.Ipv4 = &iaas.RegionalVPCIPv4{ + DefaultNameservers: model.IPv4DefaultNameservers, + } + } + + return apiClient.DefaultAPI.CreateVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region).CreateVPCRegionPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat string, async bool, region, vpcLabel string, resp *iaas.RegionalVPC) error { + if resp == nil { + return fmt.Errorf("create vpc region response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + operationState := "Created" + if async { + operationState = "Triggered creation of" + } + + p.Outputf("%s region configuration for VPC %q.\nRegion: %s\n", operationState, vpcLabel, region) + + return nil + }) +} diff --git a/internal/cmd/alpha/vpc/region/create/create_test.go b/internal/cmd/alpha/vpc/region/create/create_test.go new file mode 100644 index 000000000..430d4869f --- /dev/null +++ b/internal/cmd/alpha/vpc/region/create/create_test.go @@ -0,0 +1,200 @@ +package create + +import ( + "context" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + + "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 ( + testProjectId = uuid.NewString() + testVpcId = uuid.NewString() +) + +func TestParseInput(t *testing.T) { + for _, tt := range []struct { + name string + flag string + value string + remove bool + args []string + valid bool + nameservers []string + }{ + { + name: "defaults", + valid: true, + }, + { + name: "nameservers", + flag: ipv4DefaultNameserversFlag, + value: "8.8.8.8,8.8.4.4", + valid: true, + nameservers: []string{"8.8.8.8", "8.8.4.4"}, + }, + { + name: "empty nameservers", + flag: ipv4DefaultNameserversFlag, + valid: true, + nameservers: []string{}, + }, + { + name: "missing project", + flag: globalflags.ProjectIdFlag, + remove: true, + }, + { + name: "invalid project", + flag: globalflags.ProjectIdFlag, + value: "invalid", + }, + { + name: "missing region", + flag: globalflags.RegionFlag, + remove: true, + }, + { + name: "missing vpc", + flag: vpcIdFlag, + remove: true, + }, + { + name: "invalid vpc", + flag: vpcIdFlag, + value: "invalid", + }, + { + name: "empty vpc", + flag: vpcIdFlag, + }, + { + name: "unexpected argument", + args: []string{"unexpected"}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: "eu02", + vpcIdFlag: testVpcId, + } + if tt.flag != "" { + flagValues[tt.flag] = tt.value + if tt.remove { + delete(flagValues, tt.flag) + } + } + + var expected *inputModel + if tt.valid { + expected = &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu02", + Verbosity: globalflags.VerbosityDefault, + }, + VpcId: testVpcId, + IPv4DefaultNameservers: tt.nameservers, + } + } + + testutils.TestParseInput(t, NewCmd, parseInput, expected, tt.args, flagValues, tt.valid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + ctx := context.Background() + client := &iaas.APIClient{ + DefaultAPI: &iaas.DefaultAPIService{}, + } + + for _, nameservers := range [][]string{nil, {}, {"8.8.8.8", "8.8.4.4"}} { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu02", + }, + VpcId: testVpcId, + IPv4DefaultNameservers: nameservers, + } + + payload := iaas.CreateVPCRegionPayload{} + if nameservers != nil { + payload.Ipv4 = &iaas.RegionalVPCIPv4{ + DefaultNameservers: nameservers, + } + } + + want := client.DefaultAPI.CreateVPCRegion(ctx, testProjectId, testVpcId, "eu02").CreateVPCRegionPayload(payload) + got := buildRequest(ctx, model, client) + if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { + t.Fatalf("request mismatch (-want +got): %s", diff) + } + } +} + +func TestOutputResult(t *testing.T) { + for _, tt := range []struct { + name string + format string + async bool + resp *iaas.RegionalVPC + want string + wantErr bool + }{ + { + name: "nil", + wantErr: true, + }, + { + name: "created", + resp: &iaas.RegionalVPC{}, + want: "Created region configuration", + }, + { + name: "async", + async: true, + resp: &iaas.RegionalVPC{}, + want: "Triggered creation of", + }, + { + name: "json", + format: "json", + resp: &iaas.RegionalVPC{ + Status: new("CREATED"), + }, + want: `"status": "CREATED"`, + }, + { + name: "yaml", + format: "yaml", + resp: &iaas.RegionalVPC{ + Status: new("CREATED"), + }, + want: "status: CREATED", + }, + } { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.format, tt.async, "eu02", "my-vpc", tt.resp) + if (err != nil) != tt.wantErr { + t.Fatalf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + + if !strings.Contains(params.Out.String(), tt.want) { + t.Errorf("output %q does not contain %q", params.Out.String(), tt.want) + } + }) + } +} diff --git a/internal/cmd/alpha/vpc/region/delete/delete.go b/internal/cmd/alpha/vpc/region/delete/delete.go new file mode 100644 index 000000000..880c9365e --- /dev/null +++ b/internal/cmd/alpha/vpc/region/delete/delete.go @@ -0,0 +1,128 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api/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/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaasalpha/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaasalpha/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +const vpcIdFlag = "vpc-id" + +type inputModel struct { + *globalflags.GlobalFlagModel + VpcId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Deletes a regional configuration for a VPC", + Long: "Deletes a regional configuration for a VPC.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete the regional configuration "eu02" for a VPC with ID "xxx"`, + `$ stackit alpha vpc region delete --vpc-id xxx --region eu02`, + ), + examples.NewExample( + `Delete a regional configuration using the configured project and region`, + `$ stackit alpha vpc region delete --vpc-id xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + vpcLabel, err := iaasUtils.GetVPCName(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc name: %v", err) + } + if vpcLabel == "" { + vpcLabel = model.VpcId + } + + prompt := fmt.Sprintf("Are you sure you want to delete the regional configuration %q for VPC %q?", model.Region, vpcLabel) + if err := params.Printer.PromptForConfirmation(prompt); err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + if err := req.Execute(); err != nil { + return fmt.Errorf("delete vpc region: %w", err) + } + + if !model.Async { + err := spinner.Run(params.Printer, "Delete vpc region", func() error { + _, err := wait.DeleteVPCRegionWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId, model.Region).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for vpc region deletion: %w", err) + } + } + + operationState := "Deleted" + if model.Async { + operationState = "Triggered deletion of" + } + + params.Printer.Outputf("%s region configuration for VPC %q.\nRegion: %s\n", operationState, vpcLabel, model.Region) + + return nil + }, + } + configureFlags(cmd) + + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), vpcIdFlag, "VPC ID") + + cobra.CheckErr(flags.MarkFlagsRequired(cmd, vpcIdFlag)) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := &inputModel{ + GlobalFlagModel: globalFlags, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteVPCRegionRequest { + return apiClient.DefaultAPI.DeleteVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region) +} diff --git a/internal/cmd/alpha/vpc/region/delete/delete_test.go b/internal/cmd/alpha/vpc/region/delete/delete_test.go new file mode 100644 index 000000000..3d9ee4969 --- /dev/null +++ b/internal/cmd/alpha/vpc/region/delete/delete_test.go @@ -0,0 +1,116 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var ( + testProjectId = uuid.NewString() + testVpcId = uuid.NewString() +) + +func TestParseInput(t *testing.T) { + for _, tt := range []struct { + name string + flag string + value string + remove bool + args []string + valid bool + }{ + { + name: "base", + valid: true, + }, + { + name: "missing project", + flag: globalflags.ProjectIdFlag, + remove: true, + }, + { + name: "invalid project", + flag: globalflags.ProjectIdFlag, + value: "invalid", + }, + { + name: "missing region", + flag: globalflags.RegionFlag, + remove: true, + }, + { + name: "missing vpc", + flag: vpcIdFlag, + remove: true, + }, + { + name: "invalid vpc", + flag: vpcIdFlag, + value: "invalid", + }, + { + name: "empty vpc", + flag: vpcIdFlag, + }, + { + name: "unexpected argument", + args: []string{"unexpected"}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: "eu02", + vpcIdFlag: testVpcId, + } + if tt.flag != "" { + flagValues[tt.flag] = tt.value + if tt.remove { + delete(flagValues, tt.flag) + } + } + + var expected *inputModel + if tt.valid { + expected = &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu02", + Verbosity: globalflags.VerbosityDefault, + }, + VpcId: testVpcId, + } + } + + testutils.TestParseInput(t, NewCmd, parseInput, expected, tt.args, flagValues, tt.valid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + ctx := context.Background() + client := &iaas.APIClient{ + DefaultAPI: &iaas.DefaultAPIService{}, + } + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu02", + }, + VpcId: testVpcId, + } + + want := client.DefaultAPI.DeleteVPCRegion(ctx, testProjectId, testVpcId, "eu02") + got := buildRequest(ctx, model, client) + if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { + t.Fatalf("request mismatch (-want +got): %s", diff) + } +} diff --git a/internal/cmd/alpha/vpc/region/describe/describe.go b/internal/cmd/alpha/vpc/region/describe/describe.go new file mode 100644 index 000000000..79458f065 --- /dev/null +++ b/internal/cmd/alpha/vpc/region/describe/describe.go @@ -0,0 +1,135 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + + "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/services/iaasalpha/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaasalpha/utils" + "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 vpcIdFlag = "vpc-id" + +type inputModel struct { + *globalflags.GlobalFlagModel + VpcId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Describes a regional configuration for a VPC", + Long: "Describes a regional configuration for a VPC.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Describe the regional configuration "eu02" for a VPC with ID "xxx"`, + `$ stackit alpha vpc region describe --vpc-id xxx --region eu02`, + ), + examples.NewExample( + `Describe a regional configuration in JSON format, using the configured project and region`, + `$ stackit alpha vpc region describe --vpc-id xxx --output-format json`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + vpcName, err := iaasUtils.GetVPCName(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc name: %v", err) + vpcName = "" + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("describe vpc region: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.Region, model.VpcId, vpcName, resp) + }, + } + configureFlags(cmd) + + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), vpcIdFlag, "VPC ID") + + cobra.CheckErr(flags.MarkFlagsRequired(cmd, vpcIdFlag)) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := &inputModel{ + GlobalFlagModel: globalFlags, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetVPCRegionRequest { + return apiClient.DefaultAPI.GetVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat, region, vpcId, vpcName string, resp *iaas.RegionalVPC) error { + if resp == nil { + return fmt.Errorf("describe vpc region response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + table := tables.NewTable() + table.AddRow("ID", vpcId) + table.AddSeparator() + if vpcName != "" { + table.AddRow("NAME", vpcName) + table.AddSeparator() + } + table.AddRow("REGION", region) + table.AddSeparator() + table.AddRow("STATUS", utils.PtrString(resp.Status)) + if ipv4 := resp.Ipv4; ipv4 != nil { + table.AddSeparator() + table.AddRow("DNS NAME SERVERS", strings.Join(ipv4.DefaultNameservers, ",")) + } + + if err := table.Display(p); err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/alpha/vpc/region/describe/describe_test.go b/internal/cmd/alpha/vpc/region/describe/describe_test.go new file mode 100644 index 000000000..30a328bc6 --- /dev/null +++ b/internal/cmd/alpha/vpc/region/describe/describe_test.go @@ -0,0 +1,183 @@ +package describe + +import ( + "context" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + + "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 ( + testProjectId = uuid.NewString() + testVpcId = uuid.NewString() +) + +func TestParseInput(t *testing.T) { + for _, tt := range []struct { + name string + flag string + value string + remove bool + args []string + valid bool + }{ + { + name: "base", + valid: true, + }, + { + name: "missing project", + flag: globalflags.ProjectIdFlag, + remove: true, + }, + { + name: "invalid project", + flag: globalflags.ProjectIdFlag, + value: "invalid", + }, + { + name: "missing region", + flag: globalflags.RegionFlag, + remove: true, + }, + { + name: "missing vpc", + flag: vpcIdFlag, + remove: true, + }, + { + name: "invalid vpc", + flag: vpcIdFlag, + value: "invalid", + }, + { + name: "empty vpc", + flag: vpcIdFlag, + }, + { + name: "unexpected argument", + args: []string{"unexpected"}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: "eu02", + vpcIdFlag: testVpcId, + } + if tt.flag != "" { + flagValues[tt.flag] = tt.value + if tt.remove { + delete(flagValues, tt.flag) + } + } + + var expected *inputModel + if tt.valid { + expected = &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu02", + Verbosity: globalflags.VerbosityDefault, + }, + VpcId: testVpcId, + } + } + + testutils.TestParseInput(t, NewCmd, parseInput, expected, tt.args, flagValues, tt.valid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + ctx := context.Background() + client := &iaas.APIClient{ + DefaultAPI: &iaas.DefaultAPIService{}, + } + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu02", + }, + VpcId: testVpcId, + } + + want := client.DefaultAPI.GetVPCRegion(ctx, testProjectId, testVpcId, "eu02") + got := buildRequest(ctx, model, client) + if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { + t.Fatalf("request mismatch (-want +got): %s", diff) + } +} + +func TestOutputResult(t *testing.T) { + resp := &iaas.RegionalVPC{ + Status: new("CREATED"), + Ipv4: &iaas.RegionalVPCIPv4{ + DefaultNameservers: []string{"8.8.8.8", "8.8.4.4"}, + }, + } + + for _, tt := range []struct { + name string + format string + vpcName string + resp *iaas.RegionalVPC + want []string + wantErr bool + }{ + { + name: "nil", + wantErr: true, + }, + { + name: "empty", + resp: &iaas.RegionalVPC{}, + want: []string{testVpcId, "eu02"}, + }, + { + name: "table", + vpcName: "my-vpc", + resp: resp, + want: []string{testVpcId, "my-vpc", "eu02", "CREATED", "8.8.8.8,8.8.4.4"}, + }, + { + name: "json", + format: "json", + resp: resp, + want: []string{`"status": "CREATED"`, `"defaultNameservers"`}, + }, + { + name: "yaml", + format: "yaml", + resp: resp, + want: []string{"status: CREATED", "defaultNameservers:"}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.format, "eu02", testVpcId, tt.vpcName, tt.resp) + if (err != nil) != tt.wantErr { + t.Fatalf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + + for _, want := range tt.want { + if !strings.Contains(params.Out.String(), want) { + t.Errorf("output %q does not contain %q", params.Out.String(), want) + } + } + + if tt.format == "" && tt.vpcName == "" && strings.Contains(params.Out.String(), "NAME") { + t.Errorf("unexpected name row: %s", params.Out.String()) + } + }) + } +} diff --git a/internal/cmd/alpha/vpc/region/list/list.go b/internal/cmd/alpha/vpc/region/list/list.go new file mode 100644 index 000000000..8b4594a63 --- /dev/null +++ b/internal/cmd/alpha/vpc/region/list/list.go @@ -0,0 +1,144 @@ +package list + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + + "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/services/iaasalpha/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaasalpha/utils" + "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 vpcIdFlag = "vpc-id" + +type inputModel struct { + *globalflags.GlobalFlagModel + VpcId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all configured regions for a VPC", + Long: "Lists all configured regions for a VPC.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all configured regions for a VPC with ID "xxx"`, + `$ stackit alpha vpc region list --vpc-id xxx`, + ), + examples.NewExample( + `List all configured regions for a VPC in JSON format`, + `$ stackit alpha vpc region list --vpc-id xxx --output-format json`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + vpcLabel, err := iaasUtils.GetVPCName(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc name: %v", err) + } + if vpcLabel == "" { + vpcLabel = model.VpcId + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list vpc regions: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, vpcLabel, resp) + }, + } + configureFlags(cmd) + + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), vpcIdFlag, "VPC ID") + + cobra.CheckErr(flags.MarkFlagsRequired(cmd, vpcIdFlag)) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := &inputModel{ + GlobalFlagModel: globalFlags, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListVPCRegionsRequest { + return apiClient.DefaultAPI.ListVPCRegions(ctx, model.ProjectId, model.VpcId) +} + +func outputResult(p *print.Printer, outputFormat, vpcLabel string, resp *iaas.RegionalVPCList) error { + if resp == nil { + return fmt.Errorf("list vpc regions response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + if len(resp.Regions) == 0 { + p.Outputf("No regions found for VPC %q\n", vpcLabel) + return nil + } + + regions := make([]string, 0, len(resp.Regions)) + for region := range resp.Regions { + regions = append(regions, region) + } + + slices.Sort(regions) + + table := tables.NewTable() + table.SetHeader("REGION", "STATUS", "DNS NAME SERVERS") + for _, region := range regions { + regionConfig := resp.Regions[region] + var dnsNames string + if ipv4 := regionConfig.Ipv4; ipv4 != nil { + dnsNames = strings.Join(ipv4.DefaultNameservers, ",") + } + + table.AddRow(region, utils.PtrString(regionConfig.Status), dnsNames) + } + + if err := table.Display(p); err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/alpha/vpc/region/list/list_test.go b/internal/cmd/alpha/vpc/region/list/list_test.go new file mode 100644 index 000000000..e139704d2 --- /dev/null +++ b/internal/cmd/alpha/vpc/region/list/list_test.go @@ -0,0 +1,186 @@ +package list + +import ( + "context" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + + "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 ( + testProjectId = uuid.NewString() + testVpcId = uuid.NewString() +) + +func TestParseInput(t *testing.T) { + for _, tt := range []struct { + name string + flag string + value string + remove bool + args []string + valid bool + }{ + { + name: "no region required", + valid: true, + }, + { + name: "missing project", + flag: globalflags.ProjectIdFlag, + remove: true, + }, + { + name: "invalid project", + flag: globalflags.ProjectIdFlag, + value: "invalid", + }, + { + name: "missing vpc", + flag: vpcIdFlag, + remove: true, + }, + { + name: "invalid vpc", + flag: vpcIdFlag, + value: "invalid", + }, + { + name: "empty vpc", + flag: vpcIdFlag, + }, + { + name: "unexpected argument", + args: []string{"unexpected"}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + vpcIdFlag: testVpcId, + } + if tt.flag != "" { + flagValues[tt.flag] = tt.value + if tt.remove { + delete(flagValues, tt.flag) + } + } + + var expected *inputModel + if tt.valid { + expected = &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + VpcId: testVpcId, + } + } + + testutils.TestParseInput(t, NewCmd, parseInput, expected, tt.args, flagValues, tt.valid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + ctx := context.Background() + client := &iaas.APIClient{ + DefaultAPI: &iaas.DefaultAPIService{}, + } + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + VpcId: testVpcId, + } + + want := client.DefaultAPI.ListVPCRegions(ctx, testProjectId, testVpcId) + got := buildRequest(ctx, model, client) + if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { + t.Fatalf("request mismatch (-want +got): %s", diff) + } +} + +func TestOutputResult(t *testing.T) { + resp := &iaas.RegionalVPCList{ + Regions: map[string]iaas.RegionalVPC{ + "eu02": { + Status: new("CREATED"), + Ipv4: &iaas.RegionalVPCIPv4{ + DefaultNameservers: []string{"8.8.8.8"}, + }, + }, + "eu01": {}, + }, + } + + for _, tt := range []struct { + name string + format string + resp *iaas.RegionalVPCList + want []string + wantErr bool + }{ + { + name: "nil", + wantErr: true, + }, + { + name: "empty", + resp: &iaas.RegionalVPCList{}, + want: []string{`No regions found for VPC "my-vpc"`}, + }, + { + name: "table", + resp: resp, + want: []string{"eu01", "eu02", "CREATED", "8.8.8.8"}, + }, + { + name: "json", + format: "json", + resp: resp, + want: []string{`"regions"`, `"eu02"`, `"status": "CREATED"`}, + }, + { + name: "yaml", + format: "yaml", + resp: resp, + want: []string{"regions:", "eu02:", "status: CREATED"}, + }, + { + name: "empty json", + format: "json", + resp: &iaas.RegionalVPCList{ + Regions: map[string]iaas.RegionalVPC{}, + }, + want: []string{`"regions": {}`}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.format, "my-vpc", tt.resp) + if (err != nil) != tt.wantErr { + t.Fatalf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + + for _, want := range tt.want { + if !strings.Contains(params.Out.String(), want) { + t.Errorf("output %q does not contain %q", params.Out.String(), want) + } + } + + if tt.name == "table" && strings.Index(params.Out.String(), "eu01") > strings.Index(params.Out.String(), "eu02") { + t.Errorf("regions are not sorted: %s", params.Out.String()) + } + }) + } +} diff --git a/internal/cmd/alpha/vpc/region/region.go b/internal/cmd/alpha/vpc/region/region.go new file mode 100644 index 000000000..59a641180 --- /dev/null +++ b/internal/cmd/alpha/vpc/region/region.go @@ -0,0 +1,35 @@ +package region + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/region/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/region/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/region/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/region/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/region/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "region", + Short: "Manages regional configurations of a VPC", + Long: "Manages regional configurations of a VPC.", + 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(describe.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) +} diff --git a/internal/cmd/alpha/vpc/region/update/update.go b/internal/cmd/alpha/vpc/region/update/update.go new file mode 100644 index 000000000..e470ccc1b --- /dev/null +++ b/internal/cmd/alpha/vpc/region/update/update.go @@ -0,0 +1,134 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + + "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/services/iaasalpha/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaasalpha/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +const ( + vpcIdFlag = "vpc-id" + ipv4DefaultNameserversFlag = "ipv4-default-nameservers" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + VpcId string + IPv4DefaultNameservers []string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Updates a regional configuration for a VPC", + Long: "Updates a regional configuration for a VPC.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Update the default DNS name servers for a VPC with ID "xxx" in region "eu02"`, + `$ stackit alpha vpc region update --vpc-id xxx --region eu02 --ipv4-default-nameservers 8.8.8.8,8.8.4.4`, + ), + examples.NewExample( + `Clear the default DNS name servers, using the configured project and region`, + `$ stackit alpha vpc region update --vpc-id xxx --ipv4-default-nameservers ""`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + vpcLabel, err := iaasUtils.GetVPCName(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc name: %v", err) + } + if vpcLabel == "" { + vpcLabel = model.VpcId + } + + prompt := fmt.Sprintf("Are you sure you want to update the regional configuration %q for VPC %q?", model.Region, vpcLabel) + if err := params.Printer.PromptForConfirmation(prompt); err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update vpc region: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.Region, vpcLabel, resp) + }, + } + configureFlags(cmd) + + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), vpcIdFlag, "VPC ID") + cmd.Flags().StringSlice(ipv4DefaultNameserversFlag, nil, "List of default DNS name server IPs") + + cobra.CheckErr(flags.MarkFlagsRequired(cmd, vpcIdFlag, ipv4DefaultNameserversFlag)) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := &inputModel{ + GlobalFlagModel: globalFlags, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + IPv4DefaultNameservers: flags.FlagToStringSliceValue(p, cmd, ipv4DefaultNameserversFlag), + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateVPCRegionRequest { + payload := iaas.UpdateVPCRegionPayload{ + Ipv4: &iaas.RegionalVPCIPv4{ + DefaultNameservers: model.IPv4DefaultNameservers, + }, + } + + return apiClient.DefaultAPI.UpdateVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region).UpdateVPCRegionPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat, region, vpcLabel string, resp *iaas.RegionalVPC) error { + if resp == nil { + return fmt.Errorf("update vpc region response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Updated region configuration for VPC %q.\nRegion: %s\n", vpcLabel, region) + + return nil + }) +} diff --git a/internal/cmd/alpha/vpc/region/update/update_test.go b/internal/cmd/alpha/vpc/region/update/update_test.go new file mode 100644 index 000000000..81ebf018d --- /dev/null +++ b/internal/cmd/alpha/vpc/region/update/update_test.go @@ -0,0 +1,194 @@ +package update + +import ( + "context" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + + "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 ( + testProjectId = uuid.NewString() + testVpcId = uuid.NewString() +) + +func TestParseInput(t *testing.T) { + for _, tt := range []struct { + name string + flag string + value string + remove bool + args []string + valid bool + clear bool + }{ + { + name: "nameservers", + valid: true, + }, + { + name: "clear nameservers", + flag: ipv4DefaultNameserversFlag, + valid: true, + clear: true, + }, + { + name: "nothing to update", + flag: ipv4DefaultNameserversFlag, + remove: true, + }, + { + name: "missing project", + flag: globalflags.ProjectIdFlag, + remove: true, + }, + { + name: "invalid project", + flag: globalflags.ProjectIdFlag, + value: "invalid", + }, + { + name: "missing region", + flag: globalflags.RegionFlag, + remove: true, + }, + { + name: "missing vpc", + flag: vpcIdFlag, + remove: true, + }, + { + name: "invalid vpc", + flag: vpcIdFlag, + value: "invalid", + }, + { + name: "empty vpc", + flag: vpcIdFlag, + }, + { + name: "unexpected argument", + args: []string{"unexpected"}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: "eu02", + vpcIdFlag: testVpcId, + ipv4DefaultNameserversFlag: "8.8.8.8,8.8.4.4", + } + if tt.flag != "" { + flagValues[tt.flag] = tt.value + if tt.remove { + delete(flagValues, tt.flag) + } + } + + var expected *inputModel + if tt.valid { + expected = &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu02", + Verbosity: globalflags.VerbosityDefault, + }, + VpcId: testVpcId, + IPv4DefaultNameservers: []string{"8.8.8.8", "8.8.4.4"}, + } + if tt.clear { + expected.IPv4DefaultNameservers = []string{} + } + } + + testutils.TestParseInput(t, NewCmd, parseInput, expected, tt.args, flagValues, tt.valid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + ctx := context.Background() + client := &iaas.APIClient{ + DefaultAPI: &iaas.DefaultAPIService{}, + } + + for _, nameservers := range [][]string{{}, {"8.8.8.8", "8.8.4.4"}} { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu02", + }, + VpcId: testVpcId, + IPv4DefaultNameservers: nameservers, + } + + payload := iaas.UpdateVPCRegionPayload{ + Ipv4: &iaas.RegionalVPCIPv4{ + DefaultNameservers: nameservers, + }, + } + + want := client.DefaultAPI.UpdateVPCRegion(ctx, testProjectId, testVpcId, "eu02").UpdateVPCRegionPayload(payload) + got := buildRequest(ctx, model, client) + if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { + t.Fatalf("request mismatch (-want +got): %s", diff) + } + } +} + +func TestOutputResult(t *testing.T) { + for _, tt := range []struct { + name string + format string + resp *iaas.RegionalVPC + want string + wantErr bool + }{ + { + name: "nil", + wantErr: true, + }, + { + name: "updated", + resp: &iaas.RegionalVPC{}, + want: "Updated region configuration", + }, + { + name: "json", + format: "json", + resp: &iaas.RegionalVPC{ + Status: new("UPDATED"), + }, + want: `"status": "UPDATED"`, + }, + { + name: "yaml", + format: "yaml", + resp: &iaas.RegionalVPC{ + Status: new("UPDATED"), + }, + want: "status: UPDATED", + }, + } { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.format, "eu02", "my-vpc", tt.resp) + if (err != nil) != tt.wantErr { + t.Fatalf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + + if !strings.Contains(params.Out.String(), tt.want) { + t.Errorf("output %q does not contain %q", params.Out.String(), tt.want) + } + }) + } +} diff --git a/internal/cmd/alpha/vpc/vpc.go b/internal/cmd/alpha/vpc/vpc.go index a9209b56c..ff311d4fe 100644 --- a/internal/cmd/alpha/vpc/vpc.go +++ b/internal/cmd/alpha/vpc/vpc.go @@ -7,6 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/region" "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" @@ -31,4 +32,5 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(region.NewCmd(params)) } From 14fcb3a91d79049c393bd66aea2dd4bf503b35e5 Mon Sep 17 00:00:00 2001 From: cgoetz-inovex Date: Fri, 9 Oct 2026 16:47:16 +0200 Subject: [PATCH 2/3] Apply batched suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ruben Hönle --- internal/cmd/alpha/vpc/region/create/create.go | 2 +- internal/cmd/alpha/vpc/region/create/create_test.go | 4 ++-- internal/cmd/alpha/vpc/region/delete/delete.go | 2 +- internal/cmd/alpha/vpc/region/describe/describe.go | 2 +- internal/cmd/alpha/vpc/region/list/list.go | 4 ++-- internal/cmd/alpha/vpc/region/update/update.go | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/cmd/alpha/vpc/region/create/create.go b/internal/cmd/alpha/vpc/region/create/create.go index eb75ff3b4..8f42aa1f1 100644 --- a/internal/cmd/alpha/vpc/region/create/create.go +++ b/internal/cmd/alpha/vpc/region/create/create.go @@ -122,7 +122,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateVPCRegionRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiCreateVPCRegionRequest { payload := iaas.CreateVPCRegionPayload{} if model.IPv4DefaultNameservers != nil { payload.Ipv4 = &iaas.RegionalVPCIPv4{ diff --git a/internal/cmd/alpha/vpc/region/create/create_test.go b/internal/cmd/alpha/vpc/region/create/create_test.go index 430d4869f..0b474d535 100644 --- a/internal/cmd/alpha/vpc/region/create/create_test.go +++ b/internal/cmd/alpha/vpc/region/create/create_test.go @@ -169,7 +169,7 @@ func TestOutputResult(t *testing.T) { }, { name: "json", - format: "json", + format: print.JSONOutputFormat, resp: &iaas.RegionalVPC{ Status: new("CREATED"), }, @@ -177,7 +177,7 @@ func TestOutputResult(t *testing.T) { }, { name: "yaml", - format: "yaml", + format: print.YAMLOutputFormat, resp: &iaas.RegionalVPC{ Status: new("CREATED"), }, diff --git a/internal/cmd/alpha/vpc/region/delete/delete.go b/internal/cmd/alpha/vpc/region/delete/delete.go index 880c9365e..12f9ef43f 100644 --- a/internal/cmd/alpha/vpc/region/delete/delete.go +++ b/internal/cmd/alpha/vpc/region/delete/delete.go @@ -123,6 +123,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteVPCRegionRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiDeleteVPCRegionRequest { return apiClient.DefaultAPI.DeleteVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region) } diff --git a/internal/cmd/alpha/vpc/region/describe/describe.go b/internal/cmd/alpha/vpc/region/describe/describe.go index 79458f065..61c2f513b 100644 --- a/internal/cmd/alpha/vpc/region/describe/describe.go +++ b/internal/cmd/alpha/vpc/region/describe/describe.go @@ -101,7 +101,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetVPCRegionRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiGetVPCRegionRequest { return apiClient.DefaultAPI.GetVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region) } diff --git a/internal/cmd/alpha/vpc/region/list/list.go b/internal/cmd/alpha/vpc/region/list/list.go index 8b4594a63..5a8343937 100644 --- a/internal/cmd/alpha/vpc/region/list/list.go +++ b/internal/cmd/alpha/vpc/region/list/list.go @@ -101,7 +101,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListVPCRegionsRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiListVPCRegionsRequest { return apiClient.DefaultAPI.ListVPCRegions(ctx, model.ProjectId, model.VpcId) } @@ -116,7 +116,7 @@ func outputResult(p *print.Printer, outputFormat, vpcLabel string, resp *iaas.Re return nil } - regions := make([]string, 0, len(resp.Regions)) + regionIds := slices.Sorted(maps.Keys(resp.Regions)) for region := range resp.Regions { regions = append(regions, region) } diff --git a/internal/cmd/alpha/vpc/region/update/update.go b/internal/cmd/alpha/vpc/region/update/update.go index e470ccc1b..21b197fcb 100644 --- a/internal/cmd/alpha/vpc/region/update/update.go +++ b/internal/cmd/alpha/vpc/region/update/update.go @@ -111,7 +111,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateVPCRegionRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiUpdateVPCRegionRequest { payload := iaas.UpdateVPCRegionPayload{ Ipv4: &iaas.RegionalVPCIPv4{ DefaultNameservers: model.IPv4DefaultNameservers, From f7d710bd09d049ed68cec6e802201d0970dc1f67 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Fri, 9 Oct 2026 17:16:07 +0200 Subject: [PATCH 3/3] apply review suggestions --- .../cmd/alpha/vpc/region/create/create.go | 4 +- .../alpha/vpc/region/create/create_test.go | 78 +++++++++++++++---- .../cmd/alpha/vpc/region/delete/delete.go | 4 +- .../alpha/vpc/region/delete/delete_test.go | 2 +- .../cmd/alpha/vpc/region/describe/describe.go | 4 +- .../vpc/region/describe/describe_test.go | 41 ++++++---- internal/cmd/alpha/vpc/region/list/list.go | 12 +-- .../cmd/alpha/vpc/region/list/list_test.go | 41 ++++++---- .../cmd/alpha/vpc/region/update/update.go | 4 +- .../alpha/vpc/region/update/update_test.go | 66 ++++++++++++---- 10 files changed, 181 insertions(+), 75 deletions(-) diff --git a/internal/cmd/alpha/vpc/region/create/create.go b/internal/cmd/alpha/vpc/region/create/create.go index 8f42aa1f1..b903aeaac 100644 --- a/internal/cmd/alpha/vpc/region/create/create.go +++ b/internal/cmd/alpha/vpc/region/create/create.go @@ -72,7 +72,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { @@ -130,7 +130,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.Default } } - return apiClient.DefaultAPI.CreateVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region).CreateVPCRegionPayload(payload) + return apiClient.CreateVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region).CreateVPCRegionPayload(payload) } func outputResult(p *print.Printer, outputFormat string, async bool, region, vpcLabel string, resp *iaas.RegionalVPC) error { diff --git a/internal/cmd/alpha/vpc/region/create/create_test.go b/internal/cmd/alpha/vpc/region/create/create_test.go index 0b474d535..35655b66f 100644 --- a/internal/cmd/alpha/vpc/region/create/create_test.go +++ b/internal/cmd/alpha/vpc/region/create/create_test.go @@ -11,6 +11,7 @@ import ( iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) @@ -113,33 +114,78 @@ func TestParseInput(t *testing.T) { } func TestBuildRequest(t *testing.T) { - ctx := context.Background() - client := &iaas.APIClient{ - DefaultAPI: &iaas.DefaultAPIService{}, - } - - for _, nameservers := range [][]string{nil, {}, {"8.8.8.8", "8.8.4.4"}} { + buildRequestInputModelDefault := func(opts ...func(m *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Region: "eu02", }, VpcId: testVpcId, - IPv4DefaultNameservers: nameservers, + IPv4DefaultNameservers: []string{"8.8.8.8", "8.8.4.4"}, } - payload := iaas.CreateVPCRegionPayload{} - if nameservers != nil { - payload.Ipv4 = &iaas.RegionalVPCIPv4{ - DefaultNameservers: nameservers, - } + for _, opt := range opts { + opt(model) + } + + return model + } + + buildRequestPayloadDefault := func(opts ...func(p *iaas.CreateVPCRegionPayload)) iaas.CreateVPCRegionPayload { + payload := iaas.CreateVPCRegionPayload{ + Ipv4: &iaas.RegionalVPCIPv4{ + DefaultNameservers: []string{"8.8.8.8", "8.8.4.4"}, + }, } - want := client.DefaultAPI.CreateVPCRegion(ctx, testProjectId, testVpcId, "eu02").CreateVPCRegionPayload(payload) - got := buildRequest(ctx, model, client) - if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { - t.Fatalf("request mismatch (-want +got): %s", diff) + for _, opt := range opts { + opt(&payload) } + + return payload + } + + tests := []struct { + description string + inputModel *inputModel + wantPayload iaas.CreateVPCRegionPayload + }{ + { + description: "default", + inputModel: buildRequestInputModelDefault(), + wantPayload: buildRequestPayloadDefault(), + }, + { + description: "nameservers is empty", + inputModel: buildRequestInputModelDefault(func(m *inputModel) { + m.IPv4DefaultNameservers = []string{} + }), + wantPayload: buildRequestPayloadDefault(func(p *iaas.CreateVPCRegionPayload) { + p.Ipv4.DefaultNameservers = []string{} + }), + }, + { + description: "nameservers is nil", + inputModel: buildRequestInputModelDefault(func(m *inputModel) { + m.IPv4DefaultNameservers = nil + }), + wantPayload: buildRequestPayloadDefault(func(p *iaas.CreateVPCRegionPayload) { + p.Ipv4 = nil + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + ctx := context.Background() + client := &iaas.DefaultAPIServiceMock{} + + want := client.CreateVPCRegion(ctx, testProjectId, testVpcId, "eu02").CreateVPCRegionPayload(tt.wantPayload) + got := buildRequest(ctx, tt.inputModel, client) + if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { + t.Fatalf("request mismatch (-want +got): %s", diff) + } + }) } } diff --git a/internal/cmd/alpha/vpc/region/delete/delete.go b/internal/cmd/alpha/vpc/region/delete/delete.go index 12f9ef43f..9b52e7d84 100644 --- a/internal/cmd/alpha/vpc/region/delete/delete.go +++ b/internal/cmd/alpha/vpc/region/delete/delete.go @@ -68,7 +68,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) if err := req.Execute(); err != nil { return fmt.Errorf("delete vpc region: %w", err) @@ -124,5 +124,5 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiDeleteVPCRegionRequest { - return apiClient.DefaultAPI.DeleteVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region) + return apiClient.DeleteVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region) } diff --git a/internal/cmd/alpha/vpc/region/delete/delete_test.go b/internal/cmd/alpha/vpc/region/delete/delete_test.go index 3d9ee4969..43e07fb25 100644 --- a/internal/cmd/alpha/vpc/region/delete/delete_test.go +++ b/internal/cmd/alpha/vpc/region/delete/delete_test.go @@ -109,7 +109,7 @@ func TestBuildRequest(t *testing.T) { } want := client.DefaultAPI.DeleteVPCRegion(ctx, testProjectId, testVpcId, "eu02") - got := buildRequest(ctx, model, client) + got := buildRequest(ctx, model, client.DefaultAPI) if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { t.Fatalf("request mismatch (-want +got): %s", diff) } diff --git a/internal/cmd/alpha/vpc/region/describe/describe.go b/internal/cmd/alpha/vpc/region/describe/describe.go index 61c2f513b..80b02573a 100644 --- a/internal/cmd/alpha/vpc/region/describe/describe.go +++ b/internal/cmd/alpha/vpc/region/describe/describe.go @@ -62,7 +62,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { vpcName = "" } - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { @@ -102,7 +102,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiGetVPCRegionRequest { - return apiClient.DefaultAPI.GetVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region) + return apiClient.GetVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region) } func outputResult(p *print.Printer, outputFormat, region, vpcId, vpcName string, resp *iaas.RegionalVPC) error { diff --git a/internal/cmd/alpha/vpc/region/describe/describe_test.go b/internal/cmd/alpha/vpc/region/describe/describe_test.go index 30a328bc6..5d4740219 100644 --- a/internal/cmd/alpha/vpc/region/describe/describe_test.go +++ b/internal/cmd/alpha/vpc/region/describe/describe_test.go @@ -11,6 +11,7 @@ import ( iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) @@ -99,21 +100,33 @@ func TestParseInput(t *testing.T) { func TestBuildRequest(t *testing.T) { ctx := context.Background() - client := &iaas.APIClient{ - DefaultAPI: &iaas.DefaultAPIService{}, - } - model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ - ProjectId: testProjectId, - Region: "eu02", + client := &iaas.DefaultAPIServiceMock{} + + tests := []struct { + description string + inputModel *inputModel + want iaas.ApiGetVPCRegionRequest + }{ + { + description: "default", + inputModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu02", + }, + VpcId: testVpcId, + }, + want: client.GetVPCRegion(ctx, testProjectId, testVpcId, "eu02"), }, - VpcId: testVpcId, } - want := client.DefaultAPI.GetVPCRegion(ctx, testProjectId, testVpcId, "eu02") - got := buildRequest(ctx, model, client) - if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { - t.Fatalf("request mismatch (-want +got): %s", diff) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := buildRequest(ctx, tt.inputModel, client) + if diff := cmp.Diff(tt.want, got, cmp.AllowUnexported(tt.want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { + t.Fatalf("request mismatch (-want +got): %s", diff) + } + }) } } @@ -150,13 +163,13 @@ func TestOutputResult(t *testing.T) { }, { name: "json", - format: "json", + format: print.JSONOutputFormat, resp: resp, want: []string{`"status": "CREATED"`, `"defaultNameservers"`}, }, { name: "yaml", - format: "yaml", + format: print.YAMLOutputFormat, resp: resp, want: []string{"status: CREATED", "defaultNameservers:"}, }, diff --git a/internal/cmd/alpha/vpc/region/list/list.go b/internal/cmd/alpha/vpc/region/list/list.go index 5a8343937..61b9cf09c 100644 --- a/internal/cmd/alpha/vpc/region/list/list.go +++ b/internal/cmd/alpha/vpc/region/list/list.go @@ -3,6 +3,7 @@ package list import ( "context" "fmt" + "maps" "slices" "strings" @@ -65,7 +66,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { vpcLabel = model.VpcId } - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { @@ -102,7 +103,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiListVPCRegionsRequest { - return apiClient.DefaultAPI.ListVPCRegions(ctx, model.ProjectId, model.VpcId) + return apiClient.ListVPCRegions(ctx, model.ProjectId, model.VpcId) } func outputResult(p *print.Printer, outputFormat, vpcLabel string, resp *iaas.RegionalVPCList) error { @@ -117,15 +118,10 @@ func outputResult(p *print.Printer, outputFormat, vpcLabel string, resp *iaas.Re } regionIds := slices.Sorted(maps.Keys(resp.Regions)) - for region := range resp.Regions { - regions = append(regions, region) - } - - slices.Sort(regions) table := tables.NewTable() table.SetHeader("REGION", "STATUS", "DNS NAME SERVERS") - for _, region := range regions { + for _, region := range regionIds { regionConfig := resp.Regions[region] var dnsNames string if ipv4 := regionConfig.Ipv4; ipv4 != nil { diff --git a/internal/cmd/alpha/vpc/region/list/list_test.go b/internal/cmd/alpha/vpc/region/list/list_test.go index e139704d2..e5fee6961 100644 --- a/internal/cmd/alpha/vpc/region/list/list_test.go +++ b/internal/cmd/alpha/vpc/region/list/list_test.go @@ -11,6 +11,7 @@ import ( iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) @@ -92,20 +93,32 @@ func TestParseInput(t *testing.T) { func TestBuildRequest(t *testing.T) { ctx := context.Background() - client := &iaas.APIClient{ - DefaultAPI: &iaas.DefaultAPIService{}, - } - model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ - ProjectId: testProjectId, + client := &iaas.DefaultAPIServiceMock{} + + tests := []struct { + description string + inputModel *inputModel + want iaas.ApiListVPCRegionsRequest + }{ + { + description: "default", + inputModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + VpcId: testVpcId, + }, + want: client.ListVPCRegions(ctx, testProjectId, testVpcId), }, - VpcId: testVpcId, } - want := client.DefaultAPI.ListVPCRegions(ctx, testProjectId, testVpcId) - got := buildRequest(ctx, model, client) - if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { - t.Fatalf("request mismatch (-want +got): %s", diff) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := buildRequest(ctx, tt.inputModel, client) + if diff := cmp.Diff(tt.want, got, cmp.AllowUnexported(tt.want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { + t.Fatalf("request mismatch (-want +got): %s", diff) + } + }) } } @@ -145,19 +158,19 @@ func TestOutputResult(t *testing.T) { }, { name: "json", - format: "json", + format: print.JSONOutputFormat, resp: resp, want: []string{`"regions"`, `"eu02"`, `"status": "CREATED"`}, }, { name: "yaml", - format: "yaml", + format: print.YAMLOutputFormat, resp: resp, want: []string{"regions:", "eu02:", "status: CREATED"}, }, { name: "empty json", - format: "json", + format: print.JSONOutputFormat, resp: &iaas.RegionalVPCList{ Regions: map[string]iaas.RegionalVPC{}, }, diff --git a/internal/cmd/alpha/vpc/region/update/update.go b/internal/cmd/alpha/vpc/region/update/update.go index 21b197fcb..e83950bff 100644 --- a/internal/cmd/alpha/vpc/region/update/update.go +++ b/internal/cmd/alpha/vpc/region/update/update.go @@ -70,7 +70,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { @@ -118,7 +118,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.Default }, } - return apiClient.DefaultAPI.UpdateVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region).UpdateVPCRegionPayload(payload) + return apiClient.UpdateVPCRegion(ctx, model.ProjectId, model.VpcId, model.Region).UpdateVPCRegionPayload(payload) } func outputResult(p *print.Printer, outputFormat, region, vpcLabel string, resp *iaas.RegionalVPC) error { diff --git a/internal/cmd/alpha/vpc/region/update/update_test.go b/internal/cmd/alpha/vpc/region/update/update_test.go index 81ebf018d..58195deaa 100644 --- a/internal/cmd/alpha/vpc/region/update/update_test.go +++ b/internal/cmd/alpha/vpc/region/update/update_test.go @@ -11,6 +11,7 @@ import ( iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) @@ -115,32 +116,69 @@ func TestParseInput(t *testing.T) { } func TestBuildRequest(t *testing.T) { - ctx := context.Background() - client := &iaas.APIClient{ - DefaultAPI: &iaas.DefaultAPIService{}, - } - - for _, nameservers := range [][]string{{}, {"8.8.8.8", "8.8.4.4"}} { + buildRequestInputModelDefault := func(opts ...func(m *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Region: "eu02", }, VpcId: testVpcId, - IPv4DefaultNameservers: nameservers, + IPv4DefaultNameservers: []string{"8.8.8.8", "8.8.4.4"}, } + for _, opt := range opts { + opt(model) + } + + return model + } + + buildRequestPayloadDefault := func(opts ...func(p *iaas.UpdateVPCRegionPayload)) iaas.UpdateVPCRegionPayload { payload := iaas.UpdateVPCRegionPayload{ Ipv4: &iaas.RegionalVPCIPv4{ - DefaultNameservers: nameservers, + DefaultNameservers: []string{"8.8.8.8", "8.8.4.4"}, }, } - want := client.DefaultAPI.UpdateVPCRegion(ctx, testProjectId, testVpcId, "eu02").UpdateVPCRegionPayload(payload) - got := buildRequest(ctx, model, client) - if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { - t.Fatalf("request mismatch (-want +got): %s", diff) + for _, opt := range opts { + opt(&payload) } + + return payload + } + + tests := []struct { + description string + inputModel *inputModel + wantPayload iaas.UpdateVPCRegionPayload + }{ + { + description: "default", + inputModel: buildRequestInputModelDefault(), + wantPayload: buildRequestPayloadDefault(), + }, + { + description: "nameservers is empty", + inputModel: buildRequestInputModelDefault(func(m *inputModel) { + m.IPv4DefaultNameservers = []string{} + }), + wantPayload: buildRequestPayloadDefault(func(p *iaas.UpdateVPCRegionPayload) { + p.Ipv4.DefaultNameservers = []string{} + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + ctx := context.Background() + client := &iaas.DefaultAPIServiceMock{} + + want := client.UpdateVPCRegion(ctx, testProjectId, testVpcId, "eu02").UpdateVPCRegionPayload(tt.wantPayload) + got := buildRequest(ctx, tt.inputModel, client) + if diff := cmp.Diff(want, got, cmp.AllowUnexported(want), cmpopts.EquateComparable(ctx, iaas.DefaultAPIService{})); diff != "" { + t.Fatalf("request mismatch (-want +got): %s", diff) + } + }) } } @@ -163,7 +201,7 @@ func TestOutputResult(t *testing.T) { }, { name: "json", - format: "json", + format: print.JSONOutputFormat, resp: &iaas.RegionalVPC{ Status: new("UPDATED"), }, @@ -171,7 +209,7 @@ func TestOutputResult(t *testing.T) { }, { name: "yaml", - format: "yaml", + format: print.YAMLOutputFormat, resp: &iaas.RegionalVPC{ Status: new("UPDATED"), },