diff --git a/docs/stackit_network-area.md b/docs/stackit_network-area.md index d9ba1ecda..6f2d751f8 100644 --- a/docs/stackit_network-area.md +++ b/docs/stackit_network-area.md @@ -35,6 +35,7 @@ stackit network-area [flags] * [stackit network-area describe](./stackit_network-area_describe.md) - Shows details of a STACKIT Network Area * [stackit network-area list](./stackit_network-area_list.md) - Lists all STACKIT Network Areas (SNA) of an organization * [stackit network-area network-range](./stackit_network-area_network-range.md) - Provides functionality for network ranges in STACKIT Network Areas +* [stackit network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) * [stackit network-area route](./stackit_network-area_route.md) - Provides functionality for static routes in STACKIT Network Areas * [stackit network-area update](./stackit_network-area_update.md) - Updates a STACKIT Network Area (SNA) diff --git a/docs/stackit_network-area_create.md b/docs/stackit_network-area_create.md index 7dc278927..e9a28231d 100644 --- a/docs/stackit_network-area_create.md +++ b/docs/stackit_network-area_create.md @@ -13,32 +13,20 @@ stackit network-area create [flags] ### Examples ``` - Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network - $ stackit network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" + Create a network area with name "network-area-1" in organization with ID "xxx" + $ stackit network-area create --name network-area-1 --organization-id xxx" - Create a network area with name "network-area-2" in organization with ID "xxx" with network ranges, transfer network and DNS name server - $ stackit network-area create --name network-area-2 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --dns-name-servers "1.1.1.1" - - Create a network area with name "network-area-3" in organization with ID "xxx" with network ranges, transfer network and additional options - $ stackit network-area create --name network-area-3 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --default-prefix-length 25 --max-prefix-length 29 --min-prefix-length 24 - - Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network and labels "key=value,key1=value1" - $ stackit network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --labels key=value,key1=value1 + Create a network area with name "network-area-1" in organization with ID "xxx" with labels "key=value,key1=value1" + $ stackit network-area create --name network-area-1 --organization-id xxx --labels key=value,key1=value1 ``` ### Options ``` - --default-prefix-length int The default prefix length for networks in the network area - --dns-name-servers strings List of DNS name server IPs - -h, --help Help for "stackit network-area create" - --labels stringToString Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...' (default []) - --max-prefix-length int The maximum prefix length for networks in the network area - --min-prefix-length int The minimum prefix length for networks in the network area - -n, --name string Network area name - --network-ranges strings List of network ranges (default []) - --organization-id string Organization ID - --transfer-network string Transfer network in CIDR notation + -h, --help Help for "stackit network-area create" + --labels stringToString Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...' (default []) + -n, --name string Network area name + --organization-id string Organization ID ``` ### Options inherited from parent commands diff --git a/docs/stackit_network-area_region.md b/docs/stackit_network-area_region.md new file mode 100644 index 000000000..07fd820eb --- /dev/null +++ b/docs/stackit_network-area_region.md @@ -0,0 +1,38 @@ +## stackit network-area region + +Provides functionality for regional configuration of STACKIT Network Area (SNA) + +### Synopsis + +Provides functionality for regional configuration of STACKIT Network Area (SNA). + +``` +stackit network-area region [flags] +``` + +### Options + +``` + -h, --help Help for "stackit network-area 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 network-area](./stackit_network-area.md) - Provides functionality for STACKIT Network Area (SNA) +* [stackit network-area region create](./stackit_network-area_region_create.md) - Creates a new regional configuration for a STACKIT Network Area (SNA) +* [stackit network-area region delete](./stackit_network-area_region_delete.md) - Deletes a regional configuration for a STACKIT Network Area (SNA) +* [stackit network-area region describe](./stackit_network-area_region_describe.md) - Describes a regional configuration for a STACKIT Network Area (SNA) +* [stackit network-area region list](./stackit_network-area_region_list.md) - Lists all configured regions for a STACKIT Network Area (SNA) +* [stackit network-area region update](./stackit_network-area_region_update.md) - Updates a existing regional configuration for a STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_create.md b/docs/stackit_network-area_region_create.md new file mode 100644 index 000000000..55632632f --- /dev/null +++ b/docs/stackit_network-area_region_create.md @@ -0,0 +1,58 @@ +## stackit network-area region create + +Creates a new regional configuration for a STACKIT Network Area (SNA) + +### Synopsis + +Creates a new regional configuration for a STACKIT Network Area (SNA). + +``` +stackit network-area region create [flags] +``` + +### Examples + +``` + Create a new regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24" and ipv4 transfer network "192.168.1.0/24" + $ stackit network-area region create --network-area-id xxx --region eu02 --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 + + Create a new regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config + $ stackit config set --region eu02 + $ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 + + Create a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20" + $ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20 + + Create a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20" + $ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20 +``` + +### Options + +``` + -h, --help Help for "stackit network-area region create" + --ipv4-default-nameservers strings List of default DNS name server IPs + --ipv4-default-prefix-length int The default prefix length for networks in the network area + --ipv4-max-prefix-length int The maximum prefix length for networks in the network area + --ipv4-min-prefix-length int The minimum prefix length for networks in the network area + --ipv4-network-ranges strings Network range to create in CIDR notation (default []) + --ipv4-transfer-network string Transfer network in CIDR notation + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization 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 network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_delete.md b/docs/stackit_network-area_region_delete.md new file mode 100644 index 000000000..6f2193e5e --- /dev/null +++ b/docs/stackit_network-area_region_delete.md @@ -0,0 +1,46 @@ +## stackit network-area region delete + +Deletes a regional configuration for a STACKIT Network Area (SNA) + +### Synopsis + +Deletes a regional configuration for a STACKIT Network Area (SNA). + +``` +stackit network-area region delete [flags] +``` + +### Examples + +``` + Delete a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area region delete --network-area-id xxx --region eu02 --organization-id yyy + + Delete a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config + $ stackit config set --region eu02 + $ stackit network-area region delete --network-area-id xxx --organization-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit network-area region delete" + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization 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 network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_describe.md b/docs/stackit_network-area_region_describe.md new file mode 100644 index 000000000..e97ee813a --- /dev/null +++ b/docs/stackit_network-area_region_describe.md @@ -0,0 +1,46 @@ +## stackit network-area region describe + +Describes a regional configuration for a STACKIT Network Area (SNA) + +### Synopsis + +Describes a regional configuration for a STACKIT Network Area (SNA). + +``` +stackit network-area region describe [flags] +``` + +### Examples + +``` + Describe a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area region describe --network-area-id xxx --region eu02 --organization-id yyy + + Describe a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config + $ stackit config set --region eu02 + $ stackit network-area region describe --network-area-id xxx --organization-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit network-area region describe" + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization 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 network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_list.md b/docs/stackit_network-area_region_list.md new file mode 100644 index 000000000..2b6eaf673 --- /dev/null +++ b/docs/stackit_network-area_region_list.md @@ -0,0 +1,42 @@ +## stackit network-area region list + +Lists all configured regions for a STACKIT Network Area (SNA) + +### Synopsis + +Lists all configured regions for a STACKIT Network Area (SNA). + +``` +stackit network-area region list [flags] +``` + +### Examples + +``` + List all configured region for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area region list --network-area-id xxx --organization-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit network-area region list" + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization 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 network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_update.md b/docs/stackit_network-area_region_update.md new file mode 100644 index 000000000..400d85bc7 --- /dev/null +++ b/docs/stackit_network-area_region_update.md @@ -0,0 +1,56 @@ +## stackit network-area region update + +Updates a existing regional configuration for a STACKIT Network Area (SNA) + +### Synopsis + +Updates a existing regional configuration for a STACKIT Network Area (SNA). + +``` +stackit network-area region update [flags] +``` + +### Examples + +``` + Update a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" with new ipv4-default-nameservers "8.8.8.8" + $ stackit network-area region update --network-area-id xxx --region eu02 --organization-id yyy --ipv4-default-nameservers 8.8.8.8 + + Update a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" with new ipv4-default-nameservers "8.8.8.8", using the set region config + $ stackit config set --region eu02 + $ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-default-nameservers 8.8.8.8 + + Update a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20" + $ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20 + + Update a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20" + $ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20 +``` + +### Options + +``` + -h, --help Help for "stackit network-area region update" + --ipv4-default-nameservers strings List of default DNS name server IPs + --ipv4-default-prefix-length int The default prefix length for networks in the network area + --ipv4-max-prefix-length int The maximum prefix length for networks in the network area + --ipv4-min-prefix-length int The minimum prefix length for networks in the network area + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization 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 network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_route_create.md b/docs/stackit_network-area_route_create.md index 79d239fee..ff697f896 100644 --- a/docs/stackit_network-area_route_create.md +++ b/docs/stackit_network-area_route_create.md @@ -15,22 +15,25 @@ stackit network-area route create [flags] ### Examples ``` - Create a static route with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" - $ stackit network-area route create --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1 + Create a static route with destination "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area route create --organization-id yyy --network-area-id xxx --destination 1.1.1.0/24 --next-hop 1.1.1.1 - Create a static route with labels "key:value" and "foo:bar" with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" - $ stackit network-area route create --labels key=value,foo=bar --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1 + Create a static route with labels "key:value" and "foo:bar" with destination "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area route create --labels key=value,foo=bar --organization-id yyy --network-area-id xxx --destination 1.1.1.0/24 --next-hop 1.1.1.1 ``` ### Options ``` + --destination string Destination route. Must be a valid IPv4 or IPv6 CIDR -h, --help Help for "stackit network-area route create" --labels stringToString Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels (default []) --network-area-id string STACKIT Network Area ID - --next-hop string Next hop IP address. Must be a valid IPv4 + --next-hop-ipv4 string Next hop IPv4 address + --next-hop-ipv6 string Next hop IPv6 address + --nexthop-blackhole Sets next hop to black hole + --nexthop-internet Sets next hop to internet --organization-id string Organization ID - --prefix string Static route prefix ``` ### Options inherited from parent commands diff --git a/docs/stackit_network-area_update.md b/docs/stackit_network-area_update.md index 57b32a662..77665f0e8 100644 --- a/docs/stackit_network-area_update.md +++ b/docs/stackit_network-area_update.md @@ -20,14 +20,10 @@ stackit network-area update AREA_ID [flags] ### Options ``` - --default-prefix-length int The default prefix length for networks in the network area - --dns-name-servers strings List of DNS name server IPs - -h, --help Help for "stackit network-area update" - --labels stringToString Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...' (default []) - --max-prefix-length int The maximum prefix length for networks in the network area - --min-prefix-length int The minimum prefix length for networks in the network area - -n, --name string Network area name - --organization-id string Organization ID + -h, --help Help for "stackit network-area update" + --labels stringToString Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...' (default []) + -n, --name string Network area name + --organization-id string Organization ID ``` ### Options inherited from parent commands diff --git a/docs/stackit_network_create.md b/docs/stackit_network_create.md index 21d9e863c..146264977 100644 --- a/docs/stackit_network_create.md +++ b/docs/stackit_network_create.md @@ -26,7 +26,7 @@ stackit network create [flags] $ stackit network create --name network-1 --labels key=value,key1=value1 Create an IPv4 network with name "network-1" with DNS name servers, a prefix and a gateway - $ stackit network create --name network-1 --ipv4-dns-name-servers "1.1.1.1,8.8.8.8,9.9.9.9" --ipv4-prefix "10.1.2.0/24" --ipv4-gateway "10.1.2.3" + $ stackit network create --name network-1 --non-routed --ipv4-dns-name-servers "1.1.1.1,8.8.8.8,9.9.9.9" --ipv4-prefix "10.1.2.0/24" --ipv4-gateway "10.1.2.3" Create an IPv6 network with name "network-1" with DNS name servers, a prefix and a gateway $ stackit network create --name network-1 --ipv6-dns-name-servers "2001:4860:4860::8888,2001:4860:4860::8844" --ipv6-prefix "2001:4860:4860::8888" --ipv6-gateway "2001:4860:4860::8888" diff --git a/internal/cmd/network-area/create/create.go b/internal/cmd/network-area/create/create.go index fa97908d9..7268d0a2d 100644 --- a/internal/cmd/network-area/create/create.go +++ b/internal/cmd/network-area/create/create.go @@ -3,9 +3,12 @@ package create import ( "context" "fmt" + "os" + "strings" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "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" @@ -13,35 +16,58 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) const ( - nameFlag = "name" - organizationIdFlag = "organization-id" - dnsNameServersFlag = "dns-name-servers" - networkRangesFlag = "network-ranges" - transferNetworkFlag = "transfer-network" + nameFlag = "name" + organizationIdFlag = "organization-id" + // Deprecated: dnsNameServersFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + dnsNameServersFlag = "dns-name-servers" + // Deprecated: networkRangesFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + networkRangesFlag = "network-ranges" + // Deprecated: transferNetworkFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + transferNetworkFlag = "transfer-network" + // Deprecated: defaultPrefixLengthFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. defaultPrefixLengthFlag = "default-prefix-length" - maxPrefixLengthFlag = "max-prefix-length" - minPrefixLengthFlag = "min-prefix-length" - labelFlag = "labels" + // Deprecated: maxPrefixLengthFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + maxPrefixLengthFlag = "max-prefix-length" + // Deprecated: minPrefixLengthFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + minPrefixLengthFlag = "min-prefix-length" + labelFlag = "labels" + + deprecationMessage = "Deprecated and will be removed after April 2026. Use instead the new command `$ stackit network-area region` to configure these options for a network area." ) type inputModel struct { *globalflags.GlobalFlagModel - Name *string - OrganizationId *string - DnsNameServers *[]string - NetworkRanges *[]string - TransferNetwork *string + Name *string + OrganizationId string + // Deprecated: DnsNameServers is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + DnsNameServers *[]string + // Deprecated: NetworkRanges is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + NetworkRanges *[]string + // Deprecated: TransferNetwork is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + TransferNetwork *string + // Deprecated: DefaultPrefixLength is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. DefaultPrefixLength *int64 - MaxPrefixLength *int64 - MinPrefixLength *int64 - Labels *map[string]string + // Deprecated: MaxPrefixLength is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + MaxPrefixLength *int64 + // Deprecated: MinPrefixLength is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + MinPrefixLength *int64 + Labels *map[string]string +} + +// NetworkAreaResponses is a workaround, to keep the two responses of the iaas v2 api together for the json and yaml output +// Should be removed when the deprecated flags are removed +type NetworkAreaResponses struct { + NetworkArea iaas.NetworkArea `json:"network_area"` + RegionalArea *iaas.RegionalArea `json:"regional_area"` } func NewCmd(params *params.CmdParams) *cobra.Command { @@ -52,20 +78,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network`, - `$ stackit network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24"`, - ), - examples.NewExample( - `Create a network area with name "network-area-2" in organization with ID "xxx" with network ranges, transfer network and DNS name server`, - `$ stackit network-area create --name network-area-2 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --dns-name-servers "1.1.1.1"`, - ), - examples.NewExample( - `Create a network area with name "network-area-3" in organization with ID "xxx" with network ranges, transfer network and additional options`, - `$ stackit network-area create --name network-area-3 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --default-prefix-length 25 --max-prefix-length 29 --min-prefix-length 24`, + `Create a network area with name "network-area-1" in organization with ID "xxx"`, + `$ stackit network-area create --name network-area-1 --organization-id xxx"`, ), examples.NewExample( - `Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network and labels "key=value,key1=value1"`, - `$ stackit network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --labels key=value,key1=value1`, + `Create a network area with name "network-area-1" in organization with ID "xxx" with labels "key=value,key1=value1"`, + `$ stackit network-area create --name network-area-1 --organization-id xxx --labels key=value,key1=value1`, ), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -84,12 +102,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { var orgLabel string rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) if err == nil { - orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId) + orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, model.OrganizationId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) - orgLabel = *model.OrganizationId + orgLabel = model.OrganizationId } else if orgLabel == "" { - orgLabel = *model.OrganizationId + orgLabel = model.OrganizationId } } else { params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) @@ -109,8 +127,38 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create network area: %w", err) } + if resp == nil || resp.Id == nil { + return fmt.Errorf("create network area: empty response") + } + + responses := &NetworkAreaResponses{ + NetworkArea: *resp, + } - return outputResult(params.Printer, model.OutputFormat, orgLabel, resp) + if hasDeprecatedFlagsSet(model) { + deprecatedFlags := getConfiguredDeprecatedFlags(model) + params.Printer.Warn("the flags %q are deprecated and will be removed after April 2026. Use `$ stackit network-area region` to configure these options for a network area.\n", strings.Join(deprecatedFlags, ",")) + if resp == nil || resp.Id == nil { + return fmt.Errorf("create network area: empty response") + } + reqNetworkArea := buildRequestNetworkAreaRegion(ctx, model, *resp.Id, apiClient) + respNetworkArea, err := reqNetworkArea.Execute() + if err != nil { + return fmt.Errorf("create network area region: %w", err) + } + if !model.AssumeYes { + s := spinner.New(params.Printer) + s.Start("Create network area region") + _, err = wait.CreateNetworkAreaRegionWaitHandler(ctx, apiClient, model.OrganizationId, *resp.Id, model.Region).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for creating network area region %w", err) + } + s.Stop() + } + responses.RegionalArea = respNetworkArea + } + + return outputResult(params.Printer, model.OutputFormat, orgLabel, responses) }, } configureFlags(cmd) @@ -120,25 +168,64 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(nameFlag, "n", "", "Network area name") cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...'") cmd.Flags().StringSlice(dnsNameServersFlag, nil, "List of DNS name server IPs") cmd.Flags().Var(flags.CIDRSliceFlag(), networkRangesFlag, "List of network ranges") cmd.Flags().Var(flags.CIDRFlag(), transferNetworkFlag, "Transfer network in CIDR notation") cmd.Flags().Int64(defaultPrefixLengthFlag, 0, "The default prefix length for networks in the network area") cmd.Flags().Int64(maxPrefixLengthFlag, 0, "The maximum prefix length for networks in the network area") cmd.Flags().Int64(minPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area") - cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...'") - err := flags.MarkFlagsRequired(cmd, nameFlag, organizationIdFlag, networkRangesFlag, transferNetworkFlag) + cobra.CheckErr(cmd.Flags().MarkDeprecated(dnsNameServersFlag, deprecationMessage)) + cobra.CheckErr(cmd.Flags().MarkDeprecated(networkRangesFlag, deprecationMessage)) + cobra.CheckErr(cmd.Flags().MarkDeprecated(transferNetworkFlag, deprecationMessage)) + cobra.CheckErr(cmd.Flags().MarkDeprecated(defaultPrefixLengthFlag, deprecationMessage)) + cobra.CheckErr(cmd.Flags().MarkDeprecated(maxPrefixLengthFlag, deprecationMessage)) + cobra.CheckErr(cmd.Flags().MarkDeprecated(minPrefixLengthFlag, deprecationMessage)) + // Set the output for deprecation warnings to stderr + cmd.Flags().SetOutput(os.Stderr) + + cmd.MarkFlagsRequiredTogether(networkRangesFlag, transferNetworkFlag) + + err := flags.MarkFlagsRequired(cmd, nameFlag, organizationIdFlag) cobra.CheckErr(err) } +func hasDeprecatedFlagsSet(model *inputModel) bool { + deprecatedFlags := getConfiguredDeprecatedFlags(model) + return len(deprecatedFlags) > 0 +} + +func getConfiguredDeprecatedFlags(model *inputModel) []string { + var result []string + if model.DnsNameServers != nil { + result = append(result, dnsNameServersFlag) + } + if model.NetworkRanges != nil { + result = append(result, networkRangesFlag) + } + if model.TransferNetwork != nil { + result = append(result, transferNetworkFlag) + } + if model.DefaultPrefixLength != nil { + result = append(result, defaultPrefixLengthFlag) + } + if model.MaxPrefixLength != nil { + result = append(result, maxPrefixLengthFlag) + } + if model.MinPrefixLength != nil { + result = append(result, minPrefixLengthFlag) + } + return result +} + func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) model := inputModel{ GlobalFlagModel: globalFlags, Name: flags.FlagToStringPointer(p, cmd, nameFlag), - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, dnsNameServersFlag), NetworkRanges: flags.FlagToStringSlicePointer(p, cmd, networkRangesFlag), TransferNetwork: flags.FlagToStringPointer(p, cmd, transferNetworkFlag), @@ -148,44 +235,71 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), } + // Check if any of the deprecated **optional** fields are set and if no of the associated deprecated **required** fields is set. + hasAllRequiredRegionalAreaFieldsSet := model.NetworkRanges != nil && model.TransferNetwork != nil + hasOptionalRegionalAreaFieldsSet := model.DnsNameServers != nil || model.DefaultPrefixLength != nil || model.MaxPrefixLength != nil || model.MinPrefixLength != nil + if hasOptionalRegionalAreaFieldsSet && !hasAllRequiredRegionalAreaFieldsSet { + return nil, &cliErr.MultipleFlagsAreMissing{ + MissingFlags: []string{networkRangesFlag, transferNetworkFlag}, + SetFlags: []string{dnsNameServersFlag, defaultPrefixLengthFlag, minPrefixLengthFlag, maxPrefixLengthFlag}, + } + } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateNetworkAreaRequest { - req := apiClient.CreateNetworkArea(ctx, *model.OrganizationId) - - networkRanges := make([]iaas.NetworkRange, len(*model.NetworkRanges)) - for i, networkRange := range *model.NetworkRanges { - networkRanges[i] = iaas.NetworkRange{ - Prefix: utils.Ptr(networkRange), - } - } + req := apiClient.CreateNetworkArea(ctx, model.OrganizationId) payload := iaas.CreateNetworkAreaPayload{ Name: model.Name, Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), - AddressFamily: &iaas.CreateAreaAddressFamily{ - Ipv4: &iaas.CreateAreaIPv4{ - DefaultNameservers: model.DnsNameServers, - NetworkRanges: utils.Ptr(networkRanges), - TransferNetwork: model.TransferNetwork, - DefaultPrefixLen: model.DefaultPrefixLength, - MaxPrefixLen: model.MaxPrefixLength, - MinPrefixLen: model.MinPrefixLength, - }, - }, } return req.CreateNetworkAreaPayload(payload) } -func outputResult(p *print.Printer, outputFormat, orgLabel string, networkArea *iaas.NetworkArea) error { - if networkArea == nil { +func buildRequestNetworkAreaRegion(ctx context.Context, model *inputModel, networkAreaId string, apiClient *iaas.APIClient) iaas.ApiCreateNetworkAreaRegionRequest { + req := apiClient.CreateNetworkAreaRegion(ctx, model.OrganizationId, networkAreaId, model.Region) + + var networkRanges []iaas.NetworkRange + if model.NetworkRanges != nil { + networkRanges = make([]iaas.NetworkRange, len(*model.NetworkRanges)) + for i, networkRange := range *model.NetworkRanges { + networkRanges[i] = iaas.NetworkRange{ + Prefix: utils.Ptr(networkRange), + } + } + } + + payload := iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: model.DnsNameServers, + NetworkRanges: utils.Ptr(networkRanges), + TransferNetwork: model.TransferNetwork, + DefaultPrefixLen: model.DefaultPrefixLength, + MaxPrefixLen: model.MaxPrefixLength, + MinPrefixLen: model.MinPrefixLength, + }, + } + + return req.CreateNetworkAreaRegionPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat, orgLabel string, responses *NetworkAreaResponses) error { + if responses == nil { return fmt.Errorf("network area is nil") } - return p.OutputResult(outputFormat, networkArea, func() error { - p.Outputf("Created STACKIT Network Area for organization %q.\nNetwork area ID: %s\n", orgLabel, utils.PtrString(networkArea.AreaId)) + + prettyOutputFunc := func() error { + p.Outputf("Created STACKIT Network Area for organization %q.\nNetwork area ID: %s\n", orgLabel, utils.PtrString(responses.NetworkArea.Id)) return nil - }) + } + // If RegionalArea is NOT set in the response, then no deprecated Flags were set. + // In this case, only the response of NetworkArea should be printed in JSON and yaml output, to avoid breaking changes after the deprecated fields are removed + if responses.RegionalArea == nil { + return p.OutputResult(outputFormat, responses.NetworkArea, prettyOutputFunc) + } + return p.OutputResult(outputFormat, responses, prettyOutputFunc) } diff --git a/internal/cmd/network-area/create/create_test.go b/internal/cmd/network-area/create/create_test.go index a731541fd..9bfadd260 100644 --- a/internal/cmd/network-area/create/create_test.go +++ b/internal/cmd/network-area/create/create_test.go @@ -2,6 +2,8 @@ package create import ( "context" + "strconv" + "strings" "testing" "github.com/stackitcloud/stackit-cli/internal/cmd/params" @@ -16,24 +18,34 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) +const ( + testRegion = "eu01" + testName = "example-network-area-name" + testTransferNetwork = "100.0.0.0/24" + testDefaultPrefixLength int64 = 25 + testMaxPrefixLength int64 = 26 + testMinPrefixLength int64 = 24 +) + type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &iaas.APIClient{} -var testOrgId = uuid.NewString() +var ( + testOrgId = uuid.NewString() + testAreaId = uuid.NewString() + testDnsNameservers = []string{"1.1.1.0", "1.1.2.0"} + testNetworkRanges = []string{"192.0.0.0/24", "102.0.0.0/24"} +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - nameFlag: "example-network-area-name", - organizationIdFlag: testOrgId, - dnsNameServersFlag: "1.1.1.0,1.1.2.0", - networkRangesFlag: "192.0.0.0/24,102.0.0.0/24", - transferNetworkFlag: "100.0.0.0/24", - defaultPrefixLengthFlag: "24", - maxPrefixLengthFlag: "24", - minPrefixLengthFlag: "24", - labelFlag: "key=value", + globalflags.RegionFlag: testRegion, + + nameFlag: testName, + organizationIdFlag: testOrgId, + labelFlag: "key=value", } for _, mod := range mods { mod(flagValues) @@ -45,15 +57,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - Name: utils.Ptr("example-network-area-name"), - OrganizationId: utils.Ptr(testOrgId), - DnsNameServers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - NetworkRanges: utils.Ptr([]string{"192.0.0.0/24", "102.0.0.0/24"}), - TransferNetwork: utils.Ptr("100.0.0.0/24"), - DefaultPrefixLength: utils.Ptr(int64(24)), - MaxPrefixLength: utils.Ptr(int64(24)), - MinPrefixLength: utils.Ptr(int64(24)), + Name: utils.Ptr("example-network-area-name"), + OrganizationId: testOrgId, Labels: utils.Ptr(map[string]string{ "key": "value", }), @@ -79,23 +86,40 @@ func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaPayload)) iaas.C Labels: utils.Ptr(map[string]interface{}{ "key": "value", }), - AddressFamily: &iaas.CreateAreaAddressFamily{ - Ipv4: &iaas.CreateAreaIPv4{ - DefaultNameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - NetworkRanges: &[]iaas.NetworkRange{ - { - Prefix: utils.Ptr("192.0.0.0/24"), - }, - { - Prefix: utils.Ptr("102.0.0.0/24"), - }, - }, - TransferNetwork: utils.Ptr("100.0.0.0/24"), - DefaultPrefixLen: utils.Ptr(int64(24)), - MaxPrefixLen: utils.Ptr(int64(24)), - MinPrefixLen: utils.Ptr(int64(24)), - }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func fixtureRequestRegionalArea(mods ...func(request *iaas.ApiCreateNetworkAreaRegionRequest)) iaas.ApiCreateNetworkAreaRegionRequest { + req := testClient.CreateNetworkAreaRegion(testCtx, testOrgId, testAreaId, testRegion) + req = req.CreateNetworkAreaRegionPayload(fixtureRegionalAreaPayload()) + for _, mod := range mods { + mod(&req) + } + return req +} + +func fixtureRegionalAreaPayload(mods ...func(request *iaas.CreateNetworkAreaRegionPayload)) iaas.CreateNetworkAreaRegionPayload { + var networkRanges []iaas.NetworkRange + for _, networkRange := range testNetworkRanges { + networkRanges = append(networkRanges, iaas.NetworkRange{ + Prefix: utils.Ptr(networkRange), + }) + } + + payload := iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: utils.Ptr(testDnsNameservers), + DefaultPrefixLen: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLen: utils.Ptr(testMaxPrefixLength), + MinPrefixLen: utils.Ptr(testMinPrefixLength), + NetworkRanges: utils.Ptr(networkRanges), + TransferNetwork: utils.Ptr(testTransferNetwork), }, + Status: nil, } for _, mod := range mods { mod(&payload) @@ -119,20 +143,35 @@ func TestParseInput(t *testing.T) { expectedModel: fixtureInputModel(), }, { - description: "required only", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, dnsNameServersFlag) - delete(flagValues, defaultPrefixLengthFlag) - delete(flagValues, maxPrefixLengthFlag) - delete(flagValues, minPrefixLengthFlag) - }), + description: "with deprecated flags", + flagValues: map[string]string{ + nameFlag: testName, + organizationIdFlag: testOrgId, + + // Deprecated flags + dnsNameServersFlag: strings.Join(testDnsNameservers, ","), + networkRangesFlag: strings.Join(testNetworkRanges, ","), + transferNetworkFlag: testTransferNetwork, + defaultPrefixLengthFlag: strconv.FormatInt(testDefaultPrefixLength, 10), + maxPrefixLengthFlag: strconv.FormatInt(testMaxPrefixLength, 10), + minPrefixLengthFlag: strconv.FormatInt(testMinPrefixLength, 10), + }, isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.DnsNameServers = nil - model.DefaultPrefixLength = nil - model.MaxPrefixLength = nil - model.MinPrefixLength = nil - }), + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Name: utils.Ptr(testName), + OrganizationId: testOrgId, + + // Deprecated fields + DnsNameServers: utils.Ptr(testDnsNameservers), + NetworkRanges: utils.Ptr(testNetworkRanges), + TransferNetwork: utils.Ptr(testTransferNetwork), + DefaultPrefixLength: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLength: utils.Ptr(testMaxPrefixLength), + MinPrefixLength: utils.Ptr(testMinPrefixLength), + }, }, { description: "name missing", @@ -142,16 +181,38 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "network ranges missing", + description: "set deprecated network ranges - missing transfer network", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, networkRangesFlag) + flagValues[networkRangesFlag] = strings.Join(testNetworkRanges, ",") }), isValid: false, }, { - description: "transfer network missing", + description: "set deprecated transfer network - missing network ranges", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, transferNetworkFlag) + flagValues[transferNetworkFlag] = testTransferNetwork + }), + isValid: false, + }, + { + description: "set deprecated transfer network and network ranges", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkRangesFlag] = strings.Join(testNetworkRanges, ",") + flagValues[transferNetworkFlag] = testTransferNetwork + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NetworkRanges = utils.Ptr(testNetworkRanges) + model.TransferNetwork = utils.Ptr(testTransferNetwork) + }), + }, + { + description: "set deprecated optional flags", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[dnsNameServersFlag] = strings.Join(testDnsNameservers, ",") + flagValues[defaultPrefixLengthFlag] = strconv.FormatInt(testDefaultPrefixLength, 10) + flagValues[maxPrefixLengthFlag] = strconv.FormatInt(testMaxPrefixLength, 10) + flagValues[minPrefixLengthFlag] = strconv.FormatInt(testMinPrefixLength, 10) }), isValid: false, }, @@ -228,11 +289,63 @@ func TestBuildRequest(t *testing.T) { } } +func TestBuildRequestNetworkAreaRegion(t *testing.T) { + tests := []struct { + description string + model *inputModel + areaId string + expectedRequest iaas.ApiCreateNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(func(model *inputModel) { + // Deprecated fields + model.DnsNameServers = utils.Ptr(testDnsNameservers) + model.NetworkRanges = utils.Ptr(testNetworkRanges) + model.TransferNetwork = utils.Ptr(testTransferNetwork) + model.DefaultPrefixLength = utils.Ptr(testDefaultPrefixLength) + model.MaxPrefixLength = utils.Ptr(testMaxPrefixLength) + model.MinPrefixLength = utils.Ptr(testMinPrefixLength) + }), + areaId: testAreaId, + expectedRequest: fixtureRequestRegionalArea(), + }, + { + description: "base without network ranges", + model: fixtureInputModel(func(model *inputModel) { + // Deprecated fields + model.DnsNameServers = utils.Ptr(testDnsNameservers) + model.NetworkRanges = utils.Ptr(testNetworkRanges) + model.TransferNetwork = utils.Ptr(testTransferNetwork) + model.DefaultPrefixLength = utils.Ptr(testDefaultPrefixLength) + model.MaxPrefixLength = utils.Ptr(testMaxPrefixLength) + model.MinPrefixLength = utils.Ptr(testMinPrefixLength) + }), + areaId: testAreaId, + expectedRequest: fixtureRequestRegionalArea(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequestNetworkAreaRegion(testCtx, tt.model, testAreaId, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + func Test_outputResult(t *testing.T) { type args struct { outputFormat string orgLabel string - networkArea *iaas.NetworkArea + responses *NetworkAreaResponses } tests := []struct { name string @@ -244,10 +357,19 @@ func Test_outputResult(t *testing.T) { args: args{}, wantErr: true, }, + { + name: "set empty response", + args: args{ + responses: &NetworkAreaResponses{}, + }, + wantErr: false, + }, { name: "set empty network area", args: args{ - networkArea: &iaas.NetworkArea{}, + responses: &NetworkAreaResponses{ + NetworkArea: iaas.NetworkArea{}, + }, }, wantErr: false, }, @@ -256,9 +378,140 @@ func Test_outputResult(t *testing.T) { p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.orgLabel, tt.args.networkArea); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.orgLabel, tt.args.responses); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) } } + +func TestGetConfiguredDeprecatedFlags(t *testing.T) { + type args struct { + model *inputModel + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "no deprecated flags", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Name: utils.Ptr(testName), + OrganizationId: testOrgId, + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + DnsNameServers: nil, + NetworkRanges: nil, + TransferNetwork: nil, + DefaultPrefixLength: nil, + MaxPrefixLength: nil, + MinPrefixLength: nil, + }, + }, + want: nil, + }, + { + name: "deprecated flags", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Name: utils.Ptr(testName), + OrganizationId: testOrgId, + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + DnsNameServers: utils.Ptr(testDnsNameservers), + NetworkRanges: utils.Ptr(testNetworkRanges), + TransferNetwork: utils.Ptr(testTransferNetwork), + DefaultPrefixLength: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLength: utils.Ptr(testMaxPrefixLength), + MinPrefixLength: utils.Ptr(testMinPrefixLength), + }, + }, + want: []string{dnsNameServersFlag, networkRangesFlag, transferNetworkFlag, defaultPrefixLengthFlag, minPrefixLengthFlag, maxPrefixLengthFlag}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getConfiguredDeprecatedFlags(tt.args.model) + + less := func(a, b string) bool { + return a < b + } + if diff := cmp.Diff(tt.want, got, cmpopts.SortSlices(less)); diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestHasDeprecatedFlagsSet(t *testing.T) { + type args struct { + model *inputModel + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "no deprecated flags", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Name: utils.Ptr(testName), + OrganizationId: testOrgId, + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + DnsNameServers: nil, + NetworkRanges: nil, + TransferNetwork: nil, + DefaultPrefixLength: nil, + MaxPrefixLength: nil, + MinPrefixLength: nil, + }, + }, + want: false, + }, + { + name: "deprecated flags", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Name: utils.Ptr(testName), + OrganizationId: testOrgId, + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + DnsNameServers: utils.Ptr(testDnsNameservers), + NetworkRanges: utils.Ptr(testNetworkRanges), + TransferNetwork: utils.Ptr(testTransferNetwork), + DefaultPrefixLength: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLength: utils.Ptr(testMaxPrefixLength), + MinPrefixLength: utils.Ptr(testMinPrefixLength), + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasDeprecatedFlagsSet(tt.args.model); got != tt.want { + t.Errorf("hasDeprecatedFlagsSet() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/cmd/network-area/delete/delete.go b/internal/cmd/network-area/delete/delete.go index d16a9e656..0e42d5883 100644 --- a/internal/cmd/network-area/delete/delete.go +++ b/internal/cmd/network-area/delete/delete.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -12,17 +13,19 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - - "github.com/spf13/cobra" ) const ( areaIdArg = "AREA_ID" organizationIdFlag = "organization-id" + + deprecationMessage = "The regional network area configuration %q for the area %q still exists.\n" + + "The regional configuration of the network area was moved to the new command group `$ stackit network-area region`.\n" + + "The regional area will be automatically deleted. This behavior is deprecated and will be removed after April 2026.\n" + + "Use in the future the command `$ stackit network-area region delete` to delete the regional network area and afterwards delete the network-area with the command `$ stackit network-area delete`.\n" ) type inputModel struct { @@ -73,29 +76,31 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - // Call API - req := buildRequest(ctx, model, apiClient) - err = req.Execute() + // Check if the network area has a regional configuration + regionalArea, err := apiClient.GetNetworkAreaRegion(ctx, *model.OrganizationId, model.AreaId, model.Region).Execute() if err != nil { - return fmt.Errorf("delete network area: %w", err) + params.Printer.Debug(print.ErrorLevel, "get regional area: %v", err) } - - // Wait for async operation, if async mode not enabled - if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting network area") - _, err = wait.DeleteNetworkAreaWaitHandler(ctx, apiClient, *model.OrganizationId, model.AreaId).WaitWithContext(ctx) + if regionalArea != nil { + params.Printer.Warn(deprecationMessage, model.Region, networkAreaLabel) + err = apiClient.DeleteNetworkAreaRegion(ctx, *model.OrganizationId, model.AreaId, model.Region).Execute() if err != nil { - return fmt.Errorf("wait for network area deletion: %w", err) + return fmt.Errorf("delete network area region: %w", err) + } + _, err := wait.DeleteNetworkAreaRegionWaitHandler(ctx, apiClient, *model.OrganizationId, model.AreaId, model.Region).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait delete network area region: %w", err) } - s.Stop() } - operationState := "Deleted" - if model.Async { - operationState = "Triggered deletion of" + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete network area: %w", err) } - params.Printer.Info("%s STACKIT Network Area %q\n", operationState, networkAreaLabel) + + params.Printer.Outputf("Deleted STACKIT Network Area %q\n", networkAreaLabel) return nil }, } diff --git a/internal/cmd/network-area/describe/describe.go b/internal/cmd/network-area/describe/describe.go index b6c086a4b..4fdf257c5 100644 --- a/internal/cmd/network-area/describe/describe.go +++ b/internal/cmd/network-area/describe/describe.go @@ -126,60 +126,11 @@ func outputResult(p *print.Printer, outputFormat string, networkArea *iaas.Netwo } return p.OutputResult(outputFormat, networkArea, func() error { - var routes []string - var networkRanges []string - - if networkArea.Ipv4 != nil { - if networkArea.Ipv4.Routes != nil { - for _, route := range *networkArea.Ipv4.Routes { - routes = append(routes, fmt.Sprintf("next hop: %s\nprefix: %s", *route.Nexthop, *route.Prefix)) - } - } - - if networkArea.Ipv4.NetworkRanges != nil { - for _, networkRange := range *networkArea.Ipv4.NetworkRanges { - networkRanges = append(networkRanges, *networkRange.Prefix) - } - } - } - table := tables.NewTable() - table.AddRow("ID", utils.PtrString(networkArea.AreaId)) + table.AddRow("ID", utils.PtrString(networkArea.Id)) table.AddSeparator() table.AddRow("NAME", utils.PtrString(networkArea.Name)) table.AddSeparator() - table.AddRow("STATE", utils.PtrString(networkArea.State)) - table.AddSeparator() - if len(networkRanges) > 0 { - table.AddRow("NETWORK RANGES", strings.Join(networkRanges, ",")) - } - table.AddSeparator() - for i, route := range routes { - table.AddRow(fmt.Sprintf("STATIC ROUTE %d", i+1), route) - table.AddSeparator() - } - if networkArea.Ipv4 != nil { - if networkArea.Ipv4.TransferNetwork != nil { - table.AddRow("TRANSFER RANGE", *networkArea.Ipv4.TransferNetwork) - table.AddSeparator() - } - if networkArea.Ipv4.DefaultNameservers != nil && len(*networkArea.Ipv4.DefaultNameservers) > 0 { - table.AddRow("DNS NAME SERVERS", strings.Join(*networkArea.Ipv4.DefaultNameservers, ",")) - table.AddSeparator() - } - if networkArea.Ipv4.DefaultPrefixLen != nil { - table.AddRow("DEFAULT PREFIX LENGTH", *networkArea.Ipv4.DefaultPrefixLen) - table.AddSeparator() - } - if networkArea.Ipv4.MaxPrefixLen != nil { - table.AddRow("MAX PREFIX LENGTH", *networkArea.Ipv4.MaxPrefixLen) - table.AddSeparator() - } - if networkArea.Ipv4.MinPrefixLen != nil { - table.AddRow("MIN PREFIX LENGTH", *networkArea.Ipv4.MinPrefixLen) - table.AddSeparator() - } - } if networkArea.Labels != nil && len(*networkArea.Labels) > 0 { var labels []string for key, value := range *networkArea.Labels { @@ -195,6 +146,10 @@ func outputResult(p *print.Printer, outputFormat string, networkArea *iaas.Netwo table.AddRow("# ATTACHED PROJECTS", utils.PtrString(networkArea.ProjectCount)) table.AddSeparator() } + table.AddRow("CREATED AT", utils.PtrString(networkArea.CreatedAt)) + table.AddSeparator() + table.AddRow("UPDATED AT", utils.PtrString(networkArea.UpdatedAt)) + table.AddSeparator() err := table.Display(p) if err != nil { diff --git a/internal/cmd/network-area/list/list.go b/internal/cmd/network-area/list/list.go index 6184148aa..e37822602 100644 --- a/internal/cmd/network-area/list/list.go +++ b/internal/cmd/network-area/list/list.go @@ -150,21 +150,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli func outputResult(p *print.Printer, outputFormat string, networkAreas []iaas.NetworkArea) error { return p.OutputResult(outputFormat, networkAreas, func() error { table := tables.NewTable() - table.SetHeader("ID", "Name", "Status", "Network Ranges", "# Attached Projects") + table.SetHeader("ID", "Name", "# Attached Projects") for _, networkArea := range networkAreas { - networkRanges := "n/a" - if ipv4 := networkArea.Ipv4; ipv4 != nil { - if netRange := ipv4.NetworkRanges; netRange != nil { - networkRanges = fmt.Sprint(len(*netRange)) - } - } - table.AddRow( - utils.PtrString(networkArea.AreaId), + utils.PtrString(networkArea.Id), utils.PtrString(networkArea.Name), - utils.PtrString(networkArea.State), - networkRanges, utils.PtrString(networkArea.ProjectCount), ) table.AddSeparator() diff --git a/internal/cmd/network-area/network_area.go b/internal/cmd/network-area/network_area.go index 0b67ea3ea..637a7af0e 100644 --- a/internal/cmd/network-area/network_area.go +++ b/internal/cmd/network-area/network_area.go @@ -6,6 +6,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/list" networkrange "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/network-range" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/route" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/update" "github.com/stackitcloud/stackit-cli/internal/cmd/params" @@ -33,6 +34,7 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(networkrange.NewCmd(params)) + cmd.AddCommand(region.NewCmd(params)) cmd.AddCommand(route.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) } diff --git a/internal/cmd/network-area/region/create/create.go b/internal/cmd/network-area/region/create/create.go new file mode 100644 index 000000000..a4cda53d6 --- /dev/null +++ b/internal/cmd/network-area/region/create/create.go @@ -0,0 +1,195 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + ipv4DefaultNameservers = "ipv4-default-nameservers" + ipv4DefaultPrefixLengthFlag = "ipv4-default-prefix-length" + ipv4MaxPrefixLengthFlag = "ipv4-max-prefix-length" + ipv4MinPrefixLengthFlag = "ipv4-min-prefix-length" + ipv4NetworkRangesFlag = "ipv4-network-ranges" + ipv4TransferNetworkFlag = "ipv4-transfer-network" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string + + IPv4DefaultNameservers *[]string + IPv4DefaultPrefixLength *int64 + IPv4MaxPrefixLength *int64 + IPv4MinPrefixLength *int64 + IPv4NetworkRanges []string + IPv4TransferNetwork string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a new regional configuration for a STACKIT Network Area (SNA)", + Long: "Creates a new regional configuration for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a new regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24" and ipv4 transfer network "192.168.1.0/24"`, + `$ stackit network-area region create --network-area-id xxx --region eu02 --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24`, + ), + examples.NewExample( + `Create a new regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config`, + `$ stackit config set --region eu02`, + `$ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24`, + ), + examples.NewExample( + `Create a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20"`, + `$ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20`, + ), + examples.NewExample( + `Create a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20"`, + `$ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20`, + ), + ), + 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 network area label + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaLabel = model.NetworkAreaId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create network area region: %w", err) + } + + if resp == nil || resp.Ipv4 == nil { + return fmt.Errorf("empty response from API") + } + + if !model.Async { + s := spinner.New(params.Printer) + s.Start("Create network area region") + _, err = wait.CreateNetworkAreaRegionWaitHandler(ctx, apiClient, model.OrganizationId, model.NetworkAreaId, model.Region).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for network area region creation: %w", err) + } + s.Stop() + } + + return outputResult(params.Printer, model.OutputFormat, model.Region, networkAreaLabel, *resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.CIDRSliceFlag(), ipv4NetworkRangesFlag, "Network range to create in CIDR notation") + cmd.Flags().Var(flags.CIDRFlag(), ipv4TransferNetworkFlag, "Transfer network in CIDR notation") + cmd.Flags().StringSlice(ipv4DefaultNameservers, nil, "List of default DNS name server IPs") + cmd.Flags().Int64(ipv4DefaultPrefixLengthFlag, 0, "The default prefix length for networks in the network area") + cmd.Flags().Int64(ipv4MaxPrefixLengthFlag, 0, "The maximum prefix length for networks in the network area") + cmd.Flags().Int64(ipv4MinPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area") + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag, ipv4NetworkRangesFlag, ipv4TransferNetworkFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + IPv4DefaultNameservers: flags.FlagToStringSlicePointer(p, cmd, ipv4DefaultNameservers), + IPv4DefaultPrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4DefaultPrefixLengthFlag), + IPv4MaxPrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4MaxPrefixLengthFlag), + IPv4MinPrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4MinPrefixLengthFlag), + IPv4NetworkRanges: flags.FlagToStringSliceValue(p, cmd, ipv4NetworkRangesFlag), + IPv4TransferNetwork: flags.FlagToStringValue(p, cmd, ipv4TransferNetworkFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateNetworkAreaRegionRequest { + req := apiClient.CreateNetworkAreaRegion(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) + + var networkRange []iaas.NetworkRange + if len(model.IPv4NetworkRanges) > 0 { + networkRange = make([]iaas.NetworkRange, len(model.IPv4NetworkRanges)) + for i := range model.IPv4NetworkRanges { + networkRange[i] = iaas.NetworkRange{ + Prefix: utils.Ptr(model.IPv4NetworkRanges[i]), + } + } + } + + payload := iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: model.IPv4DefaultNameservers, + DefaultPrefixLen: model.IPv4DefaultPrefixLength, + MaxPrefixLen: model.IPv4MaxPrefixLength, + MinPrefixLen: model.IPv4MinPrefixLength, + NetworkRanges: utils.Ptr(networkRange), + TransferNetwork: utils.Ptr(model.IPv4TransferNetwork), + }, + } + return req.CreateNetworkAreaRegionPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat, region, networkAreaLabel string, regionalArea iaas.RegionalArea) error { + return p.OutputResult(outputFormat, regionalArea, func() error { + p.Outputf("Create region configuration for SNA %q.\nRegion: %s\n", networkAreaLabel, region) + return nil + }) +} diff --git a/internal/cmd/network-area/region/create/create_test.go b/internal/cmd/network-area/region/create/create_test.go new file mode 100644 index 000000000..1f8cacef2 --- /dev/null +++ b/internal/cmd/network-area/region/create/create_test.go @@ -0,0 +1,307 @@ +package create + +import ( + "context" + "strconv" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" + testDefaultPrefixLength int64 = 25 + testMaxPrefixLength int64 = 29 + testMinPrefixLength int64 = 24 + testTransferNetwork = "192.168.2.0/24" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() + testDefaultNameservers = []string{"8.8.8.8", "8.8.4.4"} + testNetworkRanges = []string{"192.168.0.0/24", "10.0.0.0/24"} +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + ipv4DefaultNameservers: strings.Join(testDefaultNameservers, ","), + ipv4DefaultPrefixLengthFlag: strconv.FormatInt(testDefaultPrefixLength, 10), + ipv4MaxPrefixLengthFlag: strconv.FormatInt(testMaxPrefixLength, 10), + ipv4MinPrefixLengthFlag: strconv.FormatInt(testMinPrefixLength, 10), + ipv4NetworkRangesFlag: strings.Join(testNetworkRanges, ","), + ipv4TransferNetworkFlag: testTransferNetwork, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + IPv4DefaultNameservers: utils.Ptr(testDefaultNameservers), + IPv4DefaultPrefixLength: utils.Ptr(testDefaultPrefixLength), + IPv4MaxPrefixLength: utils.Ptr(testMaxPrefixLength), + IPv4MinPrefixLength: utils.Ptr(testMinPrefixLength), + IPv4NetworkRanges: testNetworkRanges, + IPv4TransferNetwork: testTransferNetwork, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRegionRequest)) iaas.ApiCreateNetworkAreaRegionRequest { + request := testClient.CreateNetworkAreaRegion(testCtx, testOrgId, testAreaId, testRegion) + request = request.CreateNetworkAreaRegionPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaRegionPayload)) iaas.CreateNetworkAreaRegionPayload { + var networkRange []iaas.NetworkRange + if len(testNetworkRanges) > 0 { + networkRange = make([]iaas.NetworkRange, len(testNetworkRanges)) + for i := range testNetworkRanges { + networkRange[i] = iaas.NetworkRange{ + Prefix: utils.Ptr(testNetworkRanges[i]), + } + } + } + + payload := iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: utils.Ptr(testDefaultNameservers), + DefaultPrefixLen: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLen: utils.Ptr(testMaxPrefixLength), + MinPrefixLen: utils.Ptr(testMinPrefixLength), + NetworkRanges: utils.Ptr(networkRange), + TransferNetwork: utils.Ptr(testTransferNetwork), + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "network range missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipv4NetworkRangesFlag) + }), + isValid: false, + }, + { + description: "multiple network ranges", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4NetworkRangesFlag] = "192.168.2.0/24,10.0.0.0/24" + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.IPv4NetworkRanges = []string{"192.168.2.0/24", "10.0.0.0/24"} + }), + isValid: true, + }, + { + description: "network range invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4NetworkRangesFlag] = "invalid-cidr" + }), + isValid: false, + }, + { + description: "transfer network missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipv4TransferNetworkFlag) + }), + isValid: false, + }, + { + description: "transfer network invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4TransferNetworkFlag] = "" + }), + isValid: false, + }, + { + description: "transfer network invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4TransferNetworkFlag] = "invalid-cidr" + }), + isValid: false, + }, + { + description: "region empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.RegionFlag] = "" + }), + 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.ApiCreateNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + region string + networkAreaLabel string + regionalArea iaas.RegionalArea + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty regional area", + args: args{ + regionalArea: iaas.RegionalArea{}, + }, + wantErr: false, + }, + { + name: "output json", + args: args{ + outputFormat: print.JSONOutputFormat, + regionalArea: iaas.RegionalArea{}, + }, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.region, tt.args.networkAreaLabel, tt.args.regionalArea); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/region/delete/delete.go b/internal/cmd/network-area/region/delete/delete.go new file mode 100644 index 000000000..37f1f8bb0 --- /dev/null +++ b/internal/cmd/network-area/region/delete/delete.go @@ -0,0 +1,129 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Deletes a regional configuration for a STACKIT Network Area (SNA)", + Long: "Deletes a regional configuration for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + `$ stackit network-area region delete --network-area-id xxx --region eu02 --organization-id yyy`, + ), + examples.NewExample( + `Delete a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config`, + `$ stackit config set --region eu02`, + `$ stackit network-area region delete --network-area-id xxx --organization-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 + } + + // Get network area label + networkAreaName, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaName = model.NetworkAreaId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete network area region: %w", err) + } + + if !model.Async { + s := spinner.New(params.Printer) + s.Start("Delete network area region") + _, err = wait.DeleteNetworkAreaRegionWaitHandler(ctx, apiClient, model.OrganizationId, model.NetworkAreaId, model.Region).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for network area region deletion: %w", err) + } + s.Stop() + } + + params.Printer.Outputf("Delete regional network area %q for %q\n", model.Region, networkAreaName) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteNetworkAreaRegionRequest { + return apiClient.DeleteNetworkAreaRegion(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) +} diff --git a/internal/cmd/network-area/region/delete/delete_test.go b/internal/cmd/network-area/region/delete/delete_test.go new file mode 100644 index 000000000..919e86cb8 --- /dev/null +++ b/internal/cmd/network-area/region/delete/delete_test.go @@ -0,0 +1,168 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkAreaRegionRequest)) iaas.ApiDeleteNetworkAreaRegionRequest { + request := testClient.DeleteNetworkAreaRegion(testCtx, testOrgId, testAreaId, 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: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "region empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.RegionFlag] = "" + }), + 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.ApiDeleteNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/network-area/region/describe/describe.go b/internal/cmd/network-area/region/describe/describe.go new file mode 100644 index 000000000..71e4f5c31 --- /dev/null +++ b/internal/cmd/network-area/region/describe/describe.go @@ -0,0 +1,169 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Describes a regional configuration for a STACKIT Network Area (SNA)", + Long: "Describes a regional configuration for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Describe a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + `$ stackit network-area region describe --network-area-id xxx --region eu02 --organization-id yyy`, + ), + examples.NewExample( + `Describe a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config`, + `$ stackit config set --region eu02`, + `$ stackit network-area region describe --network-area-id xxx --organization-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 + } + + // Get network area label + networkAreaName, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + // Set explicit the networkAreaName to empty string and not to the ID, because this is used for the table output + networkAreaName = "" + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("describe network area region: %w", err) + } + + if resp == nil || resp.Ipv4 == nil { + return fmt.Errorf("empty response from API") + } + + return outputResult(params.Printer, model.OutputFormat, model.Region, model.NetworkAreaId, networkAreaName, *resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetNetworkAreaRegionRequest { + return apiClient.GetNetworkAreaRegion(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat, region, areaId, areaName string, regionalArea iaas.RegionalArea) error { + return p.OutputResult(outputFormat, regionalArea, func() error { + table := tables.NewTable() + table.AddRow("ID", areaId) + table.AddSeparator() + if areaName != "" { + table.AddRow("NAME", areaName) + table.AddSeparator() + } + table.AddRow("REGION", region) + table.AddSeparator() + table.AddRow("STATUS", utils.PtrString(regionalArea.Status)) + table.AddSeparator() + if ipv4 := regionalArea.Ipv4; ipv4 != nil { + if ipv4.NetworkRanges != nil { + var networkRanges []string + for _, networkRange := range *ipv4.NetworkRanges { + if networkRange.Prefix != nil { + networkRanges = append(networkRanges, *networkRange.Prefix) + } + } + table.AddRow("NETWORK RANGES", strings.Join(networkRanges, ",")) + table.AddSeparator() + } + if transferNetwork := ipv4.TransferNetwork; transferNetwork != nil { + table.AddRow("TRANSFER RANGE", utils.PtrString(transferNetwork)) + table.AddSeparator() + } + if defaultNameserver := ipv4.DefaultNameservers; defaultNameserver != nil && len(*defaultNameserver) > 0 { + table.AddRow("DNS NAME SERVERS", strings.Join(*defaultNameserver, ",")) + table.AddSeparator() + } + if defaultPrefixLength := ipv4.DefaultPrefixLen; defaultPrefixLength != nil { + table.AddRow("DEFAULT PREFIX LENGTH", utils.PtrString(defaultPrefixLength)) + table.AddSeparator() + } + if maxPrefixLength := ipv4.MaxPrefixLen; maxPrefixLength != nil { + table.AddRow("MAX PREFIX LENGTH", utils.PtrString(maxPrefixLength)) + table.AddSeparator() + } + if minPrefixLen := ipv4.MinPrefixLen; minPrefixLen != nil { + table.AddRow("MIN PREFIX LENGTH", utils.PtrString(minPrefixLen)) + table.AddSeparator() + } + } + + if err := table.Display(p); err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/network-area/region/describe/describe_test.go b/internal/cmd/network-area/region/describe/describe_test.go new file mode 100644 index 000000000..7d06c1794 --- /dev/null +++ b/internal/cmd/network-area/region/describe/describe_test.go @@ -0,0 +1,214 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkAreaRegionRequest)) iaas.ApiGetNetworkAreaRegionRequest { + request := testClient.GetNetworkAreaRegion(testCtx, testOrgId, testAreaId, 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: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "region empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.RegionFlag] = "" + }), + 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.ApiGetNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + areaId string + region string + networkAreaLabel string + regionalArea iaas.RegionalArea + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty regional area", + args: args{ + regionalArea: iaas.RegionalArea{}, + }, + wantErr: false, + }, + { + name: "output json", + args: args{ + outputFormat: print.JSONOutputFormat, + regionalArea: iaas.RegionalArea{}, + }, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.region, tt.args.areaId, tt.args.networkAreaLabel, tt.args.regionalArea); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/region/list/list.go b/internal/cmd/network-area/region/list/list.go new file mode 100644 index 000000000..44149c9af --- /dev/null +++ b/internal/cmd/network-area/region/list/list.go @@ -0,0 +1,153 @@ +package list + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all configured regions for a STACKIT Network Area (SNA)", + Long: "Lists all configured regions for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all configured region for a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + `$ stackit network-area region list --network-area-id xxx --organization-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 + } + + // Get network area label + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaLabel = model.NetworkAreaId + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list network area region: %w", err) + } + + if resp == nil { + return fmt.Errorf("empty response from API") + } + + return outputResult(params.Printer, model.OutputFormat, networkAreaLabel, *resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNetworkAreaRegionsRequest { + return apiClient.ListNetworkAreaRegions(ctx, model.OrganizationId, model.NetworkAreaId) +} + +func outputResult(p *print.Printer, outputFormat, areaLabel string, regionalArea iaas.RegionalAreaListResponse) error { + return p.OutputResult(outputFormat, regionalArea, func() error { + if regionalArea.Regions == nil || len(*regionalArea.Regions) == 0 { + p.Outputf("No regions found for network area %q\n", areaLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("REGION", "STATUS", "DNS NAME SERVERS", "NETWORK RANGES", "TRANSFER NETWORK") + for region, regionConfig := range *regionalArea.Regions { + var dnsNames string + var networkRanges []string + var transferNetwork string + + if ipv4 := regionConfig.Ipv4; ipv4 != nil { + // Set dnsNames + dnsNames = utils.JoinStringPtr(ipv4.DefaultNameservers, ",") + + // Set networkRanges + if ipv4.NetworkRanges != nil && len(*ipv4.NetworkRanges) > 0 { + for _, networkRange := range *ipv4.NetworkRanges { + if networkRange.Prefix != nil { + networkRanges = append(networkRanges, *networkRange.Prefix) + } + } + } + + // Set transferNetwork + transferNetwork = utils.PtrString(ipv4.TransferNetwork) + } + + table.AddRow( + region, + utils.PtrString(regionConfig.Status), + dnsNames, + strings.Join(networkRanges, ","), + transferNetwork, + ) + } + + if err := table.Display(p); err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/network-area/region/list/list_test.go b/internal/cmd/network-area/region/list/list_test.go new file mode 100644 index 000000000..bb7b3f15b --- /dev/null +++ b/internal/cmd/network-area/region/list/list_test.go @@ -0,0 +1,221 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreaRegionsRequest)) iaas.ApiListNetworkAreaRegionsRequest { + request := testClient.ListNetworkAreaRegions(testCtx, testOrgId, testAreaId) + 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: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "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.ApiListNetworkAreaRegionsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + networkAreaLabel string + regionalArea iaas.RegionalAreaListResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + regionalArea: iaas.RegionalAreaListResponse{}, + }, + wantErr: false, + }, + { + name: "set nil for regions map in response", + args: args{ + regionalArea: iaas.RegionalAreaListResponse{ + Regions: nil, + }, + }, + wantErr: false, + }, + { + name: "set empty map for regions map in response", + args: args{ + regionalArea: iaas.RegionalAreaListResponse{ + Regions: utils.Ptr(map[string]iaas.RegionalArea{}), + }, + }, + wantErr: false, + }, + { + name: "set empty region in response", + args: args{ + regionalArea: iaas.RegionalAreaListResponse{ + Regions: utils.Ptr(map[string]iaas.RegionalArea{ + "eu01": {}, + }), + }, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.regionalArea); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/region/region.go b/internal/cmd/network-area/region/region.go new file mode 100644 index 000000000..99edcace2 --- /dev/null +++ b/internal/cmd/network-area/region/region.go @@ -0,0 +1,34 @@ +package region + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/update" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "region", + Short: "Provides functionality for regional configuration of STACKIT Network Area (SNA)", + Long: "Provides functionality for regional configuration of STACKIT Network Area (SNA).", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/network-area/region/update/update.go b/internal/cmd/network-area/region/update/update.go new file mode 100644 index 000000000..9018cb175 --- /dev/null +++ b/internal/cmd/network-area/region/update/update.go @@ -0,0 +1,165 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + ipv4DefaultNameservers = "ipv4-default-nameservers" + ipv4DefaultPrefixLengthFlag = "ipv4-default-prefix-length" + ipv4MaxPrefixLengthFlag = "ipv4-max-prefix-length" + ipv4MinPrefixLengthFlag = "ipv4-min-prefix-length" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string + + IPv4DefaultNameservers *[]string + IPv4DefaultPrefixLength *int64 + IPv4MaxPrefixLength *int64 + IPv4MinPrefixLength *int64 +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Updates a existing regional configuration for a STACKIT Network Area (SNA)", + Long: "Updates a existing regional configuration for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Update a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" with new ipv4-default-nameservers "8.8.8.8"`, + `$ stackit network-area region update --network-area-id xxx --region eu02 --organization-id yyy --ipv4-default-nameservers 8.8.8.8`, + ), + examples.NewExample( + `Update a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" with new ipv4-default-nameservers "8.8.8.8", using the set region config`, + `$ stackit config set --region eu02`, + `$ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-default-nameservers 8.8.8.8`, + ), + examples.NewExample( + `Update a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20"`, + `$ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20`, + ), + examples.NewExample( + `Update a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20"`, + `$ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20`, + ), + ), + 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 network area label + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaLabel = model.NetworkAreaId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update network area region: %w", err) + } + + if resp == nil || resp.Ipv4 == nil { + return fmt.Errorf("empty response from API") + } + + return outputResult(params.Printer, model.OutputFormat, model.Region, networkAreaLabel, *resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().StringSlice(ipv4DefaultNameservers, nil, "List of default DNS name server IPs") + cmd.Flags().Int64(ipv4DefaultPrefixLengthFlag, 0, "The default prefix length for networks in the network area") + cmd.Flags().Int64(ipv4MaxPrefixLengthFlag, 0, "The maximum prefix length for networks in the network area") + cmd.Flags().Int64(ipv4MinPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area") + + // At least one of the flags is required, otherwise there is nothing to update + cmd.MarkFlagsOneRequired(ipv4DefaultNameservers, ipv4MaxPrefixLengthFlag, ipv4MinPrefixLengthFlag, ipv4DefaultPrefixLengthFlag) + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + IPv4DefaultNameservers: flags.FlagToStringSlicePointer(p, cmd, ipv4DefaultNameservers), + IPv4DefaultPrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4DefaultPrefixLengthFlag), + IPv4MaxPrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4MaxPrefixLengthFlag), + IPv4MinPrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4MinPrefixLengthFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateNetworkAreaRegionRequest { + req := apiClient.UpdateNetworkAreaRegion(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) + + payload := iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: model.IPv4DefaultNameservers, + DefaultPrefixLen: model.IPv4DefaultPrefixLength, + MaxPrefixLen: model.IPv4MaxPrefixLength, + MinPrefixLen: model.IPv4MinPrefixLength, + }, + } + return req.UpdateNetworkAreaRegionPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat, region, networkAreaLabel string, regionalArea iaas.RegionalArea) error { + return p.OutputResult(outputFormat, regionalArea, func() error { + p.Outputf("Updated region configuration for SNA %q.\nRegion: %s\n", networkAreaLabel, region) + return nil + }) +} diff --git a/internal/cmd/network-area/region/update/update_test.go b/internal/cmd/network-area/region/update/update_test.go new file mode 100644 index 000000000..73482555d --- /dev/null +++ b/internal/cmd/network-area/region/update/update_test.go @@ -0,0 +1,265 @@ +package update + +import ( + "context" + "strconv" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" + testDefaultPrefixLength int64 = 25 + testMaxPrefixLength int64 = 29 + testMinPrefixLength int64 = 24 +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() + testDefaultNameservers = []string{"8.8.8.8", "8.8.4.4"} + testNetworkRanges = []string{"192.168.0.0/24", "10.0.0.0/24"} +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + ipv4DefaultNameservers: strings.Join(testDefaultNameservers, ","), + ipv4DefaultPrefixLengthFlag: strconv.FormatInt(testDefaultPrefixLength, 10), + ipv4MaxPrefixLengthFlag: strconv.FormatInt(testMaxPrefixLength, 10), + ipv4MinPrefixLengthFlag: strconv.FormatInt(testMinPrefixLength, 10), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + IPv4DefaultNameservers: utils.Ptr(testDefaultNameservers), + IPv4DefaultPrefixLength: utils.Ptr(testDefaultPrefixLength), + IPv4MaxPrefixLength: utils.Ptr(testMaxPrefixLength), + IPv4MinPrefixLength: utils.Ptr(testMinPrefixLength), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiUpdateNetworkAreaRegionRequest)) iaas.ApiUpdateNetworkAreaRegionRequest { + request := testClient.UpdateNetworkAreaRegion(testCtx, testOrgId, testAreaId, testRegion) + request = request.UpdateNetworkAreaRegionPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.UpdateNetworkAreaRegionPayload)) iaas.UpdateNetworkAreaRegionPayload { + var networkRange []iaas.NetworkRange + if len(testNetworkRanges) > 0 { + networkRange = make([]iaas.NetworkRange, len(testNetworkRanges)) + for i := range testNetworkRanges { + networkRange[i] = iaas.NetworkRange{ + Prefix: utils.Ptr(testNetworkRanges[i]), + } + } + } + + payload := iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: utils.Ptr(testDefaultNameservers), + DefaultPrefixLen: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLen: utils.Ptr(testMaxPrefixLength), + MinPrefixLen: utils.Ptr(testMinPrefixLength), + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "no update data is set", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipv4DefaultPrefixLengthFlag) + delete(flagValues, ipv4MaxPrefixLengthFlag) + delete(flagValues, ipv4MinPrefixLengthFlag) + delete(flagValues, ipv4DefaultNameservers) + }), + isValid: false, + }, + { + description: "region empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.RegionFlag] = "" + }), + 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.ApiUpdateNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + region string + networkAreaLabel string + regionalArea iaas.RegionalArea + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty regional area", + args: args{ + regionalArea: iaas.RegionalArea{}, + }, + wantErr: false, + }, + { + name: "output json", + args: args{ + outputFormat: print.JSONOutputFormat, + regionalArea: iaas.RegionalArea{}, + }, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.region, tt.args.networkAreaLabel, tt.args.regionalArea); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/route/create/create.go b/internal/cmd/network-area/route/create/create.go index f8548316c..c8950144e 100644 --- a/internal/cmd/network-area/route/create/create.go +++ b/internal/cmd/network-area/route/create/create.go @@ -3,6 +3,8 @@ package create import ( "context" "fmt" + "net" + "os" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -21,18 +23,39 @@ import ( const ( organizationIdFlag = "organization-id" networkAreaIdFlag = "network-area-id" - prefixFlag = "prefix" - nexthopFlag = "next-hop" - labelFlag = "labels" + // Deprecated: prefixFlag is deprecated and will be removed after April 2026. Use instead destinationFlag + prefixFlag = "prefix" + destinationFlag = "destination" + // Deprecated: nexthopFlag is deprecated and will be removed after April 2026. Use instead nexthopIPv4Flag or nexthopIPv6Flag + nexthopFlag = "next-hop" + nexthopIPv4Flag = "next-hop-ipv4" + nexthopIPv6Flag = "next-hop-ipv6" + nexthopBlackholeFlag = "nexthop-blackhole" + nexthopInternetFlag = "nexthop-internet" + labelFlag = "labels" +) + +const ( + destinationCIDRv4Type = "cidrv4" + destinationCIDRv6Type = "cidrv6" + + nexthopBlackholeType = "blackhole" + nexthopInternetType = "internet" + nexthopIPv4Type = "ipv4" + nexthopIPv6Type = "ipv6" ) type inputModel struct { *globalflags.GlobalFlagModel - OrganizationId *string - NetworkAreaId *string - Prefix *string - Nexthop *string - Labels *map[string]string + OrganizationId *string + NetworkAreaId *string + DestinationV4 *string + DestinationV6 *string + NexthopV4 *string + NexthopV6 *string + NexthopBlackhole *bool + NexthopInternet *bool + Labels *map[string]string } func NewCmd(params *params.CmdParams) *cobra.Command { @@ -46,12 +69,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Create a static route with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, - "$ stackit network-area route create --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1", + `Create a static route with destination "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + "$ stackit network-area route create --organization-id yyy --network-area-id xxx --destination 1.1.1.0/24 --next-hop 1.1.1.1", ), examples.NewExample( - `Create a static route with labels "key:value" and "foo:bar" with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, - "$ stackit network-area route create --labels key=value,foo=bar --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1", + `Create a static route with labels "key:value" and "foo:bar" with destination "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + "$ stackit network-area route create --labels key=value,foo=bar --organization-id yyy --network-area-id xxx --destination 1.1.1.0/24 --next-hop 1.1.1.1", ), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -93,7 +116,27 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("empty response from API") } - route, err := iaasUtils.GetRouteFromAPIResponse(*model.Prefix, *model.Nexthop, resp.Items) + var destination string + var nexthop string + if model.DestinationV4 != nil { + destination = *model.DestinationV4 + } else if model.DestinationV6 != nil { + destination = *model.DestinationV6 + } + + if model.NexthopV4 != nil { + nexthop = *model.NexthopV4 + } else if model.NexthopV6 != nil { + nexthop = *model.NexthopV6 + } else if model.NexthopBlackhole != nil { + // For nexthopBlackhole the type is assigned to nexthop, because it doesn't have any value + nexthop = nexthopBlackholeType + } else if model.NexthopInternet != nil { + // For nexthopInternet the type is assigned to nexthop, because it doesn't have any value + nexthop = nexthopInternetType + } + + route, err := iaasUtils.GetRouteFromAPIResponse(destination, nexthop, resp.Items) if err != nil { return err } @@ -109,23 +152,83 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area ID") cmd.Flags().Var(flags.CIDRFlag(), prefixFlag, "Static route prefix") - cmd.Flags().String(nexthopFlag, "", "Next hop IP address. Must be a valid IPv4") + cmd.Flags().Var(flags.CIDRFlag(), destinationFlag, "Destination route. Must be a valid IPv4 or IPv6 CIDR") + cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels") + cmd.Flags().String(nexthopFlag, "", "Next hop IP address. Must be a valid IPv4") + cmd.Flags().String(nexthopIPv4Flag, "", "Next hop IPv4 address") + cmd.Flags().String(nexthopIPv6Flag, "", "Next hop IPv6 address") + cmd.Flags().Bool(nexthopBlackholeFlag, false, "Sets next hop to black hole") + cmd.Flags().Bool(nexthopInternetFlag, false, "Sets next hop to internet") - err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, prefixFlag, nexthopFlag) + cobra.CheckErr(cmd.Flags().MarkDeprecated(nexthopFlag, fmt.Sprintf("The flag %q is deprecated and will be removed after April 2026. Use instead %q to configure a IPv4 next hop.", nexthopFlag, nexthopBlackholeFlag))) + cobra.CheckErr(cmd.Flags().MarkDeprecated(prefixFlag, fmt.Sprintf("The flag %q is deprecated and will be removed after April 2026. Use instead %q to configure a destination.", prefixFlag, destinationFlag))) + // Set the output for deprecation warnings to stderr + cmd.Flags().SetOutput(os.Stderr) + + destinationFlags := []string{prefixFlag, destinationFlag} + nexthopFlags := []string{nexthopFlag, nexthopIPv4Flag, nexthopIPv6Flag, nexthopBlackholeFlag, nexthopInternetFlag} + cmd.MarkFlagsMutuallyExclusive(destinationFlags...) + cmd.MarkFlagsMutuallyExclusive(nexthopFlags...) + + cmd.MarkFlagsOneRequired(destinationFlags...) + cmd.MarkFlagsOneRequired(nexthopFlags...) + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) cobra.CheckErr(err) } +func parseDestination(input string) (destinationV4, destinationV6 *string, err error) { + ip, _, err := net.ParseCIDR(input) + if err != nil { + return nil, nil, fmt.Errorf("parse CIDR: %w", err) + } + if ip.To4() != nil { // CIDR is IPv4 + destinationV4 = utils.Ptr(input) + return destinationV4, nil, nil + } + // CIDR is IPv6 + destinationV6 = utils.Ptr(input) + return nil, destinationV6, nil +} + func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) + var destinationV4, destinationV6 *string + if destination := flags.FlagToStringPointer(p, cmd, destinationFlag); destination != nil { + var err error + destinationV4, destinationV6, err = parseDestination(*destination) + if err != nil { + return nil, err + } + } + if prefix := flags.FlagToStringPointer(p, cmd, prefixFlag); prefix != nil { + var err error + destinationV4, destinationV6, err = parseDestination(*prefix) + if err != nil { + return nil, err + } + } + + nexthopIPv4 := flags.FlagToStringPointer(p, cmd, nexthopIPv4Flag) + nexthopIPv6 := flags.FlagToStringPointer(p, cmd, nexthopIPv6Flag) + nexthopInternet := flags.FlagToBoolPointer(p, cmd, nexthopInternetFlag) + nexthopBlackhole := flags.FlagToBoolPointer(p, cmd, nexthopBlackholeFlag) + if nexthop := flags.FlagToStringPointer(p, cmd, nexthopFlag); nexthop != nil { + nexthopIPv4 = nexthop + } + model := inputModel{ - GlobalFlagModel: globalFlags, - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), - Prefix: flags.FlagToStringPointer(p, cmd, prefixFlag), - Nexthop: flags.FlagToStringPointer(p, cmd, nexthopFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + GlobalFlagModel: globalFlags, + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + DestinationV4: destinationV4, + DestinationV6: destinationV6, + NexthopV4: nexthopIPv4, + NexthopV6: nexthopIPv6, + NexthopBlackhole: nexthopBlackhole, + NexthopInternet: nexthopInternet, + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), } p.DebugInputModel(model) @@ -133,14 +236,62 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateNetworkAreaRouteRequest { - req := apiClient.CreateNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId) + req := apiClient.CreateNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region) + + var destinationV4 *iaas.DestinationCIDRv4 + var destinationV6 *iaas.DestinationCIDRv6 + if model.DestinationV4 != nil { + destinationV4 = &iaas.DestinationCIDRv4{ + Type: utils.Ptr(destinationCIDRv4Type), + Value: model.DestinationV4, + } + } + if model.DestinationV6 != nil { + destinationV6 = &iaas.DestinationCIDRv6{ + Type: utils.Ptr(destinationCIDRv6Type), + Value: model.DestinationV6, + } + } + + var nexthopIPv4 *iaas.NexthopIPv4 + var nexthopIPv6 *iaas.NexthopIPv6 + var nexthopBlackhole *iaas.NexthopBlackhole + var nexthopInternet *iaas.NexthopInternet + + if model.NexthopV4 != nil { + nexthopIPv4 = &iaas.NexthopIPv4{ + Type: utils.Ptr(nexthopIPv4Type), + Value: model.NexthopV4, + } + } else if model.NexthopV6 != nil { + nexthopIPv6 = &iaas.NexthopIPv6{ + Type: utils.Ptr(nexthopIPv6Type), + Value: model.NexthopV6, + } + } else if model.NexthopBlackhole != nil { + nexthopBlackhole = &iaas.NexthopBlackhole{ + Type: utils.Ptr(nexthopBlackholeType), + } + } else if model.NexthopInternet != nil { + nexthopInternet = &iaas.NexthopInternet{ + Type: utils.Ptr(nexthopInternetType), + } + } payload := iaas.CreateNetworkAreaRoutePayload{ - Ipv4: &[]iaas.Route{ + Items: &[]iaas.Route{ { - Prefix: model.Prefix, - Nexthop: model.Nexthop, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: destinationV4, + DestinationCIDRv6: destinationV6, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: nexthopIPv4, + NexthopIPv6: nexthopIPv6, + NexthopBlackhole: nexthopBlackhole, + NexthopInternet: nexthopInternet, + }, + Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), }, }, } @@ -149,7 +300,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, route iaas.Route) error { return p.OutputResult(outputFormat, route, func() error { - p.Outputf("Created static route for SNA %q.\nStatic route ID: %s\n", networkAreaLabel, utils.PtrString(route.RouteId)) + p.Outputf("Created static route for SNA %q.\nStatic route ID: %s\n", networkAreaLabel, utils.PtrString(route.Id)) return nil }) } diff --git a/internal/cmd/network-area/route/create/create_test.go b/internal/cmd/network-area/route/create/create_test.go index 1bc7cb161..b3d577b80 100644 --- a/internal/cmd/network-area/route/create/create_test.go +++ b/internal/cmd/network-area/route/create/create_test.go @@ -16,6 +16,12 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) +const ( + testRegion = "eu01" + testDestinationCIDRv4 = "1.1.1.0/24" + testNexthopIPv4 = "1.1.1.1" +) + type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -26,10 +32,12 @@ var testNetworkAreaId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, - prefixFlag: "1.1.1.0/24", - nexthopFlag: "1.1.1.1", + destinationFlag: testDestinationCIDRv4, + nexthopIPv4Flag: testNexthopIPv4, } for _, mod := range mods { mod(flagValues) @@ -41,11 +49,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), - Prefix: utils.Ptr("1.1.1.0/24"), - Nexthop: utils.Ptr("1.1.1.1"), + DestinationV4: utils.Ptr(testDestinationCIDRv4), + NexthopV4: utils.Ptr(testNexthopIPv4), } for _, mod := range mods { mod(model) @@ -54,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRouteRequest)) iaas.ApiCreateNetworkAreaRouteRequest { - request := testClient.CreateNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId) + request := testClient.CreateNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRegion) request = request.CreateNetworkAreaRoutePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -64,10 +73,20 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRouteRequest) func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaRoutePayload)) iaas.CreateNetworkAreaRoutePayload { payload := iaas.CreateNetworkAreaRoutePayload{ - Ipv4: &[]iaas.Route{ + Items: &[]iaas.Route{ { - Prefix: utils.Ptr("1.1.1.0/24"), - Nexthop: utils.Ptr("1.1.1.1"), + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: utils.Ptr(destinationCIDRv4Type), + Value: utils.Ptr(testDestinationCIDRv4), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: utils.Ptr(nexthopIPv4Type), + Value: utils.Ptr(testNexthopIPv4), + }, + }, }, }, } @@ -96,7 +115,7 @@ func TestParseInput(t *testing.T) { { description: "next hop missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, nexthopFlag) + delete(flagValues, nexthopIPv4Flag) }), isValid: false, }, @@ -148,23 +167,23 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "prefix missing", + description: "destination missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, prefixFlag) + delete(flagValues, destinationFlag) }), isValid: false, }, { - description: "prefix invalid 1", + description: "destinationFlag invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[prefixFlag] = "" + flagValues[destinationFlag] = "" }), isValid: false, }, { - description: "prefix invalid 2", + description: "destinationFlag invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[prefixFlag] = "invalid-prefix" + flagValues[destinationFlag] = "invalid-destinationFlag" }), isValid: false, }, @@ -178,6 +197,23 @@ func TestParseInput(t *testing.T) { }), isValid: true, }, + { + description: "conflicting destination and prefix set", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[prefixFlag] = testDestinationCIDRv4 + }), + isValid: false, + }, + { + description: "conflicting nexthop and nexthop-ipv4 set", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nexthopFlag] = testNexthopIPv4 + }), + isValid: false, + }, + { + description: "conflicting nexthop and nexthop-ipv4 set", + }, } for _, tt := range tests { @@ -205,7 +241,7 @@ func TestBuildRequest(t *testing.T) { }), expectedRequest: fixtureRequest(func(request *iaas.ApiCreateNetworkAreaRouteRequest) { *request = (*request).CreateNetworkAreaRoutePayload(fixturePayload(func(payload *iaas.CreateNetworkAreaRoutePayload) { - (*payload.Ipv4)[0].Labels = utils.Ptr(map[string]interface{}{"key": "value"}) + (*payload.Items)[0].Labels = utils.Ptr(map[string]interface{}{"key": "value"}) })) }), }, diff --git a/internal/cmd/network-area/route/delete/delete.go b/internal/cmd/network-area/route/delete/delete.go index 254e7fb7a..b888fb6d5 100644 --- a/internal/cmd/network-area/route/delete/delete.go +++ b/internal/cmd/network-area/route/delete/delete.go @@ -110,6 +110,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteNetworkAreaRouteRequest { - req := apiClient.DeleteNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.RouteId) + req := apiClient.DeleteNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region, model.RouteId) return req } diff --git a/internal/cmd/network-area/route/delete/delete_test.go b/internal/cmd/network-area/route/delete/delete_test.go index 0358eba8c..d34c268a7 100644 --- a/internal/cmd/network-area/route/delete/delete_test.go +++ b/internal/cmd/network-area/route/delete/delete_test.go @@ -15,6 +15,10 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) +const ( + testRegion = "eu01" +) + type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -36,6 +40,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, } @@ -49,6 +55,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), @@ -61,7 +68,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkAreaRouteRequest)) iaas.ApiDeleteNetworkAreaRouteRequest { - request := testClient.DeleteNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRouteId) + request := testClient.DeleteNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRegion, testRouteId) for _, mod := range mods { mod(&request) } diff --git a/internal/cmd/network-area/route/describe/describe.go b/internal/cmd/network-area/route/describe/describe.go index 2e54ac4c8..b650e5964 100644 --- a/internal/cmd/network-area/route/describe/describe.go +++ b/internal/cmd/network-area/route/describe/describe.go @@ -100,18 +100,47 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetNetworkAreaRouteRequest { - req := apiClient.GetNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.RouteId) + req := apiClient.GetNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region, model.RouteId) return req } func outputResult(p *print.Printer, outputFormat string, route iaas.Route) error { return p.OutputResult(outputFormat, route, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(route.RouteId)) + table.AddRow("ID", utils.PtrString(route.Id)) table.AddSeparator() - table.AddRow("PREFIX", utils.PtrString(route.Prefix)) - table.AddSeparator() - table.AddRow("NEXTHOP", utils.PtrString(route.Nexthop)) + if destination := route.Destination; destination != nil { + if destination.DestinationCIDRv4 != nil { + table.AddRow("DESTINATION TYPE", utils.PtrString(destination.DestinationCIDRv4.Type)) + table.AddSeparator() + table.AddRow("DESTINATION", utils.PtrString(destination.DestinationCIDRv4.Value)) + table.AddSeparator() + } else if destination.DestinationCIDRv6 != nil { + table.AddRow("DESTINATION TYPE", utils.PtrString(destination.DestinationCIDRv6.Type)) + table.AddSeparator() + table.AddRow("DESTINATION", utils.PtrString(destination.DestinationCIDRv6.Value)) + table.AddSeparator() + } + } + if nexthop := route.Nexthop; nexthop != nil { + if nexthop.NexthopIPv4 != nil { + table.AddRow("NEXTHOP", utils.PtrString(nexthop.NexthopIPv4.Value)) + table.AddSeparator() + table.AddRow("NEXTHOP TYPE", utils.PtrString(nexthop.NexthopIPv4.Type)) + table.AddSeparator() + } else if nexthop.NexthopIPv6 != nil { + table.AddRow("NEXTHOP", utils.PtrString(nexthop.NexthopIPv6.Value)) + table.AddSeparator() + table.AddRow("NEXTHOP TYPE", utils.PtrString(nexthop.NexthopIPv6.Type)) + table.AddSeparator() + } else if nexthop.NexthopBlackhole != nil { + table.AddRow("NEXTHOP TYPE", utils.PtrString(nexthop.NexthopBlackhole.Type)) + table.AddSeparator() + } else if nexthop.NexthopInternet != nil { + table.AddRow("NEXTHOP TYPE", utils.PtrString(nexthop.NexthopInternet.Type)) + table.AddSeparator() + } + } if route.Labels != nil && len(*route.Labels) > 0 { labels := []string{} for key, value := range *route.Labels { diff --git a/internal/cmd/network-area/route/describe/describe_test.go b/internal/cmd/network-area/route/describe/describe_test.go index 0c674de81..67345ccae 100644 --- a/internal/cmd/network-area/route/describe/describe_test.go +++ b/internal/cmd/network-area/route/describe/describe_test.go @@ -15,6 +15,10 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) +const ( + testRegion = "eu01" +) + type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -36,6 +40,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, } @@ -49,6 +55,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), @@ -61,7 +68,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkAreaRouteRequest)) iaas.ApiGetNetworkAreaRouteRequest { - request := testClient.GetNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRouteId) + request := testClient.GetNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRegion, testRouteId) for _, mod := range mods { mod(&request) } diff --git a/internal/cmd/network-area/route/list/list.go b/internal/cmd/network-area/route/list/list.go index 2af24763a..9ea4f6159 100644 --- a/internal/cmd/network-area/route/list/list.go +++ b/internal/cmd/network-area/route/list/list.go @@ -127,19 +127,45 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNetworkAreaRoutesRequest { - return apiClient.ListNetworkAreaRoutes(ctx, *model.OrganizationId, *model.NetworkAreaId) + return apiClient.ListNetworkAreaRoutes(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region) } func outputResult(p *print.Printer, outputFormat string, routes []iaas.Route) error { return p.OutputResult(outputFormat, routes, func() error { table := tables.NewTable() - table.SetHeader("Static Route ID", "Next Hop", "Prefix") + table.SetHeader("Static Route ID", "Next Hop", "Next Hop Type", "Destination") for _, route := range routes { + var nextHop string + var nextHopType string + var destination string + if routeDest := route.Destination; routeDest != nil { + if routeDest.DestinationCIDRv4 != nil { + destination = *routeDest.DestinationCIDRv4.Value + } + if routeDest.DestinationCIDRv6 != nil { + destination = *routeDest.DestinationCIDRv6.Value + } + } + if routeNexthop := route.Nexthop; routeNexthop != nil { + if routeNexthop.NexthopIPv4 != nil { + nextHop = *routeNexthop.NexthopIPv4.Value + nextHopType = *routeNexthop.NexthopIPv4.Type + } else if routeNexthop.NexthopIPv6 != nil { + nextHop = *routeNexthop.NexthopIPv6.Value + nextHopType = *routeNexthop.NexthopIPv6.Type + } else if routeNexthop.NexthopBlackhole != nil { + nextHopType = *routeNexthop.NexthopBlackhole.Type + } else if routeNexthop.NexthopInternet != nil { + nextHopType = *routeNexthop.NexthopInternet.Type + } + } + table.AddRow( - utils.PtrString(route.RouteId), - utils.PtrString(route.Nexthop), - utils.PtrString(route.Prefix), + utils.PtrString(route.Id), + nextHop, + nextHopType, + destination, ) } diff --git a/internal/cmd/network-area/route/list/list_test.go b/internal/cmd/network-area/route/list/list_test.go index 8681aa87d..365249370 100644 --- a/internal/cmd/network-area/route/list/list_test.go +++ b/internal/cmd/network-area/route/list/list_test.go @@ -16,6 +16,10 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) +const ( + testRegion = "eu01" +) + type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -25,6 +29,8 @@ var testNetworkAreaId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrganizationId, networkAreaIdFlag: testNetworkAreaId, limitFlag: "10", @@ -39,6 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: &testOrganizationId, NetworkAreaId: &testNetworkAreaId, @@ -51,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreaRoutesRequest)) iaas.ApiListNetworkAreaRoutesRequest { - request := testClient.ListNetworkAreaRoutes(testCtx, testOrganizationId, testNetworkAreaId) + request := testClient.ListNetworkAreaRoutes(testCtx, testOrganizationId, testNetworkAreaId, testRegion) for _, mod := range mods { mod(&request) } @@ -204,6 +211,24 @@ func TestOutputResult(t *testing.T) { }, wantErr: false, }, + { + name: "empty destination in route", + args: args{ + routes: []iaas.Route{{ + Destination: &iaas.RouteDestination{}, + }}, + }, + wantErr: false, + }, + { + name: "empty nexthop in route", + args: args{ + routes: []iaas.Route{{ + Nexthop: &iaas.RouteNexthop{}, + }}, + }, + wantErr: false, + }, } p := print.NewPrinter() p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) diff --git a/internal/cmd/network-area/route/update/update.go b/internal/cmd/network-area/route/update/update.go index 23e6391ff..92ca244a9 100644 --- a/internal/cmd/network-area/route/update/update.go +++ b/internal/cmd/network-area/route/update/update.go @@ -116,7 +116,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateNetworkAreaRouteRequest { - req := apiClient.UpdateNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.RouteId) + req := apiClient.UpdateNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region, model.RouteId) payload := iaas.UpdateNetworkAreaRoutePayload{ Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), @@ -128,7 +128,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, route iaas.Route) error { return p.OutputResult(outputFormat, route, func() error { - p.Outputf("Updated static route for SNA %q.\nStatic route ID: %s\n", networkAreaLabel, utils.PtrString(route.RouteId)) + p.Outputf("Updated static route for SNA %q.\nStatic route ID: %s\n", networkAreaLabel, utils.PtrString(route.Id)) return nil }) } diff --git a/internal/cmd/network-area/route/update/update_test.go b/internal/cmd/network-area/route/update/update_test.go index 03bdf6da2..da6c6e03a 100644 --- a/internal/cmd/network-area/route/update/update_test.go +++ b/internal/cmd/network-area/route/update/update_test.go @@ -14,6 +14,10 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) +const ( + testRegion = "eu01" +) + type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -35,6 +39,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, labelFlag: "value=key", @@ -74,6 +80,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), @@ -87,7 +94,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdateNetworkAreaRouteRequest)) iaas.ApiUpdateNetworkAreaRouteRequest { - request := testClient.UpdateNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRouteId) + request := testClient.UpdateNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRegion, testRouteId) request = request.UpdateNetworkAreaRoutePayload(fixturePayload()) for _, mod := range mods { mod(&request) diff --git a/internal/cmd/network-area/update/update.go b/internal/cmd/network-area/update/update.go index 855da9f10..20f426d89 100644 --- a/internal/cmd/network-area/update/update.go +++ b/internal/cmd/network-area/update/update.go @@ -3,8 +3,8 @@ package update import ( "context" "fmt" - - rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" + "os" + "strings" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -13,6 +13,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" @@ -23,26 +24,43 @@ import ( const ( areaIdArg = "AREA_ID" - nameFlag = "name" - organizationIdFlag = "organization-id" - areaIdFlag = "area-id" - dnsNameServersFlag = "dns-name-servers" + nameFlag = "name" + organizationIdFlag = "organization-id" + areaIdFlag = "area-id" + // Deprecated: dnsNameServersFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + dnsNameServersFlag = "dns-name-servers" + // Deprecated: defaultPrefixLengthFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. defaultPrefixLengthFlag = "default-prefix-length" - maxPrefixLengthFlag = "max-prefix-length" - minPrefixLengthFlag = "min-prefix-length" - labelFlag = "labels" + // Deprecated: maxPrefixLengthFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + maxPrefixLengthFlag = "max-prefix-length" + // Deprecated: minPrefixLengthFlag is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + minPrefixLengthFlag = "min-prefix-length" + labelFlag = "labels" + + deprecationMessage = "Deprecated and will be removed after April 2026. Use instead the new command `$ stackit network-area region` to configure these options for a network area." ) +// NetworkAreaResponses is a workaround, to keep the two responses of the iaas v2 api together for the json and yaml output +// Should be removed when the deprecated flags are removed +type NetworkAreaResponses struct { + NetworkArea iaas.NetworkArea `json:"network_area"` + RegionalArea *iaas.RegionalArea `json:"regional_area"` +} + type inputModel struct { *globalflags.GlobalFlagModel - AreaId string - Name *string - OrganizationId *string - DnsNameServers *[]string + AreaId string + Name *string + OrganizationId *string + // Deprecated: DnsNameServers is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + DnsNameServers *[]string + // Deprecated: DefaultPrefixLength is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. DefaultPrefixLength *int64 - MaxPrefixLength *int64 - MinPrefixLength *int64 - Labels *map[string]string + // Deprecated: MaxPrefixLength is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + MaxPrefixLength *int64 + // Deprecated: MinPrefixLength is deprecated, because with iaas v2 the create endpoint for network area was separated, remove this after April 2026. + MinPrefixLength *int64 + Labels *map[string]string } func NewCmd(params *params.CmdParams) *cobra.Command { @@ -99,7 +117,26 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("update network area: %w", err) } - return outputResult(params.Printer, model.OutputFormat, orgLabel, *resp) + if resp == nil || resp.Id == nil { + return fmt.Errorf("update network area: empty response") + } + + responses := NetworkAreaResponses{ + NetworkArea: *resp, + } + + if hasDeprecatedFlagsSet(model) { + deprecatedFlags := getConfiguredDeprecatedFlags(model) + params.Printer.Warn("the flags %q are deprecated and will be removed after April 2026. Use `$ stackit network-area region` to configure these options for a network area.\n", strings.Join(deprecatedFlags, ",")) + reqNetworkArea := buildRequestNetworkAreaRegion(ctx, model, apiClient) + respNetworkArea, err := reqNetworkArea.Execute() + if err != nil { + return fmt.Errorf("create network area region: %w", err) + } + responses.RegionalArea = respNetworkArea + } + + return outputResult(params.Printer, model.OutputFormat, orgLabel, responses) }, } configureFlags(cmd) @@ -115,6 +152,13 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(minPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area") cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...'") + cobra.CheckErr(cmd.Flags().MarkDeprecated(dnsNameServersFlag, deprecationMessage)) + cobra.CheckErr(cmd.Flags().MarkDeprecated(defaultPrefixLengthFlag, deprecationMessage)) + cobra.CheckErr(cmd.Flags().MarkDeprecated(maxPrefixLengthFlag, deprecationMessage)) + cobra.CheckErr(cmd.Flags().MarkDeprecated(minPrefixLengthFlag, deprecationMessage)) + // Set the output for deprecation warnings to stderr + cmd.Flags().SetOutput(os.Stderr) + err := flags.MarkFlagsRequired(cmd, organizationIdFlag) cobra.CheckErr(err) } @@ -140,27 +184,67 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu return &model, nil } +func hasDeprecatedFlagsSet(model *inputModel) bool { + deprecatedFlags := getConfiguredDeprecatedFlags(model) + return len(deprecatedFlags) > 0 +} + +func getConfiguredDeprecatedFlags(model *inputModel) []string { + var result []string + if model.DnsNameServers != nil { + result = append(result, dnsNameServersFlag) + } + if model.DefaultPrefixLength != nil { + result = append(result, defaultPrefixLengthFlag) + } + if model.MaxPrefixLength != nil { + result = append(result, maxPrefixLengthFlag) + } + if model.MinPrefixLength != nil { + result = append(result, minPrefixLengthFlag) + } + return result +} + func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiPartialUpdateNetworkAreaRequest { req := apiClient.PartialUpdateNetworkArea(ctx, *model.OrganizationId, model.AreaId) payload := iaas.PartialUpdateNetworkAreaPayload{ Name: model.Name, Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), - AddressFamily: &iaas.UpdateAreaAddressFamily{ - Ipv4: &iaas.UpdateAreaIPv4{ - DefaultNameservers: model.DnsNameServers, - DefaultPrefixLen: model.DefaultPrefixLength, - MaxPrefixLen: model.MaxPrefixLength, - MinPrefixLen: model.MinPrefixLength, - }, - }, } return req.PartialUpdateNetworkAreaPayload(payload) } -func outputResult(p *print.Printer, outputFormat, projectLabel string, networkArea iaas.NetworkArea) error { - return p.OutputResult(outputFormat, networkArea, func() error { +func buildRequestNetworkAreaRegion(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateNetworkAreaRegionRequest { + req := apiClient.UpdateNetworkAreaRegion(ctx, *model.OrganizationId, model.AreaId, model.Region) + + payload := iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: model.DnsNameServers, + DefaultPrefixLen: model.DefaultPrefixLength, + MaxPrefixLen: model.MaxPrefixLength, + MinPrefixLen: model.MinPrefixLength, + }, + } + + return req.UpdateNetworkAreaRegionPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, responses NetworkAreaResponses) error { + prettyOutputFunc := func() error { + p.Outputf("Updated STACKIT Network Area for project %q.\n", projectLabel) + return nil + } + + // If RegionalArea is NOT set in the reponses, then no deprecated Flags were set. + // In this case, only the response of NetworkArea should be printed in JSON and yaml output, to avoid breaking changes after the deprecated fields are removed + if responses.RegionalArea == nil { + return p.OutputResult(outputFormat, responses.NetworkArea, prettyOutputFunc) + } + + return p.OutputResult(outputFormat, responses, func() error { p.Outputf("Updated STACKIT Network Area for project %q.\n", projectLabel) return nil }) diff --git a/internal/cmd/network-area/update/update_test.go b/internal/cmd/network-area/update/update_test.go index f25018286..b46b66aab 100644 --- a/internal/cmd/network-area/update/update_test.go +++ b/internal/cmd/network-area/update/update_test.go @@ -2,6 +2,8 @@ package update import ( "context" + "strconv" + "strings" "testing" "github.com/stackitcloud/stackit-cli/internal/cmd/params" @@ -15,13 +17,25 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) +const ( + testRegion = "eu01" + testName = "example-network-area-name" + testDefaultPrefixLength int64 = 25 + testMinPrefixLength int64 = 24 + testMaxPrefixLength int64 = 26 +) + type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &iaas.APIClient{} -var testOrgId = uuid.NewString() -var testAreaId = uuid.NewString() +var ( + testOrgId = uuid.NewString() + testAreaId = uuid.NewString() + + testDnsNameservers = []string{"1.1.1.0", "1.1.2.0"} +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -35,13 +49,11 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - nameFlag: "example-network-area-name", - organizationIdFlag: testOrgId, - dnsNameServersFlag: "1.1.1.0,1.1.2.0", - defaultPrefixLengthFlag: "24", - maxPrefixLengthFlag: "24", - minPrefixLengthFlag: "24", - labelFlag: "key=value", + globalflags.RegionFlag: testRegion, + + nameFlag: testName, + organizationIdFlag: testOrgId, + labelFlag: "key=value", } for _, mod := range mods { mod(flagValues) @@ -53,14 +65,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - Name: utils.Ptr("example-network-area-name"), - OrganizationId: utils.Ptr(testOrgId), - AreaId: testAreaId, - DnsNameServers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - DefaultPrefixLength: utils.Ptr(int64(24)), - MaxPrefixLength: utils.Ptr(int64(24)), - MinPrefixLength: utils.Ptr(int64(24)), + Name: utils.Ptr(testName), + OrganizationId: utils.Ptr(testOrgId), + AreaId: testAreaId, Labels: utils.Ptr(map[string]string{ "key": "value", }), @@ -82,17 +91,33 @@ func fixtureRequest(mods ...func(request *iaas.ApiPartialUpdateNetworkAreaReques func fixturePayload(mods ...func(payload *iaas.PartialUpdateNetworkAreaPayload)) iaas.PartialUpdateNetworkAreaPayload { payload := iaas.PartialUpdateNetworkAreaPayload{ - Name: utils.Ptr("example-network-area-name"), + Name: utils.Ptr(testName), Labels: utils.Ptr(map[string]interface{}{ "key": "value", }), - AddressFamily: &iaas.UpdateAreaAddressFamily{ - Ipv4: &iaas.UpdateAreaIPv4{ - DefaultNameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - DefaultPrefixLen: utils.Ptr(int64(24)), - MaxPrefixLen: utils.Ptr(int64(24)), - MinPrefixLen: utils.Ptr(int64(24)), - }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func fixtureRequestRegionalArea(mods ...func(request *iaas.ApiUpdateNetworkAreaRegionRequest)) iaas.ApiUpdateNetworkAreaRegionRequest { + request := testClient.UpdateNetworkAreaRegion(testCtx, testOrgId, testAreaId, testRegion) + request = request.UpdateNetworkAreaRegionPayload(fixturePayloadRegionalArea()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayloadRegionalArea(mods ...func(payload *iaas.UpdateNetworkAreaRegionPayload)) iaas.UpdateNetworkAreaRegionPayload { + payload := iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: utils.Ptr(testDnsNameservers), + DefaultPrefixLen: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLen: utils.Ptr(testMaxPrefixLength), + MinPrefixLen: utils.Ptr(testMinPrefixLength), }, } for _, mod := range mods { @@ -118,20 +143,20 @@ func TestParseInput(t *testing.T) { expectedModel: fixtureInputModel(), }, { - description: "required only", + description: "with deprecated flags", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, dnsNameServersFlag) - delete(flagValues, defaultPrefixLengthFlag) - delete(flagValues, maxPrefixLengthFlag) - delete(flagValues, minPrefixLengthFlag) + flagValues[dnsNameServersFlag] = strings.Join(testDnsNameservers, ",") + flagValues[defaultPrefixLengthFlag] = strconv.FormatInt(testDefaultPrefixLength, 10) + flagValues[maxPrefixLengthFlag] = strconv.FormatInt(testMaxPrefixLength, 10) + flagValues[minPrefixLengthFlag] = strconv.FormatInt(testMinPrefixLength, 10) }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.DnsNameServers = nil - model.DefaultPrefixLength = nil - model.MaxPrefixLength = nil - model.MinPrefixLength = nil + model.DnsNameServers = utils.Ptr(testDnsNameservers) + model.DefaultPrefixLength = utils.Ptr(testDefaultPrefixLength) + model.MaxPrefixLength = utils.Ptr(testMaxPrefixLength) + model.MinPrefixLength = utils.Ptr(testMinPrefixLength) }), }, @@ -286,11 +311,44 @@ func TestBuildRequest(t *testing.T) { } } +func TestBuildRequestNetworkAreaRegion(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiUpdateNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(func(model *inputModel) { + model.DnsNameServers = utils.Ptr(testDnsNameservers) + model.DefaultPrefixLength = utils.Ptr(testDefaultPrefixLength) + model.MaxPrefixLength = utils.Ptr(testMaxPrefixLength) + model.MinPrefixLength = utils.Ptr(testMinPrefixLength) + }), + expectedRequest: fixtureRequestRegionalArea(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequestNetworkAreaRegion(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + func TestOutputResult(t *testing.T) { type args struct { outputFormat string projectLabel string - networkArea iaas.NetworkArea + responses NetworkAreaResponses } tests := []struct { name string @@ -305,7 +363,10 @@ func TestOutputResult(t *testing.T) { { name: "empty network area", args: args{ - networkArea: iaas.NetworkArea{}, + responses: NetworkAreaResponses{ + NetworkArea: iaas.NetworkArea{}, + RegionalArea: nil, + }, }, wantErr: false, }, @@ -314,9 +375,132 @@ func TestOutputResult(t *testing.T) { p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.networkArea); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.responses); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) } } + +func TestGetConfiguredDeprecatedFlags(t *testing.T) { + type args struct { + model *inputModel + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "no deprecated flags", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Name: utils.Ptr(testName), + OrganizationId: utils.Ptr(testOrgId), + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + DnsNameServers: nil, + DefaultPrefixLength: nil, + MaxPrefixLength: nil, + MinPrefixLength: nil, + }, + }, + want: nil, + }, + { + name: "deprecated flags", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Name: utils.Ptr(testName), + OrganizationId: utils.Ptr(testOrgId), + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + DnsNameServers: utils.Ptr(testDnsNameservers), + DefaultPrefixLength: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLength: utils.Ptr(testMaxPrefixLength), + MinPrefixLength: utils.Ptr(testMinPrefixLength), + }, + }, + want: []string{dnsNameServersFlag, defaultPrefixLengthFlag, minPrefixLengthFlag, maxPrefixLengthFlag}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getConfiguredDeprecatedFlags(tt.args.model) + + less := func(a, b string) bool { + return a < b + } + if diff := cmp.Diff(tt.want, got, cmpopts.SortSlices(less)); diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestHasDeprecatedFlagsSet(t *testing.T) { + type args struct { + model *inputModel + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "no deprecated flags", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Name: utils.Ptr(testName), + OrganizationId: utils.Ptr(testOrgId), + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + DnsNameServers: nil, + DefaultPrefixLength: nil, + MaxPrefixLength: nil, + MinPrefixLength: nil, + }, + }, + want: false, + }, + { + name: "deprecated flags", + args: args{ + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Name: utils.Ptr(testName), + OrganizationId: utils.Ptr(testOrgId), + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + DnsNameServers: utils.Ptr(testDnsNameservers), + DefaultPrefixLength: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLength: utils.Ptr(testMaxPrefixLength), + MinPrefixLength: utils.Ptr(testMinPrefixLength), + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasDeprecatedFlagsSet(tt.args.model); got != tt.want { + t.Errorf("hasDeprecatedFlagsSet() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index 2678d7661..814929497 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -18,6 +18,16 @@ You can configure it for all commands by running: or you can also set it through the environment variable [STACKIT_PROJECT_ID]` + MISSING_REGION = `the region is not currently set. + +It can be set on the command level by re-running your command with the --region flag. + +You can configure it for all commands by running: + + $ stackit config set --region xxx + +or you can also set it through the environment variable [STACKIT_REGION]` + EMPTY_UPDATE = `please specify at least one field to update. Get details on the available flags by re-running your command with the --help flag.` @@ -240,6 +250,12 @@ func (e *ProjectIdError) Error() string { return MISSING_PROJECT_ID } +type RegionError struct{} + +func (e *RegionError) Error() string { + return MISSING_REGION +} + type EmptyUpdateError struct{} func (e *EmptyUpdateError) Error() string { diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go index 6c3229b87..b7973c265 100644 --- a/internal/pkg/services/iaas/utils/utils.go +++ b/internal/pkg/services/iaas/utils/utils.go @@ -129,10 +129,47 @@ func GetNetworkRangePrefix(ctx context.Context, apiClient IaaSClient, organizati // GetRouteFromAPIResponse returns the static route from the API response that matches the prefix and nexthop // This works because static routes are unique by prefix and nexthop -func GetRouteFromAPIResponse(prefix, nexthop string, routes *[]iaas.Route) (iaas.Route, error) { +func GetRouteFromAPIResponse(destination, nexthop string, routes *[]iaas.Route) (iaas.Route, error) { for _, route := range *routes { - if *route.Prefix == prefix && route.Nexthop.GetActualInstance() == nexthop { - return route, nil + // Check if destination matches + if dest := route.Destination; dest != nil { + match := false + if destV4 := dest.DestinationCIDRv4; destV4 != nil { + if destV4.Value != nil && *destV4.Value == destination { + match = true + } + } else if destV6 := dest.DestinationCIDRv6; destV6 != nil { + if destV6.Value != nil && *destV6.Value == destination { + match = true + } + } + if !match { + continue + } + } + // Check if nexthop matches + if routeNexthop := route.Nexthop; routeNexthop != nil { + match := false + if nexthopIPv4 := routeNexthop.NexthopIPv4; nexthopIPv4 != nil { + if nexthopIPv4.Value != nil && *nexthopIPv4.Value == nexthop { + match = true + } + } else if nexthopIPv6 := routeNexthop.NexthopIPv6; nexthopIPv6 != nil { + if nexthopIPv6.Value != nil && *nexthopIPv6.Value == nexthop { + match = true + } + } else if nexthopInternet := routeNexthop.NexthopInternet; nexthopInternet != nil { + if nexthopInternet.Type != nil && *nexthopInternet.Type == nexthop { + match = true + } + } else if nexthopBlackhole := routeNexthop.NexthopBlackhole; nexthopBlackhole != nil { + if nexthopBlackhole.Type != nil && *nexthopBlackhole.Type == nexthop { + match = true + } + } + if match { + return route, nil + } } } return iaas.Route{}, fmt.Errorf("new static route not found in API response") diff --git a/internal/pkg/services/iaas/utils/utils_test.go b/internal/pkg/services/iaas/utils/utils_test.go index e2ccdc469..a8f530533 100644 --- a/internal/pkg/services/iaas/utils/utils_test.go +++ b/internal/pkg/services/iaas/utils/utils_test.go @@ -10,6 +10,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) +var _ IaaSClient = &IaaSClientMocked{} + type IaaSClientMocked struct { GetSecurityGroupRuleFails bool GetSecurityGroupRuleResp *iaas.SecurityGroupRule @@ -39,49 +41,49 @@ type IaaSClientMocked struct { GetSnapshotResp *iaas.Snapshot } -func (m *IaaSClientMocked) GetAffinityGroupExecute(_ context.Context, _, _ string) (*iaas.AffinityGroup, error) { +func (m *IaaSClientMocked) GetAffinityGroupExecute(_ context.Context, _, _, _ string) (*iaas.AffinityGroup, error) { if m.GetAffinityGroupsFails { return nil, fmt.Errorf("could not get affinity groups") } return m.GetAffinityGroupResp, nil } -func (m *IaaSClientMocked) GetSecurityGroupRuleExecute(_ context.Context, _, _, _ string) (*iaas.SecurityGroupRule, error) { +func (m *IaaSClientMocked) GetSecurityGroupRuleExecute(_ context.Context, _, _, _, _ string) (*iaas.SecurityGroupRule, error) { if m.GetSecurityGroupRuleFails { return nil, fmt.Errorf("could not get security group rule") } return m.GetSecurityGroupRuleResp, nil } -func (m *IaaSClientMocked) GetSecurityGroupExecute(_ context.Context, _, _ string) (*iaas.SecurityGroup, error) { +func (m *IaaSClientMocked) GetSecurityGroupExecute(_ context.Context, _, _, _ string) (*iaas.SecurityGroup, error) { if m.GetSecurityGroupFails { return nil, fmt.Errorf("could not get security group") } return m.GetSecurityGroupResp, nil } -func (m *IaaSClientMocked) GetPublicIPExecute(_ context.Context, _, _ string) (*iaas.PublicIp, error) { +func (m *IaaSClientMocked) GetPublicIPExecute(_ context.Context, _, _, _ string) (*iaas.PublicIp, error) { if m.GetPublicIpFails { return nil, fmt.Errorf("could not get public ip") } return m.GetPublicIpResp, nil } -func (m *IaaSClientMocked) GetServerExecute(_ context.Context, _, _ string) (*iaas.Server, error) { +func (m *IaaSClientMocked) GetServerExecute(_ context.Context, _, _, _ string) (*iaas.Server, error) { if m.GetServerFails { return nil, fmt.Errorf("could not get server") } return m.GetServerResp, nil } -func (m *IaaSClientMocked) GetVolumeExecute(_ context.Context, _, _ string) (*iaas.Volume, error) { +func (m *IaaSClientMocked) GetVolumeExecute(_ context.Context, _, _, _ string) (*iaas.Volume, error) { if m.GetVolumeFails { return nil, fmt.Errorf("could not get volume") } return m.GetVolumeResp, nil } -func (m *IaaSClientMocked) GetNetworkExecute(_ context.Context, _, _ string) (*iaas.Network, error) { +func (m *IaaSClientMocked) GetNetworkExecute(_ context.Context, _, _, _ string) (*iaas.Network, error) { if m.GetNetworkFails { return nil, fmt.Errorf("could not get network") } @@ -102,28 +104,28 @@ func (m *IaaSClientMocked) ListNetworkAreaProjectsExecute(_ context.Context, _, return m.GetAttachedProjectsResp, nil } -func (m *IaaSClientMocked) GetNetworkAreaRangeExecute(_ context.Context, _, _, _ string) (*iaas.NetworkRange, error) { +func (m *IaaSClientMocked) GetNetworkAreaRangeExecute(_ context.Context, _, _, _, _ string) (*iaas.NetworkRange, error) { if m.GetNetworkAreaRangeFails { return nil, fmt.Errorf("could not get network range") } return m.GetNetworkAreaRangeResp, nil } -func (m *IaaSClientMocked) GetImageExecute(_ context.Context, _, _ string) (*iaas.Image, error) { +func (m *IaaSClientMocked) GetImageExecute(_ context.Context, _, _, _ string) (*iaas.Image, error) { if m.GetImageFails { return nil, fmt.Errorf("could not get image") } return m.GetImageResp, nil } -func (m *IaaSClientMocked) GetBackupExecute(_ context.Context, _, _ string) (*iaas.Backup, error) { +func (m *IaaSClientMocked) GetBackupExecute(_ context.Context, _, _, _ string) (*iaas.Backup, error) { if m.GetBackupFails { return nil, fmt.Errorf("could not get backup") } return m.GetBackupResp, nil } -func (m *IaaSClientMocked) GetSnapshotExecute(_ context.Context, _, _ string) (*iaas.Snapshot, error) { +func (m *IaaSClientMocked) GetSnapshotExecute(_ context.Context, _, _, _ string) (*iaas.Snapshot, error) { if m.GetSnapshotFails { return nil, fmt.Errorf("could not get snapshot") } @@ -164,7 +166,7 @@ func TestGetSecurityGroupRuleName(t *testing.T) { GetSecurityGroupRuleFails: tt.args.getInstanceFails, GetSecurityGroupRuleResp: tt.args.getInstanceResp, } - got, err := GetSecurityGroupRuleName(context.Background(), m, "", "", "") + got, err := GetSecurityGroupRuleName(context.Background(), m, "", "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetSecurityGroupRuleName() error = %v, wantErr %v", err, tt.wantErr) return @@ -230,7 +232,7 @@ func TestGetSecurityGroupName(t *testing.T) { GetSecurityGroupFails: tt.args.getInstanceFails, GetSecurityGroupResp: tt.args.getInstanceResp, } - got, err := GetSecurityGroupName(context.Background(), m, "", "") + got, err := GetSecurityGroupName(context.Background(), m, "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetSecurityGroupName() error = %v, wantErr %v", err, tt.wantErr) return @@ -279,7 +281,7 @@ func TestGetPublicIp(t *testing.T) { GetPublicIpFails: tt.args.getPublicIpFails, GetPublicIpResp: tt.args.getPublicIpResp, } - gotPublicIP, gotAssociatedResource, err := GetPublicIP(context.Background(), m, "", "") + gotPublicIP, gotAssociatedResource, err := GetPublicIP(context.Background(), m, "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetPublicIP() error = %v, wantErr %v", err, tt.wantErr) return @@ -328,7 +330,7 @@ func TestGetServerName(t *testing.T) { GetServerFails: tt.args.getInstanceFails, GetServerResp: tt.args.getInstanceResp, } - got, err := GetServerName(context.Background(), m, "", "") + got, err := GetServerName(context.Background(), m, "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetServerName() error = %v, wantErr %v", err, tt.wantErr) return @@ -394,7 +396,7 @@ func TestGetVolumeName(t *testing.T) { GetVolumeFails: tt.args.getInstanceFails, GetVolumeResp: tt.args.getInstanceResp, } - got, err := GetVolumeName(context.Background(), m, "", "") + got, err := GetVolumeName(context.Background(), m, "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetVolumeName() error = %v, wantErr %v", err, tt.wantErr) return @@ -460,7 +462,7 @@ func TestGetNetworkName(t *testing.T) { GetNetworkFails: tt.args.getInstanceFails, GetNetworkResp: tt.args.getInstanceResp, } - got, err := GetNetworkName(context.Background(), m, "", "") + got, err := GetNetworkName(context.Background(), m, "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetNetworkName() error = %v, wantErr %v", err, tt.wantErr) return @@ -619,7 +621,7 @@ func TestGetNetworkRangePrefix(t *testing.T) { GetNetworkAreaRangeFails: tt.args.getNetworkAreaRangeFails, GetNetworkAreaRangeResp: tt.args.getNetworkAreaRangeResp, } - got, err := GetNetworkRangePrefix(context.Background(), m, "", "", "") + got, err := GetNetworkRangePrefix(context.Background(), m, "", "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetNetworkRangePrefix() error = %v, wantErr %v", err, tt.wantErr) return @@ -650,22 +652,210 @@ func TestGetRouteFromAPIResponse(t *testing.T) { nexthop: "1.1.1.1", routes: &[]iaas.Route{ { - Prefix: utils.Ptr("1.1.1.0/24"), - Nexthop: utils.Ptr("1.1.1.1"), + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("1.1.1.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("1.1.1.1"), + }, + }, + }, + { + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("2.2.2.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("2.2.2.2"), + }, + }, + }, + { + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("3.3.3.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: utils.Ptr("blackhole"), + }, + }, + }, + { + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("4.4.4.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: utils.Ptr("internet"), + }, + }, + }, + }, + }, + want: iaas.Route{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("1.1.1.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("1.1.1.1"), + }, + }, + }, + }, + { + name: "nexthop internet", + args: args{ + prefix: "4.4.4.0/24", + nexthop: "internet", + routes: &[]iaas.Route{ + { + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("1.1.1.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: utils.Ptr("1.1.1.1"), + }, + }, }, { - Prefix: utils.Ptr("2.2.2.0/24"), - Nexthop: utils.Ptr("2.2.2.2"), + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("2.2.2.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: utils.Ptr("2.2.2.2"), + }, + }, }, { - Prefix: utils.Ptr("3.3.3.0/24"), - Nexthop: utils.Ptr("3.3.3.3"), + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("3.3.3.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: utils.Ptr("blackhole"), + }, + }, + }, + { + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("4.4.4.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: utils.Ptr("internet"), + }, + }, }, }, }, want: iaas.Route{ - Prefix: utils.Ptr("1.1.1.0/24"), - Nexthop: utils.Ptr("1.1.1.1"), + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("4.4.4.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: utils.Ptr("internet"), + }, + }, + }, + }, + { + name: "nexthop backhole", + args: args{ + prefix: "3.3.3.0/24", + nexthop: "blackhole", + routes: &[]iaas.Route{ + { + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("1.1.1.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: utils.Ptr("1.1.1.1"), + }, + }, + }, + { + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("2.2.2.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: utils.Ptr("2.2.2.2"), + }, + }, + }, + { + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("3.3.3.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: utils.Ptr("blackhole"), + }, + }, + }, + { + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("4.4.4.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: utils.Ptr("internet"), + }, + }, + }, + }, + }, + want: iaas.Route{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("3.3.3.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: utils.Ptr("blackhole"), + }, + }, }, }, { @@ -675,12 +865,28 @@ func TestGetRouteFromAPIResponse(t *testing.T) { nexthop: "1.1.1.1", routes: &[]iaas.Route{ { - Prefix: utils.Ptr("2.2.2.0/24"), - Nexthop: utils.Ptr("2.2.2.2"), + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("2.2.2.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: utils.Ptr("2.2.2.2"), + }, + }, }, { - Prefix: utils.Ptr("3.3.3.0/24"), - Nexthop: utils.Ptr("3.3.3.3"), + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: utils.Ptr("3.3.3.0/24"), + }, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: utils.Ptr("3.3.3.3"), + }, + }, }, }, }, @@ -819,7 +1025,7 @@ func TestGetImageName(t *testing.T) { GetImageFails: tt.imageErr, GetImageResp: tt.imageResp, } - got, err := GetImageName(context.Background(), client, "", "") + got, err := GetImageName(context.Background(), client, "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetImageName() error = %v, wantErr %v", err, tt.wantErr) return @@ -876,7 +1082,7 @@ func TestGetAffinityGroupName(t *testing.T) { GetAffinityGroupsFails: tt.affinityErr, GetAffinityGroupResp: tt.affinityResp, } - got, err := GetAffinityGroupName(ctx, client, "", "") + got, err := GetAffinityGroupName(ctx, client, "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetAffinityGroupName() error = %v, wantErr %v", err, tt.wantErr) return