diff --git a/docs/data-sources/affinity_group.md b/docs/data-sources/affinity_group.md index 904746cdb..63fc0629f 100644 --- a/docs/data-sources/affinity_group.md +++ b/docs/data-sources/affinity_group.md @@ -27,9 +27,13 @@ data "stackit_affinity_group" "example" { - `affinity_group_id` (String) The affinity group ID. - `project_id` (String) STACKIT Project ID to which the affinity group is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only -- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`affinity_group_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`affinity_group_id`". - `members` (List of String) Affinity Group schema. Must have a `region` specified in the provider configuration. - `name` (String) The name of the affinity group. - `policy` (String) The policy of the affinity group. diff --git a/docs/data-sources/iaas_project.md b/docs/data-sources/iaas_project.md index 919318df8..19aea853f 100644 --- a/docs/data-sources/iaas_project.md +++ b/docs/data-sources/iaas_project.md @@ -31,5 +31,6 @@ data "stackit_iaas_project" "example" { - `created_at` (String) Date-time when the project was created. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`". - `internet_access` (Boolean) Specifies if the project has internet_access -- `state` (String) Specifies the state of the project. +- `state` (String, Deprecated) Specifies the status of the project. +- `status` (String) Specifies the status of the project. - `updated_at` (String) Date-time when the project was last updated. diff --git a/docs/data-sources/image.md b/docs/data-sources/image.md index 235665268..34fa0c358 100644 --- a/docs/data-sources/image.md +++ b/docs/data-sources/image.md @@ -27,12 +27,16 @@ data "stackit_image" "example" { - `image_id` (String) The image ID. - `project_id` (String) STACKIT project ID to which the image is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `checksum` (Attributes) Representation of an image checksum. (see [below for nested schema](#nestedatt--checksum)) - `config` (Attributes) Properties to set hardware and scheduling settings for an image. (see [below for nested schema](#nestedatt--config)) - `disk_format` (String) The disk format of the image. -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`image_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `min_disk_size` (Number) The minimum disk size of the image in GB. - `min_ram` (Number) The minimum RAM of the image in MB. diff --git a/docs/data-sources/image_v2.md b/docs/data-sources/image_v2.md index 43f713ac8..b417f17b0 100644 --- a/docs/data-sources/image_v2.md +++ b/docs/data-sources/image_v2.md @@ -105,6 +105,7 @@ data "stackit_image_v2" "filter_distro_version" { - `image_id` (String) Image ID to fetch directly - `name` (String) Exact image name to match. Optionally applies a `filter` block to further refine results in case multiple images share the same name. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name_regex`. - `name_regex` (String) Regular expression to match against image names. Optionally applies a `filter` block to narrow down results when multiple image names match the regex. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name`. +- `region` (String) The resource region. If not defined, the provider region is used. - `sort_ascending` (Boolean) If set to `true`, images are sorted in ascending lexicographical order by image name (such as `Ubuntu 18.04`, `Ubuntu 20.04`, `Ubuntu 22.04`) before selecting the first match. Defaults to `false` (descending such as `Ubuntu 22.04`, `Ubuntu 20.04`, `Ubuntu 18.04`). ### Read-Only @@ -112,7 +113,7 @@ data "stackit_image_v2" "filter_distro_version" { - `checksum` (Attributes) Representation of an image checksum. (see [below for nested schema](#nestedatt--checksum)) - `config` (Attributes) Properties to set hardware and scheduling settings for an image. (see [below for nested schema](#nestedatt--config)) - `disk_format` (String) The disk format of the image. -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`image_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `min_disk_size` (Number) The minimum disk size of the image in GB. - `min_ram` (Number) The minimum RAM of the image in MB. diff --git a/docs/data-sources/machine_type.md b/docs/data-sources/machine_type.md index 10f80fc3e..7a200ae00 100644 --- a/docs/data-sources/machine_type.md +++ b/docs/data-sources/machine_type.md @@ -63,6 +63,7 @@ stackit server machine-type list ### Optional +- `region` (String) The resource region. If not defined, the provider region is used. - `sort_ascending` (Boolean) Sort machine types by name ascending (`true`) or descending (`false`). Defaults to `false` ### Read-Only @@ -70,7 +71,7 @@ stackit server machine-type list - `description` (String) Machine type description. - `disk` (Number) Disk size in GB. - `extra_specs` (Map of String) Extra specs (e.g., CPU type, overcommit ratio). -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`image_id`". - `name` (String) Name of the machine type (e.g. 's1.2'). - `ram` (Number) RAM size in MB. - `vcpus` (Number) Number of vCPUs. diff --git a/docs/data-sources/network_area.md b/docs/data-sources/network_area.md index d561f3b38..865906768 100644 --- a/docs/data-sources/network_area.md +++ b/docs/data-sources/network_area.md @@ -29,16 +29,16 @@ data "stackit_network_area" "example" { ### Read-Only -- `default_nameservers` (List of String) List of DNS Servers/Nameservers. -- `default_prefix_length` (Number) The default prefix length for networks in the network area. +- `default_nameservers` (List of String, Deprecated) List of DNS Servers/Nameservers. +- `default_prefix_length` (Number, Deprecated) The default prefix length for networks in the network area. - `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container -- `max_prefix_length` (Number) The maximal prefix length for networks in the network area. -- `min_prefix_length` (Number) The minimal prefix length for networks in the network area. +- `max_prefix_length` (Number, Deprecated) The maximal prefix length for networks in the network area. +- `min_prefix_length` (Number, Deprecated) The minimal prefix length for networks in the network area. - `name` (String) The name of the network area. -- `network_ranges` (Attributes List) List of Network ranges. (see [below for nested schema](#nestedatt--network_ranges)) +- `network_ranges` (Attributes List, Deprecated) List of Network ranges. (see [below for nested schema](#nestedatt--network_ranges)) - `project_count` (Number) The amount of projects currently referencing this area. -- `transfer_network` (String) Classless Inter-Domain Routing (CIDR). +- `transfer_network` (String, Deprecated) Classless Inter-Domain Routing (CIDR). ### Nested Schema for `network_ranges` diff --git a/docs/data-sources/network_area_region.md b/docs/data-sources/network_area_region.md new file mode 100644 index 000000000..09ac1be3e --- /dev/null +++ b/docs/data-sources/network_area_region.md @@ -0,0 +1,57 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_network_area_region Data Source - stackit" +subcategory: "" +description: |- + Network area region data source schema. +--- + +# stackit_network_area_region (Data Source) + +Network area region data source schema. + +## Example Usage + +```terraform +data "stackit_network_area_region" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `network_area_id` (String) The network area ID. +- `organization_id` (String) STACKIT organization ID to which the network area is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`,`region`". +- `ipv4` (Attributes) The regional IPv4 config of a network area. (see [below for nested schema](#nestedatt--ipv4)) + + +### Nested Schema for `ipv4` + +Read-Only: + +- `default_nameservers` (List of String) List of DNS Servers/Nameservers. +- `default_prefix_length` (Number) The default prefix length for networks in the network area. +- `max_prefix_length` (Number) The maximal prefix length for networks in the network area. +- `min_prefix_length` (Number) The minimal prefix length for networks in the network area. +- `network_ranges` (Attributes List) List of Network ranges. (see [below for nested schema](#nestedatt--ipv4--network_ranges)) +- `transfer_network` (String) IPv4 Classless Inter-Domain Routing (CIDR). + + +### Nested Schema for `ipv4.network_ranges` + +Read-Only: + +- `network_range_id` (String) +- `prefix` (String) Classless Inter-Domain Routing (CIDR). diff --git a/docs/data-sources/network_area_route.md b/docs/data-sources/network_area_route.md index 688864a97..29b5dd1bf 100644 --- a/docs/data-sources/network_area_route.md +++ b/docs/data-sources/network_area_route.md @@ -29,9 +29,13 @@ data "stackit_network_area_route" "example" { - `network_area_route_id` (String) The network area route ID. - `organization_id` (String) STACKIT organization ID to which the network area is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only -- `id` (String) Terraform's internal data source ID. It is structured as "`organization_id`,`network_area_id`,`network_area_route_id`". +- `id` (String) Terraform's internal data source ID. It is structured as "`organization_id`,`region`,`network_area_id`,`network_area_route_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `next_hop` (String) The IP address of the routing system, that will route the prefix configured. Should be a valid IPv4 address. - `prefix` (String) The network, that is reachable though the Next Hop. Should use CIDR notation. diff --git a/docs/data-sources/network_interface.md b/docs/data-sources/network_interface.md index d6570aeaa..77e5d6ef5 100644 --- a/docs/data-sources/network_interface.md +++ b/docs/data-sources/network_interface.md @@ -29,11 +29,15 @@ data "stackit_network_interface" "example" { - `network_interface_id` (String) The network interface ID. - `project_id` (String) STACKIT project ID to which the network interface is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `allowed_addresses` (List of String) The list of CIDR (Classless Inter-Domain Routing) notations. - `device` (String) The device UUID of the network interface. -- `id` (String) Terraform's internal data source ID. It is structured as "`project_id`,`network_id`,`network_interface_id`". +- `id` (String) Terraform's internal data source ID. It is structured as "`project_id`,`region`,`network_id`,`network_interface_id`". - `ipv4` (String) The IPv4 address. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a network interface. - `mac` (String) The MAC address of network interface. diff --git a/docs/data-sources/public_ip.md b/docs/data-sources/public_ip.md index a2db13a79..1f1048788 100644 --- a/docs/data-sources/public_ip.md +++ b/docs/data-sources/public_ip.md @@ -27,9 +27,13 @@ data "stackit_public_ip" "example" { - `project_id` (String) STACKIT project ID to which the public IP is associated. - `public_ip_id` (String) The public IP ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only -- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`public_ip_id`". +- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`region`,`public_ip_id`". - `ip` (String) The IP address. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `network_interface_id` (String) Associates the public IP with a network interface or a virtual IP (ID). diff --git a/docs/data-sources/security_group.md b/docs/data-sources/security_group.md index 5a5af8a4c..2d6de8ea8 100644 --- a/docs/data-sources/security_group.md +++ b/docs/data-sources/security_group.md @@ -27,6 +27,10 @@ data "stackit_security_group" "example" { - `project_id` (String) STACKIT project ID to which the security group is associated. - `security_group_id` (String) The security group ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `description` (String) The description of the security group. diff --git a/docs/data-sources/security_group_rule.md b/docs/data-sources/security_group_rule.md index 749504dec..d5871bd2b 100644 --- a/docs/data-sources/security_group_rule.md +++ b/docs/data-sources/security_group_rule.md @@ -29,13 +29,17 @@ data "stackit_security_group_rule" "example" { - `security_group_id` (String) The security group ID. - `security_group_rule_id` (String) The security group rule ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `description` (String) The description of the security group rule. - `direction` (String) The direction of the traffic which the rule should match. Some of the possible values are: Possible values are: `ingress`, `egress`. - `ether_type` (String) The ethertype which the rule should match. - `icmp_parameters` (Attributes) ICMP Parameters. (see [below for nested schema](#nestedatt--icmp_parameters)) -- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`security_group_id`,`security_group_rule_id`". +- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`region`,`security_group_id`,`security_group_rule_id`". - `ip_range` (String) The remote IP range which the rule should match. - `port_range` (Attributes) The range of ports. (see [below for nested schema](#nestedatt--port_range)) - `protocol` (Attributes) The internet protocol which the rule should match. (see [below for nested schema](#nestedatt--protocol)) diff --git a/docs/data-sources/server.md b/docs/data-sources/server.md index 384a59a3f..b805acd94 100644 --- a/docs/data-sources/server.md +++ b/docs/data-sources/server.md @@ -27,6 +27,10 @@ data "stackit_server" "example" { - `project_id` (String) STACKIT project ID to which the server is associated. - `server_id` (String) The server ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `affinity_group` (String) The affinity group the server is assigned to. diff --git a/docs/data-sources/volume.md b/docs/data-sources/volume.md index b45c19934..a1de729e7 100644 --- a/docs/data-sources/volume.md +++ b/docs/data-sources/volume.md @@ -27,11 +27,15 @@ data "stackit_volume" "example" { - `project_id` (String) STACKIT project ID to which the volume is associated. - `volume_id` (String) The volume ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `availability_zone` (String) The availability zone of the volume. - `description` (String) The description of the volume. -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`volume_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`volume_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `name` (String) The name of the volume. - `performance_class` (String) The performance class of the volume. Possible values are documented in [Service plans BlockStorage](https://docs.stackit.cloud/stackit/en/service-plans-blockstorage-75137974.html#ServiceplansBlockStorage-CurrentlyavailableServicePlans%28performanceclasses%29) diff --git a/docs/resources/affinity_group.md b/docs/resources/affinity_group.md index 2f1cbae67..9c7835008 100644 --- a/docs/resources/affinity_group.md +++ b/docs/resources/affinity_group.md @@ -3,7 +3,7 @@ page_title: "stackit_affinity_group Resource - stackit" subcategory: "" description: |- - Affinity Group schema. Must have a region specified in the provider configuration. + Affinity Group schema. Usage with server resource "stackit_affinity_group" "affinity-group" { @@ -39,7 +39,7 @@ description: |- # stackit_affinity_group (Resource) -Affinity Group schema. Must have a `region` specified in the provider configuration. +Affinity Group schema. @@ -104,8 +104,12 @@ import { - `policy` (String) The policy of the affinity group. - `project_id` (String) STACKIT Project ID to which the affinity group is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `affinity_group_id` (String) The affinity group ID. -- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`affinity_group_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`affinity_group_id`". - `members` (List of String) The servers that are part of the affinity group. diff --git a/docs/resources/image.md b/docs/resources/image.md index abeeefc23..fd07f2860 100644 --- a/docs/resources/image.md +++ b/docs/resources/image.md @@ -51,11 +51,12 @@ import { - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `min_disk_size` (Number) The minimum disk size of the image in GB. - `min_ram` (Number) The minimum RAM of the image in MB. +- `region` (String) The resource region. If not defined, the provider region is used. ### Read-Only - `checksum` (Attributes) Representation of an image checksum. (see [below for nested schema](#nestedatt--checksum)) -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`image_id`". - `image_id` (String) The image ID. - `protected` (Boolean) Whether the image is protected. - `scope` (String) The scope of the image. diff --git a/docs/resources/network.md b/docs/resources/network.md index c11dad6b9..7d47867e7 100644 --- a/docs/resources/network.md +++ b/docs/resources/network.md @@ -34,12 +34,11 @@ resource "stackit_network" "example_routed_network" { } resource "stackit_network" "example_non_routed_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-non-routed-network" - ipv4_nameservers = ["1.2.3.4", "5.6.7.8"] - ipv4_prefix_length = 24 - ipv4_gateway = "10.1.2.3" - ipv4_prefix = "10.1.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-non-routed-network" + ipv4_nameservers = ["1.2.3.4", "5.6.7.8"] + ipv4_gateway = "10.1.2.3" + ipv4_prefix = "10.1.2.0/24" labels = { "key" = "value" } @@ -77,11 +76,9 @@ import { - `nameservers` (List of String, Deprecated) The nameservers of the network. This field is deprecated and will be removed in January 2026, use `ipv4_nameservers` to configure the nameservers for IPv4. - `no_ipv4_gateway` (Boolean) If set to `true`, the network doesn't have a gateway. - `no_ipv6_gateway` (Boolean) If set to `true`, the network doesn't have a gateway. -- `region` (String) Can only be used when experimental "network" is set. -The resource region. If not defined, the provider region is used. +- `region` (String) The resource region. If not defined, the provider region is used. - `routed` (Boolean) If set to `true`, the network is routed and therefore accessible from other networks. -- `routing_table_id` (String) Can only be used when experimental "network" is set. -The ID of the routing table associated with the network. +- `routing_table_id` (String) The ID of the routing table associated with the network. ### Read-Only diff --git a/docs/resources/network_area.md b/docs/resources/network_area.md index 46c308d35..909784c38 100644 --- a/docs/resources/network_area.md +++ b/docs/resources/network_area.md @@ -3,12 +3,12 @@ page_title: "stackit_network_area Resource - stackit" subcategory: "" description: |- - Network area resource schema. Must have a region specified in the provider configuration. + Network area resource schema. --- # stackit_network_area (Resource) -Network area resource schema. Must have a `region` specified in the provider configuration. +Network area resource schema. ## Example Usage @@ -16,12 +16,6 @@ Network area resource schema. Must have a `region` specified in the provider con resource "stackit_network_area" "example" { organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-network-area" - network_ranges = [ - { - prefix = "192.168.0.0/24" - } - ] - transfer_network = "192.168.1.0/24" labels = { "key" = "value" } @@ -40,17 +34,17 @@ import { ### Required - `name` (String) The name of the network area. -- `network_ranges` (Attributes List) List of Network ranges. (see [below for nested schema](#nestedatt--network_ranges)) - `organization_id` (String) STACKIT organization ID to which the network area is associated. -- `transfer_network` (String) Classless Inter-Domain Routing (CIDR). ### Optional -- `default_nameservers` (List of String) List of DNS Servers/Nameservers. -- `default_prefix_length` (Number) The default prefix length for networks in the network area. +- `default_nameservers` (List of String, Deprecated) List of DNS Servers/Nameservers for configuration of network area for region `eu01`. +- `default_prefix_length` (Number, Deprecated) The default prefix length for networks in the network area for region `eu01`. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container -- `max_prefix_length` (Number) The maximal prefix length for networks in the network area. -- `min_prefix_length` (Number) The minimal prefix length for networks in the network area. +- `max_prefix_length` (Number, Deprecated) The maximal prefix length for networks in the network area for region `eu01`. +- `min_prefix_length` (Number, Deprecated) The minimal prefix length for networks in the network area for region `eu01`. +- `network_ranges` (Attributes List, Deprecated) List of Network ranges for configuration of network area for region `eu01`. (see [below for nested schema](#nestedatt--network_ranges)) +- `transfer_network` (String, Deprecated) Classless Inter-Domain Routing (CIDR) for configuration of network area for region `eu01`. ### Read-Only @@ -63,8 +57,8 @@ import { Required: -- `prefix` (String) Classless Inter-Domain Routing (CIDR). +- `prefix` (String, Deprecated) Classless Inter-Domain Routing (CIDR). Read-Only: -- `network_range_id` (String) +- `network_range_id` (String, Deprecated) diff --git a/docs/resources/network_area_region.md b/docs/resources/network_area_region.md new file mode 100644 index 000000000..0fd47ae24 --- /dev/null +++ b/docs/resources/network_area_region.md @@ -0,0 +1,77 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_network_area_region Resource - stackit" +subcategory: "" +description: |- + Network area region resource schema. +--- + +# stackit_network_area_region (Resource) + +Network area region resource schema. + +## Example Usage + +```terraform +resource "stackit_network_area_region" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + ipv4 = { + transfer_network = "10.1.2.0/24" + network_ranges = [ + { + prefix = "10.0.0.0/16" + } + ] + } +} + +# Only use the import statement, if you want to import an existing network area region +import { + to = stackit_network_area_region.import-example + id = "${var.organization_id},${var.network_area_id},eu01" +} +``` + + +## Schema + +### Required + +- `ipv4` (Attributes) The regional IPv4 config of a network area. (see [below for nested schema](#nestedatt--ipv4)) +- `network_area_id` (String) The network area ID. +- `organization_id` (String) STACKIT organization ID to which the network area is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`,`region`". + + +### Nested Schema for `ipv4` + +Required: + +- `network_ranges` (Attributes List) List of Network ranges. (see [below for nested schema](#nestedatt--ipv4--network_ranges)) +- `transfer_network` (String) IPv4 Classless Inter-Domain Routing (CIDR). + +Optional: + +- `default_nameservers` (List of String) List of DNS Servers/Nameservers. +- `default_prefix_length` (Number) The default prefix length for networks in the network area. +- `max_prefix_length` (Number) The maximal prefix length for networks in the network area. +- `min_prefix_length` (Number) The minimal prefix length for networks in the network area. + + +### Nested Schema for `ipv4.network_ranges` + +Required: + +- `prefix` (String) Classless Inter-Domain Routing (CIDR). + +Read-Only: + +- `network_range_id` (String) diff --git a/docs/resources/network_area_route.md b/docs/resources/network_area_route.md index a7f054603..dc1852d7c 100644 --- a/docs/resources/network_area_route.md +++ b/docs/resources/network_area_route.md @@ -43,8 +43,9 @@ import { ### Optional - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container +- `region` (String) The resource region. If not defined, the provider region is used. ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`,`network_area_route_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`,`region`,`network_area_route_id`". - `network_area_route_id` (String) The network area route ID. diff --git a/docs/resources/network_interface.md b/docs/resources/network_interface.md index 4ef8a8719..e8ea059f2 100644 --- a/docs/resources/network_interface.md +++ b/docs/resources/network_interface.md @@ -41,13 +41,14 @@ import { - `ipv4` (String) The IPv4 address. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a network interface. - `name` (String) The name of the network interface. +- `region` (String) The resource region. If not defined, the provider region is used. - `security` (Boolean) The Network Interface Security. If set to false, then no security groups will apply to this network interface. - `security_group_ids` (List of String) The list of security group UUIDs. If security is set to false, setting this field will lead to an error. ### Read-Only - `device` (String) The device UUID of the network interface. -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`network_id`,`network_interface_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`network_id`,`network_interface_id`". - `mac` (String) The MAC address of network interface. - `network_interface_id` (String) The network interface ID. - `type` (String) Type of network interface. Some of the possible values are: Possible values are: `server`, `metadata`, `gateway`. diff --git a/docs/resources/public_ip.md b/docs/resources/public_ip.md index fad2560dd..a49cff46d 100644 --- a/docs/resources/public_ip.md +++ b/docs/resources/public_ip.md @@ -39,9 +39,10 @@ import { - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `network_interface_id` (String) Associates the public IP with a network interface or a virtual IP (ID). If you are using this resource with a Kubernetes Load Balancer or any other resource which associates a network interface implicitly, use the lifecycle `ignore_changes` property in this field to prevent unintentional removal of the network interface due to drift in the Terraform state +- `region` (String) The resource region. If not defined, the provider region is used. ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`public_ip_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`public_ip_id`". - `ip` (String) The IP address. - `public_ip_id` (String) The public IP ID. diff --git a/docs/resources/public_ip_associate.md b/docs/resources/public_ip_associate.md index 098e6ff54..16d390c9e 100644 --- a/docs/resources/public_ip_associate.md +++ b/docs/resources/public_ip_associate.md @@ -40,7 +40,11 @@ import { - `project_id` (String) STACKIT project ID to which the public IP is associated. - `public_ip_id` (String) The public IP ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`public_ip_id`,`network_interface_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`public_ip_id`,`network_interface_id`". - `ip` (String) The IP address. diff --git a/docs/resources/security_group.md b/docs/resources/security_group.md index c4f9d06c3..eec31aa00 100644 --- a/docs/resources/security_group.md +++ b/docs/resources/security_group.md @@ -40,9 +40,10 @@ import { - `description` (String) The description of the security group. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container +- `region` (String) The resource region. If not defined, the provider region is used. - `stateful` (Boolean) Configures if a security group is stateful or stateless. There can only be one type of security groups per network interface/server. ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`security_group_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`security_group_id`". - `security_group_id` (String) The security group ID. diff --git a/docs/resources/security_group_rule.md b/docs/resources/security_group_rule.md index 452f7c943..97e9fc654 100644 --- a/docs/resources/security_group_rule.md +++ b/docs/resources/security_group_rule.md @@ -52,11 +52,12 @@ import { - `ip_range` (String) The remote IP range which the rule should match. - `port_range` (Attributes) The range of ports. This should only be provided if the protocol is not ICMP. (see [below for nested schema](#nestedatt--port_range)) - `protocol` (Attributes) The internet protocol which the rule should match. (see [below for nested schema](#nestedatt--protocol)) +- `region` (String) The resource region. If not defined, the provider region is used. - `remote_security_group_id` (String) The remote security group which the rule should match. ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`security_group_id`,`security_group_rule_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`security_group_id`,`security_group_rule_id`". - `security_group_rule_id` (String) The security group rule ID. diff --git a/docs/resources/server.md b/docs/resources/server.md index 6cb003c93..f86cd8609 100644 --- a/docs/resources/server.md +++ b/docs/resources/server.md @@ -399,6 +399,7 @@ import { - `machine_type` (String) Name of the type of the machine for the server. Possible values are documented in [Virtual machine flavors](https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html) - `name` (String) The name of the server. +- `network_interfaces` (List of String) The IDs of network interfaces which should be attached to the server. Updating it will recreate the server. - `project_id` (String) STACKIT project ID to which the server is associated. ### Optional @@ -410,7 +411,7 @@ import { - `image_id` (String) The image ID to be used for an ephemeral disk on the server. - `keypair_name` (String) The name of the keypair used during server creation. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container -- `network_interfaces` (List of String) The IDs of network interfaces which should be attached to the server. Updating it will recreate the server. +- `region` (String) The resource region. If not defined, the provider region is used. - `user_data` (String) User data that is passed via cloud-init to the server. ### Read-Only diff --git a/docs/resources/server_network_interface_attach.md b/docs/resources/server_network_interface_attach.md index b6c99ce0f..4f0c5e184 100644 --- a/docs/resources/server_network_interface_attach.md +++ b/docs/resources/server_network_interface_attach.md @@ -3,12 +3,12 @@ page_title: "stackit_server_network_interface_attach Resource - stackit" subcategory: "" description: |- - Network interface attachment resource schema. Attaches a network interface to a server. Must have a region specified in the provider configuration. The attachment only takes full effect after server reboot. + Network interface attachment resource schema. Attaches a network interface to a server. The attachment only takes full effect after server reboot. --- # stackit_server_network_interface_attach (Resource) -Network interface attachment resource schema. Attaches a network interface to a server. Must have a `region` specified in the provider configuration. The attachment only takes full effect after server reboot. +Network interface attachment resource schema. Attaches a network interface to a server. The attachment only takes full effect after server reboot. ## Example Usage @@ -35,6 +35,10 @@ import { - `project_id` (String) STACKIT project ID to which the network interface attachment is associated. - `server_id` (String) The server ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`,`network_interface_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`server_id`,`network_interface_id`". diff --git a/docs/resources/server_service_account_attach.md b/docs/resources/server_service_account_attach.md index 2b02b0744..a99ccd098 100644 --- a/docs/resources/server_service_account_attach.md +++ b/docs/resources/server_service_account_attach.md @@ -35,6 +35,10 @@ import { - `server_id` (String) The server ID. - `service_account_email` (String) The service account email. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`,`service_account_email`". diff --git a/docs/resources/server_volume_attach.md b/docs/resources/server_volume_attach.md index 93c5862ea..d36aab62a 100644 --- a/docs/resources/server_volume_attach.md +++ b/docs/resources/server_volume_attach.md @@ -35,6 +35,10 @@ import { - `server_id` (String) The server ID. - `volume_id` (String) The volume ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`,`volume_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`server_id`,`volume_id`". diff --git a/docs/resources/volume.md b/docs/resources/volume.md index 8ffb571b0..f3c3aaded 100644 --- a/docs/resources/volume.md +++ b/docs/resources/volume.md @@ -44,12 +44,13 @@ import { - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `name` (String) The name of the volume. - `performance_class` (String) The performance class of the volume. Possible values are documented in [Service plans BlockStorage](https://docs.stackit.cloud/stackit/en/service-plans-blockstorage-75137974.html#ServiceplansBlockStorage-CurrentlyavailableServicePlans%28performanceclasses%29) +- `region` (String) The resource region. If not defined, the provider region is used. - `size` (Number) The size of the volume in GB. It can only be updated to a larger value than the current size. Either `size` or `source` must be provided - `source` (Attributes) The source of the volume. It can be either a volume, an image, a snapshot or a backup. Either `size` or `source` must be provided (see [below for nested schema](#nestedatt--source)) ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`volume_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`volume_id`". - `server_id` (String) The server ID of the server to which the volume is attached to. - `volume_id` (String) The volume ID. diff --git a/examples/data-sources/stackit_network_area_region/data-source.tf b/examples/data-sources/stackit_network_area_region/data-source.tf new file mode 100644 index 000000000..f673f5870 --- /dev/null +++ b/examples/data-sources/stackit_network_area_region/data-source.tf @@ -0,0 +1,4 @@ +data "stackit_network_area_region" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/resources/stackit_network/resource.tf b/examples/resources/stackit_network/resource.tf index dbf1876d5..af83b512e 100644 --- a/examples/resources/stackit_network/resource.tf +++ b/examples/resources/stackit_network/resource.tf @@ -13,12 +13,11 @@ resource "stackit_network" "example_routed_network" { } resource "stackit_network" "example_non_routed_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-non-routed-network" - ipv4_nameservers = ["1.2.3.4", "5.6.7.8"] - ipv4_prefix_length = 24 - ipv4_gateway = "10.1.2.3" - ipv4_prefix = "10.1.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-non-routed-network" + ipv4_nameservers = ["1.2.3.4", "5.6.7.8"] + ipv4_gateway = "10.1.2.3" + ipv4_prefix = "10.1.2.0/24" labels = { "key" = "value" } @@ -31,4 +30,4 @@ resource "stackit_network" "example_non_routed_network" { import { to = stackit_network.import-example id = "${var.project_id},${var.network_id}" -} \ No newline at end of file +} diff --git a/examples/resources/stackit_network_area/resource.tf b/examples/resources/stackit_network_area/resource.tf index e1cfbe0c6..a699e7cad 100644 --- a/examples/resources/stackit_network_area/resource.tf +++ b/examples/resources/stackit_network_area/resource.tf @@ -1,12 +1,6 @@ resource "stackit_network_area" "example" { organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-network-area" - network_ranges = [ - { - prefix = "192.168.0.0/24" - } - ] - transfer_network = "192.168.1.0/24" labels = { "key" = "value" } diff --git a/examples/resources/stackit_network_area_region/resource.tf b/examples/resources/stackit_network_area_region/resource.tf new file mode 100644 index 000000000..4a5bafe03 --- /dev/null +++ b/examples/resources/stackit_network_area_region/resource.tf @@ -0,0 +1,18 @@ +resource "stackit_network_area_region" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + ipv4 = { + transfer_network = "10.1.2.0/24" + network_ranges = [ + { + prefix = "10.0.0.0/16" + } + ] + } +} + +# Only use the import statement, if you want to import an existing network area region +import { + to = stackit_network_area_region.import-example + id = "${var.organization_id},${var.network_area_id},eu01" +} diff --git a/examples/resources/stackit_network_area_route/resource.tf b/examples/resources/stackit_network_area_route/resource.tf index a18d26eb8..c2046c675 100644 --- a/examples/resources/stackit_network_area_route/resource.tf +++ b/examples/resources/stackit_network_area_route/resource.tf @@ -12,4 +12,4 @@ resource "stackit_network_area_route" "example" { import { to = stackit_network_area_route.import-example id = "${var.organization_id},${var.network_area_id},${var.network_area_route_id}" -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 3a3b65a6d..b5a3a9a2f 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 github.com/stackitcloud/stackit-sdk-go/services/git v0.8.0 - github.com/stackitcloud/stackit-sdk-go/services/iaas v0.31.0 + github.com/stackitcloud/stackit-sdk-go/services/iaas v1.0.0 github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha github.com/stackitcloud/stackit-sdk-go/services/kms v1.0.0 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0 diff --git a/go.sum b/go.sum index 6d10aae8c..57444ffa2 100644 --- a/go.sum +++ b/go.sum @@ -162,8 +162,8 @@ github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 h1:CnhAMLql0MNmAeq4r github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1/go.mod h1:7Bx85knfNSBxulPdJUFuBePXNee3cO+sOTYnUG6M+iQ= github.com/stackitcloud/stackit-sdk-go/services/git v0.8.0 h1:/weT7P5Uwy1Qlhw0NidqtQBlbbb/dQehweDV/I9ShXg= github.com/stackitcloud/stackit-sdk-go/services/git v0.8.0/go.mod h1:AXFfYBJZIW1o0W0zZEb/proQMhMsb3Nn5E1htS8NDPE= -github.com/stackitcloud/stackit-sdk-go/services/iaas v0.31.0 h1:dnEjyapuv8WwRN5vE2z6+4/+ZqQTBx+bX27x2nOF7Jw= -github.com/stackitcloud/stackit-sdk-go/services/iaas v0.31.0/go.mod h1:854gnLR92NvAbJAA1xZEumrtNh1DoBP1FXTMvhwYA6w= +github.com/stackitcloud/stackit-sdk-go/services/iaas v1.0.0 h1:qLMpd5whPMLnaLEdFQjK51q/o9V6eMFMORBDSsyGyNI= +github.com/stackitcloud/stackit-sdk-go/services/iaas v1.0.0/go.mod h1:854gnLR92NvAbJAA1xZEumrtNh1DoBP1FXTMvhwYA6w= github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha h1:m1jq6a8dbUe+suFuUNdHmM/cSehpGLUtDbK1CqLqydg= github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.21-alpha/go.mod h1:Nu1b5Phsv8plgZ51+fkxPVsU91ZJ5Ayz+cthilxdmQ8= github.com/stackitcloud/stackit-sdk-go/services/kms v1.0.0 h1:zxoOv7Fu+FmdsvTKiKkbmLItrMKfL+QoVtz9ReEF30E= diff --git a/stackit/internal/services/iaas/affinitygroup/datasource.go b/stackit/internal/services/iaas/affinitygroup/datasource.go index ed4507001..38edcd53c 100644 --- a/stackit/internal/services/iaas/affinitygroup/datasource.go +++ b/stackit/internal/services/iaas/affinitygroup/datasource.go @@ -33,16 +33,18 @@ func NewAffinityGroupDatasource() datasource.DataSource { } type affinityGroupDatasource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } func (d *affinityGroupDatasource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -61,7 +63,7 @@ func (d *affinityGroupDatasource) Schema(_ context.Context, _ datasource.SchemaR MarkdownDescription: descriptionMain, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`affinity_group_id`\".", + Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`affinity_group_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -72,6 +74,11 @@ func (d *affinityGroupDatasource) Schema(_ context.Context, _ datasource.SchemaR validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "affinity_group_id": schema.StringAttribute{ Description: "The affinity group ID.", Required: true, @@ -117,11 +124,13 @@ func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadR return } projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) affinityGroupId := model.AffinityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId) - affinityGroupResp, err := d.client.GetAffinityGroupExecute(ctx, projectId, affinityGroupId) + affinityGroupResp, err := d.client.GetAffinityGroupExecute(ctx, projectId, region, affinityGroupId) if err != nil { utils.LogError( ctx, @@ -137,7 +146,7 @@ func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadR return } - err = mapFields(ctx, affinityGroupResp, &model) + err = mapFields(ctx, affinityGroupResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Processing API payload: %v", err)) } diff --git a/stackit/internal/services/iaas/affinitygroup/resource.go b/stackit/internal/services/iaas/affinitygroup/resource.go index 1110e4296..16c4d6c17 100644 --- a/stackit/internal/services/iaas/affinitygroup/resource.go +++ b/stackit/internal/services/iaas/affinitygroup/resource.go @@ -17,7 +17,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -33,12 +32,14 @@ var ( _ resource.Resource = &affinityGroupResource{} _ resource.ResourceWithConfigure = &affinityGroupResource{} _ resource.ResourceWithImportState = &affinityGroupResource{} + _ resource.ResourceWithModifyPlan = &affinityGroupResource{} ) // Model is the provider's internal model type Model struct { Id types.String `tfsdk:"id"` ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` AffinityGroupId types.String `tfsdk:"affinity_group_id"` Name types.String `tfsdk:"name"` Policy types.String `tfsdk:"policy"` @@ -51,7 +52,8 @@ func NewAffinityGroupResource() resource.Resource { // affinityGroupResource is the resource implementation. type affinityGroupResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -59,14 +61,45 @@ func (r *affinityGroupResource) Metadata(_ context.Context, req resource.Metadat resp.TypeName = req.ProviderTypeName + "_affinity_group" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *affinityGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *affinityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -75,13 +108,13 @@ func (r *affinityGroupResource) Configure(ctx context.Context, req resource.Conf } func (r *affinityGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Affinity Group schema. Must have a `region` specified in the provider configuration." + description := "Affinity Group schema." resp.Schema = schema.Schema{ Description: description, MarkdownDescription: description + "\n\n" + exampleUsageWithServer + policies, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`affinity_group_id`\".", + Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`affinity_group_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -98,6 +131,15 @@ func (r *affinityGroupResource) Schema(_ context.Context, _ resource.SchemaReque validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "affinity_group_id": schema.StringAttribute{ Description: "The affinity group ID.", Computed: true, @@ -153,8 +195,11 @@ func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateR if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) // Create new affinityGroup payload, err := toCreatePayload(&model) @@ -162,7 +207,7 @@ func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateR core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Creating API payload: %v", err)) return } - affinityGroupResp, err := r.client.CreateAffinityGroup(ctx, projectId).CreateAffinityGroupPayload(*payload).Execute() + affinityGroupResp, err := r.client.CreateAffinityGroup(ctx, projectId, region).CreateAffinityGroupPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Calling API: %v", err)) return @@ -170,7 +215,7 @@ func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateR ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupResp.Id) // Map response body to schema - err = mapFields(ctx, affinityGroupResp, &model) + err = mapFields(ctx, affinityGroupResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Processing API payload: %v", err)) return @@ -193,11 +238,13 @@ func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadReque return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) affinityGroupId := model.AffinityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId) - affinityGroupResp, err := r.client.GetAffinityGroupExecute(ctx, projectId, affinityGroupId) + affinityGroupResp, err := r.client.GetAffinityGroupExecute(ctx, projectId, region, affinityGroupId) if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -208,7 +255,7 @@ func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadReque return } - err = mapFields(ctx, affinityGroupResp, &model) + err = mapFields(ctx, affinityGroupResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Processing API payload: %v", err)) } @@ -236,12 +283,14 @@ func (r *affinityGroupResource) Delete(ctx context.Context, req resource.DeleteR } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) affinityGroupId := model.AffinityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId) // Delete existing affinity group - err := r.client.DeleteAffinityGroupExecute(ctx, projectId, affinityGroupId) + err := r.client.DeleteAffinityGroupExecute(ctx, projectId, region, affinityGroupId) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting affinity group", fmt.Sprintf("Calling API: %v", err)) return @@ -253,21 +302,20 @@ func (r *affinityGroupResource) Delete(ctx context.Context, req resource.DeleteR func (r *affinityGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing affinity group", - fmt.Sprintf("Expected import indentifier with format: [project_id],[affinity_group_id], got: %q", req.ID), + fmt.Sprintf("Expected import indentifier with format: [project_id],[region],[affinity_group_id], got: %q", req.ID), ) return } - projectId := idParts[0] - affinityGroupId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "affinity_group_id": idParts[2], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("affinity_group_id"), affinityGroupId)...) tflog.Info(ctx, "affinity group state imported") } @@ -285,7 +333,7 @@ func toCreatePayload(model *Model) (*iaas.CreateAffinityGroupPayload, error) { }, nil } -func mapFields(ctx context.Context, affinityGroupResp *iaas.AffinityGroup, model *Model) error { +func mapFields(ctx context.Context, affinityGroupResp *iaas.AffinityGroup, model *Model, region string) error { if affinityGroupResp == nil { return fmt.Errorf("response input is nil") } @@ -303,7 +351,8 @@ func mapFields(ctx context.Context, affinityGroupResp *iaas.AffinityGroup, model return fmt.Errorf("affinity group id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), affinityGroupId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, affinityGroupId) + model.Region = types.StringValue(region) if affinityGroupResp.Members != nil && len(*affinityGroupResp.Members) > 0 { members, diags := types.ListValueFrom(ctx, types.StringType, *affinityGroupResp.Members) diff --git a/stackit/internal/services/iaas/affinitygroup/resource_test.go b/stackit/internal/services/iaas/affinitygroup/resource_test.go index a4e203910..26f4bc055 100644 --- a/stackit/internal/services/iaas/affinitygroup/resource_test.go +++ b/stackit/internal/services/iaas/affinitygroup/resource_test.go @@ -11,52 +11,56 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.AffinityGroup + region string + } tests := []struct { description string - state Model - input *iaas.AffinityGroup + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - AffinityGroupId: types.StringValue("aid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + AffinityGroupId: types.StringValue("aid"), + }, + input: &iaas.AffinityGroup{ + Id: utils.Ptr("aid"), + }, + region: "eu01", }, - &iaas.AffinityGroup{ - Id: utils.Ptr("aid"), - }, - Model{ - Id: types.StringValue("pid,aid"), + expected: Model{ + Id: types.StringValue("pid,eu01,aid"), ProjectId: types.StringValue("pid"), AffinityGroupId: types.StringValue("aid"), Name: types.StringNull(), Policy: types.StringNull(), Members: types.ListNull(types.StringType), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_affinity_group_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_affinity_group_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.AffinityGroup{}, }, - &iaas.AffinityGroup{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -64,7 +68,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed") } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %v", diff) } diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go index dda89f70a..2c68b98c4 100644 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -13,6 +13,8 @@ import ( "sync" "testing" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -21,7 +23,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" waitAlpha "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -59,17 +60,17 @@ var ( //go:embed testdata/resource-network-area-max.tf resourceNetworkAreaMaxConfig string - //go:embed testdata/resource-network-v1-min.tf - resourceNetworkV1MinConfig string + //go:embed testdata/resource-network-area-region-min.tf + resourceNetworkAreaRegionMinConfig string - //go:embed testdata/resource-network-v1-max.tf - resourceNetworkV1MaxConfig string + //go:embed testdata/resource-network-area-region-max.tf + resourceNetworkAreaRegionMaxConfig string - //go:embed testdata/resource-network-v2-min.tf - resourceNetworkV2MinConfig string + //go:embed testdata/resource-network-min.tf + resourceNetworkMinConfig string - //go:embed testdata/resource-network-v2-max.tf - resourceNetworkV2MaxConfig string + //go:embed testdata/resource-network-max.tf + resourceNetworkMaxConfig string //go:embed testdata/resource-network-interface-min.tf resourceNetworkInterfaceMinConfig string @@ -105,9 +106,12 @@ const ( testNetworkAreaId = "25bbf23a-8134-4439-9f5e-1641caf8354e" ) +// SERVER - MIN + var testConfigServerVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), + "network_name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "machine_type": config.StringVariable("t1.1"), "image_id": config.StringVariable("a2c127b2-b1b5-4aee-986f-41cd11b41279"), } @@ -122,6 +126,8 @@ var testConfigServerVarsMinUpdated = func() config.Variables { return updatedConfig }() +// SERVER - MAX + var testConfigServerVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), @@ -162,17 +168,23 @@ var testConfigServerVarsMaxUpdatedDesiredStatus = func() config.Variables { return updatedConfig }() +// AFFINITY GROUP - MIN + var testConfigAffinityGroupVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "policy": config.StringVariable("hard-affinity"), } +// NETWORK INTERFACE - MIN + var testConfigNetworkInterfaceVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), } +// NETWORK INTERFACE - MAX + var testConfigNetworkInterfaceVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), @@ -195,6 +207,8 @@ var testConfigNetworkInterfaceVarsMaxUpdated = func() config.Variables { return updatedConfig }() +// VOLUME - MIN + var testConfigVolumeVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "availability_zone": config.StringVariable("eu01-1"), @@ -210,6 +224,8 @@ var testConfigVolumeVarsMinUpdated = func() config.Variables { return updatedConfig }() +// VOLUME - MAX + var testConfigVolumeVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "availability_zone": config.StringVariable("eu01-1"), @@ -232,48 +248,23 @@ var testConfigVolumeVarsMaxUpdated = func() config.Variables { return updatedConfig }() -var testConfigNetworkV1VarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), -} - -var testConfigNetworkV1VarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "ipv4_gateway": config.StringVariable("10.2.2.1"), - "ipv4_nameserver_0": config.StringVariable("10.2.2.2"), - "ipv4_nameserver_1": config.StringVariable("10.2.2.3"), - "ipv4_prefix": config.StringVariable("10.2.2.0/24"), - "ipv4_prefix_length": config.IntegerVariable(24), - "routed": config.BoolVariable(false), - "label": config.StringVariable("label"), -} - -var testConfigNetworkV1VarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigNetworkV1VarsMax { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - updatedConfig["ipv4_gateway"] = config.StringVariable("") - updatedConfig["ipv4_nameserver_0"] = config.StringVariable("10.2.2.10") - updatedConfig["label"] = config.StringVariable("updated") - return updatedConfig -}() +// NETWORK - MIN -var testConfigNetworkV2VarsMin = config.Variables{ +var testConfigNetworkVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), } -var testConfigNetworkV2VarsMinUpdated = func() config.Variables { +var testConfigNetworkVarsMinUpdated = func() config.Variables { updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigNetworkV2VarsMin) + maps.Copy(updatedConfig, testConfigNetworkVarsMin) updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) return updatedConfig }() -var testConfigNetworkV2VarsMax = config.Variables{ +// NETWORK - MAX + +var testConfigNetworkVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "ipv4_gateway": config.StringVariable("10.2.2.1"), @@ -287,9 +278,9 @@ var testConfigNetworkV2VarsMax = config.Variables{ "network_area_id": config.StringVariable(testNetworkAreaId), } -var testConfigNetworkV2VarsMaxUpdated = func() config.Variables { +var testConfigNetworkVarsMaxUpdated = func() config.Variables { updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigNetworkV2VarsMax) + maps.Copy(updatedConfig, testConfigNetworkVarsMax) updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) updatedConfig["ipv4_gateway"] = config.StringVariable("") updatedConfig["ipv4_nameserver_0"] = config.StringVariable("10.2.2.10") @@ -297,13 +288,11 @@ var testConfigNetworkV2VarsMaxUpdated = func() config.Variables { return updatedConfig }() +// NETWORK AREA - MIN + var testConfigNetworkAreaVarsMin = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "transfer_network": config.StringVariable("10.1.2.0/24"), - "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), - "route_prefix": config.StringVariable("1.1.1.0/24"), - "route_next_hop": config.StringVariable("1.1.1.1"), + "organization_id": config.StringVariable(testutil.OrganizationId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), } var testConfigNetworkAreaVarsMinUpdated = func() config.Variables { @@ -312,10 +301,11 @@ var testConfigNetworkAreaVarsMinUpdated = func() config.Variables { updatedConfig[k] = v } updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") return updatedConfig }() +// NETWORK AREA - MAX + var testConfigNetworkAreaVarsMax = config.Variables{ "organization_id": config.StringVariable(testutil.OrganizationId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), @@ -345,6 +335,52 @@ var testConfigNetworkAreaVarsMaxUpdated = func() config.Variables { return updatedConfig }() +// NETWORK AREA REGION - MIN + +var testConfigNetworkAreaRegionVarsMin = config.Variables{ + "organization_id": config.StringVariable(testutil.OrganizationId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), + "transfer_network": config.StringVariable("10.1.2.0/24"), + "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), +} + +var testConfigNetworkAreaRegionVarsMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + for k, v := range testConfigNetworkAreaRegionVarsMin { + updatedConfig[k] = v + } + updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") + return updatedConfig +}() + +// NETWORK AREA REGION - MAX + +var testConfigNetworkAreaRegionVarsMax = config.Variables{ + "organization_id": config.StringVariable(testutil.OrganizationId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), + "transfer_network": config.StringVariable("10.1.2.0/24"), + "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), + "default_nameservers": config.StringVariable("1.1.1.1"), + "default_prefix_length": config.IntegerVariable(26), + "min_prefix_length": config.IntegerVariable(25), + "max_prefix_length": config.IntegerVariable(28), +} + +var testConfigNetworkAreaRegionVarsMaxUpdated = func() config.Variables { + updatedConfig := config.Variables{} + for k, v := range testConfigNetworkAreaRegionVarsMax { + updatedConfig[k] = v + } + updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") + updatedConfig["default_nameservers"] = config.StringVariable("8.8.8.8") + updatedConfig["default_prefix_length"] = config.IntegerVariable(27) + updatedConfig["min_prefix_length"] = config.IntegerVariable(26) + updatedConfig["max_prefix_length"] = config.IntegerVariable(28) + return updatedConfig +}() + +// SECURITY GROUP - MIN + var testConfigSecurityGroupsVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), @@ -360,6 +396,8 @@ func testConfigSecurityGroupsVarsMinUpdated() config.Variables { return updatedConfig } +// SECURITY GROUP - MAX + var testConfigSecurityGroupsVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), @@ -390,6 +428,8 @@ func testConfigSecurityGroupsVarsMaxUpdated() config.Variables { return updatedConfig } +// IMAGE - MIN + var testConfigImageVarsMin = func() config.Variables { localFilePath := testutil.TestImageLocalFilePath if localFilePath == "default" { @@ -417,6 +457,8 @@ var testConfigImageVarsMinUpdated = func() config.Variables { return updatedConfig }() +// IMAGE - MAX + var testConfigImageVarsMax = func() config.Variables { localFilePath := testutil.TestImageLocalFilePath if localFilePath == "default" { @@ -476,11 +518,15 @@ var testConfigImageVarsMaxUpdated = func() config.Variables { return updatedConfig }() +// KEYPAIR - MIN + var testConfigKeyPairMin = config.Variables{ "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), "public_key": config.StringVariable(keypairPublicKey), } +// KEYPAIR - MAX + var testConfigKeyPairMax = config.Variables{ "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), "public_key": config.StringVariable(keypairPublicKey), @@ -503,290 +549,23 @@ var testConfigMachineTypeVars = config.Variables{ // if no local file is provided the test should create a default file and work with this instead of failing var localFileForIaasImage os.File -func TestAccNetworkV1Min(t *testing.T) { - t.Logf("TestAccNetworkV1Min name: %s", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkV1VarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkV1MinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkV1VarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network" "network" { - project_id = stackit_network.network.project_id - network_id = stackit_network.network.network_id - } - `, - testutil.IaaSProviderConfig(), resourceNetworkV1MinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), - ), - }, - - // Import - { - ConfigVariables: testConfigNetworkV1VarsMin, - ResourceName: "stackit_network.network", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - ), - }, - // In this minimal setup, no update can be performed - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkV1Max(t *testing.T) { - t.Logf("TestAccNetworkV1Max name: %s", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])) +func TestAccNetworkMin(t *testing.T) { + t.Logf("TestAccNetworkMin name: %s", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])) resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ // Creation { - ConfigVariables: testConfigNetworkV1VarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkV1MaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_gateway"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), - - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "public_ip"), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkV1VarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network" "network_prefix" { - project_id = stackit_network.network_prefix.project_id - network_id = stackit_network.network_prefix.network_id - } - - data "stackit_network" "network_prefix_length" { - project_id = stackit_network.network_prefix_length.project_id - network_id = stackit_network.network_prefix_length.network_id - } - `, - testutil.IaaSProviderConfig(), resourceNetworkV1MaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_gateway"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), - - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), - resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), - ), - }, - // Import - { - ConfigVariables: testConfigNetworkV1VarsMax, - ResourceName: "stackit_network.network_prefix", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network_prefix"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_gateway"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // nameservers may be returned in a randomized order, so we have to check them with a helper function - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - ), - }, - { - ConfigVariables: testConfigNetworkV1VarsMax, - ResourceName: "stackit_network.network_prefix_length", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network_prefix_length"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix_length") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - // nameservers may be returned in a randomized order, so we have to check them with a helper function - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - ), - }, - // Update - { - ConfigVariables: testConfigNetworkV1VarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkV1MaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["name"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), - - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["name"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_prefix_length"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "public_ip"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkV2Min(t *testing.T) { - t.Logf("TestAccNetworkV2Min name: %s", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckNetworkV2Destroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkV2VarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MinConfig), + ConfigVariables: testConfigNetworkVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMinConfig), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "region", testutil.Region), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), resource.TestCheckResourceAttrSet("stackit_network.network", "region"), resource.TestCheckResourceAttrSet("stackit_network.network", "routing_table_id"), @@ -794,7 +573,7 @@ func TestAccNetworkV2Min(t *testing.T) { }, // Data source { - ConfigVariables: testConfigNetworkV2VarsMin, + ConfigVariables: testConfigNetworkVarsMin, Config: fmt.Sprintf(` %s %s @@ -804,14 +583,15 @@ func TestAccNetworkV2Min(t *testing.T) { network_id = stackit_network.network.network_id } `, - testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MinConfig, + testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMinConfig, ), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])), + resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), + resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), + resource.TestCheckResourceAttr("data.stackit_network.network", "region", testutil.Region), resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("data.stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "region"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "routing_table_id"), @@ -820,30 +600,26 @@ func TestAccNetworkV2Min(t *testing.T) { // Import { - ConfigVariables: testConfigNetworkV2VarsMin, + ConfigVariables: testConfigNetworkVarsMin, ResourceName: "stackit_network.network", ImportStateIdFunc: func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources["stackit_network.network"] if !ok { return "", fmt.Errorf("couldn't find resource stackit_network.network") } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } networkId, ok := r.Primary.Attributes["network_id"] if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, networkId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil }, ImportState: true, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), resource.TestCheckResourceAttrSet("stackit_network.network", "region"), resource.TestCheckResourceAttrSet("stackit_network.network", "routing_table_id"), @@ -851,14 +627,14 @@ func TestAccNetworkV2Min(t *testing.T) { }, // Update { - ConfigVariables: testConfigNetworkV2VarsMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MinConfig), + ConfigVariables: testConfigNetworkVarsMinUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMinConfig), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMinUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMinUpdated["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMinUpdated["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMinUpdated["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), resource.TestCheckResourceAttrSet("stackit_network.network", "region"), resource.TestCheckResourceAttrSet("stackit_network.network", "routing_table_id"), @@ -869,49 +645,48 @@ func TestAccNetworkV2Min(t *testing.T) { }) } -func TestAccNetworkV2Max(t *testing.T) { - t.Logf("TestAccNetworkV2Max name: %s", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])) +func TestAccNetworkMax(t *testing.T) { + t.Logf("TestAccNetworkMax name: %s", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])) resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckNetworkV2Destroy, + CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ // Creation { - ConfigVariables: testConfigNetworkV2VarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MaxConfig), + ConfigVariables: testConfigNetworkVarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( - // TODO: enable test cases for prefix option, when the API works again // Network with prefix - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_gateway"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "public_ip"), // Network with prefix_length resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_gateway"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_gateway"), // resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "public_ip"), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), @@ -921,10 +696,10 @@ func TestAccNetworkV2Max(t *testing.T) { ), // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["network_area_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["network_area_id"])), resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), @@ -935,15 +710,15 @@ func TestAccNetworkV2Max(t *testing.T) { }, // Data source { - ConfigVariables: testConfigNetworkV2VarsMax, + ConfigVariables: testConfigNetworkVarsMax, Config: fmt.Sprintf(` %s %s - //data "stackit_network" "network_prefix" { - // project_id = stackit_network.network_prefix.project_id - // network_id = stackit_network.network_prefix.network_id - //} + data "stackit_network" "network_prefix" { + project_id = stackit_network.network_prefix.project_id + network_id = stackit_network.network_prefix.network_id + } data "stackit_network" "network_prefix_length" { project_id = stackit_network.network_prefix_length.project_id @@ -956,39 +731,38 @@ func TestAccNetworkV2Max(t *testing.T) { routing_table_id = stackit_routing_table.routing_table.routing_table_id } `, - testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MaxConfig, + testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMaxConfig, ), Check: resource.ComposeAggregateTestCheckFunc( - // TODO: enable test cases for prefix option, when the API works again // Network with prefix - // resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "network_id"), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_gateway"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - // resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - // resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "ipv6_prefixes.#"), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["project_id"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), // Network with prefix_length resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["project_id"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), // resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv4_gateway"), resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), + resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "region", testutil.Region), resource.TestCheckResourceAttrPair( @@ -997,10 +771,10 @@ func TestAccNetworkV2Max(t *testing.T) { ), // Routing table - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["network_area_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["network_area_id"])), resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.%", "0"), resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "region", testutil.Region), resource.TestCheckNoResourceAttr("data.stackit_routing_table.routing_table", "description"), @@ -1010,106 +784,100 @@ func TestAccNetworkV2Max(t *testing.T) { ), }, // Import - // TODO: enable test cases for prefix option, when the API works again - //{ - // ConfigVariables: testConfigNetworkV2VarsMax, - // ResourceName: "stackit_network.network_prefix", - // ImportStateIdFunc: func(s *terraform.State) (string, error) { - // r, ok := s.RootModule().Resources["stackit_network.network_prefix"] - // if !ok { - // return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix") - // } - // networkId, ok := r.Primary.Attributes["network_id"] - // if !ok { - // return "", fmt.Errorf("couldn't find attribute network_id") - // } - // return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil - // }, - // ImportState: true, - // Check: resource.ComposeAggregateTestCheckFunc( - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_gateway"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // // nameservers may be returned in a randomized order, so we have to check them with a helper function - // resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - // resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - // ), - // }, { - ConfigVariables: testConfigNetworkV2VarsMax, + ConfigVariables: testConfigNetworkVarsMax, + ResourceName: "stackit_network.network_prefix", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_network.network_prefix"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix") + } + networkId, ok := r.Primary.Attributes["network_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_id") + } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil + }, + ImportState: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + // nameservers may be returned in a randomized order, so we have to check them with a helper function + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + ), + }, + { + ConfigVariables: testConfigNetworkVarsMax, ResourceName: "stackit_network.network_prefix_length", ImportStateIdFunc: func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources["stackit_network.network_prefix_length"] if !ok { return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix_length") } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } networkId, ok := r.Primary.Attributes["network_id"] if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, networkId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil }, ImportState: true, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["project_id"])), // resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), // nameservers may be returned in a randomized order, so we have to check them with a helper function - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), ), }, // Update { - ConfigVariables: testConfigNetworkV2VarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MaxConfig), + ConfigVariables: testConfigNetworkVarsMaxUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( - // TODO: enable test cases for prefix option, when the API works again - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["project_id"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["name"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "ipv4_gateway"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv4_gateway"), // resource.TestCheckResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway", "true"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_0"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_1"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_prefix"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_prefix_length"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["routed"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["label"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["label"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "public_ip"), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["name"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_gateway"), // resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix_length"])), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["label"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["label"])), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "public_ip"), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), @@ -1119,10 +887,10 @@ func TestAccNetworkV2Max(t *testing.T) { ), // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["network_area_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["network_area_id"])), resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), @@ -1151,22 +919,7 @@ func TestAccNetworkAreaMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["organization_id"])), resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_ranges.0.network_range_id"), - - // Network Area Route - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "organization_id", - "stackit_network_area.network_area", "organization_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "network_area_id", - "stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["route_prefix"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["route_next_hop"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "0"), ), }, // Data source @@ -1180,12 +933,6 @@ func TestAccNetworkAreaMin(t *testing.T) { organization_id = stackit_network_area.network_area.organization_id network_area_id = stackit_network_area.network_area.network_area_id } - - data "stackit_network_area_route" "network_area_route" { - organization_id = stackit_network_area.network_area.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - network_area_route_id = stackit_network_area_route.network_area_route.network_area_route_id - } `, testutil.IaaSProviderConfig(), resourceNetworkAreaMinConfig, ), @@ -1198,26 +945,7 @@ func TestAccNetworkAreaMin(t *testing.T) { "stackit_network_area.network_area", "network_area_id", ), resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["name"])), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("data.stackit_network_area.network_area", "network_ranges.0.network_range_id"), - - // Network Area Route - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_route.network_area_route", "organization_id", - "data.stackit_network_area.network_area", "organization_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_route.network_area_route", "network_area_id", - "data.stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_route.network_area_route", "network_area_route_id", - "stackit_network_area_route.network_area_route", "network_area_route_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["route_prefix"])), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "next_hop", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["route_next_hop"])), + resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "network_ranges.#", "0"), ), }, // Import @@ -1225,36 +953,15 @@ func TestAccNetworkAreaMin(t *testing.T) { ConfigVariables: testConfigNetworkAreaVarsMinUpdated, ResourceName: "stackit_network_area.network_area", ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_area.network_area"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_area.network_area") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - return fmt.Sprintf("%s,%s", testutil.OrganizationId, networkAreaId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigNetworkAreaVarsMinUpdated, - ResourceName: "stackit_network_area_route.network_area_route", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_area_route.network_area_route"] + r, ok := s.RootModule().Resources["stackit_network_area.network_area"] if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_area_route.network_area_route") + return "", fmt.Errorf("couldn't find resource stackit_network_area.network_area") } networkAreaId, ok := r.Primary.Attributes["network_area_id"] if !ok { return "", fmt.Errorf("couldn't find attribute network_area_id") } - networkAreaRouteId, ok := r.Primary.Attributes["network_area_route_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_route_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, networkAreaRouteId), nil + return fmt.Sprintf("%s,%s", testutil.OrganizationId, networkAreaId), nil }, ImportState: true, ImportStateVerify: true, @@ -1268,22 +975,7 @@ func TestAccNetworkAreaMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["organization_id"])), resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["name"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_ranges.0.network_range_id"), - - // Network Area Route - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "organization_id", - "stackit_network_area.network_area", "organization_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "network_area_id", - "stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["route_prefix"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["route_next_hop"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "0"), ), }, // Deletion is done by the framework implicitly @@ -1403,8 +1095,21 @@ func TestAccNetworkAreaMax(t *testing.T) { } return fmt.Sprintf("%s,%s", testutil.OrganizationId, networkAreaId), nil }, - ImportState: true, - ImportStateVerify: true, + ImportState: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["organization_id"])), + resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["name"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["label"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_nameservers"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["max_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["min_prefix_length"])), + ), }, { ConfigVariables: testConfigNetworkAreaVarsMaxUpdated, @@ -1422,7 +1127,7 @@ func TestAccNetworkAreaMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_area_route_id") } - return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, networkAreaRouteId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.OrganizationId, networkAreaId, testutil.Region, networkAreaRouteId), nil }, ImportState: true, ImportStateVerify: true, @@ -1466,6 +1171,247 @@ func TestAccNetworkAreaMax(t *testing.T) { }) } +func TestAccNetworkAreaRegionMin(t *testing.T) { + t.Logf("TestAccNetworkAreaRegionMin name: %s", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["name"])) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigNetworkAreaRegionVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMinConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionCreate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + // Network Area + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["transfer_network"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckNoResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", "25"), // default value + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", "24"), // default value + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", "29"), // default value + ), + }, + // Data source + { + ConfigVariables: testConfigNetworkAreaRegionVarsMin, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_network_area_region" "network_area_region" { + organization_id = stackit_network_area_region.network_area_region.organization_id + network_area_id = stackit_network_area_region.network_area_region.network_area_id + } + `, + testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMinConfig, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionNoop), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["organization_id"])), + resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "network_area_id"), + resource.TestCheckResourceAttrPair( + "data.stackit_network_area_region.network_area_region", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["transfer_network"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", "25"), // default value + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", "24"), // default value + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", "29"), // default value + ), + }, + // Import + { + ConfigVariables: testConfigNetworkAreaRegionVarsMinUpdated, + ResourceName: "stackit_network_area_region.network_area_region", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_network_area_region.network_area_region"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network_area_region.network_area_region") + } + networkAreaId, ok := r.Primary.Attributes["network_area_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_area_id") + } + return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, testutil.Region), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigNetworkAreaRegionVarsMinUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMinConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + // Network Area + resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMinUpdated["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMinUpdated["transfer_network"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMinUpdated["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", "25"), // default value + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", "24"), // default value + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", "29"), // default value + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccNetworkAreaRegionMax(t *testing.T) { + t.Logf("TestAccNetworkAreaRegionMax name: %s", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["name"])) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigNetworkAreaRegionVarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMaxConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionCreate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + // Network Area + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["transfer_network"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_nameservers"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["min_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["max_prefix_length"])), + ), + }, + // Data source + { + ConfigVariables: testConfigNetworkAreaRegionVarsMax, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_network_area_region" "network_area_region" { + organization_id = stackit_network_area_region.network_area_region.organization_id + network_area_id = stackit_network_area_region.network_area_region.network_area_id + } + `, + testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMaxConfig, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionNoop), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["organization_id"])), + resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "network_area_id"), + resource.TestCheckResourceAttrPair( + "data.stackit_network_area_region.network_area_region", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["transfer_network"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#", "1"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_nameservers"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_prefix_length"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["min_prefix_length"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["max_prefix_length"])), + ), + }, + // Import + { + ConfigVariables: testConfigNetworkAreaRegionVarsMaxUpdated, + ResourceName: "stackit_network_area_region.network_area_region", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_network_area_region.network_area_region"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network_area_region.network_area_region") + } + networkAreaId, ok := r.Primary.Attributes["network_area_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_area_id") + } + return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, testutil.Region), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigNetworkAreaRegionVarsMaxUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMaxConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + // Network Area + resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["transfer_network"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["default_nameservers"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["default_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["min_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["max_prefix_length"])), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + func TestAccVolumeMin(t *testing.T) { t.Logf("TestAccVolumeMin name: null") resource.ParallelTest(t, resource.TestCase{ @@ -1480,6 +1426,7 @@ func TestAccVolumeMin(t *testing.T) { // Volume size resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "performance_class"), @@ -1488,6 +1435,7 @@ func TestAccVolumeMin(t *testing.T) { // Volume source resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_source", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "performance_class"), @@ -1525,6 +1473,7 @@ func TestAccVolumeMin(t *testing.T) { "stackit_volume.volume_size", "volume_id", "data.stackit_volume.volume_size", "volume_id", ), + resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), resource.TestCheckResourceAttrSet("data.stackit_volume.volume_size", "performance_class"), resource.TestCheckNoResourceAttr("data.stackit_volume.volume_size", "server_id"), @@ -1560,7 +1509,7 @@ func TestAccVolumeMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -1577,7 +1526,7 @@ func TestAccVolumeMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -1590,6 +1539,7 @@ func TestAccVolumeMin(t *testing.T) { // Volume size resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["size"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "performance_class"), @@ -1598,6 +1548,7 @@ func TestAccVolumeMin(t *testing.T) { // Volume source resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_source", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["availability_zone"])), // Volume from source doesn't change size. So here the initial size will be used resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), @@ -1629,6 +1580,7 @@ func TestAccVolumeMax(t *testing.T) { // Volume size resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), resource.TestCheckResourceAttr("stackit_volume.volume_size", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"])), @@ -1641,6 +1593,7 @@ func TestAccVolumeMax(t *testing.T) { // Volume source resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_source", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), resource.TestCheckResourceAttr("stackit_volume.volume_source", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"])), @@ -1682,6 +1635,7 @@ func TestAccVolumeMax(t *testing.T) { "stackit_volume.volume_size", "volume_id", "data.stackit_volume.volume_size", "volume_id", ), + resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), resource.TestCheckNoResourceAttr("data.stackit_volume.volume_size", "server_id"), resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), @@ -1726,7 +1680,7 @@ func TestAccVolumeMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -1743,7 +1697,7 @@ func TestAccVolumeMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -1818,7 +1772,11 @@ func TestAccServerMin(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_server.server", "desired_status"), resource.TestCheckNoResourceAttr("stackit_server.server", "user_data"), resource.TestCheckNoResourceAttr("stackit_server.server", "keypair_name"), - resource.TestCheckNoResourceAttr("stackit_server.server", "network_interfaces"), + resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"), + resource.TestCheckResourceAttrPair( + "stackit_server.server", "network_interfaces.0", + "stackit_network_interface.nic", "network_interface_id", + ), resource.TestCheckResourceAttrSet("stackit_server.server", "created_at"), resource.TestCheckResourceAttrSet("stackit_server.server", "launched_at"), resource.TestCheckResourceAttrSet("stackit_server.server", "updated_at"), @@ -1866,7 +1824,11 @@ func TestAccServerMin(t *testing.T) { resource.TestCheckNoResourceAttr("data.stackit_server.server", "desired_status"), resource.TestCheckNoResourceAttr("data.stackit_server.server", "user_data"), resource.TestCheckNoResourceAttr("data.stackit_server.server", "keypair_name"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "network_interfaces"), + resource.TestCheckResourceAttr("data.stackit_server.server", "network_interfaces.#", "1"), + resource.TestCheckResourceAttrPair( + "data.stackit_server.server", "network_interfaces.0", + "stackit_network_interface.nic", "network_interface_id", + ), resource.TestCheckResourceAttrSet("data.stackit_server.server", "created_at"), resource.TestCheckResourceAttrSet("data.stackit_server.server", "launched_at"), resource.TestCheckResourceAttrSet("data.stackit_server.server", "updated_at"), @@ -1885,7 +1847,7 @@ func TestAccServerMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute server_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, serverId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, serverId), nil }, ImportState: true, ImportStateVerify: true, @@ -1915,7 +1877,11 @@ func TestAccServerMin(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_server.server", "desired_status"), resource.TestCheckNoResourceAttr("stackit_server.server", "user_data"), resource.TestCheckNoResourceAttr("stackit_server.server", "keypair_name"), - resource.TestCheckNoResourceAttr("stackit_server.server", "network_interfaces"), + resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"), + resource.TestCheckResourceAttrPair( + "stackit_server.server", "network_interfaces.0", + "stackit_network_interface.nic", "network_interface_id", + ), resource.TestCheckResourceAttrSet("stackit_server.server", "created_at"), resource.TestCheckResourceAttrSet("stackit_server.server", "launched_at"), resource.TestCheckResourceAttrSet("stackit_server.server", "updated_at"), @@ -2121,7 +2087,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute affinity_group_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, affinityGroupId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, affinityGroupId), nil }, ImportState: true, ImportStateVerify: true, @@ -2138,7 +2104,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -2155,7 +2121,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -2176,7 +2142,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, serverId, volumeId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, serverId, volumeId), nil }, ImportState: true, ImportStateVerify: false, @@ -2193,7 +2159,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil }, ImportState: true, ImportStateVerify: true, @@ -2215,7 +2181,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -2236,7 +2202,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -2257,7 +2223,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, serverId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, serverId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: false, @@ -2295,7 +2261,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, serverId, serviceAccountEmail), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, serverId, serviceAccountEmail), nil }, ImportState: true, ImportStateVerify: false, @@ -2312,7 +2278,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute server_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, serverId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, serverId), nil }, ImportState: true, ImportStateVerify: true, @@ -2587,7 +2553,7 @@ func TestAccAffinityGroupMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute affinity_group_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, affinityGroupId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, affinityGroupId), nil }, ImportState: true, ImportStateVerify: true, @@ -2684,7 +2650,7 @@ func TestAccIaaSSecurityGroupMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, securityGroupId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId), nil }, ImportState: true, ImportStateVerify: true, @@ -2705,7 +2671,7 @@ func TestAccIaaSSecurityGroupMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_rule_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, securityGroupId, securityGroupRuleId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId, securityGroupRuleId), nil }, ImportState: true, ImportStateVerify: true, @@ -2992,7 +2958,7 @@ func TestAccIaaSSecurityGroupMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, securityGroupId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId), nil }, ImportState: true, ImportStateVerify: true, @@ -3013,7 +2979,7 @@ func TestAccIaaSSecurityGroupMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_rule_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, securityGroupId, securityGroupRuleId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId, securityGroupRuleId), nil }, ImportState: true, ImportStateVerify: true, @@ -3127,7 +3093,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["project_id"])), resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), // Public ip @@ -3180,7 +3146,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["project_id"])), resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["name"])), resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("data.stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), // Public ip @@ -3215,7 +3181,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -3232,7 +3198,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil }, ImportState: true, ImportStateVerify: true, @@ -3249,7 +3215,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute public_ip_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, publicIpId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId), nil }, ImportState: true, ImportStateVerify: true, @@ -3298,7 +3264,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), // Public ip @@ -3407,7 +3373,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"])), resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("data.stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), // Public ip @@ -3472,7 +3438,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -3489,7 +3455,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil }, ImportState: true, ImportStateVerify: true, @@ -3506,7 +3472,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute public_ip_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, publicIpId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId), nil }, ImportState: true, ImportStateVerify: true, @@ -3527,7 +3493,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -3544,7 +3510,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute public_ip_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, publicIpId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId), nil }, ImportState: true, ImportStateVerify: true, @@ -3565,7 +3531,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, publicIpId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId, networkInterfaceId), nil }, ImportState: true, Check: resource.ComposeAggregateTestCheckFunc( @@ -3603,7 +3569,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), // Public ip @@ -3629,10 +3595,10 @@ func TestAccNetworkInterfaceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_public_ip.public_ip_simple", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "public_ip_id"), resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "ip"), - resource.TestCheckResourceAttrPair( - "stackit_public_ip.public_ip_simple", "network_interface_id", - "stackit_network_interface.network_interface_simple", "network_interface_id", - ), + // The network gets re-created, which triggers a re-create of the 'network_interface_simple' NIC, which leads the 'stackit_public_ip_associate' resource to update the + // networkInterfaceId of the public IP. All that without the public ip resource noticing. So the public ip resource will still hold the networkInterfaceId of the old NIC. + // So we can only check that *some* network interface ID is set here, but can't compare it with the networkInterfaceId of the NIC resource (old vs. new NIC id) + resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "network_interface_id"), resource.TestCheckResourceAttr("stackit_public_ip.public_ip_simple", "labels.%", "0"), // Nic and public ip attach @@ -3860,7 +3826,7 @@ func TestAccImageMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute image_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, imageId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, imageId), nil }, ImportState: true, ImportStateVerify: true, @@ -3990,7 +3956,7 @@ func TestAccImageMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute image_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, imageId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, imageId), nil }, ImportState: true, ImportStateVerify: true, @@ -4035,8 +4001,8 @@ func TestAccImageMax(t *testing.T) { }) } -func TestAccImageV2DatasourceSearchVariants(t *testing.T) { - t.Log("TestDataSource Image V2 Variants") +func TestAccImageDatasourceSearchVariants(t *testing.T) { + t.Log("TestDataSource Image Variants") resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ @@ -4205,6 +4171,7 @@ func TestAccProject(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "area_id"), resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "internet_access"), resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "state"), + resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "status"), resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "created_at"), resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "updated_at"), ), @@ -4256,9 +4223,6 @@ func TestAccMachineType(t *testing.T) { func testAccCheckDestroy(s *terraform.State) error { checkFunctions := []func(s *terraform.State) error{ - testAccCheckNetworkV1Destroy, - testAccCheckNetworkInterfaceDestroy, - testAccCheckNetworkAreaDestroy, testAccCheckIaaSVolumeDestroy, testAccCheckServerDestroy, testAccCheckAffinityGroupDestroy, @@ -4266,6 +4230,10 @@ func testAccCheckDestroy(s *terraform.State) error { testAccCheckIaaSPublicIpDestroy, testAccCheckIaaSKeyPairDestroy, testAccCheckIaaSImageDestroy, + testAccCheckNetworkDestroy, + testAccCheckNetworkInterfaceDestroy, + testAccCheckNetworkAreaRegionDestroy, + testAccCheckNetworkAreaDestroy, } var errs []error @@ -4285,16 +4253,14 @@ func testAccCheckDestroy(s *terraform.State) error { return errors.Join(errs...) } -func testAccCheckNetworkV1Destroy(s *terraform.State) error { +func testAccCheckNetworkDestroy(s *terraform.State) error { ctx := context.Background() - var client *iaas.APIClient + var client *iaasalpha.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaasalpha.NewAPIClient() } else { - client, err = iaas.NewAPIClient( + client, err = iaasalpha.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } @@ -4308,8 +4274,9 @@ func testAccCheckNetworkV1Destroy(s *terraform.State) error { if rs.Type != "stackit_network" { continue } - networkId := strings.Split(rs.Primary.ID, core.Separator)[1] - err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, networkId) + region := strings.Split(rs.Primary.ID, core.Separator)[1] + networkId := strings.Split(rs.Primary.ID, core.Separator)[2] + err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, region, networkId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) { @@ -4319,7 +4286,7 @@ func testAccCheckNetworkV1Destroy(s *terraform.State) error { } errs = append(errs, fmt.Errorf("cannot trigger network deletion %q: %w", networkId, err)) } - _, err = wait.DeleteNetworkWaitHandler(ctx, client, testutil.ProjectId, networkId).WaitWithContext(ctx) + _, err = waitAlpha.DeleteNetworkWaitHandler(ctx, client, testutil.ProjectId, region, networkId).WaitWithContext(ctx) if err != nil { errs = append(errs, fmt.Errorf("cannot delete network %q: %w", networkId, err)) } @@ -4328,14 +4295,14 @@ func testAccCheckNetworkV1Destroy(s *terraform.State) error { return errors.Join(errs...) } -func testAccCheckNetworkV2Destroy(s *terraform.State) error { +func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { ctx := context.Background() - var client *iaasalpha.APIClient + var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaasalpha.NewAPIClient() + client, err = iaas.NewAPIClient() } else { - client, err = iaasalpha.NewAPIClient( + client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } @@ -4344,40 +4311,39 @@ func testAccCheckNetworkV2Destroy(s *terraform.State) error { } var errs []error - // networks + // network interfaces for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_network" { + if rs.Type != "stackit_network_interface" { continue } - region := strings.Split(rs.Primary.ID, core.Separator)[1] - networkId := strings.Split(rs.Primary.ID, core.Separator)[2] - err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, region, networkId) + ids := strings.Split(rs.Primary.ID, core.Separator) + region := ids[1] + networkId := ids[2] + networkInterfaceId := ids[3] + err := client.DeleteNicExecute(ctx, testutil.ProjectId, region, networkId, networkInterfaceId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { + if oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest { continue } } - errs = append(errs, fmt.Errorf("cannot trigger network deletion %q: %w", networkId, err)) + errs = append(errs, fmt.Errorf("cannot trigger network interface deletion %q: %w", networkInterfaceId, err)) } - _, err = waitAlpha.DeleteNetworkWaitHandler(ctx, client, testutil.ProjectId, region, networkId).WaitWithContext(ctx) if err != nil { - errs = append(errs, fmt.Errorf("cannot delete network %q: %w", networkId, err)) + errs = append(errs, fmt.Errorf("cannot delete network interface %q: %w", networkInterfaceId, err)) } } return errors.Join(errs...) } -func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { +func testAccCheckNetworkAreaRegionDestroy(s *terraform.State) error { ctx := context.Background() var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4387,31 +4353,34 @@ func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { return fmt.Errorf("creating client: %w", err) } - var errs []error - // network interfaces + // network areas + networkAreasToDestroy := []string{} for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_network_interface" { + if rs.Type != "stackit_network_area_region" { continue } - ids := strings.Split(rs.Primary.ID, core.Separator) - networkId := ids[1] - networkInterfaceId := ids[2] - err := client.DeleteNicExecute(ctx, testutil.ProjectId, networkId, networkInterfaceId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest { - continue - } - } - errs = append(errs, fmt.Errorf("cannot trigger network interface deletion %q: %w", networkInterfaceId, err)) + networkAreaId := strings.Split(rs.Primary.ID, core.Separator)[1] + networkAreasToDestroy = append(networkAreasToDestroy, networkAreaId) + } + + networkAreasResp, err := client.ListNetworkAreasExecute(ctx, testutil.OrganizationId) + if err != nil { + return fmt.Errorf("getting networkAreasResp: %w", err) + } + + networkAreas := *networkAreasResp.Items + for i := range networkAreas { + if networkAreas[i].Id == nil { + continue } - if err != nil { - errs = append(errs, fmt.Errorf("cannot delete network interface %q: %w", networkInterfaceId, err)) + if utils.Contains(networkAreasToDestroy, *networkAreas[i].Id) { + err := client.DeleteNetworkAreaRegionExecute(ctx, testutil.OrganizationId, *networkAreas[i].Id, testutil.Region) + if err != nil { + return fmt.Errorf("destroying network area %s during CheckDestroy: %w", *networkAreas[i].Id, err) + } } } - - return errors.Join(errs...) + return nil } func testAccCheckNetworkAreaDestroy(s *terraform.State) error { @@ -4419,9 +4388,7 @@ func testAccCheckNetworkAreaDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4448,13 +4415,13 @@ func testAccCheckNetworkAreaDestroy(s *terraform.State) error { networkAreas := *networkAreasResp.Items for i := range networkAreas { - if networkAreas[i].AreaId == nil { + if networkAreas[i].Id == nil { continue } - if utils.Contains(networkAreasToDestroy, *networkAreas[i].AreaId) { - err := client.DeleteNetworkAreaExecute(ctx, testutil.OrganizationId, *networkAreas[i].AreaId) + if utils.Contains(networkAreasToDestroy, *networkAreas[i].Id) { + err := client.DeleteNetworkAreaExecute(ctx, testutil.OrganizationId, *networkAreas[i].Id) if err != nil { - return fmt.Errorf("destroying network area %s during CheckDestroy: %w", *networkAreas[i].AreaId, err) + return fmt.Errorf("destroying network area %s during CheckDestroy: %w", *networkAreas[i].Id, err) } } } @@ -4466,9 +4433,7 @@ func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4488,7 +4453,7 @@ func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { volumesToDestroy = append(volumesToDestroy, volumeId) } - volumesResp, err := client.ListVolumesExecute(ctx, testutil.ProjectId) + volumesResp, err := client.ListVolumesExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting volumesResp: %w", err) } @@ -4499,7 +4464,7 @@ func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { continue } if utils.Contains(volumesToDestroy, *volumes[i].Id) { - err := client.DeleteVolumeExecute(ctx, testutil.ProjectId, *volumes[i].Id) + err := client.DeleteVolumeExecute(ctx, testutil.ProjectId, testutil.Region, *volumes[i].Id) if err != nil { return fmt.Errorf("destroying volume %s during CheckDestroy: %w", *volumes[i].Id, err) } @@ -4515,19 +4480,13 @@ func testAccCheckServerDestroy(s *terraform.State) error { var err error var alphaErr error if testutil.IaaSCustomEndpoint == "" { - alphaClient, alphaErr = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + alphaClient, alphaErr = iaas.NewAPIClient() + client, err = iaas.NewAPIClient() } else { alphaClient, alphaErr = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } if err != nil || alphaErr != nil { return fmt.Errorf("creating client: %w, %w", err, alphaErr) @@ -4540,12 +4499,12 @@ func testAccCheckServerDestroy(s *terraform.State) error { if rs.Type != "stackit_server" { continue } - // server terraform ID: "[project_id],[server_id]" - serverId := strings.Split(rs.Primary.ID, core.Separator)[1] + // server terraform ID: "[project_id],[region],[server_id]" + serverId := strings.Split(rs.Primary.ID, core.Separator)[2] serversToDestroy = append(serversToDestroy, serverId) } - serversResp, err := alphaClient.ListServersExecute(ctx, testutil.ProjectId) + serversResp, err := alphaClient.ListServersExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting serversResp: %w", err) } @@ -4556,7 +4515,7 @@ func testAccCheckServerDestroy(s *terraform.State) error { continue } if utils.Contains(serversToDestroy, *servers[i].Id) { - err := alphaClient.DeleteServerExecute(ctx, testutil.ProjectId, *servers[i].Id) + err := alphaClient.DeleteServerExecute(ctx, testutil.ProjectId, testutil.Region, *servers[i].Id) if err != nil { return fmt.Errorf("destroying server %s during CheckDestroy: %w", *servers[i].Id, err) } @@ -4575,20 +4534,20 @@ func testAccCheckServerDestroy(s *terraform.State) error { networksToDestroy = append(networksToDestroy, networkId) } - networksResp, err := client.ListNetworksExecute(ctx, testutil.ProjectId) + networksResp, err := client.ListNetworksExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting networksResp: %w", err) } networks := *networksResp.Items for i := range networks { - if networks[i].NetworkId == nil { + if networks[i].Id == nil { continue } - if utils.Contains(networksToDestroy, *networks[i].NetworkId) { - err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, *networks[i].NetworkId) + if utils.Contains(networksToDestroy, *networks[i].Id) { + err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, testutil.Region, *networks[i].Id) if err != nil { - return fmt.Errorf("destroying network %s during CheckDestroy: %w", *networks[i].NetworkId, err) + return fmt.Errorf("destroying network %s during CheckDestroy: %w", *networks[i].Id, err) } } } @@ -4601,9 +4560,7 @@ func testAccCheckAffinityGroupDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4618,12 +4575,12 @@ func testAccCheckAffinityGroupDestroy(s *terraform.State) error { if rs.Type != "stackit_affinity_group" { continue } - // affinity group terraform ID: "[project_id],[affinity_group_id]" - affinityGroupId := strings.Split(rs.Primary.ID, core.Separator)[1] + // affinity group terraform ID: "[project_id],[region],[affinity_group_id]" + affinityGroupId := strings.Split(rs.Primary.ID, core.Separator)[2] affinityGroupsToDestroy = append(affinityGroupsToDestroy, affinityGroupId) } - affinityGroupsResp, err := client.ListAffinityGroupsExecute(ctx, testutil.ProjectId) + affinityGroupsResp, err := client.ListAffinityGroupsExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting securityGroupsResp: %w", err) } @@ -4634,7 +4591,7 @@ func testAccCheckAffinityGroupDestroy(s *terraform.State) error { continue } if utils.Contains(affinityGroupsToDestroy, *affinityGroups[i].Id) { - err := client.DeleteAffinityGroupExecute(ctx, testutil.ProjectId, *affinityGroups[i].Id) + err := client.DeleteAffinityGroupExecute(ctx, testutil.ProjectId, testutil.Region, *affinityGroups[i].Id) if err != nil { return fmt.Errorf("destroying affinity group %s during CheckDestroy: %w", *affinityGroups[i].Id, err) } @@ -4648,9 +4605,7 @@ func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4665,12 +4620,12 @@ func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { if rs.Type != "stackit_security_group" { continue } - // security group terraform ID: "[project_id],[security_group_id]" - securityGroupId := strings.Split(rs.Primary.ID, core.Separator)[1] + // security group terraform ID: "[project_id],[region],[security_group_id]" + securityGroupId := strings.Split(rs.Primary.ID, core.Separator)[2] securityGroupsToDestroy = append(securityGroupsToDestroy, securityGroupId) } - securityGroupsResp, err := client.ListSecurityGroupsExecute(ctx, testutil.ProjectId) + securityGroupsResp, err := client.ListSecurityGroupsExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting securityGroupsResp: %w", err) } @@ -4681,7 +4636,7 @@ func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { continue } if utils.Contains(securityGroupsToDestroy, *securityGroups[i].Id) { - err := client.DeleteSecurityGroupExecute(ctx, testutil.ProjectId, *securityGroups[i].Id) + err := client.DeleteSecurityGroupExecute(ctx, testutil.ProjectId, testutil.Region, *securityGroups[i].Id) if err != nil { return fmt.Errorf("destroying security group %s during CheckDestroy: %w", *securityGroups[i].Id, err) } @@ -4695,9 +4650,7 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4712,12 +4665,12 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { if rs.Type != "stackit_public_ip" { continue } - // public IP terraform ID: "[project_id],[public_ip_id]" - publicIpId := strings.Split(rs.Primary.ID, core.Separator)[1] + // public IP terraform ID: "[project_id],[region],[public_ip_id]" + publicIpId := strings.Split(rs.Primary.ID, core.Separator)[2] publicIpsToDestroy = append(publicIpsToDestroy, publicIpId) } - publicIpsResp, err := client.ListPublicIPsExecute(ctx, testutil.ProjectId) + publicIpsResp, err := client.ListPublicIPsExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting publicIpsResp: %w", err) } @@ -4728,7 +4681,7 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { continue } if utils.Contains(publicIpsToDestroy, *publicIps[i].Id) { - err := client.DeletePublicIPExecute(ctx, testutil.ProjectId, *publicIps[i].Id) + err := client.DeletePublicIPExecute(ctx, testutil.ProjectId, testutil.Region, *publicIps[i].Id) if err != nil { return fmt.Errorf("destroying public IP %s during CheckDestroy: %w", *publicIps[i].Id, err) } @@ -4742,9 +4695,7 @@ func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4789,9 +4740,7 @@ func testAccCheckIaaSImageDestroy(s *terraform.State) error { var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4806,12 +4755,12 @@ func testAccCheckIaaSImageDestroy(s *terraform.State) error { if rs.Type != "stackit_image" { continue } - // Image terraform ID: "[project_id],[image_id]" - imageId := strings.Split(rs.Primary.ID, core.Separator)[1] + // Image terraform ID: "[project_id],[region],[image_id]" + imageId := strings.Split(rs.Primary.ID, core.Separator)[2] imagesToDestroy = append(imagesToDestroy, imageId) } - imagesResp, err := client.ListImagesExecute(ctx, testutil.ProjectId) + imagesResp, err := client.ListImagesExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting images: %w", err) } @@ -4822,7 +4771,7 @@ func testAccCheckIaaSImageDestroy(s *terraform.State) error { continue } if utils.Contains(imagesToDestroy, *images[i].Id) { - err := client.DeleteImageExecute(ctx, testutil.ProjectId, *images[i].Id) + err := client.DeleteImageExecute(ctx, testutil.ProjectId, testutil.Region, *images[i].Id) if err != nil { return fmt.Errorf("destroying image %s during CheckDestroy: %w", *images[i].Id, err) } diff --git a/stackit/internal/services/iaas/image/datasource.go b/stackit/internal/services/iaas/image/datasource.go index 4b24a8ff7..cb49d4f32 100644 --- a/stackit/internal/services/iaas/image/datasource.go +++ b/stackit/internal/services/iaas/image/datasource.go @@ -30,6 +30,7 @@ var ( type DataSourceModel struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ImageId types.String `tfsdk:"image_id"` Name types.String `tfsdk:"name"` DiskFormat types.String `tfsdk:"disk_format"` @@ -49,7 +50,8 @@ func NewImageDataSource() datasource.DataSource { // imageDataSource is the data source implementation. type imageDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -58,12 +60,13 @@ func (d *imageDataSource) Metadata(_ context.Context, req datasource.MetadataReq } func (d *imageDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -72,14 +75,14 @@ func (d *imageDataSource) Configure(ctx context.Context, req datasource.Configur } // Schema defines the schema for the datasource. -func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Image datasource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -90,6 +93,11 @@ func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "image_id": schema.StringAttribute{ Description: "The image ID.", Required: true, @@ -203,20 +211,23 @@ func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, } } -// // Read refreshes the Terraform state with the latest data. -func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +// Read refreshes the Terraform state with the latest data. +func (d *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) imageId := model.ImageId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "image_id", imageId) - imageResp, err := r.client.GetImage(ctx, projectId, imageId).Execute() + imageResp, err := d.client.GetImage(ctx, projectId, region, imageId).Execute() if err != nil { utils.LogError( ctx, @@ -233,7 +244,7 @@ func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, } // Map response body to schema - err = mapDataSourceFields(ctx, imageResp, &model) + err = mapDataSourceFields(ctx, imageResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Processing API payload: %v", err)) return @@ -247,7 +258,7 @@ func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, tflog.Info(ctx, "image read") } -func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel) error { +func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel, region string) error { if imageResp == nil { return fmt.Errorf("response input is nil") } @@ -264,7 +275,8 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data return fmt.Errorf("image id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, imageId) + model.Region = types.StringValue(region) // Map config var configModel = &configModel{} diff --git a/stackit/internal/services/iaas/image/datasource_test.go b/stackit/internal/services/iaas/image/datasource_test.go index a16120ac9..37d812359 100644 --- a/stackit/internal/services/iaas/image/datasource_test.go +++ b/stackit/internal/services/iaas/image/datasource_test.go @@ -12,69 +12,81 @@ import ( ) func TestMapDataSourceFields(t *testing.T) { + type args struct { + state DataSourceModel + input *iaas.Image + region string + } tests := []struct { description string - state DataSourceModel - input *iaas.Image + args args expected DataSourceModel isValid bool }{ { - "default_values", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - }, - &iaas.Image{ - Id: utils.Ptr("iid"), + description: "default_values", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + ImageId: types.StringValue("iid"), + }, + input: &iaas.Image{ + Id: utils.Ptr("iid"), + }, + region: "eu01", }, - DataSourceModel{ - Id: types.StringValue("pid,iid"), + expected: DataSourceModel{ + Id: types.StringValue("pid,eu01,iid"), ProjectId: types.StringValue("pid"), ImageId: types.StringValue("iid"), Labels: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - }, - &iaas.Image{ - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - DiskFormat: utils.Ptr("format"), - MinDiskSize: utils.Ptr(int64(1)), - MinRam: utils.Ptr(int64(1)), - Protected: utils.Ptr(true), - Scope: utils.Ptr("scope"), - Config: &iaas.ImageConfig{ - BootMenu: utils.Ptr(true), - CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), - DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), - NicModel: iaas.NewNullableString(utils.Ptr("model")), - OperatingSystem: utils.Ptr("os"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), - RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), - RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), - SecureBoot: utils.Ptr(true), - Uefi: utils.Ptr(true), - VideoModel: iaas.NewNullableString(utils.Ptr("model")), - VirtioScsi: utils.Ptr(true), + description: "simple_values", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + ImageId: types.StringValue("iid"), + Region: types.StringValue("eu01"), }, - Checksum: &iaas.ImageChecksum{ - Algorithm: utils.Ptr("algorithm"), - Digest: utils.Ptr("digest"), - }, - Labels: &map[string]interface{}{ - "key": "value", + input: &iaas.Image{ + Id: utils.Ptr("iid"), + Name: utils.Ptr("name"), + DiskFormat: utils.Ptr("format"), + MinDiskSize: utils.Ptr(int64(1)), + MinRam: utils.Ptr(int64(1)), + Protected: utils.Ptr(true), + Scope: utils.Ptr("scope"), + Config: &iaas.ImageConfig{ + BootMenu: utils.Ptr(true), + CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), + DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), + NicModel: iaas.NewNullableString(utils.Ptr("model")), + OperatingSystem: utils.Ptr("os"), + OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), + OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), + RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), + RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), + SecureBoot: utils.Ptr(true), + Uefi: utils.Ptr(true), + VideoModel: iaas.NewNullableString(utils.Ptr("model")), + VirtioScsi: utils.Ptr(true), + }, + Checksum: &iaas.ImageChecksum{ + Algorithm: utils.Ptr("algorithm"), + Digest: utils.Ptr("digest"), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, }, + region: "eu02", }, - DataSourceModel{ - Id: types.StringValue("pid,iid"), + expected: DataSourceModel{ + Id: types.StringValue("pid,eu02,iid"), ProjectId: types.StringValue("pid"), ImageId: types.StringValue("iid"), Name: types.StringValue("name"), @@ -105,47 +117,50 @@ func TestMapDataSourceFields(t *testing.T) { Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ "key": types.StringValue("value"), }), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - &iaas.Image{ - Id: utils.Ptr("iid"), + description: "empty_labels", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + ImageId: types.StringValue("iid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + input: &iaas.Image{ + Id: utils.Ptr("iid"), + }, + region: "eu01", }, - DataSourceModel{ - Id: types.StringValue("pid,iid"), + expected: DataSourceModel{ + Id: types.StringValue("pid,eu01,iid"), ProjectId: types.StringValue("pid"), ImageId: types.StringValue("iid"), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - DataSourceModel{}, - nil, - DataSourceModel{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - DataSourceModel{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.Image{}, }, - &iaas.Image{}, - DataSourceModel{}, - false, + expected: DataSourceModel{}, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -153,7 +168,7 @@ func TestMapDataSourceFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/image/resource.go b/stackit/internal/services/iaas/image/resource.go index 2d9716836..2ed8d4340 100644 --- a/stackit/internal/services/iaas/image/resource.go +++ b/stackit/internal/services/iaas/image/resource.go @@ -15,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -40,11 +39,13 @@ var ( _ resource.Resource = &imageResource{} _ resource.ResourceWithConfigure = &imageResource{} _ resource.ResourceWithImportState = &imageResource{} + _ resource.ResourceWithModifyPlan = &imageResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ImageId types.String `tfsdk:"image_id"` Name types.String `tfsdk:"name"` DiskFormat types.String `tfsdk:"disk_format"` @@ -111,7 +112,8 @@ func NewImageResource() resource.Resource { // imageResource is the resource implementation. type imageResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -119,14 +121,45 @@ func (r *imageResource) Metadata(_ context.Context, req resource.MetadataRequest resp.TypeName = req.ProviderTypeName + "_image" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *imageResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *imageResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -140,7 +173,7 @@ func (r *imageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp Description: "Image resource schema. Must have a `region` specified in the provider configuration.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -157,6 +190,15 @@ func (r *imageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "image_id": schema.StringAttribute{ Description: "The image ID.", Computed: true, @@ -378,7 +420,9 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) // Generate API request body from model payload, err := toCreatePayload(ctx, &model) @@ -388,22 +432,22 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, } // Create new image - imageCreateResp, err := r.client.CreateImage(ctx, projectId).CreateImagePayload(*payload).Execute() + imageCreateResp, err := r.client.CreateImage(ctx, projectId, region).CreateImagePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Calling API: %v", err)) return } ctx = tflog.SetField(ctx, "image_id", *imageCreateResp.Id) - // Get the image object, as the create response does not contain all fields - image, err := r.client.GetImage(ctx, projectId, *imageCreateResp.Id).Execute() + // Get the image object, as the creation response does not contain all fields + image, err := r.client.GetImage(ctx, projectId, region, *imageCreateResp.Id).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Calling API: %v", err)) return } // Map response body to schema - err = mapFields(ctx, image, &model) + err = mapFields(ctx, image, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Processing API payload: %v", err)) return @@ -424,7 +468,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, } // Wait for image to become available - waiter := wait.UploadImageWaitHandler(ctx, r.client, projectId, *imageCreateResp.Id) + waiter := wait.UploadImageWaitHandler(ctx, r.client, projectId, region, *imageCreateResp.Id) waiter = waiter.SetTimeout(7 * 24 * time.Hour) // Set timeout to one week, to make the timeout useless waitResp, err := waiter.WaitWithContext(ctx) if err != nil { @@ -433,7 +477,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, } // Map response body to schema - err = mapFields(ctx, waitResp, &model) + err = mapFields(ctx, waitResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Processing API payload: %v", err)) return @@ -448,7 +492,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, tflog.Info(ctx, "Image created") } -// // Read refreshes the Terraform state with the latest data. +// Read refreshes the Terraform state with the latest data. func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) @@ -456,12 +500,15 @@ func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) imageId := model.ImageId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "image_id", imageId) - imageResp, err := r.client.GetImage(ctx, projectId, imageId).Execute() + imageResp, err := r.client.GetImage(ctx, projectId, region, imageId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -473,7 +520,7 @@ func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp } // Map response body to schema - err = mapFields(ctx, imageResp, &model) + err = mapFields(ctx, imageResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Processing API payload: %v", err)) return @@ -496,9 +543,12 @@ func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest, if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) imageId := model.ImageId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "image_id", imageId) // Retrieve values from state @@ -516,13 +566,13 @@ func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest, return } // Update existing image - updatedImage, err := r.client.UpdateImage(ctx, projectId, imageId).UpdateImagePayload(*payload).Execute() + updatedImage, err := r.client.UpdateImage(ctx, projectId, region, imageId).UpdateImagePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(ctx, updatedImage, &model) + err = mapFields(ctx, updatedImage, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Processing API payload: %v", err)) return @@ -547,16 +597,18 @@ func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest, projectId := model.ProjectId.ValueString() imageId := model.ImageId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "image_id", imageId) + ctx = tflog.SetField(ctx, "region", region) // Delete existing image - err := r.client.DeleteImage(ctx, projectId, imageId).Execute() + err := r.client.DeleteImage(ctx, projectId, region, imageId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("Calling API: %v", err)) return } - _, err = wait.DeleteImageWaitHandler(ctx, r.client, projectId, imageId).WaitWithContext(ctx) + _, err = wait.DeleteImageWaitHandler(ctx, r.client, projectId, region, imageId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("image deletion waiting: %v", err)) return @@ -566,29 +618,28 @@ func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest, } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,image_id +// The expected format of the resource import identifier is: project_id,region,image_id func (r *imageResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing image", - fmt.Sprintf("Expected import identifier with format: [project_id],[image_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[image_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - imageId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "image_id", imageId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "image_id": idParts[2], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("image_id"), imageId)...) tflog.Info(ctx, "Image state imported") } -func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error { +func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model, region string) error { if imageResp == nil { return fmt.Errorf("response input is nil") } @@ -605,7 +656,8 @@ func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error { return fmt.Errorf("image id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, imageId) + model.Region = types.StringValue(region) // Map config var configModel = &configModel{} diff --git a/stackit/internal/services/iaas/image/resource_test.go b/stackit/internal/services/iaas/image/resource_test.go index 23b894dfc..2040bdd69 100644 --- a/stackit/internal/services/iaas/image/resource_test.go +++ b/stackit/internal/services/iaas/image/resource_test.go @@ -17,69 +17,81 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.Image + region string + } tests := []struct { description string - state Model - input *iaas.Image + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - }, - &iaas.Image{ - Id: utils.Ptr("iid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + ImageId: types.StringValue("iid"), + }, + input: &iaas.Image{ + Id: utils.Ptr("iid"), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,iid"), + expected: Model{ + Id: types.StringValue("pid,eu01,iid"), ProjectId: types.StringValue("pid"), ImageId: types.StringValue("iid"), Labels: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - }, - &iaas.Image{ - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - DiskFormat: utils.Ptr("format"), - MinDiskSize: utils.Ptr(int64(1)), - MinRam: utils.Ptr(int64(1)), - Protected: utils.Ptr(true), - Scope: utils.Ptr("scope"), - Config: &iaas.ImageConfig{ - BootMenu: utils.Ptr(true), - CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), - DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), - NicModel: iaas.NewNullableString(utils.Ptr("model")), - OperatingSystem: utils.Ptr("os"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), - RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), - RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), - SecureBoot: utils.Ptr(true), - Uefi: utils.Ptr(true), - VideoModel: iaas.NewNullableString(utils.Ptr("model")), - VirtioScsi: utils.Ptr(true), + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + ImageId: types.StringValue("iid"), + Region: types.StringValue("eu01"), }, - Checksum: &iaas.ImageChecksum{ - Algorithm: utils.Ptr("algorithm"), - Digest: utils.Ptr("digest"), - }, - Labels: &map[string]interface{}{ - "key": "value", + input: &iaas.Image{ + Id: utils.Ptr("iid"), + Name: utils.Ptr("name"), + DiskFormat: utils.Ptr("format"), + MinDiskSize: utils.Ptr(int64(1)), + MinRam: utils.Ptr(int64(1)), + Protected: utils.Ptr(true), + Scope: utils.Ptr("scope"), + Config: &iaas.ImageConfig{ + BootMenu: utils.Ptr(true), + CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), + DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), + NicModel: iaas.NewNullableString(utils.Ptr("model")), + OperatingSystem: utils.Ptr("os"), + OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), + OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), + RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), + RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), + SecureBoot: utils.Ptr(true), + Uefi: utils.Ptr(true), + VideoModel: iaas.NewNullableString(utils.Ptr("model")), + VirtioScsi: utils.Ptr(true), + }, + Checksum: &iaas.ImageChecksum{ + Algorithm: utils.Ptr("algorithm"), + Digest: utils.Ptr("digest"), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, }, + region: "eu02", }, - Model{ - Id: types.StringValue("pid,iid"), + expected: Model{ + Id: types.StringValue("pid,eu02,iid"), ProjectId: types.StringValue("pid"), ImageId: types.StringValue("iid"), Name: types.StringValue("name"), @@ -110,47 +122,48 @@ func TestMapFields(t *testing.T) { Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ "key": types.StringValue("value"), }), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - Model{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - &iaas.Image{ - Id: utils.Ptr("iid"), + description: "empty_labels", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + ImageId: types.StringValue("iid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + input: &iaas.Image{ + Id: utils.Ptr("iid"), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,iid"), + expected: Model{ + Id: types.StringValue("pid,eu01,iid"), ProjectId: types.StringValue("pid"), ImageId: types.StringValue("iid"), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.Image{}, }, - &iaas.Image{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -158,7 +171,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/imagev2/datasource.go b/stackit/internal/services/iaas/imagev2/datasource.go index e39562393..3c0878141 100644 --- a/stackit/internal/services/iaas/imagev2/datasource.go +++ b/stackit/internal/services/iaas/imagev2/datasource.go @@ -36,6 +36,7 @@ var ( type DataSourceModel struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ImageId types.String `tfsdk:"image_id"` Name types.String `tfsdk:"name"` NameRegex types.String `tfsdk:"name_regex"` @@ -113,7 +114,8 @@ func NewImageV2DataSource() datasource.DataSource { // imageDataV2Source is the data source implementation. type imageDataV2Source struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -122,17 +124,18 @@ func (d *imageDataV2Source) Metadata(_ context.Context, req datasource.MetadataR } func (d *imageDataV2Source) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_image_v2", "datasource") + features.CheckBetaResourcesEnabled(ctx, &d.providerData, &resp.Diagnostics, "stackit_image_v2", "datasource") if resp.Diagnostics.HasError() { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -189,7 +192,7 @@ func (d *imageDataV2Source) Schema(_ context.Context, _ datasource.SchemaRequest Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -200,6 +203,11 @@ func (d *imageDataV2Source) Schema(_ context.Context, _ datasource.SchemaRequest validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "image_id": schema.StringAttribute{ Description: "Image ID to fetch directly", Optional: true, @@ -357,6 +365,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest } projectID := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) imageID := model.ImageId.ValueString() name := model.Name.ValueString() nameRegex := model.NameRegex.ValueString() @@ -371,6 +380,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest } ctx = tflog.SetField(ctx, "project_id", projectID) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "image_id", imageID) ctx = tflog.SetField(ctx, "name", name) ctx = tflog.SetField(ctx, "name_regex", nameRegex) @@ -381,7 +391,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest // Case 1: Direct lookup by image ID if imageID != "" { - imageResp, err = d.client.GetImage(ctx, projectID, imageID).Execute() + imageResp, err = d.client.GetImage(ctx, projectID, region, imageID).Execute() if err != nil { utils.LogError(ctx, &resp.Diagnostics, err, "Reading image", fmt.Sprintf("Image with ID %q does not exist in project %q.", imageID, projectID), @@ -405,7 +415,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest } // Fetch all available images - imageList, err := d.client.ListImages(ctx, projectID).Execute() + imageList, err := d.client.ListImages(ctx, projectID, region).Execute() if err != nil { utils.LogError(ctx, &resp.Diagnostics, err, "List images", "Unable to fetch images", nil) return @@ -451,7 +461,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest imageResp = filteredImages[0] } - err = mapDataSourceFields(ctx, imageResp, &model) + err = mapDataSourceFields(ctx, imageResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Processing API payload: %v", err)) return @@ -467,7 +477,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest tflog.Info(ctx, "image read") } -func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel) error { +func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel, region string) error { if imageResp == nil { return fmt.Errorf("response input is nil") } @@ -484,7 +494,8 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data return fmt.Errorf("image id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, imageId) + model.Region = types.StringValue(region) // Map config var configModel = &configModel{} diff --git a/stackit/internal/services/iaas/imagev2/datasource_test.go b/stackit/internal/services/iaas/imagev2/datasource_test.go index 56b9930b1..3d27ed4f9 100644 --- a/stackit/internal/services/iaas/imagev2/datasource_test.go +++ b/stackit/internal/services/iaas/imagev2/datasource_test.go @@ -12,69 +12,81 @@ import ( ) func TestMapDataSourceFields(t *testing.T) { + type args struct { + state DataSourceModel + input *iaas.Image + region string + } tests := []struct { description string - state DataSourceModel - input *iaas.Image + args args expected DataSourceModel isValid bool }{ { - "default_values", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - }, - &iaas.Image{ - Id: utils.Ptr("iid"), + description: "default_values", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + ImageId: types.StringValue("iid"), + }, + input: &iaas.Image{ + Id: utils.Ptr("iid"), + }, + region: "eu01", }, - DataSourceModel{ - Id: types.StringValue("pid,iid"), + expected: DataSourceModel{ + Id: types.StringValue("pid,eu01,iid"), ProjectId: types.StringValue("pid"), ImageId: types.StringValue("iid"), Labels: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - }, - &iaas.Image{ - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - DiskFormat: utils.Ptr("format"), - MinDiskSize: utils.Ptr(int64(1)), - MinRam: utils.Ptr(int64(1)), - Protected: utils.Ptr(true), - Scope: utils.Ptr("scope"), - Config: &iaas.ImageConfig{ - BootMenu: utils.Ptr(true), - CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), - DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), - NicModel: iaas.NewNullableString(utils.Ptr("model")), - OperatingSystem: utils.Ptr("os"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), - RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), - RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), - SecureBoot: utils.Ptr(true), - Uefi: utils.Ptr(true), - VideoModel: iaas.NewNullableString(utils.Ptr("model")), - VirtioScsi: utils.Ptr(true), - }, - Checksum: &iaas.ImageChecksum{ - Algorithm: utils.Ptr("algorithm"), - Digest: utils.Ptr("digest"), + description: "simple_values", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + ImageId: types.StringValue("iid"), + Region: types.StringValue("eu01"), }, - Labels: &map[string]interface{}{ - "key": "value", + input: &iaas.Image{ + Id: utils.Ptr("iid"), + Name: utils.Ptr("name"), + DiskFormat: utils.Ptr("format"), + MinDiskSize: utils.Ptr(int64(1)), + MinRam: utils.Ptr(int64(1)), + Protected: utils.Ptr(true), + Scope: utils.Ptr("scope"), + Config: &iaas.ImageConfig{ + BootMenu: utils.Ptr(true), + CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), + DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), + NicModel: iaas.NewNullableString(utils.Ptr("model")), + OperatingSystem: utils.Ptr("os"), + OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), + OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), + RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), + RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), + SecureBoot: utils.Ptr(true), + Uefi: utils.Ptr(true), + VideoModel: iaas.NewNullableString(utils.Ptr("model")), + VirtioScsi: utils.Ptr(true), + }, + Checksum: &iaas.ImageChecksum{ + Algorithm: utils.Ptr("algorithm"), + Digest: utils.Ptr("digest"), + }, + Labels: &map[string]interface{}{ + "key": "value", + }, }, + region: "eu02", }, - DataSourceModel{ - Id: types.StringValue("pid,iid"), + expected: DataSourceModel{ + Id: types.StringValue("pid,eu02,iid"), ProjectId: types.StringValue("pid"), ImageId: types.StringValue("iid"), Name: types.StringValue("name"), @@ -105,47 +117,48 @@ func TestMapDataSourceFields(t *testing.T) { Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ "key": types.StringValue("value"), }), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - &iaas.Image{ - Id: utils.Ptr("iid"), + description: "empty_labels", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + ImageId: types.StringValue("iid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + input: &iaas.Image{ + Id: utils.Ptr("iid"), + }, + region: "eu01", }, - DataSourceModel{ - Id: types.StringValue("pid,iid"), + expected: DataSourceModel{ + Id: types.StringValue("pid,eu01,iid"), ProjectId: types.StringValue("pid"), ImageId: types.StringValue("iid"), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - DataSourceModel{}, - nil, - DataSourceModel{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - DataSourceModel{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.Image{}, }, - &iaas.Image{}, - DataSourceModel{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -153,7 +166,7 @@ func TestMapDataSourceFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/keypair/datasource.go b/stackit/internal/services/iaas/keypair/datasource.go index 513607f5b..5b7de81a8 100644 --- a/stackit/internal/services/iaas/keypair/datasource.go +++ b/stackit/internal/services/iaas/keypair/datasource.go @@ -21,7 +21,7 @@ var ( _ datasource.DataSource = &keyPairDataSource{} ) -// NewVolumeDataSource is a helper function to simplify the provider implementation. +// NewKeyPairDataSource is a helper function to simplify the provider implementation. func NewKeyPairDataSource() datasource.DataSource { return &keyPairDataSource{} } @@ -51,7 +51,7 @@ func (d *keyPairDataSource) Configure(ctx context.Context, req datasource.Config } // Schema defines the schema for the resource. -func (r *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Key pair resource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ @@ -84,7 +84,7 @@ func (r *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest } // Read refreshes the Terraform state with the latest data. -func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -94,7 +94,7 @@ func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest name := model.Name.ValueString() ctx = tflog.SetField(ctx, "name", name) - keypairResp, err := r.client.GetKeyPair(ctx, name).Execute() + keypairResp, err := d.client.GetKeyPair(ctx, name).Execute() if err != nil { utils.LogError( ctx, diff --git a/stackit/internal/services/iaas/machinetype/datasource.go b/stackit/internal/services/iaas/machinetype/datasource.go index ed2c1c9d1..e7df32d5d 100644 --- a/stackit/internal/services/iaas/machinetype/datasource.go +++ b/stackit/internal/services/iaas/machinetype/datasource.go @@ -7,10 +7,12 @@ import ( "sort" "strings" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" @@ -19,7 +21,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -28,6 +29,7 @@ var _ datasource.DataSource = &machineTypeDataSource{} type DataSourceModel struct { Id types.String `tfsdk:"id"` // required by Terraform to identify state ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` SortAscending types.Bool `tfsdk:"sort_ascending"` Filter types.String `tfsdk:"filter"` Description types.String `tfsdk:"description"` @@ -44,7 +46,8 @@ func NewMachineTypeDataSource() datasource.DataSource { } type machineTypeDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } func (d *machineTypeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -52,17 +55,18 @@ func (d *machineTypeDataSource) Metadata(_ context.Context, req datasource.Metad } func (d *machineTypeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_machine_type", "datasource") + features.CheckBetaResourcesEnabled(ctx, &d.providerData, &resp.Diagnostics, "stackit_machine_type", "datasource") if resp.Diagnostics.HasError() { return } - client := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + client := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -76,7 +80,7 @@ func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaReq MarkdownDescription: features.AddBetaDescription("Machine type data source.", core.Datasource), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -87,6 +91,11 @@ func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaReq validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "sort_ascending": schema.BoolAttribute{ Description: "Sort machine types by name ascending (`true`) or descending (`false`). Defaults to `false`", Optional: true, @@ -142,13 +151,15 @@ func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq } projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) sortAscending := model.SortAscending.ValueBool() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "filter_is_null", model.Filter.IsNull()) ctx = tflog.SetField(ctx, "filter_is_unknown", model.Filter.IsUnknown()) - listMachineTypeReq := d.client.ListMachineTypes(ctx, projectId) + listMachineTypeReq := d.client.ListMachineTypes(ctx, projectId, region) if !model.Filter.IsNull() && !model.Filter.IsUnknown() && strings.TrimSpace(model.Filter.ValueString()) != "" { listMachineTypeReq = listMachineTypeReq.Filter(strings.TrimSpace(model.Filter.ValueString())) @@ -183,7 +194,7 @@ func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq return } - if err := mapDataSourceFields(ctx, sorted[0], &model); err != nil { + if err := mapDataSourceFields(ctx, sorted[0], &model, region); err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading machine type", fmt.Sprintf("Failed to translate API response: %v", err)) return } @@ -195,7 +206,7 @@ func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq tflog.Info(ctx, "Successfully read machine type") } -func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, model *DataSourceModel) error { +func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, model *DataSourceModel, region string) error { if machineType == nil || model == nil { return fmt.Errorf("nil input provided") } @@ -204,7 +215,8 @@ func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, mod return fmt.Errorf("machine type name is missing") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), *machineType.Name) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, *machineType.Name) + model.Region = types.StringValue(region) model.Name = types.StringPointerValue(machineType.Name) model.Description = types.StringPointerValue(machineType.Description) model.Disk = types.Int64PointerValue(machineType.Disk) diff --git a/stackit/internal/services/iaas/machinetype/datasource_test.go b/stackit/internal/services/iaas/machinetype/datasource_test.go index 3fde4794d..949188109 100644 --- a/stackit/internal/services/iaas/machinetype/datasource_test.go +++ b/stackit/internal/services/iaas/machinetype/datasource_test.go @@ -13,32 +13,39 @@ import ( ) func TestMapDataSourceFields(t *testing.T) { + type args struct { + initial DataSourceModel + input *iaas.MachineType + region string + } tests := []struct { name string - initial DataSourceModel - input *iaas.MachineType + args args expected DataSourceModel expectError bool }{ { name: "valid simple values", - initial: DataSourceModel{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("s1.2"), - Description: utils.Ptr("general-purpose small"), - Disk: utils.Ptr(int64(20)), - Ram: utils.Ptr(int64(2048)), - Vcpus: utils.Ptr(int64(2)), - ExtraSpecs: &map[string]interface{}{ - "cpu": "amd-epycrome-7702", - "overcommit": "1", - "environment": "general", + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("pid"), }, + input: &iaas.MachineType{ + Name: utils.Ptr("s1.2"), + Description: utils.Ptr("general-purpose small"), + Disk: utils.Ptr(int64(20)), + Ram: utils.Ptr(int64(2048)), + Vcpus: utils.Ptr(int64(2)), + ExtraSpecs: &map[string]interface{}{ + "cpu": "amd-epycrome-7702", + "overcommit": "1", + "environment": "general", + }, + }, + region: "eu01", }, expected: DataSourceModel{ - Id: types.StringValue("pid,s1.2"), + Id: types.StringValue("pid,eu01,s1.2"), ProjectId: types.StringValue("pid"), Name: types.StringValue("s1.2"), Description: types.StringValue("general-purpose small"), @@ -50,42 +57,50 @@ func TestMapDataSourceFields(t *testing.T) { "overcommit": types.StringValue("1"), "environment": types.StringValue("general"), }), + Region: types.StringValue("eu01"), }, expectError: false, }, { name: "missing name should fail", - initial: DataSourceModel{ - ProjectId: types.StringValue("pid-456"), - }, - input: &iaas.MachineType{ - Description: utils.Ptr("gp-medium"), + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("pid-456"), + }, + input: &iaas.MachineType{ + Description: utils.Ptr("gp-medium"), + }, }, expected: DataSourceModel{}, expectError: true, }, { - name: "nil machineType should fail", - initial: DataSourceModel{}, - input: nil, + name: "nil machineType should fail", + args: args{ + initial: DataSourceModel{}, + input: nil, + }, expected: DataSourceModel{}, expectError: true, }, { name: "empty extraSpecs should return null map", - initial: DataSourceModel{ - ProjectId: types.StringValue("pid-789"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("m1.noextras"), - Description: utils.Ptr("no extras"), - Disk: utils.Ptr(int64(10)), - Ram: utils.Ptr(int64(1024)), - Vcpus: utils.Ptr(int64(1)), - ExtraSpecs: &map[string]interface{}{}, + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("pid-789"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("m1.noextras"), + Description: utils.Ptr("no extras"), + Disk: utils.Ptr(int64(10)), + Ram: utils.Ptr(int64(1024)), + Vcpus: utils.Ptr(int64(1)), + ExtraSpecs: &map[string]interface{}{}, + }, + region: "eu01", }, expected: DataSourceModel{ - Id: types.StringValue("pid-789,m1.noextras"), + Id: types.StringValue("pid-789,eu01,m1.noextras"), ProjectId: types.StringValue("pid-789"), Name: types.StringValue("m1.noextras"), Description: types.StringValue("no extras"), @@ -93,24 +108,28 @@ func TestMapDataSourceFields(t *testing.T) { Ram: types.Int64Value(1024), Vcpus: types.Int64Value(1), ExtraSpecs: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, expectError: false, }, { name: "nil extrasSpecs should return null map", - initial: DataSourceModel{ - ProjectId: types.StringValue("pid-987"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("g1.nil"), - Description: utils.Ptr("missing extras"), - Disk: utils.Ptr(int64(40)), - Ram: utils.Ptr(int64(8096)), - Vcpus: utils.Ptr(int64(4)), - ExtraSpecs: nil, + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("pid-987"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("g1.nil"), + Description: utils.Ptr("missing extras"), + Disk: utils.Ptr(int64(40)), + Ram: utils.Ptr(int64(8096)), + Vcpus: utils.Ptr(int64(4)), + ExtraSpecs: nil, + }, + region: "eu01", }, expected: DataSourceModel{ - Id: types.StringValue("pid-987,g1.nil"), + Id: types.StringValue("pid-987,eu01,g1.nil"), ProjectId: types.StringValue("pid-987"), Name: types.StringValue("g1.nil"), Description: types.StringValue("missing extras"), @@ -118,24 +137,27 @@ func TestMapDataSourceFields(t *testing.T) { Ram: types.Int64Value(8096), Vcpus: types.Int64Value(4), ExtraSpecs: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, expectError: false, }, { name: "invalid extraSpecs with non-string values", - initial: DataSourceModel{ - ProjectId: types.StringValue("test-err"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("invalid"), - Description: utils.Ptr("bad map"), - Disk: utils.Ptr(int64(10)), - Ram: utils.Ptr(int64(4096)), - Vcpus: utils.Ptr(int64(2)), - ExtraSpecs: &map[string]interface{}{ - "cpu": "intel", - "burst": true, // not a string - "gen": 8, // not a string + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("test-err"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("invalid"), + Description: utils.Ptr("bad map"), + Disk: utils.Ptr(int64(10)), + Ram: utils.Ptr(int64(4096)), + Vcpus: utils.Ptr(int64(2)), + ExtraSpecs: &map[string]interface{}{ + "cpu": "intel", + "burst": true, // not a string + "gen": 8, // not a string + }, }, }, expected: DataSourceModel{}, @@ -145,7 +167,7 @@ func TestMapDataSourceFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.input, &tt.initial) + err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.initial, tt.args.region) if tt.expectError { if err == nil { t.Errorf("expected error but got none") @@ -157,13 +179,13 @@ func TestMapDataSourceFields(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - diff := cmp.Diff(tt.expected, tt.initial) + diff := cmp.Diff(tt.expected, tt.args.initial) if diff != "" { t.Errorf("unexpected diff (-want +got):\n%s", diff) } // Extra sanity check for proper ID format - if id := tt.initial.Id.ValueString(); !strings.HasPrefix(id, tt.initial.ProjectId.ValueString()+",") { + if id := tt.args.initial.Id.ValueString(); !strings.HasPrefix(id, tt.args.initial.ProjectId.ValueString()+",") { t.Errorf("unexpected ID format: got %q", id) } }) diff --git a/stackit/internal/services/iaas/network/datasource.go b/stackit/internal/services/iaas/network/datasource.go index a78d11b9b..4197ee1f8 100644 --- a/stackit/internal/services/iaas/network/datasource.go +++ b/stackit/internal/services/iaas/network/datasource.go @@ -2,14 +2,11 @@ package network import ( "context" + "fmt" + "net" + "net/http" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -18,7 +15,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -27,6 +26,30 @@ var ( _ datasource.DataSource = &networkDataSource{} ) +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + NetworkId types.String `tfsdk:"network_id"` + Name types.String `tfsdk:"name"` + Nameservers types.List `tfsdk:"nameservers"` + IPv4Gateway types.String `tfsdk:"ipv4_gateway"` + IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` + IPv4Prefix types.String `tfsdk:"ipv4_prefix"` + IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` + Prefixes types.List `tfsdk:"prefixes"` + IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` + IPv6Gateway types.String `tfsdk:"ipv6_gateway"` + IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` + IPv6Prefix types.String `tfsdk:"ipv6_prefix"` + IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` + IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` + PublicIP types.String `tfsdk:"public_ip"` + Labels types.Map `tfsdk:"labels"` + Routed types.Bool `tfsdk:"routed"` + Region types.String `tfsdk:"region"` + RoutingTableID types.String `tfsdk:"routing_table_id"` +} + // NewNetworkDataSource is a helper function to simplify the provider implementation. func NewNetworkDataSource() datasource.DataSource { return &networkDataSource{} @@ -34,11 +57,8 @@ func NewNetworkDataSource() datasource.DataSource { // networkDataSource is the data source implementation. type networkDataSource struct { - client *iaas.APIClient - // alphaClient will be used in case the experimental flag "network" is set - alphaClient *iaasalpha.APIClient - isExperimental bool - providerData core.ProviderData + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -53,24 +73,11 @@ func (d *networkDataSource) Configure(ctx context.Context, req datasource.Config return } - d.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &d.providerData, features.NetworkExperiment, "stackit_network", core.Datasource, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - - if d.isExperimental { - alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.alphaClient = alphaApiClient - } else { - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - } + d.client = apiClient tflog.Info(ctx, "IaaS client configured") } @@ -197,9 +204,199 @@ func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest // Read refreshes the Terraform state with the latest data. func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - if !d.isExperimental { - v1network.DatasourceRead(ctx, req, resp, d.client) + var model DataSourceModel + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + + networkResp, err := d.client.GetNetwork(ctx, projectId, region, networkId).Execute() + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading network", + fmt.Sprintf("Network with ID %q does not exist in project %q.", networkId, projectId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + err = mapDataSourceFields(ctx, networkResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network read") +} + +func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model *DataSourceModel, region string) error { + if networkResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var networkId string + if model.NetworkId.ValueString() != "" { + networkId = model.NetworkId.ValueString() + } else if networkResp.Id != nil { + networkId = *networkResp.Id + } else { + return fmt.Errorf("network id not present") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId) + + labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) + if err != nil { + return err + } + + // IPv4 + + if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil { + model.Nameservers = types.ListNull(types.StringType) + model.IPv4Nameservers = types.ListNull(types.StringType) + } else { + respNameservers := *networkResp.Ipv4.Nameservers + modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) + modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) + if err != nil { + return fmt.Errorf("get current network nameservers from model: %w", err) + } + if errIpv4 != nil { + return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) + } + + reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) + reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers) + + nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) + ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) + if diags.HasError() { + return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) + } + if ipv4Diags.HasError() { + return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) + } + + model.Nameservers = nameserversTF + model.IPv4Nameservers = ipv4NameserversTF + } + + if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil { + model.Prefixes = types.ListNull(types.StringType) + model.IPv4Prefixes = types.ListNull(types.StringType) + } else { + respPrefixes := *networkResp.Ipv4.Prefixes + prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) + if diags.HasError() { + return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) + } + if len(respPrefixes) > 0 { + model.IPv4Prefix = types.StringValue(respPrefixes[0]) + _, netmask, err := net.ParseCIDR(respPrefixes[0]) + if err != nil { + // silently ignore parsing error for the netmask + model.IPv4PrefixLength = types.Int64Null() + } else { + ones, _ := netmask.Mask.Size() + model.IPv4PrefixLength = types.Int64Value(int64(ones)) + } + } + + model.Prefixes = prefixesTF + model.IPv4Prefixes = prefixesTF + } + + if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil { + model.IPv4Gateway = types.StringNull() + } else { + model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway()) + } + + if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil { + model.PublicIP = types.StringNull() + } else { + model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp) + } + + // IPv6 + + if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil { + model.IPv6Nameservers = types.ListNull(types.StringType) } else { - v2network.DatasourceRead(ctx, req, resp, d.alphaClient, d.providerData) + respIPv6Nameservers := *networkResp.Ipv6.Nameservers + modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) + if errIpv6 != nil { + return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) + } + + reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers) + + ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers) + if ipv6Diags.HasError() { + return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags)) + } + + model.IPv6Nameservers = ipv6NameserversTF } + + if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil { + model.IPv6Prefixes = types.ListNull(types.StringType) + } else { + respPrefixesV6 := *networkResp.Ipv6.Prefixes + prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) + if diags.HasError() { + return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) + } + if len(respPrefixesV6) > 0 { + model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) + _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) + if err != nil { + // silently ignore parsing error for the netmask + model.IPv6PrefixLength = types.Int64Null() + } else { + ones, _ := netmask.Mask.Size() + model.IPv6PrefixLength = types.Int64Value(int64(ones)) + } + } + model.IPv6Prefixes = prefixesV6TF + } + + if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil { + model.IPv6Gateway = types.StringNull() + } else { + model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway()) + } + + model.RoutingTableID = types.StringNull() + if networkResp.RoutingTableId != nil { + model.RoutingTableID = types.StringValue(*networkResp.RoutingTableId) + } + + model.NetworkId = types.StringValue(networkId) + model.Name = types.StringPointerValue(networkResp.Name) + model.Labels = labels + model.Routed = types.BoolPointerValue(networkResp.Routed) + model.Region = types.StringValue(region) + + return nil } diff --git a/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go b/stackit/internal/services/iaas/network/datasource_test.go similarity index 87% rename from stackit/internal/services/iaas/network/utils/v2network/datasource_test.go rename to stackit/internal/services/iaas/network/datasource_test.go index eba9d4117..c7c4d7f95 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go +++ b/stackit/internal/services/iaas/network/datasource_test.go @@ -1,15 +1,15 @@ -package v2network +package network import ( "context" "testing" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" ) const ( @@ -19,26 +19,26 @@ const ( func TestMapDataSourceFields(t *testing.T) { tests := []struct { description string - state networkModel.DataSourceModel - input *iaasalpha.Network + state DataSourceModel + input *iaas.Network region string - expected networkModel.DataSourceModel + expected DataSourceModel isValid bool }{ { "id_ok", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ - Gateway: iaasalpha.NewNullableString(nil), + Ipv4: &iaas.NetworkIPv4{ + Gateway: iaas.NewNullableString(nil), }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -64,14 +64,14 @@ func TestMapDataSourceFields(t *testing.T) { }, { "values_ok", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), Name: utils.Ptr("name"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Nameservers: &[]string{ "ns1", "ns2", @@ -81,9 +81,9 @@ func TestMapDataSourceFields(t *testing.T) { "10.100.10.0/16", }, PublicIp: utils.Ptr("publicIp"), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Nameservers: &[]string{ "ns1", "ns2", @@ -92,7 +92,7 @@ func TestMapDataSourceFields(t *testing.T) { "fd12:3456:789a:1::/64", "fd12:3456:789a:2::/64", }, - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, Labels: &map[string]interface{}{ "key": "value", @@ -100,7 +100,7 @@ func TestMapDataSourceFields(t *testing.T) { Routed: utils.Ptr(true), }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -146,7 +146,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv4_nameservers_changed_outside_tf", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -158,9 +158,9 @@ func TestMapDataSourceFields(t *testing.T) { types.StringValue("ns2"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Nameservers: &[]string{ "ns2", "ns3", @@ -168,7 +168,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -192,7 +192,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv6_nameservers_changed_outside_tf", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -200,9 +200,9 @@ func TestMapDataSourceFields(t *testing.T) { types.StringValue("ns2"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Nameservers: &[]string{ "ns2", "ns3", @@ -210,7 +210,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -231,7 +231,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv4_prefixes_changed_outside_tf", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -239,9 +239,9 @@ func TestMapDataSourceFields(t *testing.T) { types.StringValue("10.100.10.0/16"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Prefixes: &[]string{ "10.100.20.0/16", "10.100.10.0/16", @@ -249,7 +249,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -276,7 +276,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv6_prefixes_changed_outside_tf", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -284,9 +284,9 @@ func TestMapDataSourceFields(t *testing.T) { types.StringValue("fd12:3456:789a:2::/64"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Prefixes: &[]string{ "fd12:3456:789a:3::/64", "fd12:3456:789a:4::/64", @@ -294,7 +294,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -318,15 +318,15 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv4_ipv6_gateway_nil", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -350,20 +350,20 @@ func TestMapDataSourceFields(t *testing.T) { }, { "response_nil_fail", - networkModel.DataSourceModel{}, + DataSourceModel{}, nil, testRegion, - networkModel.DataSourceModel{}, + DataSourceModel{}, false, }, { "no_resource_id", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), }, - &iaasalpha.Network{}, + &iaas.Network{}, testRegion, - networkModel.DataSourceModel{}, + DataSourceModel{}, false, }, } diff --git a/stackit/internal/services/iaas/network/resource.go b/stackit/internal/services/iaas/network/resource.go index a1ea9e5dc..75843486d 100644 --- a/stackit/internal/services/iaas/network/resource.go +++ b/stackit/internal/services/iaas/network/resource.go @@ -3,9 +3,13 @@ package network import ( "context" "fmt" + "net" + "net/http" + "strings" "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -18,16 +22,12 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -37,6 +37,7 @@ var ( _ resource.Resource = &networkResource{} _ resource.ResourceWithConfigure = &networkResource{} _ resource.ResourceWithImportState = &networkResource{} + _ resource.ResourceWithModifyPlan = &networkResource{} ) const ( @@ -46,6 +47,32 @@ const ( "In cases where `ipv4_nameservers` are defined within the resource, the existing behavior will remain unchanged." ) +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + NetworkId types.String `tfsdk:"network_id"` + Name types.String `tfsdk:"name"` + Nameservers types.List `tfsdk:"nameservers"` + IPv4Gateway types.String `tfsdk:"ipv4_gateway"` + IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` + IPv4Prefix types.String `tfsdk:"ipv4_prefix"` + IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` + Prefixes types.List `tfsdk:"prefixes"` + IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` + IPv6Gateway types.String `tfsdk:"ipv6_gateway"` + IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` + IPv6Prefix types.String `tfsdk:"ipv6_prefix"` + IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` + IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` + PublicIP types.String `tfsdk:"public_ip"` + Labels types.Map `tfsdk:"labels"` + Routed types.Bool `tfsdk:"routed"` + NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"` + NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"` + Region types.String `tfsdk:"region"` + RoutingTableID types.String `tfsdk:"routing_table_id"` +} + // NewNetworkResource is a helper function to simplify the provider implementation. func NewNetworkResource() resource.Resource { return &networkResource{} @@ -53,11 +80,8 @@ func NewNetworkResource() resource.Resource { // networkResource is the resource implementation. type networkResource struct { - client *iaas.APIClient - // alphaClient will be used in case the experimental flag "network" is set - alphaClient *iaasalpha.APIClient - isExperimental bool - providerData core.ProviderData + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -73,31 +97,18 @@ func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureR return } - r.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &r.providerData, features.NetworkExperiment, "stackit_network", core.Resource, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - - if r.isExperimental { - alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.alphaClient = alphaApiClient - } else { - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - } + r.client = apiClient tflog.Info(ctx, "IaaS client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel model.Model + var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return @@ -107,7 +118,7 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla return } - var planModel model.Model + var planModel Model resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return @@ -118,10 +129,6 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla addIPv4Warning(&resp.Diagnostics) } - // If the v1 api is used, it's not required to get the fallback region because it isn't used - if !r.isExperimental { - return - } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) if resp.Diagnostics.HasError() { return @@ -134,7 +141,7 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla } func (r *networkResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var resourceModel model.Model + var resourceModel Model resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) if resp.Diagnostics.HasError() { return @@ -143,14 +150,6 @@ func (r *networkResource) ValidateConfig(ctx context.Context, req resource.Valid if !resourceModel.Nameservers.IsUnknown() && !resourceModel.IPv4Nameservers.IsUnknown() && !resourceModel.Nameservers.IsNull() && !resourceModel.IPv4Nameservers.IsNull() { core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "You cannot provide both the `nameservers` and `ipv4_nameservers` fields simultaneously. Please remove the deprecated `nameservers` field, and use `ipv4_nameservers` to configure nameservers for IPv4.") } - if !r.isExperimental { - if !utils.IsUndefined(resourceModel.Region) { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the `region` is not supported yet. This can only be configured when the experiments `network` is set.") - } - if !utils.IsUndefined(resourceModel.RoutingTableID) { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the field `routing_table_id` is not supported yet. This can only be configured when the experiments `network` is set.") - } - } } // ConfigValidators validates the resource configuration @@ -359,7 +358,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "routing_table_id": schema.StringAttribute{ - Description: "Can only be used when experimental \"network\" is set.\nThe ID of the routing table associated with the network.", + Description: "The ID of the routing table associated with the network.", Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ @@ -374,7 +373,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re Optional: true, // must be computed to allow for storing the override value from the provider Computed: true, - Description: "Can only be used when experimental \"network\" is set.\nThe resource region. If not defined, the provider region is used.", + Description: "The resource region. If not defined, the provider region is used.", PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplaceIfConfigured(), }, @@ -386,59 +385,568 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re // Create creates the resource and sets the initial Terraform state. func (r *networkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan - var planModel model.Model - diags := req.Plan.Get(ctx, &planModel) + var model Model + diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + // When IPv4Nameserver is not set, print warning that the behavior of ipv4_nameservers will change - if utils.IsUndefined(planModel.IPv4Nameservers) { + if utils.IsUndefined(model.IPv4Nameservers) { addIPv4Warning(&resp.Diagnostics) } - if !r.isExperimental { - v1network.Create(ctx, req, resp, r.client) - } else { - v2network.Create(ctx, req, resp, r.alphaClient) + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + // Generate API request body from model + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Create new network + + network, err := r.client.CreateNetwork(ctx, projectId, region).CreateNetworkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err)) + return + } + + networkId := *network.Id + ctx = tflog.SetField(ctx, "network_id", networkId) + + network, err = wait.CreateNetworkWaitHandler(ctx, r.client, projectId, region, networkId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) + return } + + // Map response body to schema + err = mapFields(ctx, network, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set state to fully populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network created") } // Read refreshes the Terraform state with the latest data. func (r *networkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - if !r.isExperimental { - v1network.Read(ctx, req, resp, r.client) - } else { - v2network.Read(ctx, req, resp, r.alphaClient, r.providerData) + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + ctx = tflog.SetField(ctx, "region", region) + + networkResp, err := r.client.GetNetwork(ctx, projectId, region, networkId).Execute() + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(ctx, networkResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network read") } // Update updates the resource and sets the updated Terraform state on success. func (r *networkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - if !r.isExperimental { - v1network.Update(ctx, req, resp, r.client) - } else { - v2network.Update(ctx, req, resp, r.alphaClient) + // Retrieve values from plan + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + ctx = tflog.SetField(ctx, "region", region) + + // Retrieve values from state + var stateModel Model + diags = req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from model + payload, err := toUpdatePayload(ctx, &model, &stateModel) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Creating API payload: %v", err)) + return + } + // Update existing network + err = r.client.PartialUpdateNetwork(ctx, projectId, region, networkId).PartialUpdateNetworkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err)) + return } + waitResp, err := wait.UpdateNetworkWaitHandler(ctx, r.client, projectId, region, networkId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err)) + return + } + + err = mapFields(ctx, waitResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network updated") } // Delete deletes the resource and removes the Terraform state on success. func (r *networkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - if !r.isExperimental { - v1network.Delete(ctx, req, resp, r.client) - } else { - v2network.Delete(ctx, req, resp, r.alphaClient) + // Retrieve values from state + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } + + projectId := model.ProjectId.ValueString() + networkId := model.NetworkId.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "network_id", networkId) + ctx = tflog.SetField(ctx, "region", region) + + // Delete existing network + err := r.client.DeleteNetwork(ctx, projectId, region, networkId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err)) + return + } + _, err = wait.DeleteNetworkWaitHandler(ctx, r.client, projectId, region, networkId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "Network deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,network_id +// The expected format of the resource import identifier is: project_id,region,network_id func (r *networkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - if !r.isExperimental { - v1network.ImportState(ctx, req, resp) + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing network", + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id] Got: %q", req.ID), + ) + return + } + + projectId := idParts[0] + region := idParts[1] + networkId := idParts[2] + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "network_id", networkId) + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...) + tflog.Info(ctx, "Network state imported") +} + +func mapFields(ctx context.Context, networkResp *iaas.Network, model *Model, region string) error { + if networkResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var networkId string + if model.NetworkId.ValueString() != "" { + networkId = model.NetworkId.ValueString() + } else if networkResp.Id != nil { + networkId = *networkResp.Id } else { - v2network.ImportState(ctx, req, resp) + return fmt.Errorf("network id not present") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId) + + labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) + if err != nil { + return err } + + // IPv4 + + if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil { + model.Nameservers = types.ListNull(types.StringType) + model.IPv4Nameservers = types.ListNull(types.StringType) + } else { + respNameservers := *networkResp.Ipv4.Nameservers + modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) + modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) + if err != nil { + return fmt.Errorf("get current network nameservers from model: %w", err) + } + if errIpv4 != nil { + return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) + } + + reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) + reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers) + + nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) + ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) + if diags.HasError() { + return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) + } + if ipv4Diags.HasError() { + return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) + } + + model.Nameservers = nameserversTF + model.IPv4Nameservers = ipv4NameserversTF + } + + model.IPv4PrefixLength = types.Int64Null() + if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil { + model.Prefixes = types.ListNull(types.StringType) + model.IPv4Prefixes = types.ListNull(types.StringType) + } else { + respPrefixes := *networkResp.Ipv4.Prefixes + prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) + if diags.HasError() { + return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) + } + if len(respPrefixes) > 0 { + model.IPv4Prefix = types.StringValue(respPrefixes[0]) + _, netmask, err := net.ParseCIDR(respPrefixes[0]) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("ipv4_prefix_length: %+v", err)) + // silently ignore parsing error for the netmask + model.IPv4PrefixLength = types.Int64Null() + } else { + ones, _ := netmask.Mask.Size() + model.IPv4PrefixLength = types.Int64Value(int64(ones)) + } + } + + model.Prefixes = prefixesTF + model.IPv4Prefixes = prefixesTF + } + + if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil { + model.IPv4Gateway = types.StringNull() + } else { + model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway()) + } + + if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil { + model.PublicIP = types.StringNull() + } else { + model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp) + } + + // IPv6 + + if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil { + model.IPv6Nameservers = types.ListNull(types.StringType) + } else { + respIPv6Nameservers := *networkResp.Ipv6.Nameservers + modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) + if errIpv6 != nil { + return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) + } + + reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers) + + ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers) + if ipv6Diags.HasError() { + return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags)) + } + + model.IPv6Nameservers = ipv6NameserversTF + } + + model.IPv6PrefixLength = types.Int64Null() + model.IPv6Prefix = types.StringNull() + if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil { + model.IPv6Prefixes = types.ListNull(types.StringType) + } else { + respPrefixesV6 := *networkResp.Ipv6.Prefixes + prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) + if diags.HasError() { + return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) + } + if len(respPrefixesV6) > 0 { + model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) + _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) + if err != nil { + // silently ignore parsing error for the netmask + model.IPv6PrefixLength = types.Int64Null() + } else { + ones, _ := netmask.Mask.Size() + model.IPv6PrefixLength = types.Int64Value(int64(ones)) + } + } + model.IPv6Prefixes = prefixesV6TF + } + + if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil { + model.IPv6Gateway = types.StringNull() + } else { + model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway()) + } + + model.RoutingTableID = types.StringPointerValue(networkResp.RoutingTableId) + model.NetworkId = types.StringValue(networkId) + model.Name = types.StringPointerValue(networkResp.Name) + model.Labels = labels + model.Routed = types.BoolPointerValue(networkResp.Routed) + model.Region = types.StringValue(region) + + return nil +} + +func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + var modelIPv6Nameservers []string + // Is true when IPv6Nameservers is not null or unset + if !utils.IsUndefined(model.IPv6Nameservers) { + // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. + // empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set + modelIPv6Nameservers = []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { + ipv6NameserverString, ok := ipv6ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) + } + } + + var ipv6Body *iaas.CreateNetworkIPv6 + if !utils.IsUndefined(model.IPv6PrefixLength) { + ipv6Body = &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefixLength: &iaas.CreateNetworkIPv6WithPrefixLength{ + PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength), + }, + } + + // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. + // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, + // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. + if modelIPv6Nameservers != nil { + ipv6Body.CreateNetworkIPv6WithPrefixLength.Nameservers = &modelIPv6Nameservers + } + } else if !utils.IsUndefined(model.IPv6Prefix) { + var gateway *iaas.NullableString + if model.NoIPv6Gateway.ValueBool() { + gateway = iaas.NewNullableString(nil) + } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { + gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) + } + + ipv6Body = &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ + Gateway: gateway, + Prefix: conversion.StringValueToPointer(model.IPv6Prefix), + }, + } + + // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. + // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, + // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. + if modelIPv6Nameservers != nil { + ipv6Body.CreateNetworkIPv6WithPrefix.Nameservers = &modelIPv6Nameservers + } + } + + modelIPv4Nameservers := []string{} + var modelIPv4List []attr.Value + + if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { + modelIPv4List = model.IPv4Nameservers.Elements() + } else { + modelIPv4List = model.Nameservers.Elements() + } + + for _, ipv4ns := range modelIPv4List { + ipv4NameserverString, ok := ipv4ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) + } + + var ipv4Body *iaas.CreateNetworkIPv4 + if !utils.IsUndefined(model.IPv4PrefixLength) { + ipv4Body = &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefixLength: &iaas.CreateNetworkIPv4WithPrefixLength{ + Nameservers: &modelIPv4Nameservers, + PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength), + }, + } + } else if !utils.IsUndefined(model.IPv4Prefix) { + var gateway *iaas.NullableString + if model.NoIPv4Gateway.ValueBool() { + gateway = iaas.NewNullableString(nil) + } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { + gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) + } + + ipv4Body = &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ + Nameservers: &modelIPv4Nameservers, + Prefix: conversion.StringValueToPointer(model.IPv4Prefix), + Gateway: gateway, + }, + } + } + + labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + payload := iaas.CreateNetworkPayload{ + Name: conversion.StringValueToPointer(model.Name), + Labels: &labels, + Routed: conversion.BoolValueToPointer(model.Routed), + Ipv4: ipv4Body, + Ipv6: ipv6Body, + RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID), + } + + return &payload, nil +} + +func toUpdatePayload(ctx context.Context, model, stateModel *Model) (*iaas.PartialUpdateNetworkPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + var modelIPv6Nameservers []string + // Is true when IPv6Nameservers is not null or unset + if !utils.IsUndefined(model.IPv6Nameservers) { + // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. + // empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set + modelIPv6Nameservers = []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { + ipv6NameserverString, ok := ipv6ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) + } + } + + var ipv6Body *iaas.UpdateNetworkIPv6Body + if modelIPv6Nameservers != nil || !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) { + ipv6Body = &iaas.UpdateNetworkIPv6Body{} + // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. + // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, + // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. + if modelIPv6Nameservers != nil { + ipv6Body.Nameservers = &modelIPv6Nameservers + } + + if model.NoIPv6Gateway.ValueBool() { + ipv6Body.Gateway = iaas.NewNullableString(nil) + } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { + ipv6Body.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) + } + } + + modelIPv4Nameservers := []string{} + var modelIPv4List []attr.Value + + if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { + modelIPv4List = model.IPv4Nameservers.Elements() + } else { + modelIPv4List = model.Nameservers.Elements() + } + for _, ipv4ns := range modelIPv4List { + ipv4NameserverString, ok := ipv4ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) + } + + var ipv4Body *iaas.UpdateNetworkIPv4Body + if !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() { + ipv4Body = &iaas.UpdateNetworkIPv4Body{ + Nameservers: &modelIPv4Nameservers, + } + + if model.NoIPv4Gateway.ValueBool() { + ipv4Body.Gateway = iaas.NewNullableString(nil) + } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { + ipv4Body.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) + } + } + currentLabels := stateModel.Labels + labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + payload := iaas.PartialUpdateNetworkPayload{ + Name: conversion.StringValueToPointer(model.Name), + Labels: &labels, + Ipv4: ipv4Body, + Ipv6: ipv6Body, + RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID), + } + + return &payload, nil } func addIPv4Warning(diags *diag.Diagnostics) { diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource_test.go b/stackit/internal/services/iaas/network/resource_test.go similarity index 84% rename from stackit/internal/services/iaas/network/utils/v2network/resource_test.go rename to stackit/internal/services/iaas/network/resource_test.go index 6f39b9a36..929424d62 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/resource_test.go +++ b/stackit/internal/services/iaas/network/resource_test.go @@ -1,4 +1,4 @@ -package v2network +package network import ( "context" @@ -8,34 +8,33 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) func TestMapFields(t *testing.T) { const testRegion = "region" tests := []struct { description string - state model.Model - input *iaasalpha.Network + state Model + input *iaas.Network region string - expected model.Model + expected Model isValid bool }{ { "id_ok", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ - Gateway: iaasalpha.NewNullableString(nil), + Ipv4: &iaas.NetworkIPv4{ + Gateway: iaas.NewNullableString(nil), }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -61,14 +60,14 @@ func TestMapFields(t *testing.T) { }, { "values_ok", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), Name: utils.Ptr("name"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Nameservers: utils.Ptr([]string{"ns1", "ns2"}), Prefixes: utils.Ptr( []string{ @@ -77,15 +76,15 @@ func TestMapFields(t *testing.T) { }, ), PublicIp: utils.Ptr("publicIp"), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Nameservers: utils.Ptr([]string{"ns1", "ns2"}), Prefixes: utils.Ptr([]string{ "fd12:3456:789a:1::/64", "fd12:3456:789b:1::/64", }), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, Labels: &map[string]interface{}{ "key": "value", @@ -93,7 +92,7 @@ func TestMapFields(t *testing.T) { Routed: utils.Ptr(true), }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -139,7 +138,7 @@ func TestMapFields(t *testing.T) { }, { "ipv4_nameservers_changed_outside_tf", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -151,9 +150,9 @@ func TestMapFields(t *testing.T) { types.StringValue("ns2"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Nameservers: utils.Ptr([]string{ "ns2", "ns3", @@ -161,7 +160,7 @@ func TestMapFields(t *testing.T) { }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -185,7 +184,7 @@ func TestMapFields(t *testing.T) { }, { "ipv6_nameservers_changed_outside_tf", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -193,9 +192,9 @@ func TestMapFields(t *testing.T) { types.StringValue("ns2"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Nameservers: utils.Ptr([]string{ "ns2", "ns3", @@ -203,7 +202,7 @@ func TestMapFields(t *testing.T) { }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -224,7 +223,7 @@ func TestMapFields(t *testing.T) { }, { "ipv4_prefixes_changed_outside_tf", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -232,9 +231,9 @@ func TestMapFields(t *testing.T) { types.StringValue("10.100.10.0/24"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Prefixes: utils.Ptr( []string{ "192.168.54.0/24", @@ -244,7 +243,7 @@ func TestMapFields(t *testing.T) { }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -271,7 +270,7 @@ func TestMapFields(t *testing.T) { }, { "ipv6_prefixes_changed_outside_tf", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -279,9 +278,9 @@ func TestMapFields(t *testing.T) { types.StringValue("fd12:3456:789a:2::/64"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Prefixes: utils.Ptr( []string{ "fd12:3456:789a:1::/64", @@ -291,7 +290,7 @@ func TestMapFields(t *testing.T) { }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -315,15 +314,15 @@ func TestMapFields(t *testing.T) { }, { "ipv4_ipv6_gateway_nil", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -347,20 +346,20 @@ func TestMapFields(t *testing.T) { }, { "response_nil_fail", - model.Model{}, + Model{}, nil, testRegion, - model.Model{}, + Model{}, false, }, { "no_resource_id", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), }, - &iaasalpha.Network{}, + &iaas.Network{}, testRegion, - model.Model{}, + Model{}, false, }, } @@ -386,13 +385,13 @@ func TestMapFields(t *testing.T) { func TestToCreatePayload(t *testing.T) { tests := []struct { description string - input *model.Model - expected *iaasalpha.CreateNetworkPayload + input *Model + expected *iaas.CreateNetworkPayload isValid bool }{ { "default_ok", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -405,11 +404,11 @@ func TestToCreatePayload(t *testing.T) { IPv4Gateway: types.StringValue("gateway"), IPv4Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv4: &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -426,7 +425,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv4_nameservers_okay", - &model.Model{ + &Model{ Name: types.StringValue("name"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -439,11 +438,11 @@ func TestToCreatePayload(t *testing.T) { IPv4Gateway: types.StringValue("gateway"), IPv4Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv4: &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -460,7 +459,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv6_default_ok", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -473,11 +472,11 @@ func TestToCreatePayload(t *testing.T) { IPv6Gateway: types.StringValue("gateway"), IPv6Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv6: &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -494,7 +493,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv6_nameserver_null", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListNull(types.StringType), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -504,12 +503,12 @@ func TestToCreatePayload(t *testing.T) { IPv6Gateway: types.StringValue("gateway"), IPv6Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ + Ipv6: &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ Nameservers: nil, - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Prefix: utils.Ptr("prefix"), }, }, @@ -522,7 +521,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv6_nameserver_empty_list", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -532,12 +531,12 @@ func TestToCreatePayload(t *testing.T) { IPv6Gateway: types.StringValue("gateway"), IPv6Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ + Ipv6: &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ Nameservers: utils.Ptr([]string{}), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Prefix: utils.Ptr("prefix"), }, }, @@ -559,7 +558,7 @@ func TestToCreatePayload(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) + diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -571,14 +570,14 @@ func TestToCreatePayload(t *testing.T) { func TestToUpdatePayload(t *testing.T) { tests := []struct { description string - input *model.Model - state model.Model - expected *iaasalpha.PartialUpdateNetworkPayload + input *Model + state Model + expected *iaas.PartialUpdateNetworkPayload isValid bool }{ { "default_ok", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -590,15 +589,15 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv4Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.UpdateNetworkIPv4Body{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv4: &iaas.UpdateNetworkIPv4Body{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -612,7 +611,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv4_nameservers_okay", - &model.Model{ + &Model{ Name: types.StringValue("name"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -624,15 +623,15 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv4Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.UpdateNetworkIPv4Body{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv4: &iaas.UpdateNetworkIPv4Body{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -646,7 +645,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv4_gateway_nil", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -657,14 +656,14 @@ func TestToUpdatePayload(t *testing.T) { }), Routed: types.BoolValue(true), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.UpdateNetworkIPv4Body{ + Ipv4: &iaas.UpdateNetworkIPv4Body{ Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -678,7 +677,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_default_ok", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -690,15 +689,15 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv6Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv6: &iaas.UpdateNetworkIPv6Body{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -712,7 +711,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_gateway_nil", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -723,14 +722,14 @@ func TestToUpdatePayload(t *testing.T) { }), Routed: types.BoolValue(true), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ + Ipv6: &iaas.UpdateNetworkIPv6Body{ Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -744,7 +743,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_nameserver_null", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListNull(types.StringType), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -753,16 +752,16 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv6Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ + Ipv6: &iaas.UpdateNetworkIPv6Body{ Nameservers: nil, - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, Labels: &map[string]interface{}{ "key": "value", @@ -772,7 +771,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_nameserver_empty_list", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -781,16 +780,16 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv6Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ + Ipv6: &iaas.UpdateNetworkIPv6Body{ Nameservers: utils.Ptr([]string{}), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, Labels: &map[string]interface{}{ "key": "value", @@ -809,7 +808,7 @@ func TestToUpdatePayload(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) + diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/network/utils/model/model.go b/stackit/internal/services/iaas/network/utils/model/model.go deleted file mode 100644 index 73f994ecf..000000000 --- a/stackit/internal/services/iaas/network/utils/model/model.go +++ /dev/null @@ -1,53 +0,0 @@ -package model - -import "github.com/hashicorp/terraform-plugin-framework/types" - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - NetworkId types.String `tfsdk:"network_id"` - Name types.String `tfsdk:"name"` - Nameservers types.List `tfsdk:"nameservers"` - IPv4Gateway types.String `tfsdk:"ipv4_gateway"` - IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` - IPv4Prefix types.String `tfsdk:"ipv4_prefix"` - IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` - Prefixes types.List `tfsdk:"prefixes"` - IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` - IPv6Gateway types.String `tfsdk:"ipv6_gateway"` - IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` - IPv6Prefix types.String `tfsdk:"ipv6_prefix"` - IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` - IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` - PublicIP types.String `tfsdk:"public_ip"` - Labels types.Map `tfsdk:"labels"` - Routed types.Bool `tfsdk:"routed"` - NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"` - NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"` - Region types.String `tfsdk:"region"` - RoutingTableID types.String `tfsdk:"routing_table_id"` -} - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - NetworkId types.String `tfsdk:"network_id"` - Name types.String `tfsdk:"name"` - Nameservers types.List `tfsdk:"nameservers"` - IPv4Gateway types.String `tfsdk:"ipv4_gateway"` - IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` - IPv4Prefix types.String `tfsdk:"ipv4_prefix"` - IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` - Prefixes types.List `tfsdk:"prefixes"` - IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` - IPv6Gateway types.String `tfsdk:"ipv6_gateway"` - IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` - IPv6Prefix types.String `tfsdk:"ipv6_prefix"` - IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` - IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` - PublicIP types.String `tfsdk:"public_ip"` - Labels types.Map `tfsdk:"labels"` - Routed types.Bool `tfsdk:"routed"` - Region types.String `tfsdk:"region"` - RoutingTableID types.String `tfsdk:"routing_table_id"` -} diff --git a/stackit/internal/services/iaas/network/utils/v1network/datasource.go b/stackit/internal/services/iaas/network/utils/v1network/datasource.go deleted file mode 100644 index 08f8da5bd..000000000 --- a/stackit/internal/services/iaas/network/utils/v1network/datasource.go +++ /dev/null @@ -1,203 +0,0 @@ -package v1network - -import ( - "context" - "fmt" - "net" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform - var model networkModel.DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - networkResp, err := client.GetNetwork(ctx, projectId, networkId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading network", - fmt.Sprintf("Network with ID %q does not exist in project %q.", networkId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - err = mapDataSourceFields(ctx, networkResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network read") -} - -func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model *networkModel.DataSourceModel) error { - if networkResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var networkId string - if model.NetworkId.ValueString() != "" { - networkId = model.NetworkId.ValueString() - } else if networkResp.NetworkId != nil { - networkId = *networkResp.NetworkId - } else { - return fmt.Errorf("network id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), networkId) - - labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) - if err != nil { - return err - } - - // IPv4 - - if networkResp.Nameservers == nil { - model.Nameservers = types.ListNull(types.StringType) - model.IPv4Nameservers = types.ListNull(types.StringType) - } else { - respNameservers := *networkResp.Nameservers - modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) - modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) - if err != nil { - return fmt.Errorf("get current network nameservers from model: %w", err) - } - if errIpv4 != nil { - return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) - } - - reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) - reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers) - - nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) - ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) - if diags.HasError() { - return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) - } - if ipv4Diags.HasError() { - return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) - } - - model.Nameservers = nameserversTF - model.IPv4Nameservers = ipv4NameserversTF - } - - if networkResp.Prefixes == nil { - model.Prefixes = types.ListNull(types.StringType) - model.IPv4Prefixes = types.ListNull(types.StringType) - } else { - respPrefixes := *networkResp.Prefixes - prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) - if diags.HasError() { - return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixes) > 0 { - model.IPv4Prefix = types.StringValue(respPrefixes[0]) - _, netmask, err := net.ParseCIDR(respPrefixes[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv4PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv4PrefixLength = types.Int64Value(int64(ones)) - } - } - - model.Prefixes = prefixesTF - model.IPv4Prefixes = prefixesTF - } - - model.IPv4Gateway = types.StringNull() - if networkResp.Gateway != nil { - model.IPv4Gateway = types.StringPointerValue(networkResp.GetGateway()) - } - - // IPv6 - - if networkResp.NameserversV6 == nil { - model.IPv6Nameservers = types.ListNull(types.StringType) - } else { - respIPv6Nameservers := *networkResp.NameserversV6 - modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) - if errIpv6 != nil { - return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) - } - - reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers) - - ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers) - if ipv6Diags.HasError() { - return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags)) - } - - model.IPv6Nameservers = ipv6NameserversTF - } - - if networkResp.PrefixesV6 == nil { - model.IPv6Prefixes = types.ListNull(types.StringType) - } else { - respPrefixesV6 := *networkResp.PrefixesV6 - prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) - if diags.HasError() { - return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixesV6) > 0 { - model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) - _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv6PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv6PrefixLength = types.Int64Value(int64(ones)) - } - } - model.IPv6Prefixes = prefixesV6TF - } - - model.IPv6Gateway = types.StringNull() - if networkResp.Gatewayv6 != nil { - model.IPv6Gateway = types.StringPointerValue(networkResp.GetGatewayv6()) - } - - model.NetworkId = types.StringValue(networkId) - model.Name = types.StringPointerValue(networkResp.Name) - model.PublicIP = types.StringPointerValue(networkResp.PublicIp) - model.Labels = labels - model.Routed = types.BoolPointerValue(networkResp.Routed) - model.RoutingTableID = types.StringNull() - model.Region = types.StringNull() - - return nil -} diff --git a/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go b/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go deleted file mode 100644 index 2ce9b5c96..000000000 --- a/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go +++ /dev/null @@ -1,352 +0,0 @@ -package v1network - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" -) - -func TestMapDataSourceFields(t *testing.T) { - tests := []struct { - description string - state networkModel.DataSourceModel - input *iaas.Network - expected networkModel.DataSourceModel - isValid bool - }{ - { - "id_ok", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Gateway: iaas.NewNullableString(nil), - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - IPv4Prefix: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefix: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - }, - true, - }, - { - "values_ok", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Name: utils.Ptr("name"), - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Prefixes: &[]string{ - "192.168.42.0/24", - "10.100.10.0/16", - }, - NameserversV6: &[]string{ - "ns1", - "ns2", - }, - PrefixesV6: &[]string{ - "fd12:3456:789a:1::/64", - "fd12:3456:789a:2::/64", - }, - PublicIp: utils.Ptr("publicIp"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(true), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Gatewayv6: iaas.NewNullableString(utils.Ptr("gateway")), - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefix: types.StringValue("192.168.42.0/24"), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - PublicIP: types.StringValue("publicIp"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - IPv6Gateway: types.StringValue("gateway"), - }, - true, - }, - { - "ipv4_nameservers_changed_outside_tf", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Nameservers: &[]string{ - "ns2", - "ns3", - }, - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "ipv6_nameservers_changed_outside_tf", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - NameserversV6: &[]string{ - "ns2", - "ns3", - }, - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "ipv4_prefixes_changed_outside_tf", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Prefixes: &[]string{ - "10.100.20.0/16", - "10.100.10.0/16", - }, - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Value(16), - IPv4Prefix: types.StringValue("10.100.20.0/16"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("10.100.20.0/16"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("10.100.20.0/16"), - types.StringValue("10.100.10.0/16"), - }), - }, - true, - }, - { - "ipv6_prefixes_changed_outside_tf", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - PrefixesV6: &[]string{ - "fd12:3456:789a:3::/64", - "fd12:3456:789a:4::/64", - }, - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefix: types.StringValue("fd12:3456:789a:3::/64"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:3::/64"), - types.StringValue("fd12:3456:789a:4::/64"), - }), - }, - true, - }, - { - "ipv4_ipv6_gateway_nil", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - }, - true, - }, - { - "response_nil_fail", - networkModel.DataSourceModel{}, - nil, - networkModel.DataSourceModel{}, - false, - }, - { - "no_resource_id", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - }, - &iaas.Network{}, - networkModel.DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/network/utils/v1network/resource.go b/stackit/internal/services/iaas/network/utils/v1network/resource.go deleted file mode 100644 index fa21084d2..000000000 --- a/stackit/internal/services/iaas/network/utils/v1network/resource.go +++ /dev/null @@ -1,536 +0,0 @@ -package v1network - -import ( - "context" - "fmt" - "net" - "net/http" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model networkModel.Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new network - - network, err := client.CreateNetwork(ctx, projectId).CreateNetworkPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err)) - return - } - - networkId := *network.NetworkId - network, err = wait.CreateNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) - return - } - - ctx = tflog.SetField(ctx, "network_id", networkId) - - // Map response body to schema - err = mapFields(ctx, network, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network created") -} - -func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform - var model networkModel.Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - networkResp, err := client.GetNetwork(ctx, projectId, networkId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, networkResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network read") -} - -func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model networkModel.Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - // Retrieve values from state - var stateModel networkModel.Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, &stateModel) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing network - err = client.PartialUpdateNetwork(ctx, projectId, networkId).PartialUpdateNetworkPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err)) - return - } - waitResp, err := wait.UpdateNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err)) - return - } - - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network updated") -} - -func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model networkModel.Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - // Delete existing network - err := client.DeleteNetwork(ctx, projectId, networkId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err)) - return - } - _, err = wait.DeleteNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Network deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,network_id -func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing network", - fmt.Sprintf("Expected import identifier with format: [project_id],[network_id] Got: %q", req.ID), - ) - return - } - - projectId := idParts[0] - networkId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...) - tflog.Info(ctx, "Network state imported") -} - -func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkModel.Model) error { - if networkResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var networkId string - if model.NetworkId.ValueString() != "" { - networkId = model.NetworkId.ValueString() - } else if networkResp.NetworkId != nil { - networkId = *networkResp.NetworkId - } else { - return fmt.Errorf("network id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), networkId) - - labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) - if err != nil { - return err - } - - // IPv4 - if networkResp.Nameservers == nil { - model.Nameservers = types.ListNull(types.StringType) - model.IPv4Nameservers = types.ListNull(types.StringType) - } else { - respNameservers := *networkResp.Nameservers - modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) - modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) - if err != nil { - return fmt.Errorf("get current network nameservers from model: %w", err) - } - if errIpv4 != nil { - return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) - } - - reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) - reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers) - - nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) - ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) - if diags.HasError() { - return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) - } - if ipv4Diags.HasError() { - return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) - } - - model.Nameservers = nameserversTF - model.IPv4Nameservers = ipv4NameserversTF - } - - if networkResp.Prefixes == nil { - model.Prefixes = types.ListNull(types.StringType) - model.IPv4Prefixes = types.ListNull(types.StringType) - } else { - respPrefixes := *networkResp.Prefixes - prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) - if diags.HasError() { - return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixes) > 0 { - model.IPv4Prefix = types.StringValue(respPrefixes[0]) - _, netmask, err := net.ParseCIDR(respPrefixes[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv4PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv4PrefixLength = types.Int64Value(int64(ones)) - } - } - - model.Prefixes = prefixesTF - model.IPv4Prefixes = prefixesTF - } - - if networkResp.Gateway != nil { - model.IPv4Gateway = types.StringPointerValue(networkResp.GetGateway()) - } else { - model.IPv4Gateway = types.StringNull() - } - - // IPv6 - - if networkResp.NameserversV6 == nil { - model.IPv6Nameservers = types.ListNull(types.StringType) - } else { - respIPv6Nameservers := *networkResp.NameserversV6 - modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) - if errIpv6 != nil { - return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) - } - - reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers) - - ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers) - if ipv6Diags.HasError() { - return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags)) - } - - model.IPv6Nameservers = ipv6NameserversTF - } - - if networkResp.PrefixesV6 == nil || len(*networkResp.PrefixesV6) == 0 { - model.IPv6Prefixes = types.ListNull(types.StringType) - model.IPv6Prefix = types.StringNull() - model.IPv6PrefixLength = types.Int64Null() - } else { - respPrefixesV6 := *networkResp.PrefixesV6 - prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) - if diags.HasError() { - return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixesV6) > 0 { - model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) - _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv6PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv6PrefixLength = types.Int64Value(int64(ones)) - } - } - model.IPv6Prefixes = prefixesV6TF - } - - if networkResp.Gatewayv6 != nil { - model.IPv6Gateway = types.StringPointerValue(networkResp.GetGatewayv6()) - } else { - model.IPv6Gateway = types.StringNull() - } - - model.NetworkId = types.StringValue(networkId) - model.Name = types.StringPointerValue(networkResp.Name) - model.PublicIP = types.StringPointerValue(networkResp.PublicIp) - model.Labels = labels - model.Routed = types.BoolPointerValue(networkResp.Routed) - model.Region = types.StringNull() - model.RoutingTableID = types.StringNull() - - return nil -} - -func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaas.CreateNetworkPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - addressFamily := &iaas.CreateNetworkAddressFamily{} - - var modelIPv6Nameservers []string - // Is true when IPv6Nameservers is not null or unset - if !utils.IsUndefined(model.IPv6Nameservers) { - // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. - // empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set - modelIPv6Nameservers = []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) - } - } - - if !utils.IsUndefined(model.IPv6Prefix) || !utils.IsUndefined(model.IPv6PrefixLength) || (modelIPv6Nameservers != nil) { - addressFamily.Ipv6 = &iaas.CreateNetworkIPv6Body{ - Prefix: conversion.StringValueToPointer(model.IPv6Prefix), - PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength), - } - // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. - // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, - // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. - if modelIPv6Nameservers != nil { - addressFamily.Ipv6.Nameservers = &modelIPv6Nameservers - } - - if model.NoIPv6Gateway.ValueBool() { - addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { - addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) - } - } - - modelIPv4Nameservers := []string{} - var modelIPv4List []attr.Value - - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { - modelIPv4List = model.IPv4Nameservers.Elements() - } else { - modelIPv4List = model.Nameservers.Elements() - } - - for _, ipv4ns := range modelIPv4List { - ipv4NameserverString, ok := ipv4ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) - } - - if !model.IPv4Prefix.IsNull() || !model.IPv4PrefixLength.IsNull() || !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() { - addressFamily.Ipv4 = &iaas.CreateNetworkIPv4Body{ - Nameservers: &modelIPv4Nameservers, - Prefix: conversion.StringValueToPointer(model.IPv4Prefix), - PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength), - } - - if model.NoIPv4Gateway.ValueBool() { - addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { - addressFamily.Ipv4.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) - } - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - payload := iaas.CreateNetworkPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - Routed: conversion.BoolValueToPointer(model.Routed), - } - - if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil { - payload.AddressFamily = addressFamily - } - - return &payload, nil -} - -func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) (*iaas.PartialUpdateNetworkPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - addressFamily := &iaas.UpdateNetworkAddressFamily{} - - var modelIPv6Nameservers []string - // Is true when IPv6Nameservers is not null or unset - if !utils.IsUndefined(model.IPv6Nameservers) { - // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. - // empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set - modelIPv6Nameservers = []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) - } - } - - if !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) || modelIPv6Nameservers != nil { - addressFamily.Ipv6 = &iaas.UpdateNetworkIPv6Body{} - - // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. - // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, - // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. - if modelIPv6Nameservers != nil { - addressFamily.Ipv6.Nameservers = &modelIPv6Nameservers - } - - if model.NoIPv6Gateway.ValueBool() { - addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil) - } else if !utils.IsUndefined(model.IPv6Gateway) { - addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) - } - } - - modelIPv4Nameservers := []string{} - var modelIPv4List []attr.Value - - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { - modelIPv4List = model.IPv4Nameservers.Elements() - } else { - modelIPv4List = model.Nameservers.Elements() - } - for _, ipv4ns := range modelIPv4List { - ipv4NameserverString, ok := ipv4ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) - } - - if !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() { - addressFamily.Ipv4 = &iaas.UpdateNetworkIPv4Body{ - Nameservers: &modelIPv4Nameservers, - } - - if model.NoIPv4Gateway.ValueBool() { - addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { - addressFamily.Ipv4.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) - } - } - currentLabels := stateModel.Labels - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - payload := iaas.PartialUpdateNetworkPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - } - - if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil { - payload.AddressFamily = addressFamily - } - - return &payload, nil -} diff --git a/stackit/internal/services/iaas/network/utils/v1network/resource_test.go b/stackit/internal/services/iaas/network/utils/v1network/resource_test.go deleted file mode 100644 index 9a1f289a7..000000000 --- a/stackit/internal/services/iaas/network/utils/v1network/resource_test.go +++ /dev/null @@ -1,811 +0,0 @@ -package v1network - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state model.Model - input *iaas.Network - expected model.Model - isValid bool - }{ - { - "id_ok", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Gateway: iaas.NewNullableString(nil), - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - IPv4Prefix: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefix: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - }, - true, - }, - { - "values_ok", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Name: utils.Ptr("name"), - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Prefixes: &[]string{ - "192.168.42.0/24", - "10.100.10.0/16", - }, - NameserversV6: &[]string{ - "ns1", - "ns2", - }, - PrefixesV6: &[]string{ - "fd12:3456:789a:1::/64", - "fd12:3456:789b:1::/64", - }, - PublicIp: utils.Ptr("publicIp"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(true), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Gatewayv6: iaas.NewNullableString(utils.Ptr("gateway")), - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefix: types.StringValue("192.168.42.0/24"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789b:1::/64"), - }), - IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), - PublicIP: types.StringValue("publicIp"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - IPv6Gateway: types.StringValue("gateway"), - }, - true, - }, - { - "ipv4_nameservers_changed_outside_tf", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Nameservers: &[]string{ - "ns2", - "ns3", - }, - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "ipv6_nameservers_changed_outside_tf", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - NameserversV6: &[]string{ - "ns2", - "ns3", - }, - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "ipv4_prefixes_changed_outside_tf", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/24"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Prefixes: &[]string{ - "192.168.54.0/24", - "192.168.55.0/24", - }, - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Value(24), - IPv4Prefix: types.StringValue("192.168.54.0/24"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.54.0/24"), - types.StringValue("192.168.55.0/24"), - }), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.54.0/24"), - types.StringValue("192.168.55.0/24"), - }), - }, - true, - }, - { - "ipv6_prefixes_changed_outside_tf", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - PrefixesV6: &[]string{ - "fd12:3456:789a:1::/64", - "fd12:3456:789a:2::/64", - }, - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), - }, - true, - }, - { - "ipv4_ipv6_gateway_nil", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - }, - true, - }, - { - "response_nil_fail", - model.Model{}, - nil, - model.Model{}, - false, - }, - { - "no_resource_id", - model.Model{ - ProjectId: types.StringValue("pid"), - }, - &iaas.Network{}, - model.Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *model.Model - expected *iaas.CreateNetworkPayload - isValid bool - }{ - { - "default_ok", - &model.Model{ - Name: types.StringValue("name"), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv4Gateway: types.StringValue("gateway"), - IPv4Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv4: &iaas.CreateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv4_nameservers_okay", - &model.Model{ - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv4Gateway: types.StringValue("gateway"), - IPv4Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv4: &iaas.CreateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv6_default_ok", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv6PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv6Gateway: types.StringValue("gateway"), - IPv6Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv6: &iaas.CreateNetworkIPv6Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv6_nameserver_null", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv6Gateway: types.StringValue("gateway"), - IPv6Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv6: &iaas.CreateNetworkIPv6Body{ - Nameservers: nil, - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv6_nameserver_empty_list", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), - IPv6PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv6Gateway: types.StringValue("gateway"), - IPv6Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv6: &iaas.CreateNetworkIPv6Body{ - Nameservers: utils.Ptr([]string{}), - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *model.Model - state model.Model - expected *iaas.PartialUpdateNetworkPayload - isValid bool - }{ - { - "default_ok", - &model.Model{ - Name: types.StringValue("name"), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv4_nameservers_okay", - &model.Model{ - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv4_gateway_nil", - &model.Model{ - Name: types.StringValue("name"), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_default_ok", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv6Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_gateway_nil", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_nameserver_null", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListNull(types.StringType), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv6Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: nil, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_nameserver_empty_list", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv6Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: &[]string{}, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/network/utils/v2network/datasource.go b/stackit/internal/services/iaas/network/utils/v2network/datasource.go deleted file mode 100644 index bc447b825..000000000 --- a/stackit/internal/services/iaas/network/utils/v2network/datasource.go +++ /dev/null @@ -1,215 +0,0 @@ -package v2network - -import ( - "context" - "fmt" - "net" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { // nolint:gocritic // function signature required by Terraform - var model networkModel.DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - region := providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - networkResp, err := client.GetNetwork(ctx, projectId, region, networkId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading network", - fmt.Sprintf("Network with ID %q does not exist in project %q.", networkId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - err = mapDataSourceFields(ctx, networkResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network read") -} - -func mapDataSourceFields(ctx context.Context, networkResp *iaasalpha.Network, model *networkModel.DataSourceModel, region string) error { - if networkResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var networkId string - if model.NetworkId.ValueString() != "" { - networkId = model.NetworkId.ValueString() - } else if networkResp.Id != nil { - networkId = *networkResp.Id - } else { - return fmt.Errorf("network id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId) - - labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) - if err != nil { - return err - } - - // IPv4 - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil { - model.Nameservers = types.ListNull(types.StringType) - model.IPv4Nameservers = types.ListNull(types.StringType) - } else { - respNameservers := *networkResp.Ipv4.Nameservers - modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) - modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) - if err != nil { - return fmt.Errorf("get current network nameservers from model: %w", err) - } - if errIpv4 != nil { - return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) - } - - reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) - reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers) - - nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) - ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) - if diags.HasError() { - return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) - } - if ipv4Diags.HasError() { - return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) - } - - model.Nameservers = nameserversTF - model.IPv4Nameservers = ipv4NameserversTF - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil { - model.Prefixes = types.ListNull(types.StringType) - model.IPv4Prefixes = types.ListNull(types.StringType) - } else { - respPrefixes := *networkResp.Ipv4.Prefixes - prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) - if diags.HasError() { - return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixes) > 0 { - model.IPv4Prefix = types.StringValue(respPrefixes[0]) - _, netmask, err := net.ParseCIDR(respPrefixes[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv4PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv4PrefixLength = types.Int64Value(int64(ones)) - } - } - - model.Prefixes = prefixesTF - model.IPv4Prefixes = prefixesTF - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil { - model.IPv4Gateway = types.StringNull() - } else { - model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway()) - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil { - model.PublicIP = types.StringNull() - } else { - model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp) - } - - // IPv6 - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil { - model.IPv6Nameservers = types.ListNull(types.StringType) - } else { - respIPv6Nameservers := *networkResp.Ipv6.Nameservers - modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) - if errIpv6 != nil { - return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) - } - - reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers) - - ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers) - if ipv6Diags.HasError() { - return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags)) - } - - model.IPv6Nameservers = ipv6NameserversTF - } - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil { - model.IPv6Prefixes = types.ListNull(types.StringType) - } else { - respPrefixesV6 := *networkResp.Ipv6.Prefixes - prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) - if diags.HasError() { - return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixesV6) > 0 { - model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) - _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv6PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv6PrefixLength = types.Int64Value(int64(ones)) - } - } - model.IPv6Prefixes = prefixesV6TF - } - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil { - model.IPv6Gateway = types.StringNull() - } else { - model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway()) - } - - model.RoutingTableID = types.StringNull() - if networkResp.RoutingTableId != nil { - model.RoutingTableID = types.StringValue(*networkResp.RoutingTableId) - } - - model.NetworkId = types.StringValue(networkId) - model.Name = types.StringPointerValue(networkResp.Name) - model.Labels = labels - model.Routed = types.BoolPointerValue(networkResp.Routed) - model.Region = types.StringValue(region) - - return nil -} diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource.go b/stackit/internal/services/iaas/network/utils/v2network/resource.go deleted file mode 100644 index dbf318209..000000000 --- a/stackit/internal/services/iaas/network/utils/v2network/resource.go +++ /dev/null @@ -1,581 +0,0 @@ -package v2network - -import ( - "context" - "fmt" - "net" - "net/http" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model networkModel.Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new network - - network, err := client.CreateNetwork(ctx, projectId, region).CreateNetworkPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err)) - return - } - - networkId := *network.Id - network, err = wait.CreateNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) - return - } - - ctx = tflog.SetField(ctx, "network_id", networkId) - - // Map response body to schema - err = mapFields(ctx, network, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network created") -} - -func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { // nolint:gocritic // function signature required by Terraform - var model networkModel.Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - region := providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "region", region) - - networkResp, err := client.GetNetwork(ctx, projectId, region, networkId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, networkResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network read") -} - -func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model networkModel.Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "region", region) - - // Retrieve values from state - var stateModel networkModel.Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, &stateModel) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing network - err = client.PartialUpdateNetwork(ctx, projectId, region, networkId).PartialUpdateNetworkPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err)) - return - } - waitResp, err := wait.UpdateNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err)) - return - } - - err = mapFields(ctx, waitResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network updated") -} - -func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model networkModel.Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing network - err := client.DeleteNetwork(ctx, projectId, region, networkId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err)) - return - } - _, err = wait.DeleteNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Network deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,region,network_id -func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing network", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id] Got: %q", req.ID), - ) - return - } - - projectId := idParts[0] - region := idParts[1] - networkId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_id", networkId) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...) - tflog.Info(ctx, "Network state imported") -} - -func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *networkModel.Model, region string) error { - if networkResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var networkId string - if model.NetworkId.ValueString() != "" { - networkId = model.NetworkId.ValueString() - } else if networkResp.Id != nil { - networkId = *networkResp.Id - } else { - return fmt.Errorf("network id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId) - - labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) - if err != nil { - return err - } - - // IPv4 - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil { - model.Nameservers = types.ListNull(types.StringType) - model.IPv4Nameservers = types.ListNull(types.StringType) - } else { - respNameservers := *networkResp.Ipv4.Nameservers - modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) - modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) - if err != nil { - return fmt.Errorf("get current network nameservers from model: %w", err) - } - if errIpv4 != nil { - return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) - } - - reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) - reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers) - - nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) - ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) - if diags.HasError() { - return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) - } - if ipv4Diags.HasError() { - return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) - } - - model.Nameservers = nameserversTF - model.IPv4Nameservers = ipv4NameserversTF - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil { - model.Prefixes = types.ListNull(types.StringType) - model.IPv4Prefixes = types.ListNull(types.StringType) - } else { - respPrefixes := *networkResp.Ipv4.Prefixes - prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) - if diags.HasError() { - return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixes) > 0 { - model.IPv4Prefix = types.StringValue(respPrefixes[0]) - _, netmask, err := net.ParseCIDR(respPrefixes[0]) - if err != nil { - tflog.Error(ctx, fmt.Sprintf("ipv4_prefix_length: %+v", err)) - // silently ignore parsing error for the netmask - model.IPv4PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv4PrefixLength = types.Int64Value(int64(ones)) - } - } - - model.Prefixes = prefixesTF - model.IPv4Prefixes = prefixesTF - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil { - model.IPv4Gateway = types.StringNull() - } else { - model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway()) - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil { - model.PublicIP = types.StringNull() - } else { - model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp) - } - - // IPv6 - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil { - model.IPv6Nameservers = types.ListNull(types.StringType) - } else { - respIPv6Nameservers := *networkResp.Ipv6.Nameservers - modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) - if errIpv6 != nil { - return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) - } - - reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers) - - ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers) - if ipv6Diags.HasError() { - return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags)) - } - - model.IPv6Nameservers = ipv6NameserversTF - } - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil { - model.IPv6Prefixes = types.ListNull(types.StringType) - } else { - respPrefixesV6 := *networkResp.Ipv6.Prefixes - prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) - if diags.HasError() { - return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixesV6) > 0 { - model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) - _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv6PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv6PrefixLength = types.Int64Value(int64(ones)) - } - } - model.IPv6Prefixes = prefixesV6TF - } - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil { - model.IPv6Gateway = types.StringNull() - } else { - model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway()) - } - - if networkResp.RoutingTableId != nil { - model.RoutingTableID = types.StringPointerValue(networkResp.RoutingTableId) - } else { - model.RoutingTableID = types.StringNull() - } - - model.NetworkId = types.StringValue(networkId) - model.Name = types.StringPointerValue(networkResp.Name) - model.Labels = labels - model.Routed = types.BoolPointerValue(networkResp.Routed) - model.Region = types.StringValue(region) - - return nil -} - -func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha.CreateNetworkPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - var modelIPv6Nameservers []string - // Is true when IPv6Nameservers is not null or unset - if !utils.IsUndefined(model.IPv6Nameservers) { - // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. - // empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set - modelIPv6Nameservers = []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) - } - } - - var ipv6Body *iaasalpha.CreateNetworkIPv6 - if !utils.IsUndefined(model.IPv6PrefixLength) { - ipv6Body = &iaasalpha.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefixLength: &iaasalpha.CreateNetworkIPv6WithPrefixLength{ - PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength), - }, - } - // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. - // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, - // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. - if modelIPv6Nameservers != nil { - ipv6Body.CreateNetworkIPv6WithPrefixLength.Nameservers = &modelIPv6Nameservers - } - } else if !utils.IsUndefined(model.IPv6Prefix) { - var gateway *iaasalpha.NullableString - if model.NoIPv6Gateway.ValueBool() { - gateway = iaasalpha.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { - gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) - } - - ipv6Body = &iaasalpha.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ - Gateway: gateway, - Prefix: conversion.StringValueToPointer(model.IPv6Prefix), - }, - } - // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. - // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, - // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. - if modelIPv6Nameservers != nil { - ipv6Body.CreateNetworkIPv6WithPrefix.Nameservers = &modelIPv6Nameservers - } - } - - modelIPv4Nameservers := []string{} - var modelIPv4List []attr.Value - - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { - modelIPv4List = model.IPv4Nameservers.Elements() - } else { - modelIPv4List = model.Nameservers.Elements() - } - - for _, ipv4ns := range modelIPv4List { - ipv4NameserverString, ok := ipv4ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) - } - - var ipv4Body *iaasalpha.CreateNetworkIPv4 - if !utils.IsUndefined(model.IPv4PrefixLength) { - ipv4Body = &iaasalpha.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefixLength: &iaasalpha.CreateNetworkIPv4WithPrefixLength{ - Nameservers: &modelIPv4Nameservers, - PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength), - }, - } - } else if !utils.IsUndefined(model.IPv4Prefix) { - var gateway *iaasalpha.NullableString - if model.NoIPv4Gateway.ValueBool() { - gateway = iaasalpha.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { - gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) - } - - ipv4Body = &iaasalpha.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{ - Nameservers: &modelIPv4Nameservers, - Prefix: conversion.StringValueToPointer(model.IPv4Prefix), - Gateway: gateway, - }, - } - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - payload := iaasalpha.CreateNetworkPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - Routed: conversion.BoolValueToPointer(model.Routed), - Ipv4: ipv4Body, - Ipv6: ipv6Body, - RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID), - } - - return &payload, nil -} - -func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) (*iaasalpha.PartialUpdateNetworkPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - var modelIPv6Nameservers []string - // Is true when IPv6Nameservers is not null or unset - if !utils.IsUndefined(model.IPv6Nameservers) { - // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. - // empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set - modelIPv6Nameservers = []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) - } - } - - var ipv6Body *iaasalpha.UpdateNetworkIPv6Body - if modelIPv6Nameservers != nil || !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) { - ipv6Body = &iaasalpha.UpdateNetworkIPv6Body{} - // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. - // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, - // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. - if modelIPv6Nameservers != nil { - ipv6Body.Nameservers = &modelIPv6Nameservers - } - - if model.NoIPv6Gateway.ValueBool() { - ipv6Body.Gateway = iaasalpha.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { - ipv6Body.Gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) - } - } - - modelIPv4Nameservers := []string{} - var modelIPv4List []attr.Value - - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { - modelIPv4List = model.IPv4Nameservers.Elements() - } else { - modelIPv4List = model.Nameservers.Elements() - } - for _, ipv4ns := range modelIPv4List { - ipv4NameserverString, ok := ipv4ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) - } - - var ipv4Body *iaasalpha.UpdateNetworkIPv4Body - if !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() { - ipv4Body = &iaasalpha.UpdateNetworkIPv4Body{ - Nameservers: &modelIPv4Nameservers, - } - - if model.NoIPv4Gateway.ValueBool() { - ipv4Body.Gateway = iaasalpha.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { - ipv4Body.Gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) - } - } - currentLabels := stateModel.Labels - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - payload := iaasalpha.PartialUpdateNetworkPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - Ipv4: ipv4Body, - Ipv6: ipv6Body, - RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID), - } - - return &payload, nil -} diff --git a/stackit/internal/services/iaas/networkarea/datasource.go b/stackit/internal/services/iaas/networkarea/datasource.go index 15deb0886..1b2c35fda 100644 --- a/stackit/internal/services/iaas/networkarea/datasource.go +++ b/stackit/internal/services/iaas/networkarea/datasource.go @@ -2,9 +2,15 @@ package networkarea import ( "context" + "errors" "fmt" "net/http" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" @@ -17,8 +23,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -58,6 +62,7 @@ func (d *networkAreaDataSource) Configure(ctx context.Context, req datasource.Co // Schema defines the schema for the data source. func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + deprecationMsg := "Deprecated because of the IaaS API v1 -> v2 migration. Will be removed in May 2026." description := "Network area datasource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ Description: description, @@ -99,13 +104,15 @@ func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaReq }, }, "default_nameservers": schema.ListAttribute{ - Description: "List of DNS Servers/Nameservers.", - Computed: true, - ElementType: types.StringType, + DeprecationMessage: deprecationMsg, + Description: "List of DNS Servers/Nameservers.", + Computed: true, + ElementType: types.StringType, }, "network_ranges": schema.ListNestedAttribute{ - Description: "List of Network ranges.", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "List of Network ranges.", + Computed: true, Validators: []validator.List{ listvalidator.SizeAtLeast(1), listvalidator.SizeAtMost(64), @@ -126,28 +133,32 @@ func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaReq }, }, "transfer_network": schema.StringAttribute{ - Description: "Classless Inter-Domain Routing (CIDR).", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "Classless Inter-Domain Routing (CIDR).", + Computed: true, }, "default_prefix_length": schema.Int64Attribute{ - Description: "The default prefix length for networks in the network area.", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The default prefix length for networks in the network area.", + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(24), int64validator.AtMost(29), }, }, "max_prefix_length": schema.Int64Attribute{ - Description: "The maximal prefix length for networks in the network area.", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The maximal prefix length for networks in the network area.", + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(24), int64validator.AtMost(29), }, }, "min_prefix_length": schema.Int64Attribute{ - Description: "The minimal prefix length for networks in the network area.", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The minimal prefix length for networks in the network area.", + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(22), int64validator.AtMost(29), @@ -191,13 +202,32 @@ func (d *networkAreaDataSource) Read(ctx context.Context, req datasource.ReadReq return } - networkAreaRanges := networkAreaResp.Ipv4.NetworkRanges - - err = mapFields(ctx, networkAreaResp, networkAreaRanges, &model) + err = mapFields(ctx, networkAreaResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Processing API payload: %v", err)) return } + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + networkAreaRegionResp, err := d.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if !(ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest)) { // TODO: iaas api returns http 400 in case network area region is not found + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Calling API: %v", err)) + return + } + + networkAreaRegionResp = &iaas.RegionalArea{} + } + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/iaas/networkarea/resource.go b/stackit/internal/services/iaas/networkarea/resource.go index a688e7d88..0d26c0b67 100644 --- a/stackit/internal/services/iaas/networkarea/resource.go +++ b/stackit/internal/services/iaas/networkarea/resource.go @@ -2,6 +2,7 @@ package networkarea import ( "context" + "errors" "fmt" "net/http" "strings" @@ -34,26 +35,55 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) +const ( + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + defaultValueDefaultPrefixLength = 25 + + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + defaultValueMinPrefixLength = 24 + + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + defaultValueMaxPrefixLength = 29 + + // Deprecated: Will be removed in May 2026. + deprecationWarningSummary = "Migration to new `stackit_network_area_region` resource needed" + // Deprecated: Will be removed in May 2026. + deprecationWarningDetails = "You're using deprecated features of the `stackit_network_area` resource. These will be removed in May 2026. Migrate to the new `stackit_network_area_region` resource instead." +) + // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &networkAreaResource{} - _ resource.ResourceWithConfigure = &networkAreaResource{} - _ resource.ResourceWithImportState = &networkAreaResource{} + _ resource.Resource = &networkAreaResource{} + _ resource.ResourceWithConfigure = &networkAreaResource{} + _ resource.ResourceWithImportState = &networkAreaResource{} + _ resource.ResourceWithValidateConfig = &networkAreaResource{} ) type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - Name types.String `tfsdk:"name"` - ProjectCount types.Int64 `tfsdk:"project_count"` - DefaultNameservers types.List `tfsdk:"default_nameservers"` - NetworkRanges types.List `tfsdk:"network_ranges"` - TransferNetwork types.String `tfsdk:"transfer_network"` - DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"` - MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"` - MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"` - Labels types.Map `tfsdk:"labels"` + Id types.String `tfsdk:"id"` // needed by TF + OrganizationId types.String `tfsdk:"organization_id"` + NetworkAreaId types.String `tfsdk:"network_area_id"` + Name types.String `tfsdk:"name"` + ProjectCount types.Int64 `tfsdk:"project_count"` + Labels types.Map `tfsdk:"labels"` + + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + DefaultNameservers types.List `tfsdk:"default_nameservers"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + NetworkRanges types.List `tfsdk:"network_ranges"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + TransferNetwork types.String `tfsdk:"transfer_network"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"` +} + +// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. LegacyMode checks if any of the deprecated fields are set which now relate to the network area region API resource. +func (model *Model) LegacyMode() bool { + return !model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown() || !model.TransferNetwork.IsNull() || model.TransferNetwork.IsUnknown() || !model.DefaultNameservers.IsNull() || model.DefaultNameservers.IsUnknown() || model.DefaultPrefixLength != types.Int64Value(int64(defaultValueDefaultPrefixLength)) || model.MinPrefixLength != types.Int64Value(int64(defaultValueMinPrefixLength)) || model.MaxPrefixLength != types.Int64Value(int64(defaultValueMaxPrefixLength)) } // Struct corresponding to Model.NetworkRanges[i] @@ -104,9 +134,27 @@ func (r *networkAreaResource) Configure(ctx context.Context, req resource.Config tflog.Info(ctx, "IaaS client configured") } +// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. +func (r *networkAreaResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var resourceModel Model + resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) + if resp.Diagnostics.HasError() { + return + } + + if resourceModel.NetworkRanges.IsNull() != resourceModel.TransferNetwork.IsNull() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network network area", "You have to either provide both the `network_ranges` and `transfer_network` fields simultaneously or none of them.") + } + + if (resourceModel.NetworkRanges.IsNull() || resourceModel.TransferNetwork.IsNull()) && (!resourceModel.DefaultNameservers.IsNull() || !resourceModel.DefaultPrefixLength.IsNull() || !resourceModel.MinPrefixLength.IsNull() || !resourceModel.MaxPrefixLength.IsNull()) { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network network area", "You have to provide both the `network_ranges` and `transfer_network` fields when providing one of these fields: `default_nameservers`, `default_prefix_length`, `max_prefix_length`, `min_prefix_length`") + } +} + // Schema defines the schema for the resource. func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Network area resource schema. Must have a `region` specified in the provider configuration." + deprecationMsg := "Deprecated because of the IaaS API v1 -> v2 migration. Will be removed in May 2026. Use the new `stackit_network_area_region` resource instead." + description := "Network area resource schema." resp.Schema = schema.Schema{ Description: description, MarkdownDescription: description, @@ -155,14 +203,18 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest int64validator.AtLeast(0), }, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "default_nameservers": schema.ListAttribute{ - Description: "List of DNS Servers/Nameservers.", - Optional: true, - ElementType: types.StringType, + Description: "List of DNS Servers/Nameservers for configuration of network area for region `eu01`.", + DeprecationMessage: deprecationMsg, + Optional: true, + ElementType: types.StringType, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "network_ranges": schema.ListNestedAttribute{ - Description: "List of Network ranges.", - Required: true, + Description: "List of Network ranges for configuration of network area for region `eu01`.", + DeprecationMessage: deprecationMsg, + Optional: true, Validators: []validator.List{ listvalidator.SizeAtLeast(1), listvalidator.SizeAtMost(64), @@ -170,55 +222,65 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "network_range_id": schema.StringAttribute{ - Computed: true, + DeprecationMessage: deprecationMsg, + Computed: true, Validators: []validator.String{ validate.UUID(), validate.NoSeparator(), }, }, "prefix": schema.StringAttribute{ - Description: "Classless Inter-Domain Routing (CIDR).", - Required: true, + DeprecationMessage: deprecationMsg, + Description: "Classless Inter-Domain Routing (CIDR).", + Required: true, }, }, }, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "transfer_network": schema.StringAttribute{ - Description: "Classless Inter-Domain Routing (CIDR).", - Required: true, + DeprecationMessage: deprecationMsg, + Description: "Classless Inter-Domain Routing (CIDR) for configuration of network area for region `eu01`.", + Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "default_prefix_length": schema.Int64Attribute{ - Description: "The default prefix length for networks in the network area.", - Optional: true, - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The default prefix length for networks in the network area for region `eu01`.", + Optional: true, + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(24), int64validator.AtMost(29), }, - Default: int64default.StaticInt64(25), + Default: int64default.StaticInt64(defaultValueDefaultPrefixLength), }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "max_prefix_length": schema.Int64Attribute{ - Description: "The maximal prefix length for networks in the network area.", - Optional: true, - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The maximal prefix length for networks in the network area for region `eu01`.", + Optional: true, + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(24), int64validator.AtMost(29), }, - Default: int64default.StaticInt64(29), + Default: int64default.StaticInt64(defaultValueMaxPrefixLength), }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "min_prefix_length": schema.Int64Attribute{ - Description: "The minimal prefix length for networks in the network area.", - Optional: true, - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The minimal prefix length for networks in the network area for region `eu01`.", + Optional: true, + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(8), int64validator.AtMost(29), }, - Default: int64default.StaticInt64(24), + Default: int64default.StaticInt64(defaultValueMinPrefixLength), }, "labels": schema.MapAttribute{ Description: "Labels are key-value string pairs which can be attached to a resource container", @@ -233,8 +295,7 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) if resp.Diagnostics.HasError() { return } @@ -250,31 +311,65 @@ func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateReq } // Create new network area - area, err := r.client.CreateNetworkArea(ctx, organizationId).CreateNetworkAreaPayload(*payload).Execute() + networkArea, err := r.client.CreateNetworkArea(ctx, organizationId).CreateNetworkAreaPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Calling API: %v", err)) return } - networkArea, err := wait.CreateNetworkAreaWaitHandler(ctx, r.client, organizationId, *area.AreaId).WaitWithContext(context.Background()) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Network area creation waiting: %v", err)) - return - } - networkAreaId := *networkArea.AreaId + networkAreaId := *networkArea.Id ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - networkAreaRanges := networkArea.Ipv4.NetworkRanges - // Map response body to schema - err = mapFields(ctx, networkArea, networkAreaRanges, &model) + err = mapFields(ctx, networkArea, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Processing API payload: %v", err)) return } + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + if model.LegacyMode() { + core.LogAndAddWarning(ctx, &resp.Diagnostics, deprecationWarningSummary, deprecationWarningDetails) + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + regionCreatePayload, err := toRegionCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + _, err = r.client.CreateNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").CreateNetworkAreaRegionPayload(*regionCreatePayload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + networkAreaRegionResp, err := wait.CreateNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, "eu01").WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error waiting for network area region creation", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + } else { + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) + model.DefaultNameservers = types.ListNull(types.StringType) + model.TransferNetwork = types.StringNull() + model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength) + model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength) + model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength) + } + // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) if resp.Diagnostics.HasError() { return } @@ -284,11 +379,11 @@ func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateReq // Read refreshes the Terraform state with the latest data. func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) if resp.Diagnostics.HasError() { return } + organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() ctx = tflog.SetField(ctx, "organization_id", organizationId) @@ -296,7 +391,8 @@ func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest networkAreaResp, err := r.client.GetNetworkArea(ctx, organizationId, networkAreaId).Execute() if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return @@ -305,17 +401,53 @@ func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest return } - networkAreaRanges := networkAreaResp.Ipv4.NetworkRanges - // Map response body to schema - err = mapFields(ctx, networkAreaResp, networkAreaRanges, &model) + err = mapFields(ctx, networkAreaResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Processing API payload: %v", err)) return } + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + if model.LegacyMode() { + core.LogAndAddWarning(ctx, &resp.Diagnostics, deprecationWarningSummary, deprecationWarningDetails) + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + networkAreaRegionResp, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if !(ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest)) { // TODO: iaas api returns http 400 in case network area region is not found + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Calling API: %v", err)) + return + } + + model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) + model.DefaultNameservers = types.ListNull(types.StringType) + model.TransferNetwork = types.StringNull() + model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength) + model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength) + model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength) + } else { + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + } + } else { + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) + model.DefaultNameservers = types.ListNull(types.StringType) + model.TransferNetwork = types.StringNull() + model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength) + model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength) + model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength) + } + // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) if resp.Diagnostics.HasError() { return } @@ -326,11 +458,11 @@ func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) if resp.Diagnostics.HasError() { return } + organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() ctx = tflog.SetField(ctx, "organization_id", organizationId) @@ -338,8 +470,7 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq ranges := []networkRange{} if !(model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown()) { - diags = model.NetworkRanges.ElementsAs(ctx, &ranges, false) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(model.NetworkRanges.ElementsAs(ctx, &ranges, false)...) if resp.Diagnostics.HasError() { return } @@ -347,8 +478,7 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq // Retrieve values from state var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...) if resp.Diagnostics.HasError() { return } @@ -360,44 +490,79 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq return } // Update existing network - _, err = r.client.PartialUpdateNetworkArea(ctx, organizationId, networkAreaId).PartialUpdateNetworkAreaPayload(*payload).Execute() + networkAreaUpdateResp, err := r.client.PartialUpdateNetworkArea(ctx, organizationId, networkAreaId).PartialUpdateNetworkAreaPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Calling API: %v", err)) return } - waitResp, err := wait.UpdateNetworkAreaWaitHandler(ctx, r.client, organizationId, networkAreaId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Network area update waiting: %v", err)) - return - } - // Update network ranges - err = updateNetworkRanges(ctx, organizationId, networkAreaId, ranges, r.client) + err = mapFields(ctx, networkAreaUpdateResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Updating Network ranges: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Processing API payload: %v", err)) return } - networkAreaResp, err := r.client.GetNetworkArea(ctx, organizationId, networkAreaId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + if model.LegacyMode() { + core.LogAndAddWarning(ctx, &resp.Diagnostics, deprecationWarningSummary, deprecationWarningDetails) + + // Deprecated: Update network area region payload creation. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + regionUpdatePayload, err := toRegionUpdatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Creating API payload: %v", err)) return } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Calling API: %v", err)) - return - } - networkAreaRanges := networkAreaResp.Ipv4.NetworkRanges + // Deprecated: Update network area region. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + networkAreaRegionUpdateResp, err := r.client.UpdateNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").UpdateNetworkAreaRegionPayload(*regionUpdatePayload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Calling API: %v", err)) + return + } - err = mapFields(ctx, waitResp, networkAreaRanges, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Processing API payload: %v", err)) - return + // Deprecated: Update network area region. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + err = mapNetworkAreaRegionFields(ctx, networkAreaRegionUpdateResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Deprecated: Update network ranges. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + err = updateNetworkRanges(ctx, organizationId, networkAreaId, ranges, r.client) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Updating Network ranges: %v", err)) + return + } + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + networkAreaRegionResp, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest) { // TODO: iaas api returns http 400 in case network area region is not found + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + } else { + // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) + model.DefaultNameservers = types.ListNull(types.StringType) + model.TransferNetwork = types.StringNull() + model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength) + model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength) + model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength) } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) if resp.Diagnostics.HasError() { return } @@ -425,15 +590,32 @@ func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteReq return } - // Delete existing network - err = r.client.DeleteNetworkArea(ctx, organizationId, networkAreaId).Execute() + // Get all configured regions so we can delete them one by one before deleting the network area + regionsListResp, err := r.client.ListNetworkAreaRegions(ctx, organizationId, networkAreaId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Calling API: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Calling API to list configured regions: %v", err)) return } - _, err = wait.DeleteNetworkAreaWaitHandler(ctx, r.client, organizationId, networkAreaId).WaitWithContext(ctx) + + // Delete network region configurations + for region := range *regionsListResp.Regions { + err = r.client.DeleteNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Calling API: %v", err)) + return + } + + _, err = wait.DeleteNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, region).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Waiting for networea deletion: %v", err)) + return + } + } + + // Delete existing network area + err = r.client.DeleteNetworkArea(ctx, organizationId, networkAreaId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Network area deletion waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Calling API: %v", err)) return } @@ -463,7 +645,7 @@ func (r *networkAreaResource) ImportState(ctx context.Context, req resource.Impo tflog.Info(ctx, "Network state imported") } -func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, networkAreaRangesResp *[]iaas.NetworkRange, model *Model) error { +func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, model *Model) error { if networkAreaResp == nil { return fmt.Errorf("response input is nil") } @@ -474,18 +656,41 @@ func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, networkAr var networkAreaId string if model.NetworkAreaId.ValueString() != "" { networkAreaId = model.NetworkAreaId.ValueString() - } else if networkAreaResp.AreaId != nil { - networkAreaId = *networkAreaResp.AreaId + } else if networkAreaResp.Id != nil { + networkAreaId = *networkAreaResp.Id } else { return fmt.Errorf("network area id not present") } model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), networkAreaId) - if networkAreaResp.Ipv4 == nil || networkAreaResp.Ipv4.DefaultNameservers == nil { + labels, err := iaasUtils.MapLabels(ctx, networkAreaResp.Labels, model.Labels) + if err != nil { + return err + } + + model.NetworkAreaId = types.StringValue(networkAreaId) + model.Name = types.StringPointerValue(networkAreaResp.Name) + model.ProjectCount = types.Int64PointerValue(networkAreaResp.ProjectCount) + model.Labels = labels + + return nil +} + +// Deprecated: mapRegionFields maps the region configuration for eu01 to avoid a breaking change in the Terraform provider during the IaaS v1 -> v2 API migration. Will be removed in May 2026. +func mapNetworkAreaRegionFields(ctx context.Context, networkAreaRegionResp *iaas.RegionalArea, model *Model) error { + if model == nil { + return fmt.Errorf("model input is nil") + } + if networkAreaRegionResp == nil { + return fmt.Errorf("response input is nil") + } + + // map default nameservers + if networkAreaRegionResp.Ipv4 == nil || networkAreaRegionResp.Ipv4.DefaultNameservers == nil { model.DefaultNameservers = types.ListNull(types.StringType) } else { - respDefaultNameservers := *networkAreaResp.Ipv4.DefaultNameservers + respDefaultNameservers := *networkAreaRegionResp.Ipv4.DefaultNameservers modelDefaultNameservers, err := utils.ListValuetoStringSlice(model.DefaultNameservers) if err != nil { return fmt.Errorf("get current network area default nameservers from model: %w", err) @@ -501,31 +706,28 @@ func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, networkAr model.DefaultNameservers = defaultNameserversTF } - err := mapNetworkRanges(ctx, networkAreaRangesResp, model) - if err != nil { - return fmt.Errorf("mapping network ranges: %w", err) - } - - labels, err := iaasUtils.MapLabels(ctx, networkAreaResp.Labels, model.Labels) - if err != nil { - return err + // map network ranges + if networkAreaRegionResp.Ipv4 == nil || networkAreaRegionResp.Ipv4.NetworkRanges == nil { + model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) + } else { + err := mapNetworkRanges(ctx, networkAreaRegionResp.Ipv4.NetworkRanges, model) + if err != nil { + return fmt.Errorf("mapping network ranges: %w", err) + } } - model.NetworkAreaId = types.StringValue(networkAreaId) - model.Name = types.StringPointerValue(networkAreaResp.Name) - model.ProjectCount = types.Int64PointerValue(networkAreaResp.ProjectCount) - model.Labels = labels - - if networkAreaResp.Ipv4 != nil { - model.TransferNetwork = types.StringPointerValue(networkAreaResp.Ipv4.TransferNetwork) - model.DefaultPrefixLength = types.Int64PointerValue(networkAreaResp.Ipv4.DefaultPrefixLen) - model.MaxPrefixLength = types.Int64PointerValue(networkAreaResp.Ipv4.MaxPrefixLen) - model.MinPrefixLength = types.Int64PointerValue(networkAreaResp.Ipv4.MinPrefixLen) + // map remaining fields + if networkAreaRegionResp.Ipv4 != nil { + model.TransferNetwork = types.StringPointerValue(networkAreaRegionResp.Ipv4.TransferNetwork) + model.DefaultPrefixLength = types.Int64PointerValue(networkAreaRegionResp.Ipv4.DefaultPrefixLen) + model.MaxPrefixLength = types.Int64PointerValue(networkAreaRegionResp.Ipv4.MaxPrefixLen) + model.MinPrefixLength = types.Int64PointerValue(networkAreaRegionResp.Ipv4.MinPrefixLen) } return nil } +// Deprecated: mapNetworkRanges will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only kept to circumvent breaking changes. func mapNetworkRanges(ctx context.Context, networkAreaRangesList *[]iaas.NetworkRange, model *Model) error { var diags diag.Diagnostics @@ -562,7 +764,7 @@ func mapNetworkRanges(ctx context.Context, networkAreaRangesList *[]iaas.Network var networkRangeId string for _, networkRangeElement := range *networkAreaRangesList { if *networkRangeElement.Prefix == prefix { - networkRangeId = *networkRangeElement.NetworkRangeId + networkRangeId = *networkRangeElement.Id break } } @@ -596,13 +798,26 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea return nil, fmt.Errorf("nil model") } - modelDefaultNameservers := []string{} - for _, ns := range model.DefaultNameservers.Elements() { - nameserverString, ok := ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString()) + labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + return &iaas.CreateNetworkAreaPayload{ + Name: conversion.StringValueToPointer(model.Name), + Labels: &labels, + }, nil +} + +// Deprecated: toRegionCreatePayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes. +func toRegionCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaRegionPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model) + if err != nil { + return nil, fmt.Errorf("converting default nameservers: %w", err) } networkRangesPayload, err := toNetworkRangesPayload(ctx, model) @@ -610,32 +825,57 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea return nil, fmt.Errorf("converting network ranges: %w", err) } - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) + return &iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: &modelDefaultNameservers, + DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength), + MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength), + MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength), + TransferNetwork: conversion.StringValueToPointer(model.TransferNetwork), + NetworkRanges: networkRangesPayload, + }, + }, nil +} + +func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.PartialUpdateNetworkAreaPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) } - return &iaas.CreateNetworkAreaPayload{ - Name: conversion.StringValueToPointer(model.Name), - AddressFamily: &iaas.CreateAreaAddressFamily{ - Ipv4: &iaas.CreateAreaIPv4{ - DefaultNameservers: &modelDefaultNameservers, - NetworkRanges: networkRangesPayload, - TransferNetwork: conversion.StringValueToPointer(model.TransferNetwork), - DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength), - MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength), - MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength), - }, - }, + return &iaas.PartialUpdateNetworkAreaPayload{ + Name: conversion.StringValueToPointer(model.Name), Labels: &labels, }, nil } -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.PartialUpdateNetworkAreaPayload, error) { +// Deprecated: toRegionUpdatePayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes. +func toRegionUpdatePayload(ctx context.Context, model *Model) (*iaas.UpdateNetworkAreaRegionPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } + modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model) + if err != nil { + return nil, fmt.Errorf("converting default nameservers: %w", err) + } + + return &iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: &modelDefaultNameservers, + DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength), + MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength), + MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength), + }, + }, nil +} + +// Deprecated: toDefaultNameserversPayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes. +func toDefaultNameserversPayload(_ context.Context, model *Model) ([]string, error) { modelDefaultNameservers := []string{} for _, ns := range model.DefaultNameservers.Elements() { nameserverString, ok := ns.(types.String) @@ -645,25 +885,10 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString()) } - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.PartialUpdateNetworkAreaPayload{ - Name: conversion.StringValueToPointer(model.Name), - AddressFamily: &iaas.UpdateAreaAddressFamily{ - Ipv4: &iaas.UpdateAreaIPv4{ - DefaultNameservers: &modelDefaultNameservers, - DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength), - MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength), - MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength), - }, - }, - Labels: &labels, - }, nil + return modelDefaultNameservers, nil } +// Deprecated: toNetworkRangesPayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes. func toNetworkRangesPayload(ctx context.Context, model *Model) (*[]iaas.NetworkRange, error) { if model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown() { return nil, nil @@ -690,10 +915,10 @@ func toNetworkRangesPayload(ctx context.Context, model *Model) (*[]iaas.NetworkR return &payload, nil } -// updateNetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model +// Deprecated: updateNetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model. This was only kept to make the v1 -> v2 IaaS API migration non-breaking in the Terraform provider. func updateNetworkRanges(ctx context.Context, organizationId, networkAreaId string, ranges []networkRange, client *iaas.APIClient) error { // Get network ranges current state - currentNetworkRangesResp, err := client.ListNetworkAreaRanges(ctx, organizationId, networkAreaId).Execute() + currentNetworkRangesResp, err := client.ListNetworkAreaRanges(ctx, organizationId, networkAreaId, "eu01").Execute() if err != nil { return fmt.Errorf("error reading network area ranges: %w", err) } @@ -717,13 +942,13 @@ func updateNetworkRanges(ctx context.Context, organizationId, networkAreaId stri networkRangesState[prefix] = &networkRangeState{} } networkRangesState[prefix].isCreated = true - networkRangesState[prefix].id = *networkRange.NetworkRangeId + networkRangesState[prefix].id = *networkRange.Id } // Delete network ranges for prefix, state := range networkRangesState { if !state.isInModel && state.isCreated { - err := client.DeleteNetworkAreaRange(ctx, organizationId, networkAreaId, state.id).Execute() + err := client.DeleteNetworkAreaRange(ctx, organizationId, networkAreaId, "eu01", state.id).Execute() if err != nil { return fmt.Errorf("deleting network area range '%v': %w", prefix, err) } @@ -741,7 +966,7 @@ func updateNetworkRanges(ctx context.Context, organizationId, networkAreaId stri }, } - _, err := client.CreateNetworkAreaRange(ctx, organizationId, networkAreaId).CreateNetworkAreaRangePayload(payload).Execute() + _, err := client.CreateNetworkAreaRange(ctx, organizationId, networkAreaId, "eu01").CreateNetworkAreaRangePayload(payload).Execute() if err != nil { return fmt.Errorf("creating network range '%v': %w", prefix, err) } diff --git a/stackit/internal/services/iaas/networkarea/resource_test.go b/stackit/internal/services/iaas/networkarea/resource_test.go index d91044b98..dbcdfbb5e 100644 --- a/stackit/internal/services/iaas/networkarea/resource_test.go +++ b/stackit/internal/services/iaas/networkarea/resource_test.go @@ -28,16 +28,15 @@ var testRangeId2Repeated = uuid.NewString() func TestMapFields(t *testing.T) { tests := []struct { - description string - state Model - input *iaas.NetworkArea - ListNetworkRanges *[]iaas.NetworkRange - expected Model - isValid bool + description string + state Model + input *iaas.NetworkArea + expected Model + isValid bool }{ { - "id_ok", - Model{ + description: "id_ok", + state: Model{ OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ @@ -50,32 +49,16 @@ func TestMapFields(t *testing.T) { "prefix": types.StringValue("prefix-2"), }), }), + DefaultNameservers: types.ListNull(types.StringType), }, - &iaas.NetworkArea{ - AreaId: utils.Ptr("naid"), - Ipv4: &iaas.NetworkAreaIPv4{}, + input: &iaas.NetworkArea{ + Id: utils.Ptr("naid"), }, - &[]iaas.NetworkRange{ - { - NetworkRangeId: utils.Ptr(testRangeId1), - Prefix: utils.Ptr("prefix-1"), - }, - { - NetworkRangeId: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - }, - - Model{ - Id: types.StringValue("oid,naid"), - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - Name: types.StringNull(), - DefaultNameservers: types.ListNull(types.StringType), - TransferNetwork: types.StringNull(), - DefaultPrefixLength: types.Int64Null(), - MaxPrefixLength: types.Int64Null(), - MinPrefixLength: types.Int64Null(), + expected: Model{ + Id: types.StringValue("oid,naid"), + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("naid"), + Name: types.StringNull(), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ "network_range_id": types.StringValue(testRangeId1), @@ -86,13 +69,14 @@ func TestMapFields(t *testing.T) { "prefix": types.StringValue("prefix-2"), }), }), - Labels: types.MapNull(types.StringType), + DefaultNameservers: types.ListNull(types.StringType), + Labels: types.MapNull(types.StringType), }, - true, + isValid: true, }, { - "values_ok", - Model{ + description: "values_ok", + state: Model{ OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ @@ -105,47 +89,20 @@ func TestMapFields(t *testing.T) { "prefix": types.StringValue("prefix-2"), }), }), + DefaultNameservers: types.ListNull(types.StringType), }, - &iaas.NetworkArea{ - AreaId: utils.Ptr("naid"), - Ipv4: &iaas.NetworkAreaIPv4{ - DefaultNameservers: &[]string{ - "nameserver1", - "nameserver2", - }, - TransferNetwork: utils.Ptr("network"), - DefaultPrefixLen: utils.Ptr(int64(20)), - MaxPrefixLen: utils.Ptr(int64(22)), - MinPrefixLen: utils.Ptr(int64(18)), - }, + input: &iaas.NetworkArea{ + Id: utils.Ptr("naid"), Name: utils.Ptr("name"), Labels: &map[string]interface{}{ "key": "value", }, }, - &[]iaas.NetworkRange{ - { - NetworkRangeId: utils.Ptr(testRangeId1), - Prefix: utils.Ptr("prefix-1"), - }, - { - NetworkRangeId: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - }, - Model{ + expected: Model{ Id: types.StringValue("oid,naid"), OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), Name: types.StringValue("name"), - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("nameserver1"), - types.StringValue("nameserver2"), - }), - TransferNetwork: types.StringValue("network"), - DefaultPrefixLength: types.Int64Value(20), - MaxPrefixLength: types.Int64Value(22), - MinPrefixLength: types.Int64Value(18), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ "network_range_id": types.StringValue(testRangeId1), @@ -159,12 +116,13 @@ func TestMapFields(t *testing.T) { Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ "key": types.StringValue("value"), }), + DefaultNameservers: types.ListNull(types.StringType), }, - true, + isValid: true, }, { - "model and response have ranges in different order", - Model{ + description: "default_nameservers_changed_outside_tf", + state: Model{ OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ @@ -177,48 +135,15 @@ func TestMapFields(t *testing.T) { "prefix": types.StringValue("prefix-2"), }), }), + DefaultNameservers: types.ListNull(types.StringType), }, - &iaas.NetworkArea{ - AreaId: utils.Ptr("naid"), - Ipv4: &iaas.NetworkAreaIPv4{ - DefaultNameservers: &[]string{ - "nameserver1", - "nameserver2", - }, - TransferNetwork: utils.Ptr("network"), - DefaultPrefixLen: utils.Ptr(int64(20)), - MaxPrefixLen: utils.Ptr(int64(22)), - MinPrefixLen: utils.Ptr(int64(18)), - }, - Name: utils.Ptr("name"), - }, - &[]iaas.NetworkRange{ - { - NetworkRangeId: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - { - NetworkRangeId: utils.Ptr(testRangeId3), - Prefix: utils.Ptr("prefix-3"), - }, - { - NetworkRangeId: utils.Ptr(testRangeId1), - Prefix: utils.Ptr("prefix-1"), - }, + input: &iaas.NetworkArea{ + Id: utils.Ptr("naid"), }, - Model{ + expected: Model{ Id: types.StringValue("oid,naid"), OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), - Name: types.StringValue("name"), - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("nameserver1"), - types.StringValue("nameserver2"), - }), - TransferNetwork: types.StringValue("network"), - DefaultPrefixLength: types.Int64Value(20), - MaxPrefixLength: types.Int64Value(22), - MinPrefixLength: types.Int64Value(18), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ "network_range_id": types.StringValue(testRangeId1), @@ -228,62 +153,97 @@ func TestMapFields(t *testing.T) { "network_range_id": types.StringValue(testRangeId2), "prefix": types.StringValue("prefix-2"), }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId3), - "prefix": types.StringValue("prefix-3"), - }), }), - Labels: types.MapNull(types.StringType), + Labels: types.MapNull(types.StringType), + DefaultNameservers: types.ListNull(types.StringType), }, - true, + isValid: true, }, { - "default_nameservers_changed_outside_tf", + "response_nil_fail", + Model{}, + nil, + Model{}, + false, + }, + { + "no_resource_id", Model{ OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - }, - &iaas.NetworkArea{ - AreaId: utils.Ptr("naid"), - Ipv4: &iaas.NetworkAreaIPv4{ - DefaultNameservers: &[]string{ - "ns2", - "ns3", - }, - }, }, - &[]iaas.NetworkRange{ - { - NetworkRangeId: utils.Ptr(testRangeId1), - Prefix: utils.Ptr("prefix-1"), + &iaas.NetworkArea{}, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapFields(context.Background(), tt.input, &tt.state) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +// Deprecated: Will be removed in May 2026. +func Test_MapNetworkRanges(t *testing.T) { + type args struct { + networkAreaRangesList *[]iaas.NetworkRange + model *Model + } + tests := []struct { + name string + args args + want *Model + wantErr bool + }{ + { + name: "model and response have ranges in different order", + args: args{ + model: &Model{ + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("naid"), + DefaultNameservers: types.ListNull(types.StringType), + NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ + types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ + "network_range_id": types.StringValue(testRangeId1), + "prefix": types.StringValue("prefix-1"), + }), + types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ + "network_range_id": types.StringValue(testRangeId2), + "prefix": types.StringValue("prefix-2"), + }), + }), + Labels: types.MapNull(types.StringType), }, - { - NetworkRangeId: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), + networkAreaRangesList: &[]iaas.NetworkRange{ + { + Id: utils.Ptr(testRangeId2), + Prefix: utils.Ptr("prefix-2"), + }, + { + Id: utils.Ptr(testRangeId3), + Prefix: utils.Ptr("prefix-3"), + }, + { + Id: utils.Ptr(testRangeId1), + Prefix: utils.Ptr("prefix-1"), + }, }, }, - Model{ - Id: types.StringValue("oid,naid"), + want: &Model{ OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ "network_range_id": types.StringValue(testRangeId1), @@ -293,101 +253,180 @@ func TestMapFields(t *testing.T) { "network_range_id": types.StringValue(testRangeId2), "prefix": types.StringValue("prefix-2"), }), + types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ + "network_range_id": types.StringValue(testRangeId3), + "prefix": types.StringValue("prefix-3"), + }), }), - Labels: types.MapNull(types.StringType), + Labels: types.MapNull(types.StringType), + DefaultNameservers: types.ListNull(types.StringType), }, - true, + wantErr: false, }, { - "network_ranges_changed_outside_tf", - Model{ + name: "network_ranges_changed_outside_tf", + args: args{ + model: &Model{ + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("naid"), + NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ + types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ + "network_range_id": types.StringValue(testRangeId1), + "prefix": types.StringValue("prefix-1"), + }), + types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ + "network_range_id": types.StringValue(testRangeId2), + "prefix": types.StringValue("prefix-2"), + }), + }), + Labels: types.MapNull(types.StringType), + DefaultNameservers: types.ListNull(types.StringType), + }, + networkAreaRangesList: &[]iaas.NetworkRange{ + { + Id: utils.Ptr(testRangeId2), + Prefix: utils.Ptr("prefix-2"), + }, + { + Id: utils.Ptr(testRangeId3), + Prefix: utils.Ptr("prefix-3"), + }, + }, + }, + want: &Model{ OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ "network_range_id": types.StringValue(testRangeId2), "prefix": types.StringValue("prefix-2"), }), + types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ + "network_range_id": types.StringValue(testRangeId3), + "prefix": types.StringValue("prefix-3"), + }), }), + Labels: types.MapNull(types.StringType), + DefaultNameservers: types.ListNull(types.StringType), }, - &iaas.NetworkArea{ - AreaId: utils.Ptr("naid"), - Ipv4: &iaas.NetworkAreaIPv4{}, - }, - &[]iaas.NetworkRange{ - { - NetworkRangeId: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := mapNetworkRanges(context.Background(), tt.args.networkAreaRangesList, tt.args.model); (err != nil) != tt.wantErr { + t.Errorf("mapNetworkRanges() error = %v, wantErr %v", err, tt.wantErr) + } + + diff := cmp.Diff(tt.args.model, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +// Deprecated: Will be removed in May 2026. +func TestMapNetworkAreaRegionFields(t *testing.T) { + type args struct { + networkAreaRegionResp *iaas.RegionalArea + model *Model + } + tests := []struct { + name string + args args + want *Model + wantErr bool + }{ + { + name: "default", + args: args{ + model: &Model{ + Labels: types.MapNull(types.StringType), }, - { - NetworkRangeId: utils.Ptr(testRangeId3), - Prefix: utils.Ptr("prefix-3"), + networkAreaRegionResp: &iaas.RegionalArea{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: &[]string{ + "nameserver1", + "nameserver2", + }, + TransferNetwork: utils.Ptr("network"), + DefaultPrefixLen: utils.Ptr(int64(20)), + MaxPrefixLen: utils.Ptr(int64(22)), + MinPrefixLen: utils.Ptr(int64(18)), + NetworkRanges: &[]iaas.NetworkRange{ + { + Id: utils.Ptr(testRangeId1), + Prefix: utils.Ptr("prefix-1"), + }, + { + Id: utils.Ptr(testRangeId2), + Prefix: utils.Ptr("prefix-2"), + }, + }, + }, }, }, - Model{ - Id: types.StringValue("oid,naid"), - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - DefaultNameservers: types.ListNull(types.StringType), + want: &Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("nameserver1"), + types.StringValue("nameserver2"), + }), + TransferNetwork: types.StringValue("network"), + DefaultPrefixLength: types.Int64Value(20), + MaxPrefixLength: types.Int64Value(22), + MinPrefixLength: types.Int64Value(18), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), + "network_range_id": types.StringValue(testRangeId1), + "prefix": types.StringValue("prefix-1"), }), types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId3), - "prefix": types.StringValue("prefix-3"), + "network_range_id": types.StringValue(testRangeId2), + "prefix": types.StringValue("prefix-2"), }), }), + Labels: types.MapNull(types.StringType), }, - true, + wantErr: false, }, { - "nil_network_ranges_list", - Model{}, - &iaas.NetworkArea{}, - nil, - Model{}, - false, - }, - { - "response_nil_fail", - Model{}, - nil, - nil, - Model{}, - false, + name: "model is nil", + args: args{ + model: nil, + networkAreaRegionResp: &iaas.RegionalArea{}, + }, + want: nil, + wantErr: true, }, { - "no_resource_id", - Model{ - OrganizationId: types.StringValue("oid"), + name: "network area region response is nil", + args: args{ + model: &Model{ + DefaultNameservers: types.ListNull(types.StringType), + NetworkRanges: types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}), + Labels: types.MapNull(types.StringType), + }, + networkAreaRegionResp: nil, }, - &iaas.NetworkArea{}, - &[]iaas.NetworkRange{}, - Model{}, - false, + want: &Model{ + DefaultNameservers: types.ListNull(types.StringType), + NetworkRanges: types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}), + Labels: types.MapNull(types.StringType), + }, + wantErr: true, }, } for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, tt.ListNetworkRanges, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) + t.Run(tt.name, func(t *testing.T) { + if err := mapNetworkAreaRegionFields(context.Background(), tt.args.networkAreaRegionResp, tt.args.model); (err != nil) != tt.wantErr { + t.Errorf("mapNetworkAreaRegionFields() error = %v, wantErr %v", err, tt.wantErr) } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + + diff := cmp.Diff(tt.args.model, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) } }) } @@ -404,50 +443,12 @@ func TestToCreatePayload(t *testing.T) { "default_ok", &Model{ Name: types.StringValue("name"), - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringUnknown(), - "prefix": types.StringValue("pr-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringUnknown(), - "prefix": types.StringValue("pr-2"), - }), - }), - TransferNetwork: types.StringValue("network"), - DefaultPrefixLength: types.Int64Value(20), - MaxPrefixLength: types.Int64Value(22), - MinPrefixLength: types.Int64Value(18), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ "key": types.StringValue("value"), }), }, &iaas.CreateNetworkAreaPayload{ Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateAreaAddressFamily{ - Ipv4: &iaas.CreateAreaIPv4{ - DefaultNameservers: &[]string{ - "ns1", - "ns2", - }, - NetworkRanges: &[]iaas.NetworkRange{ - { - Prefix: utils.Ptr("pr-1"), - }, - { - Prefix: utils.Ptr("pr-2"), - }, - }, - TransferNetwork: utils.Ptr("network"), - DefaultPrefixLen: utils.Ptr(int64(20)), - MaxPrefixLen: utils.Ptr(int64(22)), - MinPrefixLen: utils.Ptr(int64(18)), - }, - }, Labels: &map[string]interface{}{ "key": "value", }, @@ -474,6 +475,86 @@ func TestToCreatePayload(t *testing.T) { } } +// Deprecated: Will be removed in May 2026. +func TestToRegionCreatePayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want *iaas.CreateNetworkAreaRegionPayload + wantErr bool + }{ + { + name: "default_ok", + args: args{ + model: &Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ + types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ + "network_range_id": types.StringUnknown(), + "prefix": types.StringValue("pr-1"), + }), + types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ + "network_range_id": types.StringUnknown(), + "prefix": types.StringValue("pr-2"), + }), + }), + TransferNetwork: types.StringValue("network"), + DefaultPrefixLength: types.Int64Value(20), + MaxPrefixLength: types.Int64Value(22), + MinPrefixLength: types.Int64Value(18), + }, + }, + want: &iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: &[]string{ + "ns1", + "ns2", + }, + NetworkRanges: &[]iaas.NetworkRange{ + { + Prefix: utils.Ptr("pr-1"), + }, + { + Prefix: utils.Ptr("pr-2"), + }, + }, + TransferNetwork: utils.Ptr("network"), + DefaultPrefixLen: utils.Ptr(int64(20)), + MaxPrefixLen: utils.Ptr(int64(22)), + MinPrefixLen: utils.Ptr(int64(18)), + }, + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toRegionCreatePayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toRegionCreatePayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + func TestToUpdatePayload(t *testing.T) { tests := []struct { description string @@ -485,30 +566,12 @@ func TestToUpdatePayload(t *testing.T) { "default_ok", &Model{ Name: types.StringValue("name"), - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - DefaultPrefixLength: types.Int64Value(22), - MaxPrefixLength: types.Int64Value(24), - MinPrefixLength: types.Int64Value(20), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ "key": types.StringValue("value"), }), }, &iaas.PartialUpdateNetworkAreaPayload{ Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateAreaAddressFamily{ - Ipv4: &iaas.UpdateAreaIPv4{ - DefaultNameservers: &[]string{ - "ns1", - "ns2", - }, - DefaultPrefixLen: utils.Ptr(int64(22)), - MaxPrefixLen: utils.Ptr(int64(24)), - MinPrefixLen: utils.Ptr(int64(20)), - }, - }, Labels: &map[string]interface{}{ "key": "value", }, @@ -535,24 +598,84 @@ func TestToUpdatePayload(t *testing.T) { } } +// Deprecated: Will be removed in May 2026. +func TestToRegionUpdatePayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want *iaas.UpdateNetworkAreaRegionPayload + wantErr bool + }{ + { + name: "default_ok", + args: args{ + model: &Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + DefaultPrefixLength: types.Int64Value(22), + MaxPrefixLength: types.Int64Value(24), + MinPrefixLength: types.Int64Value(20), + }, + }, + want: &iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: &[]string{ + "ns1", + "ns2", + }, + DefaultPrefixLen: utils.Ptr(int64(22)), + MaxPrefixLen: utils.Ptr(int64(24)), + MinPrefixLen: utils.Ptr(int64(20)), + }, + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toRegionUpdatePayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toRegionUpdatePayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + func TestUpdateNetworkRanges(t *testing.T) { getAllNetworkRangesResp := iaas.NetworkRangeListResponse{ Items: &[]iaas.NetworkRange{ { - Prefix: utils.Ptr("pr-1"), - NetworkRangeId: utils.Ptr(testRangeId1), + Prefix: utils.Ptr("pr-1"), + Id: utils.Ptr(testRangeId1), }, { - Prefix: utils.Ptr("pr-2"), - NetworkRangeId: utils.Ptr(testRangeId2), + Prefix: utils.Ptr("pr-2"), + Id: utils.Ptr(testRangeId2), }, { - Prefix: utils.Ptr("pr-3"), - NetworkRangeId: utils.Ptr(testRangeId3), + Prefix: utils.Ptr("pr-3"), + Id: utils.Ptr(testRangeId3), }, { - Prefix: utils.Ptr("pr-2"), - NetworkRangeId: utils.Ptr(testRangeId2Repeated), + Prefix: utils.Ptr("pr-2"), + Id: utils.Ptr(testRangeId2Repeated), }, }, } @@ -903,8 +1026,8 @@ func TestUpdateNetworkRanges(t *testing.T) { } resp := iaas.NetworkRange{ - Prefix: utils.Ptr("prefix"), - NetworkRangeId: utils.Ptr("id-range"), + Prefix: utils.Ptr("prefix"), + Id: utils.Ptr("id-range"), } respBytes, err := json.Marshal(resp) if err != nil { @@ -930,7 +1053,7 @@ func TestUpdateNetworkRanges(t *testing.T) { var prefix string for _, rangeItem := range *getAllNetworkRangesResp.Items { - if *rangeItem.NetworkRangeId == networkRangeId { + if *rangeItem.Id == networkRangeId { prefix = *rangeItem.Prefix } } @@ -963,14 +1086,14 @@ func TestUpdateNetworkRanges(t *testing.T) { // Setup server and client router := mux.NewRouter() - router.HandleFunc("/v1/organizations/{organizationId}/network-areas/{areaId}/network-ranges", func(w http.ResponseWriter, r *http.Request) { + router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { getAllNetworkRangesHandler(w, r) } else if r.Method == "POST" { createNetworkRangeHandler(w, r) } }) - router.HandleFunc("/v1/organizations/{organizationId}/network-areas/{areaId}/network-ranges/{networkRangeId}", deleteNetworkRangeHandler) + router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges/{networkRangeId}", deleteNetworkRangeHandler) mockedServer := httptest.NewServer(router) defer mockedServer.Close() client, err := iaas.NewAPIClient( diff --git a/stackit/internal/services/iaas/networkarearegion/datasource.go b/stackit/internal/services/iaas/networkarearegion/datasource.go new file mode 100644 index 000000000..efa9648ab --- /dev/null +++ b/stackit/internal/services/iaas/networkarearegion/datasource.go @@ -0,0 +1,181 @@ +package networkarearegion + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &networkAreaRegionDataSource{} +) + +// NewNetworkAreaRegionDataSource is a helper function to simplify the provider implementation. +func NewNetworkAreaRegionDataSource() datasource.DataSource { + return &networkAreaRegionDataSource{} +} + +// networkAreaRegionDataSource is the data source implementation. +type networkAreaRegionDataSource struct { + client *iaas.APIClient + providerData core.ProviderData +} + +// Metadata returns the data source type name. +func (d *networkAreaRegionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_network_area_region" +} + +func (d *networkAreaRegionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") +} + +// Schema defines the schema for the resource. +func (d *networkAreaRegionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + description := "Network area region data source schema." + + resp.Schema = schema.Schema{ + MarkdownDescription: description, + Description: description, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`\".", + Computed: true, + }, + "organization_id": schema.StringAttribute{ + Description: "STACKIT organization ID to which the network area is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "network_area_id": schema.StringAttribute{ + Description: "The network area ID.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, + "ipv4": schema.SingleNestedAttribute{ + Computed: true, + Description: "The regional IPv4 config of a network area.", + Attributes: map[string]schema.Attribute{ + "default_nameservers": schema.ListAttribute{ + Description: "List of DNS Servers/Nameservers.", + Computed: true, + ElementType: types.StringType, + }, + "network_ranges": schema.ListNestedAttribute{ + Description: "List of Network ranges.", + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(64), + }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "network_range_id": schema.StringAttribute{ + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "prefix": schema.StringAttribute{ + Description: "Classless Inter-Domain Routing (CIDR).", + Computed: true, + }, + }, + }, + }, + "transfer_network": schema.StringAttribute{ + Description: "IPv4 Classless Inter-Domain Routing (CIDR).", + Computed: true, + }, + "default_prefix_length": schema.Int64Attribute{ + Description: "The default prefix length for networks in the network area.", + Computed: true, + }, + "max_prefix_length": schema.Int64Attribute{ + Description: "The maximal prefix length for networks in the network area.", + Computed: true, + }, + "min_prefix_length": schema.Int64Attribute{ + Description: "The minimal prefix length for networks in the network area.", + Computed: true, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *networkAreaRegionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) + + networkAreaRegionResp, err := d.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() + if err != nil { + utils.LogError(ctx, &resp.Diagnostics, err, "Reading network area region", fmt.Sprintf("Region configuration for %q for network area %q does not exist.", region, networkAreaId), nil) + resp.State.RemoveResource(ctx) + return + } + + // Map response body to schema + err = mapFields(ctx, networkAreaRegionResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network area region read") +} diff --git a/stackit/internal/services/iaas/networkarearegion/resource.go b/stackit/internal/services/iaas/networkarearegion/resource.go new file mode 100644 index 000000000..6a2d2e3e6 --- /dev/null +++ b/stackit/internal/services/iaas/networkarearegion/resource.go @@ -0,0 +1,697 @@ +package networkarearegion + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &networkAreaRegionResource{} + _ resource.ResourceWithConfigure = &networkAreaRegionResource{} + _ resource.ResourceWithImportState = &networkAreaRegionResource{} + _ resource.ResourceWithModifyPlan = &networkAreaRegionResource{} +) + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + OrganizationId types.String `tfsdk:"organization_id"` + NetworkAreaId types.String `tfsdk:"network_area_id"` + Region types.String `tfsdk:"region"` + Ipv4 *ipv4Model `tfsdk:"ipv4"` +} + +// Struct corresponding to Model.Ipv4 +type ipv4Model struct { + DefaultNameservers types.List `tfsdk:"default_nameservers"` + NetworkRanges []networkRangeModel `tfsdk:"network_ranges"` + TransferNetwork types.String `tfsdk:"transfer_network"` + DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"` + MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"` + MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"` +} + +// Struct corresponding to Model.NetworkRanges[i] +type networkRangeModel struct { + Prefix types.String `tfsdk:"prefix"` + NetworkRangeId types.String `tfsdk:"network_range_id"` +} + +// NewNetworkAreaRegionResource is a helper function to simplify the provider implementation. +func NewNetworkAreaRegionResource() resource.Resource { + return &networkAreaRegionResource{} +} + +// networkAreaRegionResource is the resource implementation. +type networkAreaRegionResource struct { + client *iaas.APIClient + providerData core.ProviderData +} + +// Metadata returns the resource type name. +func (r *networkAreaRegionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_network_area_region" +} + +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *networkAreaRegionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +// Configure adds the provider configured client to the resource. +func (r *networkAreaRegionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") +} + +// Schema defines the schema for the resource. +func (r *networkAreaRegionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + description := "Network area region resource schema." + + resp.Schema = schema.Schema{ + Description: description, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "organization_id": schema.StringAttribute{ + Description: "STACKIT organization ID to which the network area is associated.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "network_area_id": schema.StringAttribute{ + Description: "The network area ID.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "ipv4": schema.SingleNestedAttribute{ + Description: "The regional IPv4 config of a network area.", + Required: true, + Attributes: map[string]schema.Attribute{ + "default_nameservers": schema.ListAttribute{ + Description: "List of DNS Servers/Nameservers.", + Optional: true, + ElementType: types.StringType, + }, + "network_ranges": schema.ListNestedAttribute{ + Description: "List of Network ranges.", + Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(64), + }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "network_range_id": schema.StringAttribute{ + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "prefix": schema.StringAttribute{ + Description: "Classless Inter-Domain Routing (CIDR).", + Required: true, + }, + }, + }, + }, + "transfer_network": schema.StringAttribute{ + Description: "IPv4 Classless Inter-Domain Routing (CIDR).", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "default_prefix_length": schema.Int64Attribute{ + Description: "The default prefix length for networks in the network area.", + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.AtLeast(24), + int64validator.AtMost(29), + }, + Default: int64default.StaticInt64(25), + }, + "max_prefix_length": schema.Int64Attribute{ + Description: "The maximal prefix length for networks in the network area.", + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.AtLeast(24), + int64validator.AtMost(29), + }, + Default: int64default.StaticInt64(29), + }, + "min_prefix_length": schema.Int64Attribute{ + Description: "The minimal prefix length for networks in the network area.", + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.AtLeast(8), + int64validator.AtMost(29), + }, + Default: int64default.StaticInt64(24), + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *networkAreaRegionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) + + // Generate API request body from model + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Create new network area region configuration + keyPair, err := r.client.CreateNetworkAreaRegion(ctx, organizationId, networkAreaId, region).CreateNetworkAreaRegionPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "organization_id": organizationId, + "network_area_id": networkAreaId, + "region": region, + }) + + // wait for creation of network area region to complete + _, err = wait.CreateNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, region).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err)) + return + } + + // Map response body to schema + err = mapFields(ctx, keyPair, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set state to fully populated data + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network area region created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *networkAreaRegionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) + + networkAreaRegionResp, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + } + + // Map response body to schema + err = mapFields(ctx, networkAreaRegionResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network area region read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *networkAreaRegionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) + + // Retrieve values from state + var stateModel Model + resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from model + payload, err := toUpdatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Update existing network area region configuration + _, err = r.client.UpdateNetworkAreaRegion(ctx, organizationId, networkAreaId, region).UpdateNetworkAreaRegionPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = updateIpv4NetworkRanges(ctx, organizationId, networkAreaId, model.Ipv4.NetworkRanges, r.client, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Updating Network ranges: %v", err)) + return + } + + updatedNetworkAreaRegion, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFields(ctx, updatedNetworkAreaRegion, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "network area region updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *networkAreaRegionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from state + var model Model + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) + + // Delete network area region configuration + err := r.client.DeleteNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Calling API: %v", err)) + return + } + + _, err = wait.DeleteNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, region).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("network area deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "Network area region deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the resource import identifier is: organization_id,network_area_id,region +func (r *networkAreaRegionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing network area region", + fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id],[region] Got: %q", req.ID), + ) + return + } + + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "organization_id": idParts[0], + "network_area_id": idParts[1], + "region": idParts[2], + }) + + tflog.Info(ctx, "Network area region state imported") +} + +// mapFields maps the API response values to the Terraform resource model fields +func mapFields(ctx context.Context, networkAreaRegion *iaas.RegionalArea, model *Model, region string) error { + if networkAreaRegion == nil { + return fmt.Errorf("network are region input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), model.NetworkAreaId.ValueString(), region) + model.Region = types.StringValue(region) + + model.Ipv4 = &ipv4Model{} + if networkAreaRegion.Ipv4 != nil { + model.Ipv4.TransferNetwork = types.StringPointerValue(networkAreaRegion.Ipv4.TransferNetwork) + model.Ipv4.DefaultPrefixLength = types.Int64PointerValue(networkAreaRegion.Ipv4.DefaultPrefixLen) + model.Ipv4.MaxPrefixLength = types.Int64PointerValue(networkAreaRegion.Ipv4.MaxPrefixLen) + model.Ipv4.MinPrefixLength = types.Int64PointerValue(networkAreaRegion.Ipv4.MinPrefixLen) + } + + // map default nameservers + if networkAreaRegion.Ipv4 == nil || networkAreaRegion.Ipv4.DefaultNameservers == nil { + model.Ipv4.DefaultNameservers = types.ListNull(types.StringType) + } else { + respDefaultNameservers := *networkAreaRegion.Ipv4.DefaultNameservers + modelDefaultNameservers, err := utils.ListValuetoStringSlice(model.Ipv4.DefaultNameservers) + if err != nil { + return fmt.Errorf("get current network area default nameservers from model: %w", err) + } + + reconciledDefaultNameservers := utils.ReconcileStringSlices(modelDefaultNameservers, respDefaultNameservers) + + defaultNameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledDefaultNameservers) + if diags.HasError() { + return fmt.Errorf("map network area default nameservers: %w", core.DiagsToError(diags)) + } + + model.Ipv4.DefaultNameservers = defaultNameserversTF + } + + // map network ranges + err := mapIpv4NetworkRanges(ctx, networkAreaRegion.Ipv4.NetworkRanges, model) + if err != nil { + return fmt.Errorf("mapping network ranges: %w", err) + } + + return nil +} + +// mapFields maps the API ipv4 network ranges response values to the Terraform resource model fields +func mapIpv4NetworkRanges(_ context.Context, networkAreaRangesList *[]iaas.NetworkRange, model *Model) error { + if networkAreaRangesList == nil { + return fmt.Errorf("nil network area ranges list") + } + if len(*networkAreaRangesList) == 0 { + model.Ipv4.NetworkRanges = []networkRangeModel{} + return nil + } + + modelNetworkRangePrefixes := []string{} + for _, m := range model.Ipv4.NetworkRanges { + modelNetworkRangePrefixes = append(modelNetworkRangePrefixes, m.Prefix.ValueString()) + } + + apiNetworkRangePrefixes := []string{} + for _, n := range *networkAreaRangesList { + apiNetworkRangePrefixes = append(apiNetworkRangePrefixes, *n.Prefix) + } + + reconciledRangePrefixes := utils.ReconcileStringSlices(modelNetworkRangePrefixes, apiNetworkRangePrefixes) + + model.Ipv4.NetworkRanges = []networkRangeModel{} + for _, prefix := range reconciledRangePrefixes { + var networkRangeId string + for _, networkRangeElement := range *networkAreaRangesList { + if *networkRangeElement.Prefix == prefix { + networkRangeId = *networkRangeElement.Id + break + } + } + + model.Ipv4.NetworkRanges = append(model.Ipv4.NetworkRanges, networkRangeModel{ + Prefix: types.StringValue(prefix), + NetworkRangeId: types.StringValue(networkRangeId), + }) + } + + return nil +} + +func toDefaultNameserversPayload(_ context.Context, model *Model) ([]string, error) { + if model == nil { + return nil, fmt.Errorf("model is nil") + } + + modelDefaultNameservers := []string{} + for _, ns := range model.Ipv4.DefaultNameservers.Elements() { + nameserverString, ok := ns.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString()) + } + + return modelDefaultNameservers, nil +} + +func toNetworkRangesPayload(_ context.Context, model *Model) (*[]iaas.NetworkRange, error) { + if model == nil { + return nil, fmt.Errorf("model is nil") + } + + if len(model.Ipv4.NetworkRanges) == 0 { + return nil, nil + } + + payload := []iaas.NetworkRange{} + for _, networkRange := range model.Ipv4.NetworkRanges { + payload = append(payload, iaas.NetworkRange{ + Prefix: conversion.StringValueToPointer(networkRange.Prefix), + }) + } + + return &payload, nil +} + +func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaRegionPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } else if model.Ipv4 == nil { + return nil, fmt.Errorf("nil model.Ipv4") + } + + modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model) + if err != nil { + return nil, fmt.Errorf("converting default nameservers: %w", err) + } + + networkRangesPayload, err := toNetworkRangesPayload(ctx, model) + if err != nil { + return nil, fmt.Errorf("converting network ranges: %w", err) + } + + return &iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: &modelDefaultNameservers, + DefaultPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.DefaultPrefixLength), + MaxPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MaxPrefixLength), + MinPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MinPrefixLength), + TransferNetwork: conversion.StringValueToPointer(model.Ipv4.TransferNetwork), + NetworkRanges: networkRangesPayload, + }, + }, nil +} + +func toUpdatePayload(ctx context.Context, model *Model) (*iaas.UpdateNetworkAreaRegionPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model) + if err != nil { + return nil, fmt.Errorf("converting default nameservers: %w", err) + } + + return &iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: &modelDefaultNameservers, + DefaultPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.DefaultPrefixLength), + MaxPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MaxPrefixLength), + MinPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MinPrefixLength), + }, + }, nil +} + +// updateIpv4NetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model. +func updateIpv4NetworkRanges(ctx context.Context, organizationId, networkAreaId string, ranges []networkRangeModel, client *iaas.APIClient, region string) error { + // Get network ranges current state + currentNetworkRangesResp, err := client.ListNetworkAreaRanges(ctx, organizationId, networkAreaId, region).Execute() + if err != nil { + return fmt.Errorf("error reading network area ranges: %w", err) + } + + type networkRangeState struct { + isInModel bool + isCreated bool + id string + } + + networkRangesState := make(map[string]*networkRangeState) + for _, nwRange := range ranges { + networkRangesState[nwRange.Prefix.ValueString()] = &networkRangeState{ + isInModel: true, + } + } + + for _, networkRange := range *currentNetworkRangesResp.Items { + prefix := *networkRange.Prefix + if _, ok := networkRangesState[prefix]; !ok { + networkRangesState[prefix] = &networkRangeState{} + } + networkRangesState[prefix].isCreated = true + networkRangesState[prefix].id = *networkRange.Id + } + + // Delete network ranges + for prefix, state := range networkRangesState { + if !state.isInModel && state.isCreated { + err := client.DeleteNetworkAreaRange(ctx, organizationId, networkAreaId, region, state.id).Execute() + if err != nil { + return fmt.Errorf("deleting network area range '%v': %w", prefix, err) + } + } + } + + // Create network ranges + for prefix, state := range networkRangesState { + if state.isInModel && !state.isCreated { + payload := iaas.CreateNetworkAreaRangePayload{ + Ipv4: &[]iaas.NetworkRange{ + { + Prefix: sdkUtils.Ptr(prefix), + }, + }, + } + + _, err := client.CreateNetworkAreaRange(ctx, organizationId, networkAreaId, region).CreateNetworkAreaRangePayload(payload).Execute() + if err != nil { + return fmt.Errorf("creating network range '%v': %w", prefix, err) + } + } + } + + return nil +} diff --git a/stackit/internal/services/iaas/networkarearegion/resource_test.go b/stackit/internal/services/iaas/networkarearegion/resource_test.go new file mode 100644 index 000000000..978ca80af --- /dev/null +++ b/stackit/internal/services/iaas/networkarearegion/resource_test.go @@ -0,0 +1,1052 @@ +package networkarearegion + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gorilla/mux" + "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" +) + +var ( + organizationId = uuid.NewString() + networkAreaId = uuid.NewString() + + networkRangeId1 = uuid.NewString() + networkRangeId2 = uuid.NewString() + networkRangeId3 = uuid.NewString() + networkRangeId4 = uuid.NewString() + networkRangeId5 = uuid.NewString() + networkRangeId2Repeated = uuid.NewString() +) + +func Test_mapFields(t *testing.T) { + type args struct { + networkAreaRegion *iaas.RegionalArea + model *Model + region string + } + tests := []struct { + name string + args args + want *Model + wantErr bool + }{ + { + name: "default", + args: args{ + model: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + }, + networkAreaRegion: &iaas.RegionalArea{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: &[]string{ + "nameserver1", + "nameserver2", + }, + TransferNetwork: utils.Ptr("network"), + DefaultPrefixLen: utils.Ptr(int64(20)), + MaxPrefixLen: utils.Ptr(int64(22)), + MinPrefixLen: utils.Ptr(int64(18)), + NetworkRanges: &[]iaas.NetworkRange{ + { + Id: utils.Ptr(networkRangeId1), + Prefix: utils.Ptr("prefix-1"), + }, + { + Id: utils.Ptr(networkRangeId2), + Prefix: utils.Ptr("prefix-2"), + }, + }, + }, + }, + region: "eu01", + }, + want: &Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s,eu01", organizationId, networkAreaId)), + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Region: types.StringValue("eu01"), + + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("nameserver1"), + types.StringValue("nameserver2"), + }), + TransferNetwork: types.StringValue("network"), + DefaultPrefixLength: types.Int64Value(20), + MaxPrefixLength: types.Int64Value(22), + MinPrefixLength: types.Int64Value(18), + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("prefix-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + }, + }, + }, + wantErr: false, + }, + { + name: "model is nil", + args: args{ + model: nil, + networkAreaRegion: &iaas.RegionalArea{}, + }, + want: nil, + wantErr: true, + }, + { + name: "network area region response is nil", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListNull(types.StringType), + NetworkRanges: []networkRangeModel{}, + }, + }, + }, + want: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListNull(types.StringType), + NetworkRanges: []networkRangeModel{}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if err := mapFields(ctx, tt.args.networkAreaRegion, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { + t.Errorf("mapFields() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(tt.args.model, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_toCreatePayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want *iaas.CreateNetworkAreaRegionPayload + wantErr bool + }{ + { + name: "default_ok", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringUnknown(), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringUnknown(), + Prefix: types.StringValue("pr-2"), + }, + }, + TransferNetwork: types.StringValue("network"), + DefaultPrefixLength: types.Int64Value(20), + MaxPrefixLength: types.Int64Value(22), + MinPrefixLength: types.Int64Value(18), + }, + }, + }, + want: &iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: &[]string{ + "ns1", + "ns2", + }, + NetworkRanges: &[]iaas.NetworkRange{ + { + Prefix: utils.Ptr("pr-1"), + }, + { + Prefix: utils.Ptr("pr-2"), + }, + }, + TransferNetwork: utils.Ptr("network"), + DefaultPrefixLen: utils.Ptr(int64(20)), + MaxPrefixLen: utils.Ptr(int64(22)), + MinPrefixLen: utils.Ptr(int64(18)), + }, + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toCreatePayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toCreatePayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_toUpdatePayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want *iaas.UpdateNetworkAreaRegionPayload + wantErr bool + }{ + { + name: "default_ok", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + DefaultPrefixLength: types.Int64Value(22), + MaxPrefixLength: types.Int64Value(24), + MinPrefixLength: types.Int64Value(20), + }, + }, + }, + want: &iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: &[]string{ + "ns1", + "ns2", + }, + DefaultPrefixLen: utils.Ptr(int64(22)), + MaxPrefixLen: utils.Ptr(int64(24)), + MinPrefixLen: utils.Ptr(int64(20)), + }, + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toUpdatePayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toUpdatePayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_mapIpv4NetworkRanges(t *testing.T) { + type args struct { + networkAreaRangesList *[]iaas.NetworkRange + model *Model + } + tests := []struct { + name string + args args + want *Model + wantErr bool + }{ + { + name: "model and response have ranges in different order", + args: args{ + model: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListNull(types.StringType), + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("prefix-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + }, + }, + }, + networkAreaRangesList: &[]iaas.NetworkRange{ + { + Id: utils.Ptr(networkRangeId2), + Prefix: utils.Ptr("prefix-2"), + }, + { + Id: utils.Ptr(networkRangeId3), + Prefix: utils.Ptr("prefix-3"), + }, + { + Id: utils.Ptr(networkRangeId1), + Prefix: utils.Ptr("prefix-1"), + }, + }, + }, + want: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("prefix-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("prefix-3"), + }, + }, + DefaultNameservers: types.ListNull(types.StringType), + }, + }, + wantErr: false, + }, + { + name: "network_ranges_changed_outside_tf", + args: args{ + model: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("prefix-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + }, + DefaultNameservers: types.ListNull(types.StringType), + }, + }, + networkAreaRangesList: &[]iaas.NetworkRange{ + { + Id: utils.Ptr(networkRangeId2), + Prefix: utils.Ptr("prefix-2"), + }, + { + Id: utils.Ptr(networkRangeId3), + Prefix: utils.Ptr("prefix-3"), + }, + }, + }, + want: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("prefix-3"), + }, + }, + DefaultNameservers: types.ListNull(types.StringType), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := mapIpv4NetworkRanges(context.Background(), tt.args.networkAreaRangesList, tt.args.model); (err != nil) != tt.wantErr { + t.Errorf("mapIpv4NetworkRanges() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(tt.args.model, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_updateIpv4NetworkRanges(t *testing.T) { + getAllNetworkRangesResp := iaas.NetworkRangeListResponse{ + Items: &[]iaas.NetworkRange{ + { + Prefix: utils.Ptr("pr-1"), + Id: utils.Ptr(networkRangeId1), + }, + { + Prefix: utils.Ptr("pr-2"), + Id: utils.Ptr(networkRangeId2), + }, + { + Prefix: utils.Ptr("pr-3"), + Id: utils.Ptr(networkRangeId3), + }, + { + Prefix: utils.Ptr("pr-2"), + Id: utils.Ptr(networkRangeId2Repeated), + }, + }, + } + getAllNetworkRangesRespBytes, err := json.Marshal(getAllNetworkRangesResp) + if err != nil { + t.Fatalf("Failed to marshal get all network ranges response: %v", err) + } + + // This is the response used whenever an API returns a failure response + failureRespBytes := []byte("{\"message\": \"Something bad happened\"") + + type args struct { + networkRanges []networkRangeModel + } + tests := []struct { + description string + args args + + expectedNetworkRangesStates map[string]bool // Keys are prefix; value is true if prefix should exist at the end, false if should be deleted + isValid bool + + // mock control + createNetworkRangesFails bool + deleteNetworkRangesFails bool + getAllNetworkRangesFails bool + }{ + { + description: "no_changes", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": true, + "pr-3": true, + }, + isValid: true, + }, + { + description: "create_network_ranges", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": true, + "pr-3": true, + "pr-4": true, + }, + isValid: true, + }, + { + description: "delete_network_ranges", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": false, + "pr-3": true, + }, + isValid: true, + }, + { + description: "multiple_changes", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId5), + Prefix: types.StringValue("pr-5"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": false, + "pr-3": true, + "pr-4": true, + "pr-5": true, + }, + isValid: true, + }, + { + description: "multiple_changes_repetition", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId5), + Prefix: types.StringValue("pr-5"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId5), + Prefix: types.StringValue("pr-5"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": false, + "pr-3": true, + "pr-4": true, + "pr-5": true, + }, + isValid: true, + }, + { + description: "multiple_changes_2", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId5), + Prefix: types.StringValue("pr-5"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": false, + "pr-2": false, + "pr-3": false, + "pr-4": true, + "pr-5": true, + }, + isValid: true, + }, + { + description: "multiple_changes_3", + args: args{ + networkRanges: []networkRangeModel{}, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": false, + "pr-2": false, + "pr-3": false, + }, + isValid: true, + }, + { + description: "get_fails", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + }, + }, + getAllNetworkRangesFails: true, + isValid: false, + }, + { + description: "create_fails_1", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + }, + }, + createNetworkRangesFails: true, + isValid: false, + }, + { + description: "create_fails_2", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + }, + }, + createNetworkRangesFails: true, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": true, + "pr-3": false, + }, + isValid: true, + }, + { + description: "delete_fails_1", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + }, + }, + deleteNetworkRangesFails: true, + isValid: false, + }, + { + description: "delete_fails_2", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + }, + }, + deleteNetworkRangesFails: true, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": true, + "pr-3": true, + "pr-4": true, + }, + isValid: true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + // Will be compared to tt.expectedNetworkRangesStates at the end + networkRangesStates := make(map[string]bool) + networkRangesStates["pr-1"] = true + networkRangesStates["pr-2"] = true + networkRangesStates["pr-3"] = true + + // Handler for getting all network ranges + getAllNetworkRangesHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + if tt.getAllNetworkRangesFails { + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) + if err != nil { + t.Errorf("Get all network ranges handler: failed to write bad response: %v", err) + } + return + } + + _, err := w.Write(getAllNetworkRangesRespBytes) + if err != nil { + t.Errorf("Get all network ranges handler: failed to write response: %v", err) + } + }) + + // Handler for creating network range + createNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var payload iaas.CreateNetworkAreaRangePayload + err := decoder.Decode(&payload) + if err != nil { + t.Errorf("Create network range handler: failed to parse payload") + return + } + if payload.Ipv4 == nil { + t.Errorf("Create network range handler: nil Ipv4") + return + } + ipv4 := *payload.Ipv4 + + for _, networkRange := range ipv4 { + prefix := *networkRange.Prefix + if prefixExists, prefixWasCreated := networkRangesStates[prefix]; prefixWasCreated && prefixExists { + t.Errorf("Create network range handler: attempted to create range '%v' that already exists", *payload.Ipv4) + return + } + w.Header().Set("Content-Type", "application/json") + if tt.createNetworkRangesFails { + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) + if err != nil { + t.Errorf("Create network ranges handler: failed to write bad response: %v", err) + } + return + } + + resp := iaas.NetworkRange{ + Prefix: utils.Ptr("prefix"), + Id: utils.Ptr("id-range"), + } + respBytes, err := json.Marshal(resp) + if err != nil { + t.Errorf("Create network range handler: failed to marshal response: %v", err) + return + } + _, err = w.Write(respBytes) + if err != nil { + t.Errorf("Create network range handler: failed to write response: %v", err) + } + networkRangesStates[prefix] = true + } + }) + + // Handler for deleting Network range + deleteNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + networkRangeId, ok := vars["networkRangeId"] + if !ok { + t.Errorf("Delete network range handler: no range ID") + return + } + + var prefix string + for _, rangeItem := range *getAllNetworkRangesResp.Items { + if *rangeItem.Id == networkRangeId { + prefix = *rangeItem.Prefix + } + } + prefixExists, prefixWasCreated := networkRangesStates[prefix] + if !prefixWasCreated { + t.Errorf("Delete network range handler: attempted to delete range '%v' that wasn't created", prefix) + return + } + if prefixWasCreated && !prefixExists { + t.Errorf("Delete network range handler: attempted to delete range '%v' that was already deleted", prefix) + return + } + + w.Header().Set("Content-Type", "application/json") + if tt.deleteNetworkRangesFails { + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) + if err != nil { + t.Errorf("Delete network range handler: failed to write bad response: %v", err) + } + return + } + + _, err = w.Write([]byte("{}")) + if err != nil { + t.Errorf("Delete network range handler: failed to write response: %v", err) + } + networkRangesStates[prefix] = false + }) + + // Setup server and client + router := mux.NewRouter() + router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + getAllNetworkRangesHandler(w, r) + } else if r.Method == "POST" { + createNetworkRangeHandler(w, r) + } + }) + router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges/{networkRangeId}", deleteNetworkRangeHandler) + mockedServer := httptest.NewServer(router) + defer mockedServer.Close() + client, err := iaas.NewAPIClient( + config.WithEndpoint(mockedServer.URL), + config.WithoutAuthentication(), + ) + if err != nil { + t.Fatalf("Failed to initialize client: %v", err) + } + + // Run test + err = updateIpv4NetworkRanges(context.Background(), organizationId, networkAreaId, tt.args.networkRanges, client, testRegion) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(networkRangesStates, tt.expectedNetworkRangesStates) + if diff != "" { + t.Fatalf("Network range states do not match: %s", diff) + } + } + }) + } +} + +func Test_toDefaultNameserversPayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "values_ok", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("1.1.1.1"), + types.StringValue("8.8.8.8"), + types.StringValue("9.9.9.9"), + }), + }, + }, + }, + want: []string{ + "1.1.1.1", + "8.8.8.8", + "9.9.9.9", + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toDefaultNameserversPayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toDefaultNameserversPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("toDefaultNameserversPayload() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_toNetworkRangesPayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want *[]iaas.NetworkRange + wantErr bool + }{ + { + name: "values_ok", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{ + { + Prefix: types.StringValue("prefix-1"), + }, + { + Prefix: types.StringValue("prefix-2"), + }, + }, + }, + }, + }, + want: &[]iaas.NetworkRange{ + { + Prefix: utils.Ptr("prefix-1"), + }, + { + Prefix: utils.Ptr("prefix-2"), + }, + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + wantErr: true, + }, + { + name: "network ranges is nil", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + NetworkRanges: nil, + }, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "network ranges has length 0", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{}, + }, + }, + }, + want: nil, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toNetworkRangesPayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toNetworkRangesPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/stackit/internal/services/iaas/networkarearoute/datasource.go b/stackit/internal/services/iaas/networkarearoute/datasource.go index 19b139d3f..31942cd6e 100644 --- a/stackit/internal/services/iaas/networkarearoute/datasource.go +++ b/stackit/internal/services/iaas/networkarearoute/datasource.go @@ -31,7 +31,8 @@ func NewNetworkAreaRouteDataSource() datasource.DataSource { // networkDataSource is the data source implementation. type networkAreaRouteDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *networkAreaRouteDataSource) Metadata(_ context.Context, req datasource. } func (d *networkAreaRouteDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -61,7 +63,7 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche MarkdownDescription: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal data source ID. It is structured as \"`organization_id`,`network_area_id`,`network_area_route_id`\".", + Description: "Terraform's internal data source ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`network_area_route_id`\".", Computed: true, }, "organization_id": schema.StringAttribute{ @@ -80,6 +82,11 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "network_area_route_id": schema.StringAttribute{ Description: "The network area route ID.", Required: true, @@ -113,14 +120,17 @@ func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.Re if resp.Diagnostics.HasError() { return } + organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) networkAreaRouteId := model.NetworkAreaRouteId.ValueString() ctx = tflog.SetField(ctx, "organization_id", organizationId) ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) - networkAreaRouteResp, err := d.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, networkAreaRouteId).Execute() + networkAreaRouteResp, err := d.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).Execute() if err != nil { utils.LogError( ctx, @@ -136,11 +146,12 @@ func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.Re return } - err = mapFields(ctx, networkAreaRouteResp, &model) + err = mapFields(ctx, networkAreaRouteResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/iaas/networkarearoute/resource.go b/stackit/internal/services/iaas/networkarearoute/resource.go index e29b3fdc0..50b82174b 100644 --- a/stackit/internal/services/iaas/networkarearoute/resource.go +++ b/stackit/internal/services/iaas/networkarearoute/resource.go @@ -6,11 +6,12 @@ import ( "net/http" "strings" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -30,11 +31,13 @@ var ( _ resource.Resource = &networkAreaRouteResource{} _ resource.ResourceWithConfigure = &networkAreaRouteResource{} _ resource.ResourceWithImportState = &networkAreaRouteResource{} + _ resource.ResourceWithModifyPlan = &networkAreaRouteResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF OrganizationId types.String `tfsdk:"organization_id"` + Region types.String `tfsdk:"region"` NetworkAreaId types.String `tfsdk:"network_area_id"` NetworkAreaRouteId types.String `tfsdk:"network_area_route_id"` NextHop types.String `tfsdk:"next_hop"` @@ -49,7 +52,8 @@ func NewNetworkAreaRouteResource() resource.Resource { // networkResource is the resource implementation. type networkAreaRouteResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -57,14 +61,45 @@ func (r *networkAreaRouteResource) Metadata(_ context.Context, req resource.Meta resp.TypeName = req.ProviderTypeName + "_network_area_route" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *networkAreaRouteResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *networkAreaRouteResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -80,7 +115,7 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe MarkdownDescription: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`network_area_route_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`,`network_area_route_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -97,6 +132,15 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "network_area_id": schema.StringAttribute{ Description: "The network area ID to which the network area route is associated.", Required: true, @@ -161,8 +205,10 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea } organizationId := model.OrganizationId.ValueString() - ctx = tflog.SetField(ctx, "organization_id", organizationId) + region := r.providerData.GetRegionWithOverride(model.Region) networkAreaId := model.NetworkAreaId.ValueString() + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) // Generate API request body from model @@ -173,7 +219,7 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea } // Create new network area route - routes, err := r.client.CreateNetworkAreaRoute(ctx, organizationId, networkAreaId).CreateNetworkAreaRoutePayload(*payload).Execute() + routes, err := r.client.CreateNetworkAreaRoute(ctx, organizationId, networkAreaId, region).CreateNetworkAreaRoutePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route", fmt.Sprintf("Calling API: %v", err)) return @@ -191,12 +237,12 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea // Gets the route ID from the first element, routes.Items[0] routeItems := *routes.Items route := routeItems[0] - routeId := *route.RouteId + routeId := *route.Id ctx = tflog.SetField(ctx, "network_area_route_id", routeId) // Map response body to schema - err = mapFields(ctx, &route, &model) + err = mapFields(ctx, &route, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route.", fmt.Sprintf("Processing API payload: %v", err)) return @@ -220,12 +266,14 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe } organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) networkAreaRouteId := model.NetworkAreaRouteId.ValueString() ctx = tflog.SetField(ctx, "organization_id", organizationId) ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) - networkAreaRouteResp, err := r.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, networkAreaRouteId).Execute() + networkAreaRouteResp, err := r.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -237,7 +285,7 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe } // Map response body to schema - err = mapFields(ctx, networkAreaRouteResp, &model) + err = mapFields(ctx, networkAreaRouteResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route", fmt.Sprintf("Processing API payload: %v", err)) return @@ -263,13 +311,15 @@ func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.Dele organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) networkAreaRouteId := model.NetworkAreaRouteId.ValueString() ctx = tflog.SetField(ctx, "organization_id", organizationId) ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) // Delete existing network - err := r.client.DeleteNetworkAreaRoute(ctx, organizationId, networkAreaId, networkAreaRouteId).Execute() + err := r.client.DeleteNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area route", fmt.Sprintf("Calling API: %v", err)) return @@ -290,9 +340,11 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) networkAreaRouteId := model.NetworkAreaRouteId.ValueString() ctx = tflog.SetField(ctx, "organization_id", organizationId) ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) // Retrieve values from state @@ -310,13 +362,13 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda return } // Update existing network area route - networkAreaRouteResp, err := r.client.UpdateNetworkAreaRoute(ctx, organizationId, networkAreaId, networkAreaRouteId).UpdateNetworkAreaRoutePayload(*payload).Execute() + networkAreaRouteResp, err := r.client.UpdateNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).UpdateNetworkAreaRoutePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(ctx, networkAreaRouteResp, &model) + err = mapFields(ctx, networkAreaRouteResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Processing API payload: %v", err)) return @@ -334,28 +386,25 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda func (r *networkAreaRouteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing network area route", - fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id],[network_area_route_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id],[region],[network_area_route_id] Got: %q", req.ID), ) return } - organizationId := idParts[0] - networkAreaId := idParts[1] - networkAreaRouteId := idParts[2] - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "organization_id": idParts[0], + "network_area_id": idParts[1], + "region": idParts[2], + "network_area_route_id": idParts[3], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), organizationId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_area_id"), networkAreaId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_area_route_id"), networkAreaRouteId)...) tflog.Info(ctx, "Network area route state imported") } -func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model) error { +func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model, region string) error { if networkAreaRoute == nil { return fmt.Errorf("response input is nil") } @@ -366,13 +415,14 @@ func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model) var networkAreaRouteId string if model.NetworkAreaRouteId.ValueString() != "" { networkAreaRouteId = model.NetworkAreaRouteId.ValueString() - } else if networkAreaRoute.RouteId != nil { - networkAreaRouteId = *networkAreaRoute.RouteId + } else if networkAreaRoute.Id != nil { + networkAreaRouteId = *networkAreaRoute.Id } else { return fmt.Errorf("network area route id not present") } - model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), model.NetworkAreaId.ValueString(), networkAreaRouteId) + model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), model.NetworkAreaId.ValueString(), region, networkAreaRouteId) + model.Region = types.StringValue(region) labels, err := iaasUtils.MapLabels(ctx, networkAreaRoute.Labels, model.Labels) if err != nil { @@ -380,9 +430,20 @@ func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model) } model.NetworkAreaRouteId = types.StringValue(networkAreaRouteId) - model.NextHop = types.StringPointerValue(networkAreaRoute.Nexthop) - model.Prefix = types.StringPointerValue(networkAreaRoute.Prefix) model.Labels = labels + + if networkAreaRoute.Nexthop != nil && networkAreaRoute.Nexthop.NexthopIPv4 != nil { + model.NextHop = types.StringPointerValue(networkAreaRoute.Nexthop.NexthopIPv4.Value) + } else { + model.NextHop = types.StringNull() + } + + if networkAreaRoute.Destination != nil && networkAreaRoute.Destination.DestinationCIDRv4 != nil { + model.Prefix = types.StringPointerValue(networkAreaRoute.Destination.DestinationCIDRv4.Value) + } else { + model.Prefix = types.StringNull() + } + return nil } @@ -397,11 +458,22 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea } return &iaas.CreateNetworkAreaRoutePayload{ - Ipv4: &[]iaas.Route{ + Items: &[]iaas.Route{ { - Prefix: conversion.StringValueToPointer(model.Prefix), - Nexthop: conversion.StringValueToPointer(model.NextHop), - Labels: &labels, + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: sdkUtils.Ptr("cidrv4"), + Value: conversion.StringValueToPointer(model.Prefix), + }, + DestinationCIDRv6: nil, + }, + Labels: &labels, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: sdkUtils.Ptr("ipv4"), + Value: conversion.StringValueToPointer(model.NextHop), + }, + }, }, }, }, nil diff --git a/stackit/internal/services/iaas/networkarearoute/resource_test.go b/stackit/internal/services/iaas/networkarearoute/resource_test.go index d210e0592..9e2689c2b 100644 --- a/stackit/internal/services/iaas/networkarearoute/resource_test.go +++ b/stackit/internal/services/iaas/networkarearoute/resource_test.go @@ -4,56 +4,80 @@ import ( "context" "testing" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.Route + region string + } tests := []struct { description string - state Model - input *iaas.Route + args args expected Model isValid bool }{ { - "id_ok", - Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkAreaRouteId: types.StringValue("narid"), + description: "id_ok", + args: args{ + state: Model{ + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("naid"), + NetworkAreaRouteId: types.StringValue("narid"), + }, + input: &iaas.Route{}, + region: "eu01", }, - &iaas.Route{}, - Model{ - Id: types.StringValue("oid,naid,narid"), + expected: Model{ + Id: types.StringValue("oid,naid,eu01,narid"), OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), NetworkAreaRouteId: types.StringValue("narid"), Prefix: types.StringNull(), NextHop: types.StringNull(), Labels: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "values_ok", - Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkAreaRouteId: types.StringValue("narid"), - }, - &iaas.Route{ - Prefix: utils.Ptr("prefix"), - Nexthop: utils.Ptr("hop"), - Labels: &map[string]interface{}{ - "key": "value", + description: "values_ok", + args: args{ + state: Model{ + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("naid"), + NetworkAreaRouteId: types.StringValue("narid"), + Region: types.StringValue("eu01"), + }, + input: &iaas.Route{ + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("prefix"), + }, + DestinationCIDRv6: nil, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("hop"), + }, + }, + Labels: &map[string]interface{}{ + "key": "value", + }, }, + region: "eu02", }, - Model{ - Id: types.StringValue("oid,naid,narid"), + expected: Model{ + Id: types.StringValue("oid,naid,eu02,narid"), OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), NetworkAreaRouteId: types.StringValue("narid"), @@ -62,40 +86,36 @@ func TestMapFields(t *testing.T) { Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ "key": types.StringValue("value"), }), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "response_fields_nil_fail", - Model{}, - &iaas.Route{ - Prefix: nil, - Nexthop: nil, + description: "response_fields_nil_fail", + args: args{ + input: &iaas.Route{ + Destination: nil, + Nexthop: nil, + }, }, - Model{}, - false, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), + description: "no_resource_id", + args: args{ + state: Model{ + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("naid"), + }, + input: &iaas.Route{}, }, - &iaas.Route{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -103,7 +123,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -129,10 +149,21 @@ func TestToCreatePayload(t *testing.T) { }), }, expected: &iaas.CreateNetworkAreaRoutePayload{ - Ipv4: &[]iaas.Route{ + Items: &[]iaas.Route{ { - Prefix: utils.Ptr("prefix"), - Nexthop: utils.Ptr("hop"), + Destination: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: utils.Ptr("cidrv4"), + Value: utils.Ptr("prefix"), + }, + DestinationCIDRv6: nil, + }, + Nexthop: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: utils.Ptr("ipv4"), + Value: utils.Ptr("hop"), + }, + }, Labels: &map[string]interface{}{ "key": "value", }, diff --git a/stackit/internal/services/iaas/networkinterface/datasource.go b/stackit/internal/services/iaas/networkinterface/datasource.go index 710cac526..04f305272 100644 --- a/stackit/internal/services/iaas/networkinterface/datasource.go +++ b/stackit/internal/services/iaas/networkinterface/datasource.go @@ -24,14 +24,15 @@ var ( _ datasource.DataSource = &networkInterfaceDataSource{} ) -// NewNetworkDataSource is a helper function to simplify the provider implementation. +// NewNetworkInterfaceDataSource is a helper function to simplify the provider implementation. func NewNetworkInterfaceDataSource() datasource.DataSource { return &networkInterfaceDataSource{} } // networkInterfaceDataSource is the data source implementation. type networkInterfaceDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *networkInterfaceDataSource) Metadata(_ context.Context, req datasource. } func (d *networkInterfaceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -63,7 +65,7 @@ func (d *networkInterfaceDataSource) Schema(_ context.Context, _ datasource.Sche Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal data source ID. It is structured as \"`project_id`,`network_id`,`network_interface_id`\".", + Description: "Terraform's internal data source ID. It is structured as \"`project_id`,`region`,`network_id`,`network_interface_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -74,6 +76,11 @@ func (d *networkInterfaceDataSource) Schema(_ context.Context, _ datasource.Sche validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "network_id": schema.StringAttribute{ Description: "The network ID to which the network interface is associated.", Required: true, @@ -141,14 +148,17 @@ func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.Re if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) networkId := model.NetworkId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_id", networkId) ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - networkInterfaceResp, err := d.client.GetNic(ctx, projectId, networkId, networkInterfaceId).Execute() + networkInterfaceResp, err := d.client.GetNic(ctx, projectId, region, networkId, networkInterfaceId).Execute() if err != nil { utils.LogError( ctx, @@ -164,7 +174,7 @@ func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.Re return } - err = mapFields(ctx, networkInterfaceResp, &model) + err = mapFields(ctx, networkInterfaceResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/iaas/networkinterface/resource.go b/stackit/internal/services/iaas/networkinterface/resource.go index d84d1a938..acc117437 100644 --- a/stackit/internal/services/iaas/networkinterface/resource.go +++ b/stackit/internal/services/iaas/networkinterface/resource.go @@ -40,6 +40,7 @@ type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` NetworkId types.String `tfsdk:"network_id"` + Region types.String `tfsdk:"region"` NetworkInterfaceId types.String `tfsdk:"network_interface_id"` Name types.String `tfsdk:"name"` AllowedAddresses types.List `tfsdk:"allowed_addresses"` @@ -59,7 +60,8 @@ func NewNetworkInterfaceResource() resource.Resource { // networkResource is the resource implementation. type networkInterfaceResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // ModifyPlan implements resource.ResourceWithModifyPlan. @@ -92,6 +94,17 @@ func (r *networkInterfaceResource) ModifyPlan(ctx context.Context, req resource. if resp.Diagnostics.HasError() { return } + + // Use the modifier to set the effective region in the current plan. + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } } // Metadata returns the resource type name. @@ -101,12 +114,13 @@ func (r *networkInterfaceResource) Metadata(_ context.Context, req resource.Meta // Configure adds the provider configured client to the resource. func (r *networkInterfaceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -124,7 +138,7 @@ func (r *networkInterfaceResource) Schema(_ context.Context, _ resource.SchemaRe Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`,`network_interface_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`network_id`,`network_interface_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -164,6 +178,15 @@ func (r *networkInterfaceResource) Schema(_ context.Context, _ resource.SchemaRe validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "name": schema.StringAttribute{ Description: "The name of the network interface.", Optional: true, @@ -258,8 +281,10 @@ func (r *networkInterfaceResource) Create(ctx context.Context, req resource.Crea } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) networkId := model.NetworkId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_id", networkId) // Generate API request body from model @@ -270,7 +295,7 @@ func (r *networkInterfaceResource) Create(ctx context.Context, req resource.Crea } // Create new network interface - networkInterface, err := r.client.CreateNic(ctx, projectId, networkId).CreateNicPayload(*payload).Execute() + networkInterface, err := r.client.CreateNic(ctx, projectId, region, networkId).CreateNicPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Calling API: %v", err)) return @@ -281,7 +306,7 @@ func (r *networkInterfaceResource) Create(ctx context.Context, req resource.Crea ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) // Map response body to schema - err = mapFields(ctx, networkInterface, &model) + err = mapFields(ctx, networkInterface, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Processing API payload: %v", err)) return @@ -304,13 +329,15 @@ func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRe return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) networkId := model.NetworkId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_id", networkId) ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - networkInterfaceResp, err := r.client.GetNic(ctx, projectId, networkId, networkInterfaceId).Execute() + networkInterfaceResp, err := r.client.GetNic(ctx, projectId, region, networkId, networkInterfaceId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -322,7 +349,7 @@ func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRe } // Map response body to schema - err = mapFields(ctx, networkInterfaceResp, &model) + err = mapFields(ctx, networkInterfaceResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Processing API payload: %v", err)) return @@ -346,9 +373,11 @@ func (r *networkInterfaceResource) Update(ctx context.Context, req resource.Upda return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) networkId := model.NetworkId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_id", networkId) ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) @@ -367,13 +396,13 @@ func (r *networkInterfaceResource) Update(ctx context.Context, req resource.Upda return } // Update existing network - nicResp, err := r.client.UpdateNic(ctx, projectId, networkId, networkInterfaceId).UpdateNicPayload(*payload).Execute() + nicResp, err := r.client.UpdateNic(ctx, projectId, region, networkId, networkInterfaceId).UpdateNicPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(ctx, nicResp, &model) + err = mapFields(ctx, nicResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Processing API payload: %v", err)) return @@ -397,14 +426,16 @@ func (r *networkInterfaceResource) Delete(ctx context.Context, req resource.Dele } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) networkId := model.NetworkId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "network_id", networkId) ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) // Delete existing network interface - err := r.client.DeleteNic(ctx, projectId, networkId, networkInterfaceId).Execute() + err := r.client.DeleteNic(ctx, projectId, region, networkId, networkInterfaceId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network interface", fmt.Sprintf("Calling API: %v", err)) return @@ -418,28 +449,25 @@ func (r *networkInterfaceResource) Delete(ctx context.Context, req resource.Dele func (r *networkInterfaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing network interface", - fmt.Sprintf("Expected import identifier with format: [project_id],[network_id],[network_interface_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id],[network_interface_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - networkId := idParts[1] - networkInterfaceId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "network_id": idParts[2], + "network_interface_id": idParts[3], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_interface_id"), networkInterfaceId)...) tflog.Info(ctx, "Network interface state imported") } -func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model) error { +func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model, region string) error { if networkInterfaceResp == nil { return fmt.Errorf("response input is nil") } @@ -456,7 +484,8 @@ func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model return fmt.Errorf("network interface id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.NetworkId.ValueString(), networkInterfaceId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.NetworkId.ValueString(), networkInterfaceId) + model.Region = types.StringValue(region) respAllowedAddresses := []string{} var diags diag.Diagnostics diff --git a/stackit/internal/services/iaas/networkinterface/resource_test.go b/stackit/internal/services/iaas/networkinterface/resource_test.go index 070c3c28f..e549f7d3a 100644 --- a/stackit/internal/services/iaas/networkinterface/resource_test.go +++ b/stackit/internal/services/iaas/networkinterface/resource_test.go @@ -12,25 +12,32 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.NIC + region string + } tests := []struct { description string - state Model - input *iaas.NIC + args args expected Model isValid bool }{ { - "id_ok", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - }, - &iaas.NIC{ - Id: utils.Ptr("nicid"), + description: "id_ok", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + NetworkInterfaceId: types.StringValue("nicid"), + }, + input: &iaas.NIC{ + Id: utils.Ptr("nicid"), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,nid,nicid"), + expected: Model{ + Id: types.StringValue("pid,eu01,nid,nicid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), NetworkInterfaceId: types.StringValue("nicid"), @@ -43,41 +50,46 @@ func TestMapFields(t *testing.T) { Mac: types.StringNull(), Type: types.StringNull(), Labels: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "values_ok", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - }, - &iaas.NIC{ - Id: utils.Ptr("nicid"), - Name: utils.Ptr("name"), - AllowedAddresses: &[]iaas.AllowedAddressesInner{ - { - String: utils.Ptr("aa1"), - }, + description: "values_ok", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + NetworkInterfaceId: types.StringValue("nicid"), + Region: types.StringValue("eu01"), }, - SecurityGroups: &[]string{ - "prefix1", - "prefix2", - }, - Ipv4: utils.Ptr("ipv4"), - Ipv6: utils.Ptr("ipv6"), - NicSecurity: utils.Ptr(true), - Device: utils.Ptr("device"), - Mac: utils.Ptr("mac"), - Status: utils.Ptr("status"), - Type: utils.Ptr("type"), - Labels: &map[string]interface{}{ - "label1": "ref1", + input: &iaas.NIC{ + Id: utils.Ptr("nicid"), + Name: utils.Ptr("name"), + AllowedAddresses: &[]iaas.AllowedAddressesInner{ + { + String: utils.Ptr("aa1"), + }, + }, + SecurityGroups: &[]string{ + "prefix1", + "prefix2", + }, + Ipv4: utils.Ptr("ipv4"), + Ipv6: utils.Ptr("ipv6"), + NicSecurity: utils.Ptr(true), + Device: utils.Ptr("device"), + Mac: utils.Ptr("mac"), + Status: utils.Ptr("status"), + Type: utils.Ptr("type"), + Labels: &map[string]interface{}{ + "label1": "ref1", + }, }, + region: "eu02", }, - Model{ - Id: types.StringValue("pid,nid,nicid"), + expected: Model{ + Id: types.StringValue("pid,eu02,nid,nicid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), NetworkInterfaceId: types.StringValue("nicid"), @@ -95,29 +107,33 @@ func TestMapFields(t *testing.T) { Mac: types.StringValue("mac"), Type: types.StringValue("type"), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"label1": types.StringValue("ref1")}), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "allowed_addresses_changed_outside_tf", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("aa1"), - }), - }, - &iaas.NIC{ - Id: utils.Ptr("nicid"), - AllowedAddresses: &[]iaas.AllowedAddressesInner{ - { - String: utils.Ptr("aa2"), + description: "allowed_addresses_changed_outside_tf", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + NetworkInterfaceId: types.StringValue("nicid"), + AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("aa1"), + }), + }, + input: &iaas.NIC{ + Id: utils.Ptr("nicid"), + AllowedAddresses: &[]iaas.AllowedAddressesInner{ + { + String: utils.Ptr("aa2"), + }, }, }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,nid,nicid"), + expected: Model{ + Id: types.StringValue("pid,eu01,nid,nicid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), NetworkInterfaceId: types.StringValue("nicid"), @@ -127,23 +143,27 @@ func TestMapFields(t *testing.T) { types.StringValue("aa2"), }), Labels: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "empty_list_allowed_addresses", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{}), - }, - &iaas.NIC{ - Id: utils.Ptr("nicid"), - AllowedAddresses: nil, + description: "empty_list_allowed_addresses", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + NetworkInterfaceId: types.StringValue("nicid"), + AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{}), + }, + input: &iaas.NIC{ + Id: utils.Ptr("nicid"), + AllowedAddresses: nil, + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,nid,nicid"), + expected: Model{ + Id: types.StringValue("pid,eu01,nid,nicid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), NetworkInterfaceId: types.StringValue("nicid"), @@ -151,29 +171,34 @@ func TestMapFields(t *testing.T) { SecurityGroupIds: types.ListNull(types.StringType), AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{}), Labels: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", + args: args{ + state: Model{}, + input: nil, + }, + expected: Model{}, + isValid: false, }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.NIC{}, }, - &iaas.NIC{}, - Model{}, - false, + expected: Model{}, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -181,7 +206,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/networkinterfaceattach/resource.go b/stackit/internal/services/iaas/networkinterfaceattach/resource.go index 1517dd80f..cfe7cba03 100644 --- a/stackit/internal/services/iaas/networkinterfaceattach/resource.go +++ b/stackit/internal/services/iaas/networkinterfaceattach/resource.go @@ -11,7 +11,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -30,11 +29,13 @@ var ( _ resource.Resource = &networkInterfaceAttachResource{} _ resource.ResourceWithConfigure = &networkInterfaceAttachResource{} _ resource.ResourceWithImportState = &networkInterfaceAttachResource{} + _ resource.ResourceWithModifyPlan = &networkInterfaceAttachResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` NetworkInterfaceId types.String `tfsdk:"network_interface_id"` } @@ -46,7 +47,8 @@ func NewNetworkInterfaceAttachResource() resource.Resource { // networkInterfaceAttachResource is the resource implementation. type networkInterfaceAttachResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -54,14 +56,45 @@ func (r *networkInterfaceAttachResource) Metadata(_ context.Context, req resourc resp.TypeName = req.ProviderTypeName + "_server_network_interface_attach" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *networkInterfaceAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -71,13 +104,13 @@ func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req reso // Schema defines the schema for the resource. func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Network interface attachment resource schema. Attaches a network interface to a server. Must have a `region` specified in the provider configuration. The attachment only takes full effect after server reboot." + description := "Network interface attachment resource schema. Attaches a network interface to a server. The attachment only takes full effect after server reboot." resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`network_interface_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`network_interface_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -94,6 +127,15 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "server_id": schema.StringAttribute{ Description: "The server ID.", Required: true, @@ -131,20 +173,22 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() - ctx = tflog.SetField(ctx, "server_id", serverId) networkInterfaceId := model.NetworkInterfaceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) // Create new network interface attachment - err := r.client.AddNicToServer(ctx, projectId, serverId, networkInterfaceId).Execute() + err := r.client.AddNicToServer(ctx, projectId, region, serverId, networkInterfaceId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching network interface to server", fmt.Sprintf("Calling API: %v", err)) return } - model.Id = utils.BuildInternalTerraformId(projectId, serverId, networkInterfaceId) + model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, networkInterfaceId) // Set state to fully populated data diags = resp.State.Set(ctx, model) @@ -164,13 +208,14 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. return } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() - ctx = tflog.SetField(ctx, "server_id", serverId) networkInterfaceId := model.NetworkInterfaceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - nics, err := r.client.ListServerNics(ctx, projectId, serverId).Execute() + nics, err := r.client.ListServerNICs(ctx, projectId, region, serverId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -222,14 +267,16 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() - ctx = tflog.SetField(ctx, "server_id", serverId) network_interfaceId := model.NetworkInterfaceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "network_interface_id", network_interfaceId) // Remove network_interface from server - err := r.client.RemoveNicFromServer(ctx, projectId, serverId, network_interfaceId).Execute() + err := r.client.RemoveNicFromServer(ctx, projectId, region, serverId, network_interfaceId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing network interface from server", fmt.Sprintf("Calling API: %v", err)) return @@ -243,23 +290,20 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing network_interface attachment", - fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[network_interface_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[network_interface_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - serverId := idParts[1] - network_interfaceId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "network_interface_id", network_interfaceId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ + "project_id": idParts[0], + "region": idParts[1], + "server_id": idParts[2], + "network_interface_id": idParts[3], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_interface_id"), network_interfaceId)...) tflog.Info(ctx, "Network interface attachment state imported") } diff --git a/stackit/internal/services/iaas/project/datasource.go b/stackit/internal/services/iaas/project/datasource.go index c5be5e8a0..15e783d71 100644 --- a/stackit/internal/services/iaas/project/datasource.go +++ b/stackit/internal/services/iaas/project/datasource.go @@ -28,9 +28,12 @@ type DatasourceModel struct { ProjectId types.String `tfsdk:"project_id"` AreaId types.String `tfsdk:"area_id"` InternetAccess types.Bool `tfsdk:"internet_access"` - State types.String `tfsdk:"state"` + Status types.String `tfsdk:"status"` CreatedAt types.String `tfsdk:"created_at"` UpdatedAt types.String `tfsdk:"updated_at"` + + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + State types.String `tfsdk:"state"` } // NewProjectDataSource is a helper function to simplify the provider implementation. @@ -70,7 +73,7 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest "project_id": "STACKIT project ID.", "area_id": "The area ID to which the project belongs to.", "internet_access": "Specifies if the project has internet_access", - "state": "Specifies the state of the project.", + "status": "Specifies the status of the project.", "created_at": "Date-time when the project was created.", "updated_at": "Date-time when the project was last updated.", } @@ -98,8 +101,14 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest Description: descriptions["internet_access"], Computed: true, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "state": schema.StringAttribute{ - Description: descriptions["state"], + DeprecationMessage: "Deprecated: Will be removed in May 2026. Use the `status` field instead.", + Description: descriptions["status"], + Computed: true, + }, + "status": schema.StringAttribute{ + Description: descriptions["status"], Computed: true, }, "created_at": schema.StringAttribute{ @@ -165,8 +174,8 @@ func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) erro var projectId string if model.ProjectId.ValueString() != "" { projectId = model.ProjectId.ValueString() - } else if projectResp.ProjectId != nil { - projectId = *projectResp.ProjectId + } else if projectResp.Id != nil { + projectId = *projectResp.Id } else { return fmt.Errorf("project id is not present") } @@ -197,7 +206,8 @@ func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) erro model.AreaId = areaId model.InternetAccess = types.BoolPointerValue(projectResp.InternetAccess) - model.State = types.StringPointerValue(projectResp.State) + model.State = types.StringPointerValue(projectResp.Status) + model.Status = types.StringPointerValue(projectResp.Status) model.CreatedAt = createdAt model.UpdatedAt = updatedAt return nil diff --git a/stackit/internal/services/iaas/project/datasource_test.go b/stackit/internal/services/iaas/project/datasource_test.go index adbd5ec26..d2e574893 100644 --- a/stackit/internal/services/iaas/project/datasource_test.go +++ b/stackit/internal/services/iaas/project/datasource_test.go @@ -34,7 +34,7 @@ func TestMapDataSourceFields(t *testing.T) { ProjectId: types.StringValue(projectId), }, input: &iaas.Project{ - ProjectId: utils.Ptr(projectId), + Id: utils.Ptr(projectId), }, expected: &DatasourceModel{ Id: types.StringValue(projectId), @@ -48,13 +48,12 @@ func TestMapDataSourceFields(t *testing.T) { ProjectId: types.StringValue(projectId), }, input: &iaas.Project{ - AreaId: utils.Ptr(iaas.AreaId{String: utils.Ptr("aid")}), - CreatedAt: utils.Ptr(testTimestamp()), - InternetAccess: utils.Ptr(true), - OpenstackProjectId: utils.Ptr("oid"), - ProjectId: utils.Ptr(projectId), - State: utils.Ptr("CREATED"), - UpdatedAt: utils.Ptr(testTimestamp()), + AreaId: utils.Ptr(iaas.AreaId{String: utils.Ptr("aid")}), + CreatedAt: utils.Ptr(testTimestamp()), + InternetAccess: utils.Ptr(true), + Id: utils.Ptr(projectId), + Status: utils.Ptr("CREATED"), + UpdatedAt: utils.Ptr(testTimestamp()), }, expected: &DatasourceModel{ Id: types.StringValue(projectId), @@ -62,6 +61,7 @@ func TestMapDataSourceFields(t *testing.T) { AreaId: types.StringValue("aid"), InternetAccess: types.BoolValue(true), State: types.StringValue("CREATED"), + Status: types.StringValue("CREATED"), CreatedAt: types.StringValue(testTimestampValue), UpdatedAt: types.StringValue(testTimestampValue), }, @@ -76,7 +76,7 @@ func TestMapDataSourceFields(t *testing.T) { AreaId: utils.Ptr(iaas.AreaId{ StaticAreaID: iaas.STATICAREAID_PUBLIC.Ptr(), }), - ProjectId: utils.Ptr(projectId), + Id: utils.Ptr(projectId), }, expected: &DatasourceModel{ Id: types.StringValue(projectId), diff --git a/stackit/internal/services/iaas/publicip/datasource.go b/stackit/internal/services/iaas/publicip/datasource.go index b9e177cf0..509ed843e 100644 --- a/stackit/internal/services/iaas/publicip/datasource.go +++ b/stackit/internal/services/iaas/publicip/datasource.go @@ -24,14 +24,15 @@ var ( _ datasource.DataSource = &publicIpDataSource{} ) -// NewVolumeDataSource is a helper function to simplify the provider implementation. +// NewPublicIpDataSource is a helper function to simplify the provider implementation. func NewPublicIpDataSource() datasource.DataSource { return &publicIpDataSource{} } // publicIpDataSource is the data source implementation. type publicIpDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *publicIpDataSource) Metadata(_ context.Context, req datasource.Metadata } func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -54,14 +56,14 @@ func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.Confi } // Schema defines the schema for the resource. -func (r *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Public IP resource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`public_ip_id`\".", + Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`region`,`public_ip_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -72,6 +74,11 @@ func (r *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaReques validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "public_ip_id": schema.StringAttribute{ Description: "The public IP ID.", Required: true, @@ -110,11 +117,13 @@ func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadReques return } projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) publicIpId := model.PublicIpId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - publicIpResp, err := d.client.GetPublicIP(ctx, projectId, publicIpId).Execute() + publicIpResp, err := d.client.GetPublicIP(ctx, projectId, region, publicIpId).Execute() if err != nil { utils.LogError( ctx, @@ -130,7 +139,7 @@ func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadReques return } - err = mapFields(ctx, publicIpResp, &model) + err = mapFields(ctx, publicIpResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/iaas/publicip/resource.go b/stackit/internal/services/iaas/publicip/resource.go index 04e84a663..2b0d01ff5 100644 --- a/stackit/internal/services/iaas/publicip/resource.go +++ b/stackit/internal/services/iaas/publicip/resource.go @@ -10,7 +10,6 @@ import ( iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -30,11 +29,13 @@ var ( _ resource.Resource = &publicIpResource{} _ resource.ResourceWithConfigure = &publicIpResource{} _ resource.ResourceWithImportState = &publicIpResource{} + _ resource.ResourceWithModifyPlan = &publicIpResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` PublicIpId types.String `tfsdk:"public_ip_id"` Ip types.String `tfsdk:"ip"` NetworkInterfaceId types.String `tfsdk:"network_interface_id"` @@ -48,7 +49,8 @@ func NewPublicIpResource() resource.Resource { // publicIpResource is the resource implementation. type publicIpResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -56,14 +58,45 @@ func (r *publicIpResource) Metadata(_ context.Context, req resource.MetadataRequ resp.TypeName = req.ProviderTypeName + "_public_ip" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *publicIpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *publicIpResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -79,7 +112,7 @@ func (r *publicIpResource) Schema(_ context.Context, _ resource.SchemaRequest, r Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`public_ip_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`public_ip_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -96,6 +129,15 @@ func (r *publicIpResource) Schema(_ context.Context, _ resource.SchemaRequest, r validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "public_ip_id": schema.StringAttribute{ Description: "The public IP ID.", Computed: true, @@ -146,7 +188,9 @@ func (r *publicIpResource) Create(ctx context.Context, req resource.CreateReques } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) // Generate API request body from model payload, err := toCreatePayload(ctx, &model) @@ -157,7 +201,7 @@ func (r *publicIpResource) Create(ctx context.Context, req resource.CreateReques // Create new public IP - publicIp, err := r.client.CreatePublicIP(ctx, projectId).CreatePublicIPPayload(*payload).Execute() + publicIp, err := r.client.CreatePublicIP(ctx, projectId, region).CreatePublicIPPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", fmt.Sprintf("Calling API: %v", err)) return @@ -166,7 +210,7 @@ func (r *publicIpResource) Create(ctx context.Context, req resource.CreateReques ctx = tflog.SetField(ctx, "public_ip_id", *publicIp.Id) // Map response body to schema - err = mapFields(ctx, publicIp, &model) + err = mapFields(ctx, publicIp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", fmt.Sprintf("Processing API payload: %v", err)) return @@ -189,11 +233,13 @@ func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, r return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) publicIpId := model.PublicIpId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - publicIpResp, err := r.client.GetPublicIP(ctx, projectId, publicIpId).Execute() + publicIpResp, err := r.client.GetPublicIP(ctx, projectId, region, publicIpId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -205,7 +251,7 @@ func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, r } // Map response body to schema - err = mapFields(ctx, publicIpResp, &model) + err = mapFields(ctx, publicIpResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Processing API payload: %v", err)) return @@ -229,8 +275,10 @@ func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateReques return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) publicIpId := model.PublicIpId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) // Retrieve values from state @@ -248,13 +296,13 @@ func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateReques return } // Update existing public IP - updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, publicIpId).UpdatePublicIPPayload(*payload).Execute() + updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, region, publicIpId).UpdatePublicIPPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(ctx, updatedPublicIp, &model) + err = mapFields(ctx, updatedPublicIp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", fmt.Sprintf("Processing API payload: %v", err)) return @@ -278,12 +326,14 @@ func (r *publicIpResource) Delete(ctx context.Context, req resource.DeleteReques } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) publicIpId := model.PublicIpId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) // Delete existing publicIp - err := r.client.DeletePublicIP(ctx, projectId, publicIpId).Execute() + err := r.client.DeletePublicIP(ctx, projectId, region, publicIpId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting public IP", fmt.Sprintf("Calling API: %v", err)) return @@ -297,25 +347,24 @@ func (r *publicIpResource) Delete(ctx context.Context, req resource.DeleteReques func (r *publicIpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing public IP", - fmt.Sprintf("Expected import identifier with format: [project_id],[public_ip_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[public_ip_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - publicIpId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "public_ip_id": idParts[2], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("public_ip_id"), publicIpId)...) tflog.Info(ctx, "public IP state imported") } -func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model) error { +func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model, region string) error { if publicIpResp == nil { return fmt.Errorf("response input is nil") } @@ -332,7 +381,8 @@ func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model) e return fmt.Errorf("public IP id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), publicIpId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, publicIpId) + model.Region = types.StringValue(region) labels, err := iaasUtils.MapLabels(ctx, publicIpResp.Labels, model.Labels) if err != nil { diff --git a/stackit/internal/services/iaas/publicip/resource_test.go b/stackit/internal/services/iaas/publicip/resource_test.go index 1eda0e8d1..d1797897d 100644 --- a/stackit/internal/services/iaas/publicip/resource_test.go +++ b/stackit/internal/services/iaas/publicip/resource_test.go @@ -12,49 +12,61 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.PublicIp + region string + } tests := []struct { description string - state Model - input *iaas.PublicIp + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - }, - &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - NetworkInterface: iaas.NewNullableString(nil), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + PublicIpId: types.StringValue("pipid"), + }, + input: &iaas.PublicIp{ + Id: utils.Ptr("pipid"), + NetworkInterface: iaas.NewNullableString(nil), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,pipid"), + expected: Model{ + Id: types.StringValue("pid,eu01,pipid"), ProjectId: types.StringValue("pid"), PublicIpId: types.StringValue("pipid"), Ip: types.StringNull(), Labels: types.MapNull(types.StringType), NetworkInterfaceId: types.StringNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - }, - &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - Ip: utils.Ptr("ip"), - Labels: &map[string]interface{}{ - "key": "value", + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + PublicIpId: types.StringValue("pipid"), + Region: types.StringValue("eu01"), }, - NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")), + input: &iaas.PublicIp{ + Id: utils.Ptr("pipid"), + Ip: utils.Ptr("ip"), + Labels: &map[string]interface{}{ + "key": "value", + }, + NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")), + }, + region: "eu02", }, - Model{ - Id: types.StringValue("pid,pipid"), + expected: Model{ + Id: types.StringValue("pid,eu02,pipid"), ProjectId: types.StringValue("pid"), PublicIpId: types.StringValue("pipid"), Ip: types.StringValue("ip"), @@ -62,69 +74,74 @@ func TestMapFields(t *testing.T) { "key": types.StringValue("value"), }), NetworkInterfaceId: types.StringValue("interface"), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")), + description: "empty_labels", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + PublicIpId: types.StringValue("pipid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + input: &iaas.PublicIp{ + Id: utils.Ptr("pipid"), + NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,pipid"), + expected: Model{ + Id: types.StringValue("pid,eu01,pipid"), ProjectId: types.StringValue("pid"), PublicIpId: types.StringValue("pipid"), Ip: types.StringNull(), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), NetworkInterfaceId: types.StringValue("interface"), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "network_interface_id_nil", - Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - }, - &iaas.PublicIp{ - Id: utils.Ptr("pipid"), + description: "network_interface_id_nil", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + PublicIpId: types.StringValue("pipid"), + }, + input: &iaas.PublicIp{ + Id: utils.Ptr("pipid"), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,pipid"), + expected: Model{ + Id: types.StringValue("pid,eu01,pipid"), ProjectId: types.StringValue("pid"), PublicIpId: types.StringValue("pipid"), Ip: types.StringNull(), Labels: types.MapNull(types.StringType), NetworkInterfaceId: types.StringNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.PublicIp{}, }, - &iaas.PublicIp{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -132,7 +149,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/publicipassociate/resource.go b/stackit/internal/services/iaas/publicipassociate/resource.go index d19c5de4b..182e255c3 100644 --- a/stackit/internal/services/iaas/publicipassociate/resource.go +++ b/stackit/internal/services/iaas/publicipassociate/resource.go @@ -10,7 +10,6 @@ import ( iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -30,11 +29,13 @@ var ( _ resource.Resource = &publicIpAssociateResource{} _ resource.ResourceWithConfigure = &publicIpAssociateResource{} _ resource.ResourceWithImportState = &publicIpAssociateResource{} + _ resource.ResourceWithModifyPlan = &publicIpAssociateResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` PublicIpId types.String `tfsdk:"public_ip_id"` Ip types.String `tfsdk:"ip"` NetworkInterfaceId types.String `tfsdk:"network_interface_id"` @@ -47,7 +48,8 @@ func NewPublicIpAssociateResource() resource.Resource { // publicIpAssociateResource is the resource implementation. type publicIpAssociateResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -55,14 +57,45 @@ func (r *publicIpAssociateResource) Metadata(_ context.Context, req resource.Met resp.TypeName = req.ProviderTypeName + "_public_ip_associate" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *publicIpAssociateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *publicIpAssociateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -88,7 +121,7 @@ func (r *publicIpAssociateResource) Schema(_ context.Context, _ resource.SchemaR Description: fmt.Sprintf("%s\n\n%s", descriptions["main"], descriptions["warning_message"]), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`public_ip_id`,`network_interface_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`public_ip_id`,`network_interface_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -105,6 +138,15 @@ func (r *publicIpAssociateResource) Schema(_ context.Context, _ resource.SchemaR validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "public_ip_id": schema.StringAttribute{ Description: "The public IP ID.", Required: true, @@ -151,9 +193,11 @@ func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.Cre return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) publicIpId := model.PublicIpId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) @@ -164,13 +208,13 @@ func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.Cre return } // Update existing public IP - updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, publicIpId).UpdatePublicIPPayload(*payload).Execute() + updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, region, publicIpId).UpdatePublicIPPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error associating public IP to network interface", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(updatedPublicIp, &model) + err = mapFields(updatedPublicIp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error associating public IP to network interface", fmt.Sprintf("Processing API payload: %v", err)) return @@ -192,13 +236,15 @@ func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadR return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) publicIpId := model.PublicIpId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - publicIpResp, err := r.client.GetPublicIP(ctx, projectId, publicIpId).Execute() + publicIpResp, err := r.client.GetPublicIP(ctx, projectId, region, publicIpId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -210,7 +256,7 @@ func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadR } // Map response body to schema - err = mapFields(publicIpResp, &model) + err = mapFields(publicIpResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP association", fmt.Sprintf("Processing API payload: %v", err)) return @@ -240,9 +286,11 @@ func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.Del } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) publicIpId := model.PublicIpId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) @@ -250,7 +298,7 @@ func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.Del NetworkInterface: iaas.NewNullableString(nil), } - _, err := r.client.UpdatePublicIP(ctx, projectId, publicIpId).UpdatePublicIPPayload(*payload).Execute() + _, err := r.client.UpdatePublicIP(ctx, projectId, region, publicIpId).UpdatePublicIPPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting public IP association", fmt.Sprintf("Calling API: %v", err)) return @@ -264,28 +312,25 @@ func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.Del func (r *publicIpAssociateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing public IP associate", - fmt.Sprintf("Expected import identifier with format: [project_id],[public_ip_id],[network_interface_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[public_ip_id],[network_interface_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - publicIpId := idParts[1] - networkInterfaceId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "public_ip_id": idParts[2], + "network_interface_id": idParts[3], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("public_ip_id"), publicIpId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_interface_id"), networkInterfaceId)...) tflog.Info(ctx, "public IP state imported") } -func mapFields(publicIpResp *iaas.PublicIp, model *Model) error { +func mapFields(publicIpResp *iaas.PublicIp, model *Model, region string) error { if publicIpResp == nil { return fmt.Errorf("response input is nil") } @@ -309,8 +354,9 @@ func mapFields(publicIpResp *iaas.PublicIp, model *Model) error { } model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), publicIpId, model.NetworkInterfaceId.ValueString(), + model.ProjectId.ValueString(), region, publicIpId, model.NetworkInterfaceId.ValueString(), ) + model.Region = types.StringValue(region) model.PublicIpId = types.StringValue(publicIpId) model.Ip = types.StringPointerValue(publicIpResp.Ip) diff --git a/stackit/internal/services/iaas/publicipassociate/resource_test.go b/stackit/internal/services/iaas/publicipassociate/resource_test.go index a15cf34bc..f1c09f5a9 100644 --- a/stackit/internal/services/iaas/publicipassociate/resource_test.go +++ b/stackit/internal/services/iaas/publicipassociate/resource_test.go @@ -10,74 +10,82 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.PublicIp + region string + } tests := []struct { description string - state Model - input *iaas.PublicIp + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - NetworkInterfaceId: types.StringValue("nicid"), - }, - &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + PublicIpId: types.StringValue("pipid"), + NetworkInterfaceId: types.StringValue("nicid"), + }, + input: &iaas.PublicIp{ + Id: utils.Ptr("pipid"), + NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,pipid,nicid"), + expected: Model{ + Id: types.StringValue("pid,eu01,pipid,nicid"), ProjectId: types.StringValue("pid"), PublicIpId: types.StringValue("pipid"), Ip: types.StringNull(), NetworkInterfaceId: types.StringValue("nicid"), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - NetworkInterfaceId: types.StringValue("nicid"), + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + PublicIpId: types.StringValue("pipid"), + NetworkInterfaceId: types.StringValue("nicid"), + }, + input: &iaas.PublicIp{ + Id: utils.Ptr("pipid"), + Ip: utils.Ptr("ip"), + NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")), + }, + region: "eu02", }, - &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - Ip: utils.Ptr("ip"), - NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")), - }, - Model{ - Id: types.StringValue("pid,pipid,nicid"), + expected: Model{ + Id: types.StringValue("pid,eu02,pipid,nicid"), ProjectId: types.StringValue("pid"), PublicIpId: types.StringValue("pipid"), Ip: types.StringValue("ip"), NetworkInterfaceId: types.StringValue("nicid"), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.PublicIp{}, }, - &iaas.PublicIp{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(tt.input, &tt.state) + err := mapFields(tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -85,7 +93,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/securitygroup/datasource.go b/stackit/internal/services/iaas/securitygroup/datasource.go index 171f5aba6..d888ce817 100644 --- a/stackit/internal/services/iaas/securitygroup/datasource.go +++ b/stackit/internal/services/iaas/securitygroup/datasource.go @@ -31,7 +31,8 @@ func NewSecurityGroupDataSource() datasource.DataSource { // securityGroupDataSource is the data source implementation. type securityGroupDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *securityGroupDataSource) Metadata(_ context.Context, req datasource.Met } func (d *securityGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -54,7 +56,7 @@ func (d *securityGroupDataSource) Configure(ctx context.Context, req datasource. } // Schema defines the schema for the resource. -func (r *securityGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *securityGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Security group datasource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, @@ -72,6 +74,11 @@ func (r *securityGroupDataSource) Schema(_ context.Context, _ datasource.SchemaR validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "security_group_id": schema.StringAttribute{ Description: "The security group ID.", Required: true, @@ -110,11 +117,13 @@ func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadR return } projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) securityGroupId := model.SecurityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - securityGroupResp, err := d.client.GetSecurityGroup(ctx, projectId, securityGroupId).Execute() + securityGroupResp, err := d.client.GetSecurityGroup(ctx, projectId, region, securityGroupId).Execute() if err != nil { utils.LogError( ctx, @@ -130,7 +139,7 @@ func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadR return } - err = mapFields(ctx, securityGroupResp, &model) + err = mapFields(ctx, securityGroupResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/iaas/securitygroup/resource.go b/stackit/internal/services/iaas/securitygroup/resource.go index 3663c25b3..f43e66f39 100644 --- a/stackit/internal/services/iaas/securitygroup/resource.go +++ b/stackit/internal/services/iaas/securitygroup/resource.go @@ -12,7 +12,6 @@ import ( iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -33,11 +32,13 @@ var ( _ resource.Resource = &securityGroupResource{} _ resource.ResourceWithConfigure = &securityGroupResource{} _ resource.ResourceWithImportState = &securityGroupResource{} + _ resource.ResourceWithModifyPlan = &securityGroupResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` SecurityGroupId types.String `tfsdk:"security_group_id"` Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` @@ -52,7 +53,8 @@ func NewSecurityGroupResource() resource.Resource { // securityGroupResource is the resource implementation. type securityGroupResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -60,14 +62,45 @@ func (r *securityGroupResource) Metadata(_ context.Context, req resource.Metadat resp.TypeName = req.ProviderTypeName + "_security_group" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *securityGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *securityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -83,7 +116,7 @@ func (r *securityGroupResource) Schema(_ context.Context, _ resource.SchemaReque Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -100,6 +133,15 @@ func (r *securityGroupResource) Schema(_ context.Context, _ resource.SchemaReque validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "security_group_id": schema.StringAttribute{ Description: "The security group ID.", Computed: true, @@ -163,7 +205,9 @@ func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateR } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) // Generate API request body from model payload, err := toCreatePayload(ctx, &model) @@ -174,7 +218,7 @@ func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateR // Create new security group - securityGroup, err := r.client.CreateSecurityGroup(ctx, projectId).CreateSecurityGroupPayload(*payload).Execute() + securityGroup, err := r.client.CreateSecurityGroup(ctx, projectId, region).CreateSecurityGroupPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", fmt.Sprintf("Calling API: %v", err)) return @@ -185,7 +229,7 @@ func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateR ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) // Map response body to schema - err = mapFields(ctx, securityGroup, &model) + err = mapFields(ctx, securityGroup, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", fmt.Sprintf("Processing API payload: %v", err)) return @@ -208,11 +252,13 @@ func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadReque return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) securityGroupId := model.SecurityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "security_id", securityGroupId) - securityGroupResp, err := r.client.GetSecurityGroup(ctx, projectId, securityGroupId).Execute() + securityGroupResp, err := r.client.GetSecurityGroup(ctx, projectId, region, securityGroupId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -224,7 +270,7 @@ func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadReque } // Map response body to schema - err = mapFields(ctx, securityGroupResp, &model) + err = mapFields(ctx, securityGroupResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Processing API payload: %v", err)) return @@ -248,8 +294,10 @@ func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateR return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) securityGroupId := model.SecurityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) // Retrieve values from state @@ -267,13 +315,13 @@ func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateR return } // Update existing security group - updatedSecurityGroup, err := r.client.UpdateSecurityGroup(ctx, projectId, securityGroupId).UpdateSecurityGroupPayload(*payload).Execute() + updatedSecurityGroup, err := r.client.UpdateSecurityGroup(ctx, projectId, region, securityGroupId).UpdateSecurityGroupPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(ctx, updatedSecurityGroup, &model) + err = mapFields(ctx, updatedSecurityGroup, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", fmt.Sprintf("Processing API payload: %v", err)) return @@ -297,12 +345,14 @@ func (r *securityGroupResource) Delete(ctx context.Context, req resource.DeleteR } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) securityGroupId := model.SecurityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) // Delete existing security group - err := r.client.DeleteSecurityGroup(ctx, projectId, securityGroupId).Execute() + err := r.client.DeleteSecurityGroup(ctx, projectId, region, securityGroupId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting security group", fmt.Sprintf("Calling API: %v", err)) return @@ -316,25 +366,24 @@ func (r *securityGroupResource) Delete(ctx context.Context, req resource.DeleteR func (r *securityGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing security group", - fmt.Sprintf("Expected import identifier with format: [project_id],[security_group_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[security_group_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - securityGroupId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "security_group_id": idParts[2], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("security_group_id"), securityGroupId)...) tflog.Info(ctx, "security group state imported") } -func mapFields(ctx context.Context, securityGroupResp *iaas.SecurityGroup, model *Model) error { +func mapFields(ctx context.Context, securityGroupResp *iaas.SecurityGroup, model *Model, region string) error { if securityGroupResp == nil { return fmt.Errorf("response input is nil") } @@ -351,7 +400,8 @@ func mapFields(ctx context.Context, securityGroupResp *iaas.SecurityGroup, model return fmt.Errorf("security group id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), securityGroupId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, securityGroupId) + model.Region = types.StringValue(region) labels, err := iaasUtils.MapLabels(ctx, securityGroupResp.Labels, model.Labels) if err != nil { diff --git a/stackit/internal/services/iaas/securitygroup/resource_test.go b/stackit/internal/services/iaas/securitygroup/resource_test.go index 2b4c12367..374986564 100644 --- a/stackit/internal/services/iaas/securitygroup/resource_test.go +++ b/stackit/internal/services/iaas/securitygroup/resource_test.go @@ -12,51 +12,62 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.SecurityGroup + region string + } tests := []struct { description string - state Model - input *iaas.SecurityGroup + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - }, - &iaas.SecurityGroup{ - Id: utils.Ptr("sgid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + }, + input: &iaas.SecurityGroup{ + Id: utils.Ptr("sgid"), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,sgid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sgid"), ProjectId: types.StringValue("pid"), SecurityGroupId: types.StringValue("sgid"), Name: types.StringNull(), Labels: types.MapNull(types.StringType), Description: types.StringNull(), Stateful: types.BoolNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - }, - // &sourceModel{}, - &iaas.SecurityGroup{ - Id: utils.Ptr("sgid"), - Name: utils.Ptr("name"), - Stateful: utils.Ptr(true), - Labels: &map[string]interface{}{ - "key": "value", + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + Region: types.StringValue("eu01"), }, - Description: utils.Ptr("desc"), + input: &iaas.SecurityGroup{ + Id: utils.Ptr("sgid"), + Name: utils.Ptr("name"), + Stateful: utils.Ptr(true), + Labels: &map[string]interface{}{ + "key": "value", + }, + Description: utils.Ptr("desc"), + }, + region: "eu02", }, - Model{ - Id: types.StringValue("pid,sgid"), + expected: Model{ + Id: types.StringValue("pid,eu02,sgid"), ProjectId: types.StringValue("pid"), SecurityGroupId: types.StringValue("sgid"), Name: types.StringValue("name"), @@ -65,50 +76,51 @@ func TestMapFields(t *testing.T) { }), Description: types.StringValue("desc"), Stateful: types.BoolValue(true), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - }, - &iaas.SecurityGroup{ - Id: utils.Ptr("sgid"), - Labels: &map[string]interface{}{}, + description: "empty_labels", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + }, + input: &iaas.SecurityGroup{ + Id: utils.Ptr("sgid"), + Labels: &map[string]interface{}{}, + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,sgid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sgid"), ProjectId: types.StringValue("pid"), SecurityGroupId: types.StringValue("sgid"), Name: types.StringNull(), Labels: types.MapNull(types.StringType), Description: types.StringNull(), Stateful: types.BoolNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.SecurityGroup{}, }, - &iaas.SecurityGroup{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -116,7 +128,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/securitygrouprule/datasource.go b/stackit/internal/services/iaas/securitygrouprule/datasource.go index c286fcc4b..750f83069 100644 --- a/stackit/internal/services/iaas/securitygrouprule/datasource.go +++ b/stackit/internal/services/iaas/securitygrouprule/datasource.go @@ -30,7 +30,8 @@ func NewSecurityGroupRuleDataSource() datasource.DataSource { // securityGroupRuleDataSource is the data source implementation. type securityGroupRuleDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -39,12 +40,13 @@ func (d *securityGroupRuleDataSource) Metadata(_ context.Context, req datasource } func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -53,7 +55,7 @@ func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasou } // Schema defines the schema for the resource. -func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { directionOptions := []string{"ingress", "egress"} description := "Security group datasource schema. Must have a `region` specified in the provider configuration." @@ -62,7 +64,7 @@ func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.Sch Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`security_group_id`,`security_group_rule_id`\".", + Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`region`,`security_group_id`,`security_group_rule_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -89,6 +91,11 @@ func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.Sch validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "direction": schema.StringAttribute{ Description: "The direction of the traffic which the rule should match. Some of the possible values are: " + utils.FormatPossibleValues(directionOptions...), Computed: true, @@ -164,13 +171,15 @@ func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.R return } projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) securityGroupId := model.SecurityGroupId.ValueString() securityGroupRuleId := model.SecurityGroupRuleId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId) - securityGroupRuleResp, err := d.client.GetSecurityGroupRule(ctx, projectId, securityGroupId, securityGroupRuleId).Execute() + securityGroupRuleResp, err := d.client.GetSecurityGroupRule(ctx, projectId, region, securityGroupId, securityGroupRuleId).Execute() if err != nil { utils.LogError( ctx, @@ -186,7 +195,7 @@ func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.R return } - err = mapFields(securityGroupRuleResp, &model) + err = mapFields(securityGroupRuleResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/iaas/securitygrouprule/resource.go b/stackit/internal/services/iaas/securitygrouprule/resource.go index 00b24a5c0..ab1bdc192 100644 --- a/stackit/internal/services/iaas/securitygrouprule/resource.go +++ b/stackit/internal/services/iaas/securitygrouprule/resource.go @@ -34,11 +34,13 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &securityGroupRuleResource{} - _ resource.ResourceWithConfigure = &securityGroupRuleResource{} - _ resource.ResourceWithImportState = &securityGroupRuleResource{} - icmpProtocols = []string{"icmp", "ipv6-icmp"} - protocolsPossibleValues = []string{ + _ resource.Resource = &securityGroupRuleResource{} + _ resource.ResourceWithConfigure = &securityGroupRuleResource{} + _ resource.ResourceWithImportState = &securityGroupRuleResource{} + _ resource.ResourceWithModifyPlan = &securityGroupRuleResource{} + + icmpProtocols = []string{"icmp", "ipv6-icmp"} + protocolsPossibleValues = []string{ "ah", "dccp", "egp", "esp", "gre", "icmp", "igmp", "ipip", "ipv6-encap", "ipv6-frag", "ipv6-icmp", "ipv6-nonxt", "ipv6-opts", "ipv6-route", "ospf", "pgm", "rsvp", "sctp", "tcp", "udp", "udplite", "vrrp", } @@ -47,6 +49,7 @@ var ( type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` SecurityGroupId types.String `tfsdk:"security_group_id"` SecurityGroupRuleId types.String `tfsdk:"security_group_rule_id"` Direction types.String `tfsdk:"direction"` @@ -99,7 +102,8 @@ func NewSecurityGroupRuleResource() resource.Resource { // securityGroupRuleResource is the resource implementation. type securityGroupRuleResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -107,14 +111,45 @@ func (r *securityGroupRuleResource) Metadata(_ context.Context, req resource.Met resp.TypeName = req.ProviderTypeName + "_security_group_rule" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *securityGroupRuleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *securityGroupRuleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -122,7 +157,7 @@ func (r *securityGroupRuleResource) Configure(ctx context.Context, req resource. tflog.Info(ctx, "iaas client configured") } -func (r securityGroupRuleResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { +func (r *securityGroupRuleResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var model Model resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) @@ -178,7 +213,7 @@ func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaR Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`,`security_group_rule_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`,`security_group_rule_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -196,6 +231,15 @@ func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaR validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "security_group_id": schema.StringAttribute{ Description: "The security group ID.", Required: true, @@ -390,8 +434,10 @@ func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.Cre } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) securityGroupId := model.SecurityGroupId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) var icmpParameters *icmpParametersModel @@ -432,7 +478,7 @@ func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.Cre } // Create new security group rule - securityGroupRule, err := r.client.CreateSecurityGroupRule(ctx, projectId, securityGroupId).CreateSecurityGroupRulePayload(*payload).Execute() + securityGroupRule, err := r.client.CreateSecurityGroupRule(ctx, projectId, region, securityGroupId).CreateSecurityGroupRulePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Calling API: %v", err)) return @@ -441,7 +487,7 @@ func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.Cre ctx = tflog.SetField(ctx, "security_group_rule_id", *securityGroupRule.Id) // Map response body to schema - err = mapFields(securityGroupRule, &model) + err = mapFields(securityGroupRule, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Processing API payload: %v", err)) return @@ -464,13 +510,15 @@ func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadR return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) securityGroupId := model.SecurityGroupId.ValueString() securityGroupRuleId := model.SecurityGroupRuleId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId) - securityGroupRuleResp, err := r.client.GetSecurityGroupRule(ctx, projectId, securityGroupId, securityGroupRuleId).Execute() + securityGroupRuleResp, err := r.client.GetSecurityGroupRule(ctx, projectId, region, securityGroupId, securityGroupRuleId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -482,7 +530,7 @@ func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadR } // Map response body to schema - err = mapFields(securityGroupRuleResp, &model) + err = mapFields(securityGroupRuleResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Processing API payload: %v", err)) return @@ -513,14 +561,16 @@ func (r *securityGroupRuleResource) Delete(ctx context.Context, req resource.Del } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) securityGroupId := model.SecurityGroupId.ValueString() securityGroupRuleId := model.SecurityGroupRuleId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId) // Delete existing security group rule - err := r.client.DeleteSecurityGroupRule(ctx, projectId, securityGroupId, securityGroupRuleId).Execute() + err := r.client.DeleteSecurityGroupRule(ctx, projectId, region, securityGroupId, securityGroupRuleId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting security group rule", fmt.Sprintf("Calling API: %v", err)) return @@ -534,28 +584,25 @@ func (r *securityGroupRuleResource) Delete(ctx context.Context, req resource.Del func (r *securityGroupRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing security group rule", - fmt.Sprintf("Expected import identifier with format: [project_id],[security_group_id],[security_group_rule_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[security_group_id],[security_group_rule_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - securityGroupId := idParts[1] - securityGroupRuleId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "security_group_id": idParts[2], + "security_group_rule_id": idParts[3], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("security_group_id"), securityGroupId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("security_group_rule_id"), securityGroupRuleId)...) tflog.Info(ctx, "security group rule state imported") } -func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model) error { +func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model, region string) error { if securityGroupRuleResp == nil { return fmt.Errorf("response input is nil") } @@ -572,7 +619,8 @@ func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model) erro return fmt.Errorf("security group rule id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.SecurityGroupId.ValueString(), securityGroupRuleId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.SecurityGroupId.ValueString(), securityGroupRuleId) + model.Region = types.StringValue(region) model.SecurityGroupRuleId = types.StringValue(securityGroupRuleId) model.Direction = types.StringPointerValue(securityGroupRuleResp.Direction) model.Description = types.StringPointerValue(securityGroupRuleResp.Description) diff --git a/stackit/internal/services/iaas/securitygrouprule/resource_test.go b/stackit/internal/services/iaas/securitygrouprule/resource_test.go index ef6dc0069..dbf46f59e 100644 --- a/stackit/internal/services/iaas/securitygrouprule/resource_test.go +++ b/stackit/internal/services/iaas/securitygrouprule/resource_test.go @@ -52,25 +52,32 @@ var fixtureCreateProtocol = iaas.CreateProtocol{ } func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.SecurityGroupRule + region string + } tests := []struct { description string - state Model - input *iaas.SecurityGroupRule + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + SecurityGroupRuleId: types.StringValue("sgrid"), + }, + input: &iaas.SecurityGroupRule{ + Id: utils.Ptr("sgrid"), + }, + region: "eu01", }, - &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - }, - Model{ - Id: types.StringValue("pid,sgid,sgrid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sgid,sgrid"), ProjectId: types.StringValue("pid"), SecurityGroupId: types.StringValue("sgid"), SecurityGroupRuleId: types.StringValue("sgrid"), @@ -82,29 +89,34 @@ func TestMapFields(t *testing.T) { IcmpParameters: types.ObjectNull(icmpParametersTypes), PortRange: types.ObjectNull(portRangeTypes), Protocol: types.ObjectNull(protocolTypes), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + SecurityGroupRuleId: types.StringValue("sgrid"), + Region: types.StringValue("eu01"), + }, + input: &iaas.SecurityGroupRule{ + Id: utils.Ptr("sgrid"), + Description: utils.Ptr("desc"), + Direction: utils.Ptr("ingress"), + Ethertype: utils.Ptr("ether"), + IpRange: utils.Ptr("iprange"), + RemoteSecurityGroupId: utils.Ptr("remote"), + IcmpParameters: &fixtureIcmpParameters, + PortRange: &fixturePortRange, + Protocol: &fixtureProtocol, + }, + region: "eu02", }, - &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - Description: utils.Ptr("desc"), - Direction: utils.Ptr("ingress"), - Ethertype: utils.Ptr("ether"), - IpRange: utils.Ptr("iprange"), - RemoteSecurityGroupId: utils.Ptr("remote"), - IcmpParameters: &fixtureIcmpParameters, - PortRange: &fixturePortRange, - Protocol: &fixtureProtocol, - }, - Model{ - Id: types.StringValue("pid,sgid,sgrid"), + expected: Model{ + Id: types.StringValue("pid,eu02,sgid,sgrid"), ProjectId: types.StringValue("pid"), SecurityGroupId: types.StringValue("sgid"), SecurityGroupRuleId: types.StringValue("sgrid"), @@ -116,26 +128,30 @@ func TestMapFields(t *testing.T) { IcmpParameters: fixtureModelIcmpParameters, PortRange: fixtureModelPortRange, Protocol: fixtureModelProtocol, + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "protocol_only_with_name", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{ - "name": types.StringValue("name"), - "number": types.Int64Null(), - }), - }, - &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - Protocol: &fixtureProtocol, + description: "protocol_only_with_name", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + SecurityGroupRuleId: types.StringValue("sgrid"), + Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{ + "name": types.StringValue("name"), + "number": types.Int64Null(), + }), + }, + input: &iaas.SecurityGroupRule{ + Id: utils.Ptr("sgrid"), + Protocol: &fixtureProtocol, + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,sgid,sgrid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sgid,sgrid"), ProjectId: types.StringValue("pid"), SecurityGroupId: types.StringValue("sgid"), SecurityGroupRuleId: types.StringValue("sgrid"), @@ -147,26 +163,30 @@ func TestMapFields(t *testing.T) { IcmpParameters: types.ObjectNull(icmpParametersTypes), PortRange: types.ObjectNull(portRangeTypes), Protocol: fixtureModelProtocol, + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "protocol_only_with_number", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{ - "name": types.StringNull(), - "number": types.Int64Value(1), - }), - }, - &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - Protocol: &fixtureProtocol, + description: "protocol_only_with_number", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + SecurityGroupRuleId: types.StringValue("sgrid"), + Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{ + "name": types.StringNull(), + "number": types.Int64Value(1), + }), + }, + input: &iaas.SecurityGroupRule{ + Id: utils.Ptr("sgrid"), + Protocol: &fixtureProtocol, + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,sgid,sgrid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sgid,sgrid"), ProjectId: types.StringValue("pid"), SecurityGroupId: types.StringValue("sgid"), SecurityGroupRuleId: types.StringValue("sgrid"), @@ -178,30 +198,27 @@ func TestMapFields(t *testing.T) { IcmpParameters: types.ObjectNull(icmpParametersTypes), PortRange: types.ObjectNull(portRangeTypes), Protocol: fixtureModelProtocol, + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + }, + input: &iaas.SecurityGroupRule{}, }, - &iaas.SecurityGroupRule{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(tt.input, &tt.state) + err := mapFields(tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -209,7 +226,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/server/datasource.go b/stackit/internal/services/iaas/server/datasource.go index ce2159613..80bb8eb83 100644 --- a/stackit/internal/services/iaas/server/datasource.go +++ b/stackit/internal/services/iaas/server/datasource.go @@ -30,6 +30,7 @@ var ( type DataSourceModel struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` MachineType types.String `tfsdk:"machine_type"` Name types.String `tfsdk:"name"` @@ -58,7 +59,8 @@ func NewServerDataSource() datasource.DataSource { // serverDataSource is the data source implementation. type serverDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -67,12 +69,13 @@ func (d *serverDataSource) Metadata(_ context.Context, req datasource.MetadataRe } func (d *serverDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -81,7 +84,7 @@ func (d *serverDataSource) Configure(ctx context.Context, req datasource.Configu } // Schema defines the schema for the datasource. -func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Server datasource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, @@ -99,6 +102,11 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "server_id": schema.StringAttribute{ Description: "The server ID.", Required: true, @@ -175,8 +183,8 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, } } -// // Read refreshes the Terraform state with the latest data. -func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +// Read refreshes the Terraform state with the latest data. +func (d *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -184,11 +192,13 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, return } projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "server_id", serverId) - serverReq := r.client.GetServer(ctx, projectId, serverId) + serverReq := d.client.GetServer(ctx, projectId, region, serverId) serverReq = serverReq.Details(true) serverResp, err := serverReq.Execute() if err != nil { @@ -207,7 +217,7 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, } // Map response body to schema - err = mapDataSourceFields(ctx, serverResp, &model) + err = mapDataSourceFields(ctx, serverResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Processing API payload: %v", err)) return @@ -221,7 +231,7 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, tflog.Info(ctx, "server read") } -func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *DataSourceModel) error { +func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *DataSourceModel, region string) error { if serverResp == nil { return fmt.Errorf("response input is nil") } @@ -238,7 +248,8 @@ func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *Da return fmt.Errorf("server id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), serverId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, serverId) + model.Region = types.StringValue(region) labels, err := iaasUtils.MapLabels(ctx, serverResp.Labels, model.Labels) if err != nil { diff --git a/stackit/internal/services/iaas/server/datasource_test.go b/stackit/internal/services/iaas/server/datasource_test.go index bb709d15c..56c2be530 100644 --- a/stackit/internal/services/iaas/server/datasource_test.go +++ b/stackit/internal/services/iaas/server/datasource_test.go @@ -12,24 +12,31 @@ import ( ) func TestMapDataSourceFields(t *testing.T) { + type args struct { + state DataSourceModel + input *iaas.Server + region string + } tests := []struct { description string - state DataSourceModel - input *iaas.Server + args args expected DataSourceModel isValid bool }{ { - "default_values", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - }, - &iaas.Server{ - Id: utils.Ptr("sid"), + description: "default_values", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + }, + input: &iaas.Server{ + Id: utils.Ptr("sid"), + }, + region: "eu01", }, - DataSourceModel{ - Id: types.StringValue("pid,sid"), + expected: DataSourceModel{ + Id: types.StringValue("pid,eu01,sid"), ProjectId: types.StringValue("pid"), ServerId: types.StringValue("sid"), Name: types.StringNull(), @@ -43,40 +50,45 @@ func TestMapDataSourceFields(t *testing.T) { CreatedAt: types.StringNull(), UpdatedAt: types.StringNull(), LaunchedAt: types.StringNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - }, - &iaas.Server{ - Id: utils.Ptr("sid"), - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", + description: "simple_values", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Region: types.StringValue("eu01"), }, - ImageId: utils.Ptr("image_id"), - Nics: &[]iaas.ServerNetwork{ - { - NicId: utils.Ptr("nic1"), + input: &iaas.Server{ + Id: utils.Ptr("sid"), + Name: utils.Ptr("name"), + AvailabilityZone: utils.Ptr("zone"), + Labels: &map[string]interface{}{ + "key": "value", }, - { - NicId: utils.Ptr("nic2"), + ImageId: utils.Ptr("image_id"), + Nics: &[]iaas.ServerNetwork{ + { + NicId: utils.Ptr("nic1"), + }, + { + NicId: utils.Ptr("nic2"), + }, }, + KeypairName: utils.Ptr("keypair_name"), + AffinityGroup: utils.Ptr("group_id"), + CreatedAt: utils.Ptr(testTimestamp()), + UpdatedAt: utils.Ptr(testTimestamp()), + LaunchedAt: utils.Ptr(testTimestamp()), + Status: utils.Ptr("active"), }, - KeypairName: utils.Ptr("keypair_name"), - AffinityGroup: utils.Ptr("group_id"), - CreatedAt: utils.Ptr(testTimestamp()), - UpdatedAt: utils.Ptr(testTimestamp()), - LaunchedAt: utils.Ptr(testTimestamp()), - Status: utils.Ptr("active"), + region: "eu02", }, - DataSourceModel{ - Id: types.StringValue("pid,sid"), + expected: DataSourceModel{ + Id: types.StringValue("pid,eu02,sid"), ProjectId: types.StringValue("pid"), ServerId: types.StringValue("sid"), Name: types.StringValue("name"), @@ -94,21 +106,25 @@ func TestMapDataSourceFields(t *testing.T) { CreatedAt: types.StringValue(testTimestampValue), UpdatedAt: types.StringValue(testTimestampValue), LaunchedAt: types.StringValue(testTimestampValue), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - &iaas.Server{ - Id: utils.Ptr("sid"), + description: "empty_labels", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + input: &iaas.Server{ + Id: utils.Ptr("sid"), + }, + region: "eu01", }, - DataSourceModel{ - Id: types.StringValue("pid,sid"), + expected: DataSourceModel{ + Id: types.StringValue("pid,eu01,sid"), ProjectId: types.StringValue("pid"), ServerId: types.StringValue("sid"), Name: types.StringNull(), @@ -122,29 +138,26 @@ func TestMapDataSourceFields(t *testing.T) { CreatedAt: types.StringNull(), UpdatedAt: types.StringNull(), LaunchedAt: types.StringNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - DataSourceModel{}, - nil, - DataSourceModel{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - DataSourceModel{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: DataSourceModel{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.Server{}, }, - &iaas.Server{}, - DataSourceModel{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -152,7 +165,7 @@ func TestMapDataSourceFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/server/resource.go b/stackit/internal/services/iaas/server/resource.go index a32561196..024968ede 100644 --- a/stackit/internal/services/iaas/server/resource.go +++ b/stackit/internal/services/iaas/server/resource.go @@ -42,6 +42,7 @@ var ( _ resource.Resource = &serverResource{} _ resource.ResourceWithConfigure = &serverResource{} _ resource.ResourceWithImportState = &serverResource{} + _ resource.ResourceWithModifyPlan = &serverResource{} supportedSourceTypes = []string{"volume", "image"} desiredStatusOptions = []string{modelStateActive, modelStateInactive, modelStateDeallocated} @@ -56,6 +57,7 @@ const ( type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` MachineType types.String `tfsdk:"machine_type"` Name types.String `tfsdk:"name"` @@ -100,7 +102,8 @@ func NewServerResource() resource.Resource { // serverResource is the resource implementation. type serverResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -108,7 +111,37 @@ func (r *serverResource) Metadata(_ context.Context, req resource.MetadataReques resp.TypeName = req.ProviderTypeName + "_server" } -func (r serverResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *serverResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var model Model resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) if resp.Diagnostics.HasError() { @@ -147,12 +180,13 @@ func (r *serverResource) ConfigValidators(_ context.Context) []resource.ConfigVa // Configure adds the provider configured client to the resource. func (r *serverResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -184,6 +218,15 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "server_id": schema.StringAttribute{ Description: "The server ID.", Computed: true, @@ -298,13 +341,14 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res }, "network_interfaces": schema.ListAttribute{ Description: "The IDs of network interfaces which should be attached to the server. Updating it will recreate the server.", - Optional: true, + Required: true, ElementType: types.StringType, Validators: []validator.List{ listvalidator.ValueStringsAre( validate.UUID(), validate.NoSeparator(), ), + listvalidator.SizeAtLeast(1), }, PlanModifiers: []planmodifier.List{ listplanmodifier.RequiresReplace(), @@ -428,7 +472,9 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) // Generate API request body from model payload, err := toCreatePayload(ctx, &model) @@ -439,14 +485,14 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, // Create new server - server, err := r.client.CreateServer(ctx, projectId).CreateServerPayload(*payload).Execute() + server, err := r.client.CreateServer(ctx, projectId, region).CreateServerPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Calling API: %v", err)) return } serverId := *server.Id - _, err = wait.CreateServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx) + _, err = wait.CreateServerWaitHandler(ctx, r.client, projectId, region, serverId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err)) return @@ -454,7 +500,7 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, ctx = tflog.SetField(ctx, "server_id", serverId) // Get Server with details - serverReq := r.client.GetServer(ctx, projectId, serverId) + serverReq := r.client.GetServer(ctx, projectId, region, serverId) serverReq = serverReq.Details(true) server, err = serverReq.Execute() if err != nil { @@ -462,14 +508,14 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, } // Map response body to schema - err = mapFields(ctx, server, &model) + err = mapFields(ctx, server, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Processing API payload: %v", err)) return } - if err := updateServerStatus(ctx, r.client, server.Status, &model); err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creting server", fmt.Sprintf("update server state: %v", err)) + if err := updateServerStatus(ctx, r.client, server.Status, &model, region); err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("update server state: %v", err)) return } @@ -486,41 +532,41 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, // client operations in [updateServerStatus] type serverControlClient interface { wait.APIClientInterface - StartServerExecute(ctx context.Context, projectId string, serverId string) error - StopServerExecute(ctx context.Context, projectId string, serverId string) error - DeallocateServerExecute(ctx context.Context, projectId string, serverId string) error + StartServerExecute(ctx context.Context, projectId string, region string, serverId string) error + StopServerExecute(ctx context.Context, projectId string, region string, serverId string) error + DeallocateServerExecute(ctx context.Context, projectId string, region string, serverId string) error } -func startServer(ctx context.Context, client serverControlClient, projectId, serverId string) error { +func startServer(ctx context.Context, client serverControlClient, projectId, region, serverId string) error { tflog.Debug(ctx, "starting server to enter active state") - if err := client.StartServerExecute(ctx, projectId, serverId); err != nil { + if err := client.StartServerExecute(ctx, projectId, region, serverId); err != nil { return fmt.Errorf("cannot start server: %w", err) } - _, err := wait.StartServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx) + _, err := wait.StartServerWaitHandler(ctx, client, projectId, region, serverId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("cannot check started server: %w", err) } return nil } -func stopServer(ctx context.Context, client serverControlClient, projectId, serverId string) error { +func stopServer(ctx context.Context, client serverControlClient, projectId, region, serverId string) error { tflog.Debug(ctx, "stopping server to enter inactive state") - if err := client.StopServerExecute(ctx, projectId, serverId); err != nil { + if err := client.StopServerExecute(ctx, projectId, region, serverId); err != nil { return fmt.Errorf("cannot stop server: %w", err) } - _, err := wait.StopServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx) + _, err := wait.StopServerWaitHandler(ctx, client, projectId, region, serverId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("cannot check stopped server: %w", err) } return nil } -func deallocatServer(ctx context.Context, client serverControlClient, projectId, serverId string) error { +func deallocateServer(ctx context.Context, client serverControlClient, projectId, region, serverId string) error { tflog.Debug(ctx, "deallocating server to enter shelved state") - if err := client.DeallocateServerExecute(ctx, projectId, serverId); err != nil { + if err := client.DeallocateServerExecute(ctx, projectId, region, serverId); err != nil { return fmt.Errorf("cannot deallocate server: %w", err) } - _, err := wait.DeallocateServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx) + _, err := wait.DeallocateServerWaitHandler(ctx, client, projectId, region, serverId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("cannot check deallocated server: %w", err) } @@ -528,7 +574,7 @@ func deallocatServer(ctx context.Context, client serverControlClient, projectId, } // updateServerStatus applies the appropriate server state changes for the actual current and the intended state -func updateServerStatus(ctx context.Context, client serverControlClient, currentState *string, model *Model) error { +func updateServerStatus(ctx context.Context, client serverControlClient, currentState *string, model *Model, region string) error { if currentState == nil { tflog.Warn(ctx, "no current state available, not updating server state") return nil @@ -537,52 +583,52 @@ func updateServerStatus(ctx context.Context, client serverControlClient, current case wait.ServerActiveStatus: switch strings.ToUpper(model.DesiredStatus.ValueString()) { case wait.ServerInactiveStatus: - if err := stopServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { + if err := stopServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { return err } case wait.ServerDeallocatedStatus: - if err := deallocatServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { + if err := deallocateServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { return err } default: tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString())) - if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { + if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { return err } } case wait.ServerInactiveStatus: switch strings.ToUpper(model.DesiredStatus.ValueString()) { case wait.ServerActiveStatus: - if err := startServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { + if err := startServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { return err } case wait.ServerDeallocatedStatus: - if err := deallocatServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { + if err := deallocateServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { return err } default: tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString())) - if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { + if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { return err } } case wait.ServerDeallocatedStatus: switch strings.ToUpper(model.DesiredStatus.ValueString()) { case wait.ServerActiveStatus: - if err := startServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { + if err := startServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { return err } case wait.ServerInactiveStatus: - if err := stopServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { + if err := stopServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { return err } default: tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString())) - if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { + if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { return err } } @@ -593,7 +639,7 @@ func updateServerStatus(ctx context.Context, client serverControlClient, current return nil } -// // Read refreshes the Terraform state with the latest data. +// Read refreshes the Terraform state with the latest data. func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) @@ -602,11 +648,13 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "server_id", serverId) - serverReq := r.client.GetServer(ctx, projectId, serverId) + serverReq := r.client.GetServer(ctx, projectId, region, serverId) serverReq = serverReq.Details(true) serverResp, err := serverReq.Execute() if err != nil { @@ -620,7 +668,7 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res } // Map response body to schema - err = mapFields(ctx, serverResp, &model) + err = mapFields(ctx, serverResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Processing API payload: %v", err)) return @@ -634,7 +682,7 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res tflog.Info(ctx, "server read") } -func (r *serverResource) updateServerAttributes(ctx context.Context, model, stateModel *Model) (*iaas.Server, error) { +func (r *serverResource) updateServerAttributes(ctx context.Context, model, stateModel *Model, region string) (*iaas.Server, error) { // Generate API request body from model payload, err := toUpdatePayload(ctx, model, stateModel.Labels) if err != nil { @@ -645,7 +693,7 @@ func (r *serverResource) updateServerAttributes(ctx context.Context, model, stat var updatedServer *iaas.Server // Update existing server - updatedServer, err = r.client.UpdateServer(ctx, projectId, serverId).UpdateServerPayload(*payload).Execute() + updatedServer, err = r.client.UpdateServer(ctx, projectId, region, serverId).UpdateServerPayload(*payload).Execute() if err != nil { return nil, fmt.Errorf("Calling API: %w", err) } @@ -656,12 +704,12 @@ func (r *serverResource) updateServerAttributes(ctx context.Context, model, stat payload := iaas.ResizeServerPayload{ MachineType: modelMachineType, } - err := r.client.ResizeServer(ctx, projectId, serverId).ResizeServerPayload(payload).Execute() + err := r.client.ResizeServer(ctx, projectId, region, serverId).ResizeServerPayload(payload).Execute() if err != nil { return nil, fmt.Errorf("Resizing the server, calling API: %w", err) } - _, err = wait.ResizeServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx) + _, err = wait.ResizeServerWaitHandler(ctx, r.client, projectId, region, serverId).WaitWithContext(ctx) if err != nil { return nil, fmt.Errorf("server resize waiting: %w", err) } @@ -681,8 +729,10 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "server_id", serverId) // Retrieve values from state @@ -697,31 +747,31 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, server *iaas.Server err error ) - if server, err = r.client.GetServer(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()).Execute(); err != nil { + if server, err = r.client.GetServer(ctx, projectId, region, serverId).Execute(); err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error retrieving server state", fmt.Sprintf("Getting server state: %v", err)) } if model.DesiredStatus.ValueString() == modelStateDeallocated { // if the target state is "deallocated", we have to perform the server update first // and then shelve it afterwards. A shelved server cannot be updated - _, err = r.updateServerAttributes(ctx, &model, &stateModel) + _, err = r.updateServerAttributes(ctx, &model, &stateModel, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error()) return } - if err := updateServerStatus(ctx, r.client, server.Status, &model); err != nil { + if err := updateServerStatus(ctx, r.client, server.Status, &model, region); err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error()) return } } else { // potentially unfreeze first and update afterwards - if err := updateServerStatus(ctx, r.client, server.Status, &model); err != nil { + if err := updateServerStatus(ctx, r.client, server.Status, &model, region); err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error()) return } - _, err = r.updateServerAttributes(ctx, &model, &stateModel) + _, err = r.updateServerAttributes(ctx, &model, &stateModel, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error()) return @@ -729,7 +779,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, } // Re-fetch the server data, to get the details values. - serverReq := r.client.GetServer(ctx, projectId, serverId) + serverReq := r.client.GetServer(ctx, projectId, region, serverId) serverReq = serverReq.Details(true) updatedServer, err := serverReq.Execute() if err != nil { @@ -737,7 +787,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, return } - err = mapFields(ctx, updatedServer, &model) + err = mapFields(ctx, updatedServer, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Processing API payload: %v", err)) return @@ -762,17 +812,19 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest, } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "server_id", serverId) // Delete existing server - err := r.client.DeleteServer(ctx, projectId, serverId).Execute() + err := r.client.DeleteServer(ctx, projectId, region, serverId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("Calling API: %v", err)) return } - _, err = wait.DeleteServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx) + _, err = wait.DeleteServerWaitHandler(ctx, r.client, projectId, region, serverId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("server deletion waiting: %v", err)) return @@ -786,25 +838,24 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest, func (r *serverResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing server", - fmt.Sprintf("Expected import identifier with format: [project_id],[server_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - serverId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "server_id": idParts[2], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...) tflog.Info(ctx, "server state imported") } -func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error { +func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model, region string) error { if serverResp == nil { return fmt.Errorf("response input is nil") } @@ -821,7 +872,8 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error return fmt.Errorf("server id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), serverId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, serverId) + model.Region = types.StringValue(region) labels, err := iaasUtils.MapLabels(ctx, serverResp.Labels, model.Labels) if err != nil { @@ -958,9 +1010,9 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo return nil, fmt.Errorf("converting to Go map: %w", err) } - var bootVolumePayload *iaas.CreateServerPayloadBootVolume + var bootVolumePayload *iaas.ServerBootVolume if !bootVolume.SourceId.IsNull() && !bootVolume.SourceType.IsNull() { - bootVolumePayload = &iaas.CreateServerPayloadBootVolume{ + bootVolumePayload = &iaas.ServerBootVolume{ PerformanceClass: conversion.StringValueToPointer(bootVolume.PerformanceClass), Size: conversion.Int64ValueToPointer(bootVolume.Size), Source: &iaas.BootVolumeSource{ @@ -982,22 +1034,22 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo userData = &encodedUserData } - var network *iaas.CreateServerPayloadNetworking - if !model.NetworkInterfaces.IsNull() && !model.NetworkInterfaces.IsUnknown() { - var nicIds []string - for _, nic := range model.NetworkInterfaces.Elements() { - nicString, ok := nic.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - nicIds = append(nicIds, nicString.ValueString()) + if model.NetworkInterfaces.IsNull() || model.NetworkInterfaces.IsUnknown() { + return nil, fmt.Errorf("nil network interfaces") + } + var nicIds []string + for _, nic := range model.NetworkInterfaces.Elements() { + nicString, ok := nic.(types.String) + if !ok { + return nil, fmt.Errorf("type assertion failed") } + nicIds = append(nicIds, nicString.ValueString()) + } - network = &iaas.CreateServerPayloadNetworking{ - CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{ - NicIds: &nicIds, - }, - } + network := &iaas.CreateServerPayloadAllOfNetworking{ + CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{ + NicIds: &nicIds, + }, } return &iaas.CreateServerPayload{ diff --git a/stackit/internal/services/iaas/server/resource_test.go b/stackit/internal/services/iaas/server/resource_test.go index d9dac877b..ad1c70741 100644 --- a/stackit/internal/services/iaas/server/resource_test.go +++ b/stackit/internal/services/iaas/server/resource_test.go @@ -26,24 +26,31 @@ func testTimestamp() time.Time { } func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.Server + region string + } tests := []struct { description string - state Model - input *iaas.Server + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - }, - &iaas.Server{ - Id: utils.Ptr("sid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + }, + input: &iaas.Server{ + Id: utils.Ptr("sid"), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,sid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sid"), ProjectId: types.StringValue("pid"), ServerId: types.StringValue("sid"), Name: types.StringNull(), @@ -57,40 +64,45 @@ func TestMapFields(t *testing.T) { CreatedAt: types.StringNull(), UpdatedAt: types.StringNull(), LaunchedAt: types.StringNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - }, - &iaas.Server{ - Id: utils.Ptr("sid"), - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Region: types.StringValue("eu01"), }, - ImageId: utils.Ptr("image_id"), - Nics: &[]iaas.ServerNetwork{ - { - NicId: utils.Ptr("nic1"), + input: &iaas.Server{ + Id: utils.Ptr("sid"), + Name: utils.Ptr("name"), + AvailabilityZone: utils.Ptr("zone"), + Labels: &map[string]interface{}{ + "key": "value", }, - { - NicId: utils.Ptr("nic2"), + ImageId: utils.Ptr("image_id"), + Nics: &[]iaas.ServerNetwork{ + { + NicId: utils.Ptr("nic1"), + }, + { + NicId: utils.Ptr("nic2"), + }, }, + KeypairName: utils.Ptr("keypair_name"), + AffinityGroup: utils.Ptr("group_id"), + CreatedAt: utils.Ptr(testTimestamp()), + UpdatedAt: utils.Ptr(testTimestamp()), + LaunchedAt: utils.Ptr(testTimestamp()), + Status: utils.Ptr("active"), }, - KeypairName: utils.Ptr("keypair_name"), - AffinityGroup: utils.Ptr("group_id"), - CreatedAt: utils.Ptr(testTimestamp()), - UpdatedAt: utils.Ptr(testTimestamp()), - LaunchedAt: utils.Ptr(testTimestamp()), - Status: utils.Ptr("active"), + region: "eu02", }, - Model{ - Id: types.StringValue("pid,sid"), + expected: Model{ + Id: types.StringValue("pid,eu02,sid"), ProjectId: types.StringValue("pid"), ServerId: types.StringValue("sid"), Name: types.StringValue("name"), @@ -105,21 +117,25 @@ func TestMapFields(t *testing.T) { CreatedAt: types.StringValue(testTimestampValue), UpdatedAt: types.StringValue(testTimestampValue), LaunchedAt: types.StringValue(testTimestampValue), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - Model{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - &iaas.Server{ - Id: utils.Ptr("sid"), + description: "empty_labels", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + input: &iaas.Server{ + Id: utils.Ptr("sid"), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,sid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sid"), ProjectId: types.StringValue("pid"), ServerId: types.StringValue("sid"), Name: types.StringNull(), @@ -133,29 +149,26 @@ func TestMapFields(t *testing.T) { CreatedAt: types.StringNull(), UpdatedAt: types.StringNull(), LaunchedAt: types.StringNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.Server{}, }, - &iaas.Server{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -163,7 +176,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -180,8 +193,8 @@ func TestToCreatePayload(t *testing.T) { isValid bool }{ { - "ok", - &Model{ + description: "ok", + input: &Model{ Name: types.StringValue("name"), AvailabilityZone: types.StringValue("zone"), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -199,14 +212,18 @@ func TestToCreatePayload(t *testing.T) { KeypairName: types.StringValue("keypair"), MachineType: types.StringValue("machine_type"), UserData: types.StringValue(userData), + NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("nic1"), + types.StringValue("nic2"), + }), }, - &iaas.CreateServerPayload{ + expected: &iaas.CreateServerPayload{ Name: utils.Ptr("name"), AvailabilityZone: utils.Ptr("zone"), Labels: &map[string]interface{}{ "key": "value", }, - BootVolume: &iaas.CreateServerPayloadBootVolume{ + BootVolume: &iaas.ServerBootVolume{ PerformanceClass: utils.Ptr("class"), Size: utils.Ptr(int64(1)), Source: &iaas.BootVolumeSource{ @@ -218,12 +235,17 @@ func TestToCreatePayload(t *testing.T) { KeypairName: utils.Ptr("keypair"), MachineType: utils.Ptr("machine_type"), UserData: utils.Ptr([]byte(base64EncodedUserData)), + Networking: &iaas.CreateServerPayloadAllOfNetworking{ + CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{ + NicIds: &[]string{"nic1", "nic2"}, + }, + }, }, - true, + isValid: true, }, { - "delete on termination is set to true", - &Model{ + description: "delete on termination is set to true", + input: &Model{ Name: types.StringValue("name"), AvailabilityZone: types.StringValue("zone"), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -241,14 +263,18 @@ func TestToCreatePayload(t *testing.T) { KeypairName: types.StringValue("keypair"), MachineType: types.StringValue("machine_type"), UserData: types.StringValue(userData), + NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("nic1"), + types.StringValue("nic2"), + }), }, - &iaas.CreateServerPayload{ + expected: &iaas.CreateServerPayload{ Name: utils.Ptr("name"), AvailabilityZone: utils.Ptr("zone"), Labels: &map[string]interface{}{ "key": "value", }, - BootVolume: &iaas.CreateServerPayloadBootVolume{ + BootVolume: &iaas.ServerBootVolume{ PerformanceClass: utils.Ptr("class"), Size: utils.Ptr(int64(1)), Source: &iaas.BootVolumeSource{ @@ -261,8 +287,13 @@ func TestToCreatePayload(t *testing.T) { KeypairName: utils.Ptr("keypair"), MachineType: utils.Ptr("machine_type"), UserData: utils.Ptr([]byte(base64EncodedUserData)), + Networking: &iaas.CreateServerPayloadAllOfNetworking{ + CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{ + NicIds: &[]string{"nic1", "nic2"}, + }, + }, }, - true, + isValid: true, }, } for _, tt := range tests { @@ -327,47 +358,47 @@ func TestToUpdatePayload(t *testing.T) { } } -var _ serverControlClient = (*mockServerControlClient)(nil) +var _ serverControlClient = &mockServerControlClient{} // mockServerControlClient mocks the [serverControlClient] interface with // pluggable functions type mockServerControlClient struct { wait.APIClientInterface startServerCalled int - startServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error + startServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error stopServerCalled int - stopServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error + stopServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error deallocateServerCalled int - deallocateServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error + deallocateServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error getServerCalled int - getServerExecute func(callNo int, ctx context.Context, projectId, serverId string) (*iaas.Server, error) + getServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) (*iaas.Server, error) } // DeallocateServerExecute implements serverControlClient. -func (t *mockServerControlClient) DeallocateServerExecute(ctx context.Context, projectId, serverId string) error { +func (t *mockServerControlClient) DeallocateServerExecute(ctx context.Context, projectId, region, serverId string) error { t.deallocateServerCalled++ - return t.deallocateServerExecute(t.deallocateServerCalled, ctx, projectId, serverId) + return t.deallocateServerExecute(t.deallocateServerCalled, ctx, projectId, region, serverId) } // GetServerExecute implements serverControlClient. -func (t *mockServerControlClient) GetServerExecute(ctx context.Context, projectId, serverId string) (*iaas.Server, error) { +func (t *mockServerControlClient) GetServerExecute(ctx context.Context, projectId, region, serverId string) (*iaas.Server, error) { t.getServerCalled++ - return t.getServerExecute(t.getServerCalled, ctx, projectId, serverId) + return t.getServerExecute(t.getServerCalled, ctx, projectId, region, serverId) } // StartServerExecute implements serverControlClient. -func (t *mockServerControlClient) StartServerExecute(ctx context.Context, projectId, serverId string) error { +func (t *mockServerControlClient) StartServerExecute(ctx context.Context, projectId, region, serverId string) error { t.startServerCalled++ - return t.startServerExecute(t.startServerCalled, ctx, projectId, serverId) + return t.startServerExecute(t.startServerCalled, ctx, projectId, region, serverId) } // StopServerExecute implements serverControlClient. -func (t *mockServerControlClient) StopServerExecute(ctx context.Context, projectId, serverId string) error { +func (t *mockServerControlClient) StopServerExecute(ctx context.Context, projectId, region, serverId string) error { t.stopServerCalled++ - return t.stopServerExecute(t.stopServerCalled, ctx, projectId, serverId) + return t.stopServerExecute(t.stopServerCalled, ctx, projectId, region, serverId) } func Test_serverResource_updateServerStatus(t *testing.T) { @@ -379,6 +410,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { type args struct { currentState *string model Model + region string } type want struct { err bool @@ -398,7 +430,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "no desired status", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: utils.Ptr(wait.ServerActiveStatus), @@ -422,7 +454,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "desired inactive state", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(no int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) { var state string if no <= 1 { state = wait.ServerActiveStatus @@ -434,7 +466,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { Status: &state, }, nil }, - stopServerExecute: func(_ int, _ context.Context, _, _ string) error { return nil }, + stopServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil }, }, }, args: args{ @@ -455,7 +487,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "desired deallocated state", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(no int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) { var state string switch no { case 1: @@ -470,7 +502,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { Status: &state, }, nil }, - deallocateServerExecute: func(_ int, _ context.Context, _, _ string) error { return nil }, + deallocateServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil }, }, }, args: args{ @@ -491,7 +523,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "don't call start if active", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: utils.Ptr(wait.ServerActiveStatus), @@ -516,7 +548,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "don't call stop if inactive", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: utils.Ptr(wait.ServerInactiveStatus), @@ -541,7 +573,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "don't call dealloacate if deallocated", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: utils.Ptr(wait.ServerDeallocatedStatus), @@ -566,7 +598,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model) + err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model, tt.args.region) if (err != nil) != tt.want.err { t.Errorf("inconsistent error, want %v and got %v", tt.want.err, err) } diff --git a/stackit/internal/services/iaas/serviceaccountattach/resource.go b/stackit/internal/services/iaas/serviceaccountattach/resource.go index f5eaed4d2..fc7171e46 100644 --- a/stackit/internal/services/iaas/serviceaccountattach/resource.go +++ b/stackit/internal/services/iaas/serviceaccountattach/resource.go @@ -11,7 +11,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -27,41 +26,75 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &networkInterfaceAttachResource{} - _ resource.ResourceWithConfigure = &networkInterfaceAttachResource{} - _ resource.ResourceWithImportState = &networkInterfaceAttachResource{} + _ resource.Resource = &serviceAccountAttachResource{} + _ resource.ResourceWithConfigure = &serviceAccountAttachResource{} + _ resource.ResourceWithImportState = &serviceAccountAttachResource{} + _ resource.ResourceWithModifyPlan = &serviceAccountAttachResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` ServiceAccountEmail types.String `tfsdk:"service_account_email"` } // NewServiceAccountAttachResource is a helper function to simplify the provider implementation. func NewServiceAccountAttachResource() resource.Resource { - return &networkInterfaceAttachResource{} + return &serviceAccountAttachResource{} } -// networkInterfaceAttachResource is the resource implementation. -type networkInterfaceAttachResource struct { - client *iaas.APIClient +// serviceAccountAttachResource is the resource implementation. +type serviceAccountAttachResource struct { + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. -func (r *networkInterfaceAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *serviceAccountAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_server_service_account_attach" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *serviceAccountAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. -func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) +func (r *serviceAccountAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -70,7 +103,7 @@ func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req reso } // Schema defines the schema for the resource. -func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *serviceAccountAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { description := "Service account attachment resource schema. Attaches a service account to a server. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, @@ -94,6 +127,15 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "server_id": schema.StringAttribute{ Description: "The server ID.", Required: true, @@ -117,7 +159,7 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc } // Create creates the resource and sets the initial Terraform state. -func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) @@ -127,14 +169,16 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() - ctx = tflog.SetField(ctx, "server_id", serverId) serviceAccountEmail := model.ServiceAccountEmail.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) // Create new service account attachment - _, err := r.client.AddServiceAccountToServer(ctx, projectId, serverId, serviceAccountEmail).Execute() + _, err := r.client.AddServiceAccountToServer(ctx, projectId, region, serverId, serviceAccountEmail).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching service account to server", fmt.Sprintf("Calling API: %v", err)) return @@ -152,7 +196,7 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc } // Read refreshes the Terraform state with the latest data. -func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -160,13 +204,15 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. return } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() - ctx = tflog.SetField(ctx, "server_id", serverId) serviceAccountEmail := model.ServiceAccountEmail.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) - serviceAccounts, err := r.client.ListServerServiceAccounts(ctx, projectId, serverId).Execute() + serviceAccounts, err := r.client.ListServerServiceAccounts(ctx, projectId, region, serverId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -203,12 +249,12 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. } // Update updates the resource and sets the updated Terraform state on success. -func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update is not supported, all fields require replace } // Delete deletes the resource and removes the Terraform state on success. -func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) @@ -218,14 +264,15 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() - ctx = tflog.SetField(ctx, "server_id", serverId) service_accountId := model.ServiceAccountEmail.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "service_account_email", service_accountId) // Remove service_account from server - _, err := r.client.RemoveServiceAccountFromServer(ctx, projectId, serverId, service_accountId).Execute() + _, err := r.client.RemoveServiceAccountFromServer(ctx, projectId, region, serverId, service_accountId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing service account from server", fmt.Sprintf("Calling API: %v", err)) return @@ -236,26 +283,23 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc // ImportState imports a resource into the Terraform state on success. // The expected format of the resource import identifier is: project_id,server_id -func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *serviceAccountAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing service_account attachment", - fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[service_account_email] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[service_account_email] Got: %q", req.ID), ) return } - projectId := idParts[0] - serverId := idParts[1] - service_accountId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "service_account_email", service_accountId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "server_id": idParts[2], + "service_account_email": idParts[3], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_account_email"), service_accountId)...) tflog.Info(ctx, "Service account attachment state imported") } diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-min.tf b/stackit/internal/services/iaas/testdata/resource-network-area-min.tf index e1cfee288..5dde515db 100644 --- a/stackit/internal/services/iaas/testdata/resource-network-area-min.tf +++ b/stackit/internal/services/iaas/testdata/resource-network-area-min.tf @@ -1,26 +1,8 @@ variable "organization_id" {} variable "name" {} -variable "transfer_network" {} -variable "network_ranges_prefix" {} - -variable "route_prefix" {} -variable "route_next_hop" {} resource "stackit_network_area" "network_area" { - organization_id = var.organization_id - name = var.name - transfer_network = var.transfer_network - network_ranges = [ - { - prefix = var.network_ranges_prefix - } - ] + organization_id = var.organization_id + name = var.name } - -resource "stackit_network_area_route" "network_area_route" { - organization_id = stackit_network_area.network_area.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - prefix = var.route_prefix - next_hop = var.route_next_hop -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf b/stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf new file mode 100644 index 000000000..1d207e455 --- /dev/null +++ b/stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf @@ -0,0 +1,33 @@ +variable "organization_id" {} + +variable "name" {} +variable "transfer_network" {} +variable "network_ranges_prefix" {} +variable "default_prefix_length" {} +variable "min_prefix_length" {} +variable "max_prefix_length" {} +variable "default_nameservers" {} + +resource "stackit_network_area" "network_area" { + organization_id = var.organization_id + name = var.name +} + +resource "stackit_network_area_region" "network_area_region" { + organization_id = var.organization_id + network_area_id = stackit_network_area.network_area.network_area_id + ipv4 = { + transfer_network = var.transfer_network + network_ranges = [ + { + prefix = var.network_ranges_prefix + } + ] + default_prefix_length = var.default_prefix_length + min_prefix_length = var.min_prefix_length + max_prefix_length = var.max_prefix_length + default_nameservers = [ + var.default_nameservers + ] + } +} diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf b/stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf new file mode 100644 index 000000000..19ebe100c --- /dev/null +++ b/stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf @@ -0,0 +1,23 @@ +variable "organization_id" {} + +variable "name" {} +variable "transfer_network" {} +variable "network_ranges_prefix" {} + +resource "stackit_network_area" "network_area" { + organization_id = var.organization_id + name = var.name +} + +resource "stackit_network_area_region" "network_area_region" { + organization_id = var.organization_id + network_area_id = stackit_network_area.network_area.network_area_id + ipv4 = { + transfer_network = var.transfer_network + network_ranges = [ + { + prefix = var.network_ranges_prefix + } + ] + } +} diff --git a/stackit/internal/services/iaas/testdata/resource-network-v2-max.tf b/stackit/internal/services/iaas/testdata/resource-network-max.tf similarity index 64% rename from stackit/internal/services/iaas/testdata/resource-network-v2-max.tf rename to stackit/internal/services/iaas/testdata/resource-network-max.tf index 283ccdbe4..4af77d83f 100644 --- a/stackit/internal/services/iaas/testdata/resource-network-v2-max.tf +++ b/stackit/internal/services/iaas/testdata/resource-network-max.tf @@ -10,18 +10,18 @@ variable "label" {} variable "organization_id" {} variable "network_area_id" {} -# resource "stackit_network" "network_prefix" { -# project_id = var.project_id -# name = var.name -# # ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null -# # no_ipv4_gateway = var.ipv4_gateway != "" ? null : true -# ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] -# ipv4_prefix = var.ipv4_prefix -# routed = var.routed -# labels = { -# "acc-test" : var.label -# } -# } +resource "stackit_network" "network_prefix" { + project_id = var.project_id + name = var.name + # ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null + # no_ipv4_gateway = var.ipv4_gateway != "" ? null : true + ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] + ipv4_prefix = var.ipv4_prefix + routed = var.routed + labels = { + "acc-test" : var.label + } +} resource "stackit_network" "network_prefix_length" { project_id = var.project_id @@ -34,6 +34,8 @@ resource "stackit_network" "network_prefix_length" { "acc-test" : var.label } routing_table_id = stackit_routing_table.routing_table.routing_table_id + + depends_on = [stackit_network.network_prefix] } resource "stackit_routing_table" "routing_table" { diff --git a/stackit/internal/services/iaas/testdata/resource-network-v1-min.tf b/stackit/internal/services/iaas/testdata/resource-network-min.tf similarity index 100% rename from stackit/internal/services/iaas/testdata/resource-network-v1-min.tf rename to stackit/internal/services/iaas/testdata/resource-network-min.tf diff --git a/stackit/internal/services/iaas/testdata/resource-network-v1-max.tf b/stackit/internal/services/iaas/testdata/resource-network-v1-max.tf deleted file mode 100644 index cb56bc529..000000000 --- a/stackit/internal/services/iaas/testdata/resource-network-v1-max.tf +++ /dev/null @@ -1,35 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "ipv4_gateway" {} -variable "ipv4_nameserver_0" {} -variable "ipv4_nameserver_1" {} -variable "ipv4_prefix" {} -variable "ipv4_prefix_length" {} -variable "routed" {} -variable "label" {} - -resource "stackit_network" "network_prefix" { - project_id = var.project_id - name = var.name - ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null - no_ipv4_gateway = var.ipv4_gateway != "" ? null : true - ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] - ipv4_prefix = var.ipv4_prefix - routed = var.routed - labels = { - "acc-test" : var.label - } -} - - -resource "stackit_network" "network_prefix_length" { - project_id = var.project_id - name = var.name - no_ipv4_gateway = true - ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] - ipv4_prefix_length = var.ipv4_prefix_length - routed = var.routed - labels = { - "acc-test" : var.label - } -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-network-v2-min.tf b/stackit/internal/services/iaas/testdata/resource-network-v2-min.tf deleted file mode 100644 index e2748bdd6..000000000 --- a/stackit/internal/services/iaas/testdata/resource-network-v2-min.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "project_id" {} -variable "name" {} - -resource "stackit_network" "network" { - project_id = var.project_id - name = var.name -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-server-min.tf b/stackit/internal/services/iaas/testdata/resource-server-min.tf index 0bf78dc90..6f3ba8947 100644 --- a/stackit/internal/services/iaas/testdata/resource-server-min.tf +++ b/stackit/internal/services/iaas/testdata/resource-server-min.tf @@ -1,8 +1,18 @@ variable "project_id" {} variable "name" {} +variable "network_name" {} variable "machine_type" {} variable "image_id" {} +resource "stackit_network" "network" { + project_id = var.project_id + name = var.network_name +} + +resource "stackit_network_interface" "nic" { + project_id = var.project_id + network_id = stackit_network.network.network_id +} resource "stackit_server" "server" { project_id = var.project_id @@ -14,4 +24,7 @@ resource "stackit_server" "server" { source_id = var.image_id delete_on_termination = true } + network_interfaces = [ + stackit_network_interface.nic.network_interface_id + ] } diff --git a/stackit/internal/services/iaas/testdata/resource-volume-max.tf b/stackit/internal/services/iaas/testdata/resource-volume-max.tf index 8a85430ec..54c590f63 100644 --- a/stackit/internal/services/iaas/testdata/resource-volume-max.tf +++ b/stackit/internal/services/iaas/testdata/resource-volume-max.tf @@ -23,8 +23,9 @@ resource "stackit_volume" "volume_source" { availability_zone = var.availability_zone name = var.name description = var.description - performance_class = var.performance_class - size = var.size + # TODO: keep commented until IaaS API bug is resolved + #performance_class = var.performance_class + size = var.size source = { id = stackit_volume.volume_size.volume_id type = "volume" diff --git a/stackit/internal/services/iaas/utils/util.go b/stackit/internal/services/iaas/utils/util.go index 7d7a2492e..79368cf4c 100644 --- a/stackit/internal/services/iaas/utils/util.go +++ b/stackit/internal/services/iaas/utils/util.go @@ -21,9 +21,8 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } if providerData.IaaSCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.IaaSCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := iaas.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/iaas/utils/util_test.go b/stackit/internal/services/iaas/utils/util_test.go index dce0d0365..79af11748 100644 --- a/stackit/internal/services/iaas/utils/util_test.go +++ b/stackit/internal/services/iaas/utils/util_test.go @@ -49,7 +49,6 @@ func TestConfigureClient(t *testing.T) { }, expected: func() *iaas.APIClient { apiClient, err := iaas.NewAPIClient( - config.WithRegion("eu01"), utils.UserAgentConfigOption(testVersion), ) if err != nil { diff --git a/stackit/internal/services/iaas/volume/datasource.go b/stackit/internal/services/iaas/volume/datasource.go index ff89d2732..4d4337d4f 100644 --- a/stackit/internal/services/iaas/volume/datasource.go +++ b/stackit/internal/services/iaas/volume/datasource.go @@ -31,7 +31,8 @@ func NewVolumeDataSource() datasource.DataSource { // volumeDataSource is the data source implementation. type volumeDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *volumeDataSource) Metadata(_ context.Context, req datasource.MetadataRe } func (d *volumeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -54,14 +56,14 @@ func (d *volumeDataSource) Configure(ctx context.Context, req datasource.Configu } // Schema defines the schema for the resource. -func (r *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Volume resource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`volume_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`volume_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -72,6 +74,11 @@ func (r *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "volume_id": schema.StringAttribute{ Description: "The volume ID.", Required: true, @@ -140,11 +147,13 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, return } projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) volumeId := model.VolumeId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "volume_id", volumeId) - volumeResp, err := d.client.GetVolume(ctx, projectId, volumeId).Execute() + volumeResp, err := d.client.GetVolume(ctx, projectId, region, volumeId).Execute() if err != nil { utils.LogError( ctx, @@ -160,7 +169,7 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, return } - err = mapFields(ctx, volumeResp, &model) + err = mapFields(ctx, volumeResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/iaas/volume/resource.go b/stackit/internal/services/iaas/volume/resource.go index 8f6bd16b2..f04e792f9 100644 --- a/stackit/internal/services/iaas/volume/resource.go +++ b/stackit/internal/services/iaas/volume/resource.go @@ -37,6 +37,7 @@ var ( _ resource.Resource = &volumeResource{} _ resource.ResourceWithConfigure = &volumeResource{} _ resource.ResourceWithImportState = &volumeResource{} + _ resource.ResourceWithModifyPlan = &volumeResource{} SupportedSourceTypes = []string{"volume", "image", "snapshot", "backup"} ) @@ -44,6 +45,7 @@ var ( type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` VolumeId types.String `tfsdk:"volume_id"` Name types.String `tfsdk:"name"` AvailabilityZone types.String `tfsdk:"availability_zone"` @@ -74,7 +76,8 @@ func NewVolumeResource() resource.Resource { // volumeResource is the resource implementation. type volumeResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -82,6 +85,36 @@ func (r *volumeResource) Metadata(_ context.Context, req resource.MetadataReques resp.TypeName = req.ProviderTypeName + "_volume" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *volumeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // ConfigValidators validates the resource configuration func (r *volumeResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ @@ -94,12 +127,13 @@ func (r *volumeResource) ConfigValidators(_ context.Context) []resource.ConfigVa // Configure adds the provider configured client to the resource. func (r *volumeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -115,7 +149,7 @@ func (r *volumeResource) Schema(_ context.Context, _ resource.SchemaRequest, res Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`volume_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`volume_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -132,6 +166,15 @@ func (r *volumeResource) Schema(_ context.Context, _ resource.SchemaRequest, res validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "volume_id": schema.StringAttribute{ Description: "The volume ID.", Computed: true, @@ -288,7 +331,9 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) var source = &sourceModel{} if !(model.Source.IsNull() || model.Source.IsUnknown()) { @@ -308,14 +353,14 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, // Create new volume - volume, err := r.client.CreateVolume(ctx, projectId).CreateVolumePayload(*payload).Execute() + volume, err := r.client.CreateVolume(ctx, projectId, region).CreateVolumePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Calling API: %v", err)) return } volumeId := *volume.Id - volume, err = wait.CreateVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx) + volume, err = wait.CreateVolumeWaitHandler(ctx, r.client, projectId, region, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("volume creation waiting: %v", err)) return @@ -324,7 +369,7 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, ctx = tflog.SetField(ctx, "volume_id", volumeId) // Map response body to schema - err = mapFields(ctx, volume, &model) + err = mapFields(ctx, volume, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Processing API payload: %v", err)) return @@ -346,12 +391,15 @@ func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, res if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) volumeId := model.VolumeId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "volume_id", volumeId) - volumeResp, err := r.client.GetVolume(ctx, projectId, volumeId).Execute() + volumeResp, err := r.client.GetVolume(ctx, projectId, region, volumeId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -363,7 +411,7 @@ func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, res } // Map response body to schema - err = mapFields(ctx, volumeResp, &model) + err = mapFields(ctx, volumeResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err)) return @@ -387,8 +435,10 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, return } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) volumeId := model.VolumeId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "volume_id", volumeId) // Retrieve values from state @@ -406,7 +456,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, return } // Update existing volume - updatedVolume, err := r.client.UpdateVolume(ctx, projectId, volumeId).UpdateVolumePayload(*payload).Execute() + updatedVolume, err := r.client.UpdateVolume(ctx, projectId, region, volumeId).UpdateVolumePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Calling API: %v", err)) return @@ -422,7 +472,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, payload := iaas.ResizeVolumePayload{ Size: modelSize, } - err := r.client.ResizeVolume(ctx, projectId, volumeId).ResizeVolumePayload(payload).Execute() + err := r.client.ResizeVolume(ctx, projectId, region, volumeId).ResizeVolumePayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Resizing the volume, calling API: %v", err)) } @@ -430,7 +480,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, updatedVolume.Size = modelSize } } - err = mapFields(ctx, updatedVolume, &model) + err = mapFields(ctx, updatedVolume, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Processing API payload: %v", err)) return @@ -454,17 +504,19 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) volumeId := model.VolumeId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "volume_id", volumeId) // Delete existing volume - err := r.client.DeleteVolume(ctx, projectId, volumeId).Execute() + err := r.client.DeleteVolume(ctx, projectId, region, volumeId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("Calling API: %v", err)) return } - _, err = wait.DeleteVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx) + _, err = wait.DeleteVolumeWaitHandler(ctx, r.client, projectId, region, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("volume deletion waiting: %v", err)) return @@ -478,25 +530,24 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, func (r *volumeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing volume", - fmt.Sprintf("Expected import identifier with format: [project_id],[volume_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[volume_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - volumeId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "volume_id", volumeId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "volume_id": idParts[2], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("volume_id"), volumeId)...) tflog.Info(ctx, "volume state imported") } -func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error { +func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model, region string) error { if volumeResp == nil { return fmt.Errorf("response input is nil") } @@ -513,7 +564,8 @@ func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error return fmt.Errorf("Volume id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), volumeId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, volumeId) + model.Region = types.StringValue(region) labels, err := iaasUtils.MapLabels(ctx, volumeResp.Labels, model.Labels) if err != nil { diff --git a/stackit/internal/services/iaas/volume/resource_test.go b/stackit/internal/services/iaas/volume/resource_test.go index 819d594ec..14f456a73 100644 --- a/stackit/internal/services/iaas/volume/resource_test.go +++ b/stackit/internal/services/iaas/volume/resource_test.go @@ -12,24 +12,31 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.Volume + region string + } tests := []struct { description string - state Model - input *iaas.Volume + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - }, - &iaas.Volume{ - Id: utils.Ptr("nid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + VolumeId: types.StringValue("nid"), + }, + input: &iaas.Volume{ + Id: utils.Ptr("nid"), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,nid"), + expected: Model{ + Id: types.StringValue("pid,eu01,nid"), ProjectId: types.StringValue("pid"), VolumeId: types.StringValue("nid"), Name: types.StringNull(), @@ -40,30 +47,35 @@ func TestMapFields(t *testing.T) { ServerId: types.StringNull(), Size: types.Int64Null(), Source: types.ObjectNull(sourceTypes), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - }, - &iaas.Volume{ - Id: utils.Ptr("nid"), - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + VolumeId: types.StringValue("nid"), + Region: types.StringValue("eu01"), }, - Description: utils.Ptr("desc"), - PerformanceClass: utils.Ptr("class"), - ServerId: utils.Ptr("sid"), - Size: utils.Ptr(int64(1)), - Source: &iaas.VolumeSource{}, + input: &iaas.Volume{ + Id: utils.Ptr("nid"), + Name: utils.Ptr("name"), + AvailabilityZone: utils.Ptr("zone"), + Labels: &map[string]interface{}{ + "key": "value", + }, + Description: utils.Ptr("desc"), + PerformanceClass: utils.Ptr("class"), + ServerId: utils.Ptr("sid"), + Size: utils.Ptr(int64(1)), + Source: &iaas.VolumeSource{}, + }, + region: "eu02", }, - Model{ - Id: types.StringValue("pid,nid"), + expected: Model{ + Id: types.StringValue("pid,eu02,nid"), ProjectId: types.StringValue("pid"), VolumeId: types.StringValue("nid"), Name: types.StringValue("name"), @@ -79,21 +91,25 @@ func TestMapFields(t *testing.T) { "type": types.StringNull(), "id": types.StringNull(), }), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - Model{ - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - &iaas.Volume{ - Id: utils.Ptr("nid"), + description: "empty_labels", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + VolumeId: types.StringValue("nid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + input: &iaas.Volume{ + Id: utils.Ptr("nid"), + }, + region: "eu01", }, - Model{ - Id: types.StringValue("pid,nid"), + expected: Model{ + Id: types.StringValue("pid,eu01,nid"), ProjectId: types.StringValue("pid"), VolumeId: types.StringValue("nid"), Name: types.StringNull(), @@ -104,29 +120,28 @@ func TestMapFields(t *testing.T) { ServerId: types.StringNull(), Size: types.Int64Null(), Source: types.ObjectNull(sourceTypes), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.Volume{}, }, - &iaas.Volume{}, - Model{}, - false, + expected: Model{}, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -134,7 +149,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/volumeattach/resource.go b/stackit/internal/services/iaas/volumeattach/resource.go index c5a851cf3..bef62d5e5 100644 --- a/stackit/internal/services/iaas/volumeattach/resource.go +++ b/stackit/internal/services/iaas/volumeattach/resource.go @@ -11,7 +11,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -32,11 +31,13 @@ var ( _ resource.Resource = &volumeAttachResource{} _ resource.ResourceWithConfigure = &volumeAttachResource{} _ resource.ResourceWithImportState = &volumeAttachResource{} + _ resource.ResourceWithModifyPlan = &volumeAttachResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` VolumeId types.String `tfsdk:"volume_id"` } @@ -48,7 +49,8 @@ func NewVolumeAttachResource() resource.Resource { // volumeAttachResource is the resource implementation. type volumeAttachResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -56,14 +58,45 @@ func (r *volumeAttachResource) Metadata(_ context.Context, req resource.Metadata resp.TypeName = req.ProviderTypeName + "_server_volume_attach" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *volumeAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *volumeAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -79,7 +112,7 @@ func (r *volumeAttachResource) Schema(_ context.Context, _ resource.SchemaReques Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`volume_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`volume_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -96,6 +129,15 @@ func (r *volumeAttachResource) Schema(_ context.Context, _ resource.SchemaReques validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "server_id": schema.StringAttribute{ Description: "The server ID.", Required: true, @@ -133,10 +175,12 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() - ctx = tflog.SetField(ctx, "server_id", serverId) volumeId := model.VolumeId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "volume_id", volumeId) // Create new Volume attachment @@ -144,19 +188,19 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe payload := iaas.AddVolumeToServerPayload{ DeleteOnTermination: sdkUtils.Ptr(false), } - _, err := r.client.AddVolumeToServer(ctx, projectId, serverId, volumeId).AddVolumeToServerPayload(payload).Execute() + _, err := r.client.AddVolumeToServer(ctx, projectId, region, serverId, volumeId).AddVolumeToServerPayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("Calling API: %v", err)) return } - _, err = wait.AddVolumeToServerWaitHandler(ctx, r.client, projectId, serverId, volumeId).WaitWithContext(ctx) + _, err = wait.AddVolumeToServerWaitHandler(ctx, r.client, projectId, region, serverId, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("volume attachment waiting: %v", err)) return } - model.Id = utils.BuildInternalTerraformId(projectId, serverId, volumeId) + model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, volumeId) // Set state to fully populated data diags = resp.State.Set(ctx, model) @@ -176,13 +220,15 @@ func (r *volumeAttachResource) Read(ctx context.Context, req resource.ReadReques return } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() - ctx = tflog.SetField(ctx, "server_id", serverId) volumeId := model.VolumeId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "volume_id", volumeId) - _, err := r.client.GetAttachedVolume(ctx, projectId, serverId, volumeId).Execute() + _, err := r.client.GetAttachedVolume(ctx, projectId, region, serverId, volumeId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -218,20 +264,22 @@ func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRe } projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) + region := r.providerData.GetRegionWithOverride(model.Region) serverId := model.ServerId.ValueString() - ctx = tflog.SetField(ctx, "server_id", serverId) volumeId := model.VolumeId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "volume_id", volumeId) // Remove volume from server - err := r.client.RemoveVolumeFromServer(ctx, projectId, serverId, volumeId).Execute() + err := r.client.RemoveVolumeFromServer(ctx, projectId, region, serverId, volumeId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("Calling API: %v", err)) return } - _, err = wait.RemoveVolumeFromServerWaitHandler(ctx, r.client, projectId, serverId, volumeId).WaitWithContext(ctx) + _, err = wait.RemoveVolumeFromServerWaitHandler(ctx, r.client, projectId, region, serverId, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("volume removal waiting: %v", err)) return @@ -245,23 +293,20 @@ func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRe func (r *volumeAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing volume attachment", - fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[volume_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[volume_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - serverId := idParts[1] - volumeId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "volume_id", volumeId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "server_id": idParts[2], + "volume_id": idParts[3], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("volume_id"), volumeId)...) tflog.Info(ctx, "Volume attachment state imported") } diff --git a/stackit/internal/services/logme/logme_acc_test.go b/stackit/internal/services/logme/logme_acc_test.go index d138380ed..03cb3b4f0 100644 --- a/stackit/internal/services/logme/logme_acc_test.go +++ b/stackit/internal/services/logme/logme_acc_test.go @@ -261,7 +261,7 @@ func TestAccLogMeMaxResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.0", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog1"])), resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.1", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog2"])), - // // Credential data + // Credential data resource.TestCheckResourceAttrPair( "stackit_logme_credential.credential", "project_id", "stackit_logme_instance.instance", "project_id", diff --git a/stackit/internal/services/serverupdate/serverupdate_acc_test.go b/stackit/internal/services/serverupdate/serverupdate_acc_test.go index 33a0253a1..3d45a70a1 100644 --- a/stackit/internal/services/serverupdate/serverupdate_acc_test.go +++ b/stackit/internal/services/serverupdate/serverupdate_acc_test.go @@ -121,7 +121,7 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "id"), ), }, - // // Import + // Import { ConfigVariables: testConfigVarsMin, ResourceName: "stackit_server_update_schedule.test_schedule", @@ -139,7 +139,7 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) { ImportState: true, ImportStateVerify: true, }, - // // Update + // Update { ConfigVariables: configVarsMinUpdated(), Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMinConfig, @@ -209,7 +209,7 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "id"), ), }, - // // Import + // Import { ConfigVariables: testConfigVarsMax, ResourceName: "stackit_server_update_schedule.test_schedule", @@ -227,7 +227,7 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) { ImportState: true, ImportStateVerify: true, }, - // // Update + // Update { ConfigVariables: configVarsMaxUpdated(), Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMaxConfig, diff --git a/stackit/provider.go b/stackit/provider.go index fbc149f25..98162cce9 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -31,6 +31,7 @@ import ( machineType "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/machinetype" iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network" iaasNetworkArea "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarea" + iaasNetworkAreaRegion "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearegion" iaasNetworkAreaRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearoute" iaasNetworkInterface "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterface" iaasNetworkInterfaceAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterfaceattach" @@ -482,6 +483,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource iaasImageV2.NewImageV2DataSource, iaasNetwork.NewNetworkDataSource, iaasNetworkArea.NewNetworkAreaDataSource, + iaasNetworkAreaRegion.NewNetworkAreaRegionDataSource, iaasNetworkAreaRoute.NewNetworkAreaRouteDataSource, iaasNetworkInterface.NewNetworkInterfaceDataSource, iaasVolume.NewVolumeDataSource, @@ -553,6 +555,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { iaasImage.NewImageResource, iaasNetwork.NewNetworkResource, iaasNetworkArea.NewNetworkAreaResource, + iaasNetworkAreaRegion.NewNetworkAreaRegionResource, iaasNetworkAreaRoute.NewNetworkAreaRouteResource, iaasNetworkInterface.NewNetworkInterfaceResource, iaasVolume.NewVolumeResource,