diff --git a/docs/stackit_alpha_vpc.md b/docs/stackit_alpha_vpc.md index bd8aa7965..7dcf577eb 100644 --- a/docs/stackit_alpha_vpc.md +++ b/docs/stackit_alpha_vpc.md @@ -34,6 +34,7 @@ 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 network-range](./stackit_alpha_vpc_network-range.md) - Provides functionality for network ranges in VPC * [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_create.md b/docs/stackit_alpha_vpc_create.md index 212490e48..0052021ef 100644 --- a/docs/stackit_alpha_vpc_create.md +++ b/docs/stackit_alpha_vpc_create.md @@ -23,7 +23,7 @@ stackit alpha vpc create [flags] ``` --description string Description of the VPC -h, --help Help for "stackit alpha vpc create" - --labels stringToString Comma separated list of labels of the VPC (default []) + --labels stringToString Labels are key-value string pairs which can be attached to a vpc. E.g. '--labels key1=value1,key2=value2,...' (default []) --name string Name of the VPC ``` diff --git a/docs/stackit_alpha_vpc_network-range.md b/docs/stackit_alpha_vpc_network-range.md new file mode 100644 index 000000000..77c671b00 --- /dev/null +++ b/docs/stackit_alpha_vpc_network-range.md @@ -0,0 +1,38 @@ +## stackit alpha vpc network-range + +Provides functionality for network ranges in VPC + +### Synopsis + +Provides functionality for network ranges in VPC. + +``` +stackit alpha vpc network-range [flags] +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc network-range" +``` + +### 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 network-range create](./stackit_alpha_vpc_network-range_create.md) - Creates a regional network range in a VPC +* [stackit alpha vpc network-range delete](./stackit_alpha_vpc_network-range_delete.md) - Deletes a regional network range in a VPC +* [stackit alpha vpc network-range describe](./stackit_alpha_vpc_network-range_describe.md) - Shows details of a network range in a VPC +* [stackit alpha vpc network-range list](./stackit_alpha_vpc_network-range_list.md) - Lists all network ranges in a VPC +* [stackit alpha vpc network-range update](./stackit_alpha_vpc_network-range_update.md) - Updates a regional network range in a VPC + diff --git a/docs/stackit_alpha_vpc_network-range_create.md b/docs/stackit_alpha_vpc_network-range_create.md new file mode 100644 index 000000000..af2cf3f22 --- /dev/null +++ b/docs/stackit_alpha_vpc_network-range_create.md @@ -0,0 +1,50 @@ +## stackit alpha vpc network-range create + +Creates a regional network range in a VPC + +### Synopsis + +Creates a regional network range in a VPC. +The vpc needs to be enabled in the specified region before creating a network range. + +``` +stackit alpha vpc network-range create [flags] +``` + +### Examples + +``` + Create a network range in a VPC with ID "xxx" with prefix "1.1.1.0/24" and with ip version 4 + $ stackit alpha vpc network-range create --vpc-id xxx --prefix "1.1.1.0/24" --ip-version ipv4 +``` + +### Options + +``` + --default-prefix-length int The default prefix length for network ranges in the VPC + --description string Description of the network range + -h, --help Help for "stackit alpha vpc network-range create" + --ip-version string IP version of the network-range (one of: [ipv4]) + --labels stringToString Labels are key-value string pairs which can be attached to a network range. E.g. '--labels key1=value1,key2=value2,...' (default []) + --max-prefix-length int The maximal prefix length for network ranges in the VPC + --min-prefix-length int The minimal prefix length for network ranges in the VPC + --nameservers strings A list containing DNS Servers + --prefix string Network range to create in CIDR notation + --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 network-range](./stackit_alpha_vpc_network-range.md) - Provides functionality for network ranges in VPC + diff --git a/docs/stackit_alpha_vpc_network-range_delete.md b/docs/stackit_alpha_vpc_network-range_delete.md new file mode 100644 index 000000000..ccc119b7d --- /dev/null +++ b/docs/stackit_alpha_vpc_network-range_delete.md @@ -0,0 +1,41 @@ +## stackit alpha vpc network-range delete + +Deletes a regional network range in a VPC + +### Synopsis + +Deletes a regional network range in a VPC. + +``` +stackit alpha vpc network-range delete NETWORK_RANGE_ID [flags] +``` + +### Examples + +``` + Delete network range with id "xxx" in a VPC with ID "yyy" + $ stackit alpha vpc network-range delete xxx --vpc-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc network-range 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 network-range](./stackit_alpha_vpc_network-range.md) - Provides functionality for network ranges in VPC + diff --git a/docs/stackit_alpha_vpc_network-range_describe.md b/docs/stackit_alpha_vpc_network-range_describe.md new file mode 100644 index 000000000..151218abf --- /dev/null +++ b/docs/stackit_alpha_vpc_network-range_describe.md @@ -0,0 +1,41 @@ +## stackit alpha vpc network-range describe + +Shows details of a network range in a VPC + +### Synopsis + +Shows details of a network range in a VPC. + +``` +stackit alpha vpc network-range describe NETWORK_RANGE_ID [flags] +``` + +### Examples + +``` + Show details of a network range with ID "xxx" in a VPC with ID "yyy" + $ stackit alpha vpc network-range describe xxx --vpc-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc network-range 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 network-range](./stackit_alpha_vpc_network-range.md) - Provides functionality for network ranges in VPC + diff --git a/docs/stackit_alpha_vpc_network-range_list.md b/docs/stackit_alpha_vpc_network-range_list.md new file mode 100644 index 000000000..3e801adb2 --- /dev/null +++ b/docs/stackit_alpha_vpc_network-range_list.md @@ -0,0 +1,48 @@ +## stackit alpha vpc network-range list + +Lists all network ranges in a VPC + +### Synopsis + +Lists all network ranges in a VPC. + +``` +stackit alpha vpc network-range list [flags] +``` + +### Examples + +``` + Lists all network ranges in a VPC with ID "xxx" + $ stackit alpha vpc network-range list --vpc-id xxx + + Lists all network ranges in a VPC with ID "xxx" in JSON format + $ stackit alpha vpc network-range list --vpc-id xxx --output-format json + + Lists up to 10 network ranges in a VPC with ID "xxx" + $ stackit alpha vpc network-range list --vpc-id xxx --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit alpha vpc network-range list" + --limit int Maximum number of entries to 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 network-range](./stackit_alpha_vpc_network-range.md) - Provides functionality for network ranges in VPC + diff --git a/docs/stackit_alpha_vpc_network-range_update.md b/docs/stackit_alpha_vpc_network-range_update.md new file mode 100644 index 000000000..2d31cb69b --- /dev/null +++ b/docs/stackit_alpha_vpc_network-range_update.md @@ -0,0 +1,51 @@ +## stackit alpha vpc network-range update + +Updates a regional network range in a VPC + +### Synopsis + +Updates a regional network range in a VPC. + +``` +stackit alpha vpc network-range update NETWORK_RANGE_ID [flags] +``` + +### Examples + +``` + Update a network range with ID "xxx" in a VPC with ID "yyy" and with ip version "ipv4" to new nameservers "1.2.3.4,5.6.7.8" + $ stackit alpha vpc network-range update xxx --vpc-id yyy --ip-version ipv4 --nameservers "1.2.3.4,5.6.7.8" + + Update a network range with ID "xxx" in a VPC with ID "yyy" and with ip version "ipv4" to new description "updated network range" and default prefix length 24 + $ stackit alpha vpc network-range update xxx --vpc-id yyy --ip-version ipv4 --description "updated network range" --default-prefix-length 24 +``` + +### Options + +``` + --default-prefix-length int The default prefix length for network ranges in the VPC + --description string Description of the network range + -h, --help Help for "stackit alpha vpc network-range update" + --ip-version string IP version of the network-range (one of: [ipv4]) + --labels stringToString Labels are key-value string pairs which can be attached to a network range. E.g. '--labels key1=value1,key2=value2,...' (default []) + --max-prefix-length int The maximal prefix length for network ranges in the VPC + --min-prefix-length int The minimal prefix length for network ranges in the VPC + --nameservers strings A list containing DNS Servers + --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 network-range](./stackit_alpha_vpc_network-range.md) - Provides functionality for network ranges in VPC + diff --git a/docs/stackit_alpha_vpc_update.md b/docs/stackit_alpha_vpc_update.md index d500e3279..c1a26051a 100644 --- a/docs/stackit_alpha_vpc_update.md +++ b/docs/stackit_alpha_vpc_update.md @@ -25,7 +25,7 @@ stackit alpha vpc update VPC_ID [flags] ``` --description string Description of the VPC -h, --help Help for "stackit alpha vpc update" - --labels stringToString Comma separated list of labels of the VPC (default []) + --labels stringToString Labels are key-value string pairs which can be attached to a vpc. E.g. '--labels key1=value1,key2=value2,...' (default []) --name string Name of the VPC ``` diff --git a/internal/cmd/alpha/vpc/create/create.go b/internal/cmd/alpha/vpc/create/create.go index 57669841b..263cf6c52 100644 --- a/internal/cmd/alpha/vpc/create/create.go +++ b/internal/cmd/alpha/vpc/create/create.go @@ -87,7 +87,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(nameFlag, "", "Name of the VPC") cmd.Flags().String(descriptionFlag, "", "Description of the VPC") - cmd.Flags().StringToString(labelsFlag, nil, "Comma separated list of labels of the VPC") + cmd.Flags().StringToString(labelsFlag, nil, "Labels are key-value string pairs which can be attached to a vpc. E.g. '--labels key1=value1,key2=value2,...'") err := cmd.MarkFlagRequired(nameFlag) cobra.CheckErr(err) diff --git a/internal/cmd/alpha/vpc/network-range/create/create.go b/internal/cmd/alpha/vpc/network-range/create/create.go new file mode 100644 index 000000000..c6911f0d8 --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/create/create.go @@ -0,0 +1,214 @@ +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" + prefixFlag = "prefix" + descriptionFlag = "description" + defaultPrefixLengthFlag = "default-prefix-length" + maxPrefixLengthFlag = "max-prefix-length" + minPrefixLengthFlag = "min-prefix-length" + nameserversFlag = "nameservers" + labelsFlag = "labels" +) + +var ( + ipVersionFlag = flags.StringEnumFlag( + "ip-version", + iaas.AllowedNetworkRangeIPv4RequestIpVersionEnumValues, + "IP version of the network-range", + ) +) + +type inputModel struct { + *globalflags.GlobalFlagModel + VpcId string + Prefix string + IpVersion iaas.NetworkRangeIPv4RequestIpVersion + Description *string + DefaultPrefixLength *int64 + MaxPrefixLength *int64 + MinPrefixLength *int64 + Nameservers []string + Labels map[string]any +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a regional network range in a VPC", + Long: fmt.Sprintf("%s\n%s", + "Creates a regional network range in a VPC.", + "The vpc needs to be enabled in the specified region before creating a network range.", + ), + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a network range in a VPC with ID "xxx" with prefix "1.1.1.0/24" and with ip version 4`, + `$ stackit alpha vpc network-range create --vpc-id xxx --prefix "1.1.1.0/24" --ip-version ipv4`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Get vpc label + vpcLabel, err := iaasUtils.GetVPCName(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc name: %v", err) + vpcLabel = model.VpcId + } + + prompt := fmt.Sprintf("Are you sure you want to create a network range for VPC %q?", vpcLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create network range: %w", err) + } + + if !model.Async { + var networkRangeId string + if ipv4 := resp.VPCNetworkRangeIPv4; ipv4 != nil && ipv4.Id != nil { + networkRangeId = *ipv4.Id + } else if ipv6 := resp.VPCNetworkRangeIPv6; ipv6 != nil && ipv6.Id != nil { + networkRangeId = *ipv6.Id + } else { + return fmt.Errorf("network range ID is missing in API response") + } + + err = spinner.Run(params.Printer, "Creating VPC Network range", func() error { + resp, err = wait.CreateVPCNetworkRangeWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId, model.Region, networkRangeId).WaitWithContext(ctx) + return err + }) + if err != nil { + if err != nil { + return fmt.Errorf("wait for network range %q creation: %w", networkRangeId, err) + } + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, vpcLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), vpcIdFlag, "VPC ID") + cmd.Flags().Var(flags.CIDRFlag(), prefixFlag, "Network range to create in CIDR notation") + cmd.Flags().String(descriptionFlag, "", "Description of the network range") + ipVersionFlag.Register(cmd.Flags()) + cmd.Flags().Int64(defaultPrefixLengthFlag, 0, "The default prefix length for network ranges in the VPC") + cmd.Flags().Int64(maxPrefixLengthFlag, 0, "The maximal prefix length for network ranges in the VPC") + cmd.Flags().Int64(minPrefixLengthFlag, 0, "The minimal prefix length for network ranges in the VPC") + cmd.Flags().StringSlice(nameserversFlag, nil, "A list containing DNS Servers") + cmd.Flags().StringToString(labelsFlag, nil, "Labels are key-value string pairs which can be attached to a network range. E.g. '--labels key1=value1,key2=value2,...'") + + err := flags.MarkFlagsRequired(cmd, vpcIdFlag, prefixFlag, ipVersionFlag.Name()) + 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{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + Prefix: flags.FlagToStringValue(p, cmd, prefixFlag), + IpVersion: ipVersionFlag.Get(), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + DefaultPrefixLength: flags.FlagToInt64Pointer(p, cmd, defaultPrefixLengthFlag), + MaxPrefixLength: flags.FlagToInt64Pointer(p, cmd, maxPrefixLengthFlag), + MinPrefixLength: flags.FlagToInt64Pointer(p, cmd, minPrefixLengthFlag), + Nameservers: flags.FlagToStringSliceValue(p, cmd, nameserversFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelsFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) (iaas.ApiCreateVPCNetworkRangeRequest, error) { + if model.IpVersion != "ipv4" { + return iaas.ApiCreateVPCNetworkRangeRequest{}, fmt.Errorf("unsupported IP version: %q", model.IpVersion) + } + + payload := iaas.CreateVPCNetworkRangePayload{ + NetworkRangeIPv4Request: &iaas.NetworkRangeIPv4Request{ + DefaultPrefixLen: model.DefaultPrefixLength, + Description: model.Description, + Prefix: model.Prefix, + IpVersion: model.IpVersion, + MaxPrefixLen: model.MaxPrefixLength, + MinPrefixLen: model.MinPrefixLength, + Nameservers: model.Nameservers, + Labels: model.Labels, + }, + } + return apiClient.CreateVPCNetworkRange(ctx, model.ProjectId, model.VpcId, model.Region).CreateVPCNetworkRangePayload(payload), nil +} + +func outputResult(p *print.Printer, outputFormat string, async bool, vpcLabel string, networkRange *iaas.VPCNetworkRange) error { + if networkRange == nil { + return fmt.Errorf("create vpc network range response is nil") + } + return p.OutputResult(outputFormat, networkRange, func() error { + var networkRangeId string + if ipv4 := networkRange.VPCNetworkRangeIPv4; ipv4 != nil && ipv4.Id != nil { + networkRangeId = *ipv4.Id + } else if ipv6 := networkRange.VPCNetworkRangeIPv6; ipv6 != nil && ipv6.Id != nil { + networkRangeId = *ipv6.Id + } else { + return fmt.Errorf("network range ID is missing in API response") + } + + operationState := "Created" + if async { + operationState = "Triggered creation of" + } + + p.Outputf("%s network range for VPC %q.\nNetwork range ID: %s\n", operationState, vpcLabel, networkRangeId) + return nil + }) +} diff --git a/internal/cmd/alpha/vpc/network-range/create/create_test.go b/internal/cmd/alpha/vpc/network-range/create/create_test.go new file mode 100644 index 000000000..e86bc54c6 --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/create/create_test.go @@ -0,0 +1,303 @@ +package create + +import ( + "context" + "maps" + "slices" + "strconv" + "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" +) + +const ( + testRegion = "eu01" + testPrefix = "10.0.10.0/24" + testDescription = "test-description" + testLabelString = "env=qa,endpoint=alpha" + testDefaultPrefixLength int64 = 25 + testMaxPrefixLength int64 = 29 + testMinPrefixLength int64 = 24 + testIpVersion = iaas.NETWORKRANGEIPV4REQUESTIPVERSION_IPV4 +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testVpcId = uuid.NewString() + testNameservers = []string{"1.1.1.1", "8.8.8.8"} + testLabel = map[string]any{ + "env": "qa", + "endpoint": "alpha", + } +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + + vpcIdFlag: testVpcId, + prefixFlag: testPrefix, + descriptionFlag: testDescription, + defaultPrefixLengthFlag: strconv.Itoa(int(testDefaultPrefixLength)), + maxPrefixLengthFlag: strconv.Itoa(int(testMaxPrefixLength)), + minPrefixLengthFlag: strconv.Itoa(int(testMinPrefixLength)), + nameserversFlag: strings.Join(testNameservers, ","), + labelsFlag: testLabelString, + ipVersionFlag.Name(): string(testIpVersion), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + ProjectId: testProjectId, + }, + VpcId: testVpcId, + Prefix: testPrefix, + IpVersion: testIpVersion, + Description: new(testDescription), + DefaultPrefixLength: new(testDefaultPrefixLength), + MaxPrefixLength: new(testMaxPrefixLength), + MinPrefixLength: new(testMinPrefixLength), + Nameservers: slices.Clone(testNameservers), + Labels: maps.Clone(testLabel), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiCreateVPCNetworkRangeRequest)) iaas.ApiCreateVPCNetworkRangeRequest { + request := testClient.DefaultAPI.CreateVPCNetworkRange(testCtx, testProjectId, testVpcId, testRegion) + request = request.CreateVPCNetworkRangePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.CreateVPCNetworkRangePayload)) iaas.CreateVPCNetworkRangePayload { + payload := iaas.CreateVPCNetworkRangePayload{ + NetworkRangeIPv4Request: &iaas.NetworkRangeIPv4Request{ + DefaultPrefixLen: new(testDefaultPrefixLength), + Description: new(testDescription), + IpVersion: testIpVersion, + Labels: maps.Clone(testLabel), + MaxPrefixLen: new(testMaxPrefixLength), + MinPrefixLen: new(testMinPrefixLength), + Nameservers: slices.Clone(testNameservers), + Prefix: testPrefix, + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + aclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "prefix missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, prefixFlag) + }), + isValid: false, + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "vpc id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, vpcIdFlag) + }), + isValid: false, + }, + { + description: "vpc id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "" + }), + isValid: false, + }, + { + description: "vpc id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "invalid-uuid" + }), + 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 iaas.ApiCreateVPCNetworkRangeRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + if err != nil { + t.Fatalf("test failed: %v", err) + } + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + testNetworkRangeId := uuid.NewString() + + type args struct { + outputFormat string + async bool + vpcLabel string + networkRange *iaas.VPCNetworkRange + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "empty network range", + args: args{ + networkRange: &iaas.VPCNetworkRange{}, + }, + wantErr: true, + }, + { + name: "empty ipv4 network range", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv4: &iaas.VPCNetworkRangeIPv4{}, + }, + }, + wantErr: true, + }, + { + name: "ipv4 network range with ID", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv4: &iaas.VPCNetworkRangeIPv4{ + Id: new(testNetworkRangeId), + }, + }, + }, + wantErr: false, + }, + { + name: "empty ipv6 network range", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv6: &iaas.VPCNetworkRangeIPv6{}, + }, + }, + wantErr: true, + }, + { + name: "ipv6 network range with ID", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv6: &iaas.VPCNetworkRangeIPv6{ + Id: new(testNetworkRangeId), + }, + }, + }, + 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.vpcLabel, tt.args.networkRange); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/alpha/vpc/network-range/delete/delete.go b/internal/cmd/alpha/vpc/network-range/delete/delete.go new file mode 100644 index 000000000..db644a7cb --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/delete/delete.go @@ -0,0 +1,138 @@ +package delete + +import ( + "context" + "fmt" + + 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" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +const ( + networkRangeIdArg = "NETWORK_RANGE_ID" + + vpcIdFlag = "vpc-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + VpcId string + NetworkRangeId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", networkRangeIdArg), + Short: "Deletes a regional network range in a VPC", + Long: "Deletes a regional network range in a VPC.", + Args: args.SingleArg(networkRangeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete network range with id "xxx" in a VPC with ID "yyy"`, + `$ stackit alpha vpc network-range delete xxx --vpc-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + 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) + vpcLabel = model.VpcId + } + + networkRangeLabel, err := iaasUtils.GetVPCNetworkRangePrefix(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId, model.Region, model.NetworkRangeId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc network range prefix: %v", err) + networkRangeLabel = model.NetworkRangeId + } else if networkRangeLabel == "" { + networkRangeLabel = model.NetworkRangeId + } + + prompt := fmt.Sprintf("Are you sure you want to delete network range %q on VPC %q?", networkRangeLabel, vpcLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient.DefaultAPI) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete vpc network range: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Deleting VPC network range", func() error { + _, err = wait.DeleteVPCNetworkRangeWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId, model.Region, model.NetworkRangeId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for VPC network range deletion: %w", err) + } + } + + operationState := "Deleted" + if model.Async { + operationState = "Triggered deletion of" + } + + params.Printer.Outputf("%s network range %q on VPC %q\n", operationState, networkRangeLabel, vpcLabel) + 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, inputArgs []string) (*inputModel, error) { + networkRangeId := inputArgs[0] + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + NetworkRangeId: networkRangeId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiDeleteVPCNetworkRangeRequest { + return apiClient.DeleteVPCNetworkRange(ctx, model.ProjectId, model.VpcId, model.Region, model.NetworkRangeId) +} diff --git a/internal/cmd/alpha/vpc/network-range/delete/delete_test.go b/internal/cmd/alpha/vpc/network-range/delete/delete_test.go new file mode 100644 index 000000000..e23a333a3 --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/delete/delete_test.go @@ -0,0 +1,248 @@ +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/testparams" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + + testVpcId = uuid.NewString() + testProjectId = uuid.NewString() + testNetworkRangeId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testNetworkRangeId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + + vpcIdFlag: testVpcId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + ProjectId: testProjectId, + }, + VpcId: testVpcId, + NetworkRangeId: testNetworkRangeId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiDeleteVPCNetworkRangeRequest)) iaas.ApiDeleteVPCNetworkRangeRequest { + request := testClient.DefaultAPI.DeleteVPCNetworkRange(testCtx, testProjectId, testVpcId, testRegion, testNetworkRangeId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + aclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no args", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "network range id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "network range id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "vpc id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, vpcIdFlag) + }), + isValid: false, + }, + { + description: "vpc id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "" + }), + isValid: false, + }, + { + description: "vpc id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiDeleteVPCNetworkRangeRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/alpha/vpc/network-range/describe/describe.go b/internal/cmd/alpha/vpc/network-range/describe/describe.go new file mode 100644 index 000000000..80b117a25 --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/describe/describe.go @@ -0,0 +1,183 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + 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/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" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +const ( + networkRangeIdArg = "NETWORK_RANGE_ID" + + vpcIdFlag = "vpc-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + VpcId string + NetworkRangeId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", networkRangeIdArg), + Short: "Shows details of a network range in a VPC", + Long: "Shows details of a network range in a VPC.", + Args: args.SingleArg(networkRangeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Show details of a network range with ID "xxx" in a VPC with ID "yyy"`, + `$ stackit alpha vpc network-range describe xxx --vpc-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient.DefaultAPI) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("describe network range: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), vpcIdFlag, "VPC ID") + + err := flags.MarkFlagsRequired(cmd, vpcIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + networkRangeId := inputArgs[0] + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + NetworkRangeId: networkRangeId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiGetVPCNetworkRangeRequest { + return apiClient.GetVPCNetworkRange(ctx, model.ProjectId, model.VpcId, model.Region, model.NetworkRangeId) +} + +func outputResult(p *print.Printer, outputFormat string, networkRange *iaas.VPCNetworkRange) error { + if networkRange == nil { + return fmt.Errorf("network range is nil") + } + + return p.OutputResult(outputFormat, networkRange, func() error { + table := tables.NewTable() + + // iaas.VPCNetworkRange is a oneOf, either IPv4 or IPv6 is set + if ipv4 := networkRange.VPCNetworkRangeIPv4; ipv4 != nil { + table.AddRow("ID", utils.PtrString(ipv4.Id)) + table.AddSeparator() + table.AddRow("PREFIX", ipv4.Prefix) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(ipv4.Description)) + table.AddSeparator() + table.AddRow("NAMESERVERS", strings.Join(ipv4.Nameservers, ",")) + table.AddSeparator() + table.AddRow("DEFAULT PREFIX LENGTH", utils.PtrString(ipv4.DefaultPrefixLen)) + table.AddSeparator() + table.AddRow("MAX PREFIX LENGTH", utils.PtrString(ipv4.MaxPrefixLen)) + table.AddSeparator() + table.AddRow("MIN PREFIX LENGTH", utils.PtrString(ipv4.MinPrefixLen)) + table.AddSeparator() + table.AddRow("IP VERSION", ipv4.IpVersion) + table.AddSeparator() + table.AddRow("STATUS", utils.PtrString(ipv4.Status)) + table.AddSeparator() + + var labels []string + for key, value := range ipv4.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + table.AddRow("LABELS", strings.Join(labels, "\n")) + table.AddSeparator() + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(ipv4.CreatedAt)) + table.AddSeparator() + table.AddRow("UPDATED AT", utils.ConvertTimePToDateTimeString(ipv4.UpdatedAt)) + table.AddSeparator() + } else if ipv6 := networkRange.VPCNetworkRangeIPv6; ipv6 != nil { + table.AddRow("ID", utils.PtrString(ipv6.Id)) + table.AddSeparator() + table.AddRow("PREFIX", ipv6.Prefix) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(ipv6.Description)) + table.AddSeparator() + table.AddRow("NAMESERVERS", strings.Join(ipv6.Nameservers, ",")) + table.AddSeparator() + table.AddRow("DEFAULT PREFIX LENGTH", utils.PtrString(ipv6.DefaultPrefixLen)) + table.AddSeparator() + table.AddRow("MAX PREFIX LENGTH", utils.PtrString(ipv6.MaxPrefixLen)) + table.AddSeparator() + table.AddRow("MIN PREFIX LENGTH", utils.PtrString(ipv6.MinPrefixLen)) + table.AddSeparator() + table.AddRow("IP VERSION", ipv6.IpVersion) + table.AddSeparator() + table.AddRow("STATUS", utils.PtrString(ipv6.Status)) + table.AddSeparator() + + var labels []string + for key, value := range ipv6.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + table.AddRow("LABELS", strings.Join(labels, "\n")) + table.AddSeparator() + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(ipv6.CreatedAt)) + table.AddSeparator() + table.AddRow("UPDATED AT", utils.ConvertTimePToDateTimeString(ipv6.UpdatedAt)) + table.AddSeparator() + } else { + return fmt.Errorf("unsupported or empty network range payload") + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/alpha/vpc/network-range/describe/describe_test.go b/internal/cmd/alpha/vpc/network-range/describe/describe_test.go new file mode 100644 index 000000000..a7a87c2c7 --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/describe/describe_test.go @@ -0,0 +1,253 @@ +package describe + +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/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testVpcId = uuid.NewString() + testNetworkRangeId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testNetworkRangeId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + + vpcIdFlag: testVpcId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + VpcId: testVpcId, + NetworkRangeId: testNetworkRangeId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiGetVPCNetworkRangeRequest)) iaas.ApiGetVPCNetworkRangeRequest { + request := testClient.DefaultAPI.GetVPCNetworkRange(testCtx, testProjectId, testVpcId, testRegion, testNetworkRangeId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + aclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "vpc id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, vpcIdFlag) + }), + isValid: false, + }, + { + description: "vpc id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "" + }), + isValid: false, + }, + { + description: "vpc id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "network range id missing", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "network range id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "network range 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 iaas.ApiGetVPCNetworkRangeRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + networkRange *iaas.VPCNetworkRange + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "set empty networkRange", + args: args{ + networkRange: &iaas.VPCNetworkRange{}, + }, + wantErr: true, + }, + { + name: "set empty ipv4 networkRange", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv4: &iaas.VPCNetworkRangeIPv4{}, + }, + }, + wantErr: false, + }, + { + name: "set empty ipv6 networkRange", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv6: &iaas.VPCNetworkRangeIPv6{}, + }, + }, + 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.networkRange); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/alpha/vpc/network-range/list/list.go b/internal/cmd/alpha/vpc/network-range/list/list.go new file mode 100644 index 000000000..ec0596ba7 --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/list/list.go @@ -0,0 +1,164 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "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/utils" + + "github.com/spf13/cobra" +) + +const ( + limitFlag = "limit" + vpcIdFlag = "vpc-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 + VpcId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all network ranges in a VPC", + Long: "Lists all network ranges in a VPC.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Lists all network ranges in a VPC with ID "xxx"`, + "$ stackit alpha vpc network-range list --vpc-id xxx", + ), + examples.NewExample( + `Lists all network ranges in a VPC with ID "xxx" in JSON format`, + "$ stackit alpha vpc network-range list --vpc-id xxx --output-format json", + ), + examples.NewExample( + `Lists up to 10 network ranges in a VPC with ID "xxx"`, + "$ stackit alpha vpc network-range list --vpc-id xxx --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient.DefaultAPI) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list network ranges: %w", err) + } + + items := resp.GetItems() + + vpcLabel, err := iaasUtils.GetVPCName(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc name: %v", err) + vpcLabel = model.VpcId + } + + // Truncate output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, vpcLabel, items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().Var(flags.UUIDFlag(), vpcIdFlag, "VPC ID") + + err := flags.MarkFlagsRequired(cmd, vpcIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &cliErr.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) iaas.ApiListVPCNetworkRangesRequest { + return apiClient.ListVPCNetworkRanges(ctx, model.ProjectId, model.VpcId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat, vpcLabel string, networkRanges []iaas.VPCNetworkRange) error { + return p.OutputResult(outputFormat, networkRanges, func() error { + if len(networkRanges) == 0 { + p.Outputf("No network ranges found for VPC %q\n", vpcLabel) + return nil + } + table := tables.NewTable() + table.SetHeader("ID", "PREFIX", "DESCRIPTION", "IP VERSION", "STATUS") + + for _, networkRange := range networkRanges { + if ipv4 := networkRange.VPCNetworkRangeIPv4; ipv4 != nil { + table.AddRow( + utils.PtrString(ipv4.Id), + ipv4.Prefix, + utils.PtrString(ipv4.Description), + ipv4.IpVersion, + utils.PtrString(ipv4.Status), + ) + } else if ipv6 := networkRange.VPCNetworkRangeIPv6; ipv6 != nil { + table.AddRow( + utils.PtrString(ipv6.Id), + ipv6.Prefix, + utils.PtrString(ipv6.Description), + ipv6.IpVersion, + utils.PtrString(ipv6.Status), + ) + } + } + + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/alpha/vpc/network-range/list/list_test.go b/internal/cmd/alpha/vpc/network-range/list/list_test.go new file mode 100644 index 000000000..795f9d30e --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/list/list_test.go @@ -0,0 +1,227 @@ +package list + +import ( + "context" + "strconv" + "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" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testVpcId = uuid.NewString() + testLimit int64 = 10 +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + + vpcIdFlag: testVpcId, + limitFlag: strconv.Itoa(int(testLimit)), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + VpcId: testVpcId, + Limit: new(testLimit), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiListVPCNetworkRangesRequest)) iaas.ApiListVPCNetworkRangesRequest { + request := testClient.DefaultAPI.ListVPCNetworkRanges(testCtx, testProjectId, testVpcId, 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 values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "vpc id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, vpcIdFlag) + }), + isValid: false, + }, + { + description: "vpc id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "" + }), + isValid: false, + }, + { + description: "vpc id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + 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 iaas.ApiListVPCNetworkRangesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + vpcLabel string + networkRanges []iaas.VPCNetworkRange + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "empty network ranges slice", + args: args{ + networkRanges: []iaas.VPCNetworkRange{}, + }, + wantErr: false, + }, + { + name: "empty network range in network ranges slice", + args: args{ + networkRanges: []iaas.VPCNetworkRange{{}}, + }, + 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.vpcLabel, tt.args.networkRanges); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/alpha/vpc/network-range/network_range.go b/internal/cmd/alpha/vpc/network-range/network_range.go new file mode 100644 index 000000000..c5c1aac61 --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/network_range.go @@ -0,0 +1,34 @@ +package networkranges + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/network-range/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/network-range/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/network-range/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/network-range/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/network-range/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" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "network-range", + Short: "Provides functionality for network ranges in VPC", + Long: "Provides functionality for network ranges in 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(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/alpha/vpc/network-range/update/update.go b/internal/cmd/alpha/vpc/network-range/update/update.go new file mode 100644 index 000000000..b54bf0aec --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/update/update.go @@ -0,0 +1,222 @@ +package update + +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" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + networkRangeIdArg = "NETWORK_RANGE_ID" + + vpcIdFlag = "vpc-id" + descriptionFlag = "description" + defaultPrefixLengthFlag = "default-prefix-length" + maxPrefixLengthFlag = "max-prefix-length" + minPrefixLengthFlag = "min-prefix-length" + nameserversFlag = "nameservers" + labelsFlag = "labels" +) + +var ( + ipVersionFlag = flags.StringEnumFlag( + "ip-version", + iaas.AllowedV1UpdateVPCNetworkRangeIPv4IpVersionEnumValues, + "IP version of the network-range", + ) +) + +type inputModel struct { + *globalflags.GlobalFlagModel + NetworkRangeId string + VpcId string + IpVersion iaas.V1UpdateVPCNetworkRangeIPv4IpVersion + Description *string + DefaultPrefixLength *int64 + MaxPrefixLength *int64 + MinPrefixLength *int64 + Nameservers []string + Labels map[string]any +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", networkRangeIdArg), + Short: "Updates a regional network range in a VPC", + Long: "Updates a regional network range in a VPC.", + Args: args.SingleArg(networkRangeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update a network range with ID "xxx" in a VPC with ID "yyy" and with ip version "ipv4" to new nameservers "1.2.3.4,5.6.7.8"`, + `$ stackit alpha vpc network-range update xxx --vpc-id yyy --ip-version ipv4 --nameservers "1.2.3.4,5.6.7.8"`, + ), + examples.NewExample( + `Update a network range with ID "xxx" in a VPC with ID "yyy" and with ip version "ipv4" to new description "updated network range" and default prefix length 24`, + `$ stackit alpha vpc network-range update xxx --vpc-id yyy --ip-version ipv4 --description "updated network range" --default-prefix-length 24`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Get vpc label + vpcLabel, err := iaasUtils.GetVPCName(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc name: %v", err) + vpcLabel = model.VpcId + } + + networkRangeLabel, err := iaasUtils.GetVPCNetworkRangePrefix(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId, model.Region, model.NetworkRangeId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get vpc network range prefix: %v", err) + networkRangeLabel = model.NetworkRangeId + } else if networkRangeLabel == "" { + networkRangeLabel = model.NetworkRangeId + } + + prompt := fmt.Sprintf("Are you sure you want to update the network range %q for VPC %q?", networkRangeLabel, vpcLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update network range: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Updating VPC network range", func() error { + _, err = wait.UpdateVPCNetworkRangeWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.VpcId, model.Region, model.NetworkRangeId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for VPC network range update: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, vpcLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), vpcIdFlag, "VPC ID") + cmd.Flags().String(descriptionFlag, "", "Description of the network range") + ipVersionFlag.Register(cmd.Flags()) + cmd.Flags().Int64(defaultPrefixLengthFlag, 0, "The default prefix length for network ranges in the VPC") + cmd.Flags().Int64(maxPrefixLengthFlag, 0, "The maximal prefix length for network ranges in the VPC") + cmd.Flags().Int64(minPrefixLengthFlag, 0, "The minimal prefix length for network ranges in the VPC") + cmd.Flags().StringSlice(nameserversFlag, nil, "A list containing DNS Servers") + cmd.Flags().StringToString(labelsFlag, nil, "Labels are key-value string pairs which can be attached to a network range. E.g. '--labels key1=value1,key2=value2,...'") + + cmd.MarkFlagsOneRequired( + descriptionFlag, + defaultPrefixLengthFlag, + maxPrefixLengthFlag, + minPrefixLengthFlag, + nameserversFlag, + labelsFlag, + ) + cobra.CheckErr(flags.MarkFlagsRequired(cmd, vpcIdFlag, ipVersionFlag.Name())) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + networkRangeId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkRangeId: networkRangeId, + VpcId: flags.FlagToStringValue(p, cmd, vpcIdFlag), + IpVersion: ipVersionFlag.Get(), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + DefaultPrefixLength: flags.FlagToInt64Pointer(p, cmd, defaultPrefixLengthFlag), + MaxPrefixLength: flags.FlagToInt64Pointer(p, cmd, maxPrefixLengthFlag), + MinPrefixLength: flags.FlagToInt64Pointer(p, cmd, minPrefixLengthFlag), + Nameservers: flags.FlagToStringSliceValue(p, cmd, nameserversFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelsFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient iaas.DefaultAPI) (iaas.ApiUpdateVPCNetworkRangeRequest, error) { + if model.IpVersion != "ipv4" { + return iaas.ApiUpdateVPCNetworkRangeRequest{}, fmt.Errorf("unsupported IP version: %q", model.IpVersion) + } + + payload := iaas.UpdateVPCNetworkRangePayload{ + V1UpdateVPCNetworkRangeIPv4: &iaas.V1UpdateVPCNetworkRangeIPv4{ + DefaultPrefixLen: model.DefaultPrefixLength, + Description: model.Description, + IpVersion: model.IpVersion, + MaxPrefixLen: model.MaxPrefixLength, + MinPrefixLen: model.MinPrefixLength, + Nameservers: model.Nameservers, + Labels: model.Labels, + }, + } + return apiClient.UpdateVPCNetworkRange(ctx, model.ProjectId, model.VpcId, model.Region, model.NetworkRangeId).UpdateVPCNetworkRangePayload(payload), nil +} + +func outputResult(p *print.Printer, outputFormat string, async bool, vpcLabel string, networkRange *iaas.VPCNetworkRange) error { + if networkRange == nil { + return fmt.Errorf("update vpc network range response is nil") + } + return p.OutputResult(outputFormat, networkRange, func() error { + var networkRangeId string + if ipv4 := networkRange.VPCNetworkRangeIPv4; ipv4 != nil && ipv4.Id != nil { + networkRangeId = *ipv4.Id + } else if ipv6 := networkRange.VPCNetworkRangeIPv6; ipv6 != nil && ipv6.Id != nil { + networkRangeId = *ipv6.Id + } else { + return fmt.Errorf("network range ID is missing in API response") + } + + operationState := "Updated" + if async { + operationState = "Triggered update of" + } + + p.Outputf("%s network range for VPC %q.\nNetwork range ID: %s\n", operationState, vpcLabel, networkRangeId) + return nil + }) +} diff --git a/internal/cmd/alpha/vpc/network-range/update/update_test.go b/internal/cmd/alpha/vpc/network-range/update/update_test.go new file mode 100644 index 000000000..d437b90e8 --- /dev/null +++ b/internal/cmd/alpha/vpc/network-range/update/update_test.go @@ -0,0 +1,343 @@ +package update + +import ( + "context" + "maps" + "slices" + "strconv" + "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" +) + +const ( + testRegion = "eu01" + testDescription = "test-description" + testLabelString = "env=qa,endpoint=alpha" + testDefaultPrefixLength int64 = 25 + testMaxPrefixLength int64 = 29 + testMinPrefixLength int64 = 24 + testIpVersion = iaas.V1UPDATEVPCNETWORKRANGEIPV4IPVERSION_IPV4 +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testVpcId = uuid.NewString() + testNetworkRangeId = uuid.NewString() + testNameservers = []string{"1.1.1.1", "8.8.8.8"} + testLabel = map[string]any{ + "env": "qa", + "endpoint": "alpha", + } +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testNetworkRangeId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + + vpcIdFlag: testVpcId, + descriptionFlag: testDescription, + defaultPrefixLengthFlag: strconv.Itoa(int(testDefaultPrefixLength)), + maxPrefixLengthFlag: strconv.Itoa(int(testMaxPrefixLength)), + minPrefixLengthFlag: strconv.Itoa(int(testMinPrefixLength)), + nameserversFlag: strings.Join(testNameservers, ","), + labelsFlag: testLabelString, + ipVersionFlag.Name(): string(testIpVersion), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + ProjectId: testProjectId, + }, + NetworkRangeId: testNetworkRangeId, + VpcId: testVpcId, + IpVersion: testIpVersion, + Description: new(testDescription), + DefaultPrefixLength: new(testDefaultPrefixLength), + MaxPrefixLength: new(testMaxPrefixLength), + MinPrefixLength: new(testMinPrefixLength), + Nameservers: slices.Clone(testNameservers), + Labels: maps.Clone(testLabel), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiUpdateVPCNetworkRangeRequest)) iaas.ApiUpdateVPCNetworkRangeRequest { + request := testClient.DefaultAPI.UpdateVPCNetworkRange(testCtx, testProjectId, testVpcId, testRegion, testNetworkRangeId) + request = request.UpdateVPCNetworkRangePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.UpdateVPCNetworkRangePayload)) iaas.UpdateVPCNetworkRangePayload { + payload := iaas.UpdateVPCNetworkRangePayload{ + V1UpdateVPCNetworkRangeIPv4: &iaas.V1UpdateVPCNetworkRangeIPv4{ + DefaultPrefixLen: new(testDefaultPrefixLength), + Description: new(testDescription), + IpVersion: testIpVersion, + Labels: maps.Clone(testLabel), + MaxPrefixLen: new(testMaxPrefixLength), + MinPrefixLen: new(testMinPrefixLength), + Nameservers: slices.Clone(testNameservers), + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + aclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "missing network range id", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "network range id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "network range id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "vpc id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, vpcIdFlag) + }), + isValid: false, + }, + { + description: "vpc id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "" + }), + isValid: false, + }, + { + description: "vpc id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[vpcIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "no updatable flags provided", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, descriptionFlag) + delete(flagValues, defaultPrefixLengthFlag) + delete(flagValues, maxPrefixLengthFlag) + delete(flagValues, minPrefixLengthFlag) + delete(flagValues, nameserversFlag) + delete(flagValues, labelsFlag) + }), + 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 iaas.ApiUpdateVPCNetworkRangeRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + if err != nil { + t.Fatalf("test failed: %v", err) + } + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + testNetworkRangeId := uuid.NewString() + + type args struct { + outputFormat string + async bool + vpcLabel string + networkRange *iaas.VPCNetworkRange + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "empty network range", + args: args{ + networkRange: &iaas.VPCNetworkRange{}, + }, + wantErr: true, + }, + { + name: "empty ipv4 network range", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv4: &iaas.VPCNetworkRangeIPv4{}, + }, + }, + wantErr: true, + }, + { + name: "ipv4 network range with ID", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv4: &iaas.VPCNetworkRangeIPv4{ + Id: new(testNetworkRangeId), + }, + }, + }, + wantErr: false, + }, + { + name: "empty ipv6 network range", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv6: &iaas.VPCNetworkRangeIPv6{}, + }, + }, + wantErr: true, + }, + { + name: "ipv6 network range with ID", + args: args{ + networkRange: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv6: &iaas.VPCNetworkRangeIPv6{ + Id: new(testNetworkRangeId), + }, + }, + }, + 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.vpcLabel, tt.args.networkRange); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/alpha/vpc/update/update.go b/internal/cmd/alpha/vpc/update/update.go index a6ebbad39..b6458446e 100644 --- a/internal/cmd/alpha/vpc/update/update.go +++ b/internal/cmd/alpha/vpc/update/update.go @@ -93,7 +93,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(nameFlag, "", "Name of the VPC") cmd.Flags().String(descriptionFlag, "", "Description of the VPC") - cmd.Flags().StringToString(labelsFlag, nil, "Comma separated list of labels of the VPC") + cmd.Flags().StringToString(labelsFlag, nil, "Labels are key-value string pairs which can be attached to a vpc. E.g. '--labels key1=value1,key2=value2,...'") cmd.MarkFlagsOneRequired(nameFlag, descriptionFlag, labelsFlag) } diff --git a/internal/cmd/alpha/vpc/vpc.go b/internal/cmd/alpha/vpc/vpc.go index ff311d4fe..0e4458dbc 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" + networkRange "github.com/stackitcloud/stackit-cli/internal/cmd/alpha/vpc/network-range" "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" @@ -31,6 +32,7 @@ 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(networkRange.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) cmd.AddCommand(region.NewCmd(params)) } diff --git a/internal/pkg/services/iaasalpha/utils/utils.go b/internal/pkg/services/iaasalpha/utils/utils.go index eda55e037..23d7ba07f 100644 --- a/internal/pkg/services/iaasalpha/utils/utils.go +++ b/internal/pkg/services/iaasalpha/utils/utils.go @@ -22,3 +22,21 @@ func GetVPCName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, vpcId } return resp.Name, nil } + +func GetVPCNetworkRangePrefix(ctx context.Context, apiClient iaas.DefaultAPI, projectId, vpcId, region, networkRangeId string) (string, error) { + resp, err := apiClient.GetVPCNetworkRange(ctx, projectId, vpcId, region, networkRangeId).Execute() + if err != nil { + return "", fmt.Errorf("get vpc network range: %w", err) + } + + if resp != nil { + if resp.VPCNetworkRangeIPv4 != nil { + return resp.VPCNetworkRangeIPv4.Prefix, nil + } + if resp.VPCNetworkRangeIPv6 != nil { + return resp.VPCNetworkRangeIPv6.Prefix, nil + } + } + + return "", ErrResponseNil +} diff --git a/internal/pkg/services/iaasalpha/utils/utils_test.go b/internal/pkg/services/iaasalpha/utils/utils_test.go new file mode 100644 index 000000000..1a98e45f3 --- /dev/null +++ b/internal/pkg/services/iaasalpha/utils/utils_test.go @@ -0,0 +1,162 @@ +package utils + +import ( + "context" + "fmt" + "testing" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2alpha1api" +) + +type IaaSClientMocked struct { + GetVPCFails bool + GetVPCResp *iaas.VPC + GetVPCNetworkRangeFails bool + GetVPCNetworkRangeResp *iaas.VPCNetworkRange +} + +func newMock(m *IaaSClientMocked) iaas.DefaultAPI { + return iaas.DefaultAPIServiceMock{ + GetVPCExecuteMock: new(func(_ iaas.ApiGetVPCRequest) (*iaas.VPC, error) { + if m.GetVPCFails { + return nil, fmt.Errorf("could not get vpc") + } + return m.GetVPCResp, nil + }), + GetVPCNetworkRangeExecuteMock: new(func(_ iaas.ApiGetVPCNetworkRangeRequest) (*iaas.VPCNetworkRange, error) { + if m.GetVPCNetworkRangeFails { + return nil, fmt.Errorf("could not get vpc network range") + } + return m.GetVPCNetworkRangeResp, nil + }), + } +} + +func TestGetVPCName(t *testing.T) { + type args struct { + getVpcFails bool + getVpcResp *iaas.VPC + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "base", + args: args{ + getVpcResp: &iaas.VPC{ + Name: "test-vpc", + }, + }, + want: "test-vpc", + }, + { + name: "nil response", + args: args{ + getVpcResp: nil, + }, + wantErr: true, + }, + { + name: "get vpc fails", + args: args{ + getVpcFails: true, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &IaaSClientMocked{ + GetVPCFails: tt.args.getVpcFails, + GetVPCResp: tt.args.getVpcResp, + } + got, err := GetVPCName(context.Background(), newMock(m), "", "") + if (err != nil) != tt.wantErr { + t.Errorf("GetVPCName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetVPCName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetVPCNetworkRangePrefix(t *testing.T) { + type args struct { + getVpcNetworkRangeFails bool + getVpcNetworkRangeResp *iaas.VPCNetworkRange + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "base ipv4", + args: args{ + getVpcNetworkRangeResp: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv4: &iaas.VPCNetworkRangeIPv4{ + Prefix: "test", + }, + }, + }, + want: "test", + }, + { + name: "base ipv6", + args: args{ + getVpcNetworkRangeResp: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv6: &iaas.VPCNetworkRangeIPv6{ + Prefix: "test", + }, + }, + }, + want: "test", + }, + { + name: "nil response", + args: args{ + getVpcNetworkRangeResp: nil, + }, + wantErr: true, + }, + { + name: "get network range fails", + args: args{ + getVpcNetworkRangeFails: true, + }, + wantErr: true, + }, + { + name: "nil ipv4 and ipv6", + args: args{ + getVpcNetworkRangeResp: &iaas.VPCNetworkRange{ + VPCNetworkRangeIPv4: nil, + VPCNetworkRangeIPv6: nil, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &IaaSClientMocked{ + GetVPCNetworkRangeFails: tt.args.getVpcNetworkRangeFails, + GetVPCNetworkRangeResp: tt.args.getVpcNetworkRangeResp, + } + got, err := GetVPCNetworkRangePrefix(context.Background(), newMock(m), "", "", "", "") + if (err != nil) != tt.wantErr { + t.Errorf("GetVPCNetworkRangePrefix() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetVPCNetworkRangePrefix() = %v, want %v", got, tt.want) + } + }) + } +}