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,