Merge branch 'stackitcloud:main' into alpha

This commit is contained in:
Marcel S. Henselin 2025-12-17 16:09:23 +01:00 committed by GitHub
commit c07c81b091
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
125 changed files with 8395 additions and 6044 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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).
<a id="nestedatt--network_ranges"></a>
### Nested Schema for `network_ranges`

View file

@ -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 generated by tfplugindocs -->
## 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))
<a id="nestedatt--ipv4"></a>
### 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).
<a id="nestedatt--ipv4--network_ranges"></a>
### Nested Schema for `ipv4.network_ranges`
Read-Only:
- `network_range_id` (String)
- `prefix` (String) Classless Inter-Domain Routing (CIDR).

View file

@ -29,9 +29,30 @@ 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`".
- `destination` (Attributes) Destination of the route. (see [below for nested schema](#nestedatt--destination))
- `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.
- `next_hop` (Attributes) Next hop destination. (see [below for nested schema](#nestedatt--next_hop))
<a id="nestedatt--destination"></a>
### Nested Schema for `destination`
Read-Only:
- `type` (String) CIDRV type. Possible values are: `cidrv4`, `cidrv6`.
- `value` (String) An CIDR string.
<a id="nestedatt--next_hop"></a>
### Nested Schema for `next_hop`
Read-Only:
- `type` (String) Type of the next hop. Possible values are: `blackhole`, `internet`, `ipv4`, `ipv6`.
- `value` (String) Either IPv4 or IPv6 (not set for blackhole and internet).

View file

@ -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.

View file

@ -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).

View file

@ -27,10 +27,14 @@ 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.
- `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`".
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
- `name` (String) The name of the security group.
- `stateful` (Boolean) Configures if a security group is stateful or stateless. There can only be one type of security groups per network interface/server.

View file

@ -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))

View file

@ -27,13 +27,17 @@ 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.
- `availability_zone` (String) The availability zone of the server.
- `boot_volume` (Attributes) The boot volume for the server (see [below for nested schema](#nestedatt--boot_volume))
- `created_at` (String) Date-time when the server was created
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`".
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`server_id`".
- `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

View file

@ -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/products/storage/block-storage/basics/service-plans/#currently-available-service-plans-performance-classes)

View file

@ -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.
@ -91,7 +91,7 @@ resource "stackit_affinity_group" "example" {
# Only use the import statement, if you want to import an existing affinity group
import {
to = stackit_affinity_group.import-example
id = "${var.project_id},${var.affinity_group_id}"
id = "${var.project_id},${var.region},${var.affinity_group_id}"
}
```
@ -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.

View file

@ -31,7 +31,7 @@ resource "stackit_image" "example_image" {
# }
import {
to = stackit_image.import-example
id = "${var.project_id},${var.image_id}"
id = "${var.project_id},${var.region},${var.image_id}"
}
```
@ -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.

View file

@ -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"
}
@ -51,7 +50,7 @@ resource "stackit_network" "example_non_routed_network" {
# These attributes cannot be configured together: [ipv4_prefix,ipv4_prefix_length,ipv4_gateway]
import {
to = stackit_network.import-example
id = "${var.project_id},${var.network_id}"
id = "${var.project_id},${var.region},${var.network_id}"
}
```
@ -77,15 +76,13 @@ 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
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`network_id`".
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`network_id`".
- `ipv4_prefixes` (List of String) The IPv4 prefixes of the network.
- `ipv6_prefixes` (List of String) The IPv6 prefixes of the network.
- `network_id` (String) The network ID.

View file

@ -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)

View file

@ -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},${var.region}"
}
```
<!-- schema generated by tfplugindocs -->
## 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`".
<a id="nestedatt--ipv4"></a>
### 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.
<a id="nestedatt--ipv4--network_ranges"></a>
### Nested Schema for `ipv4.network_ranges`
Required:
- `prefix` (String) Classless Inter-Domain Routing (CIDR).
Read-Only:
- `network_range_id` (String)

View file

@ -3,7 +3,7 @@
page_title: "stackit_network_area_route Resource - stackit"
subcategory: ""
description: |-
Network area route resource schema. Must have a region specified in the provider configuration.
Network area route resource schema. Must have a `region` specified in the provider configuration.
---
# stackit_network_area_route (Resource)
@ -16,8 +16,14 @@ Network area route resource schema. Must have a `region` specified in the provid
resource "stackit_network_area_route" "example" {
organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
prefix = "192.168.0.0/24"
next_hop = "192.168.0.0"
destination = {
type = "cidrv4"
value = "192.168.0.0/24"
}
next_hop = {
type = "ipv4"
value = "192.168.0.0"
}
labels = {
"key" = "value"
}
@ -26,7 +32,43 @@ resource "stackit_network_area_route" "example" {
# Only use the import statement, if you want to import an existing network area route
import {
to = stackit_network_area_route.import-example
id = "${var.organization_id},${var.network_area_id},${var.network_area_route_id}"
id = "${var.organization_id},${var.network_area_id},${var.region},${var.network_area_route_id}"
}
```
## Migration of IaaS resources from versions <= v0.74.0
The release of the STACKIT IaaS API v2 provides a lot of new features, but also includes some breaking changes
(when coming from v1 of the STACKIT IaaS API) which must be somehow represented on Terraform side. The
`stackit_network_area_route` resource did undergo some changes. See the example below how to migrate your resources.
### Breaking change: Network area route resource (stackit_network_area_route)
**Configuration for <= v0.74.0**
```terraform
resource "stackit_network_area_route" "example" {
organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
prefix = "192.168.0.0/24" # prefix field got removed for provider versions > v0.74.0, use the new destination field instead
next_hop = "192.168.0.0" # schema of the next_hop field changed, see below
}
```
**Configuration for > v0.74.0**
```terraform
resource "stackit_network_area_route" "example" {
organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
destination = { # the new 'destination' field replaces the old 'prefix' field
type = "cidrv4"
value = "192.168.0.0/24" # migration: put the value of the old 'prefix' field here
}
next_hop = {
type = "ipv4"
value = "192.168.0.0" # migration: put the value of the old 'next_hop' field here
}
}
```
@ -35,16 +77,38 @@ import {
### Required
- `destination` (Attributes) Destination of the route. (see [below for nested schema](#nestedatt--destination))
- `network_area_id` (String) The network area ID to which the network area route is associated.
- `next_hop` (String) The IP address of the routing system, that will route the prefix configured. Should be a valid IPv4 address.
- `next_hop` (Attributes) Next hop destination. (see [below for nested schema](#nestedatt--next_hop))
- `organization_id` (String) STACKIT organization ID to which the network area is associated.
- `prefix` (String) The network, that is reachable though the Next Hop. Should use CIDR notation.
### 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.
<a id="nestedatt--destination"></a>
### Nested Schema for `destination`
Required:
- `type` (String) CIDRV type. Possible values are: `cidrv4`, `cidrv6`. Only `cidrv4` is supported currently.
- `value` (String) An CIDR string.
<a id="nestedatt--next_hop"></a>
### Nested Schema for `next_hop`
Required:
- `type` (String) Type of the next hop. Possible values are: `blackhole`, `internet`, `ipv4`, `ipv6`. Only `ipv4` supported currently.
Optional:
- `value` (String) Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported currently.

View file

@ -23,7 +23,7 @@ resource "stackit_network_interface" "example" {
# Only use the import statement, if you want to import an existing network interface
import {
to = stackit_network_interface.import-example
id = "${var.project_id},${var.network_id},${var.network_interface_id}"
id = "${var.project_id},${var.region},${var.network_id},${var.network_interface_id}"
}
```
@ -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`.

View file

@ -24,7 +24,7 @@ resource "stackit_public_ip" "example" {
# Only use the import statement, if you want to import an existing public ip
import {
to = stackit_public_ip.import-example
id = "${var.project_id},${var.public_ip_id}"
id = "${var.project_id},${var.region},${var.public_ip_id}"
}
```
@ -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.

View file

@ -27,7 +27,7 @@ resource "stackit_public_ip_associate" "example" {
# Only use the import statement, if you want to import an existing public ip associate
import {
to = stackit_public_ip_associate.import-example
id = "${var.project_id},${var.public_ip_id},${var.network_interface_id}"
id = "${var.project_id},${var.region},${var.public_ip_id},${var.network_interface_id}"
}
```
@ -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.

View file

@ -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.

View file

@ -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.
<a id="nestedatt--icmp_parameters"></a>

View file

@ -388,7 +388,7 @@ resource "stackit_server" "example" {
# }
import {
to = stackit_server.import-example
id = "${var.project_id},${var.server_id}"
id = "${var.project_id},${var.region},${var.server_id}"
}
```
@ -410,13 +410,14 @@ 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.
- `network_interfaces` (List of String) The IDs of network interfaces which should be attached to the server. Updating it will recreate the server. **Required when (re-)creating servers. Still marked as optional in the schema to not introduce breaking changes. There will be a migration path for this field soon.**
- `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
- `created_at` (String) Date-time when the server was created
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`".
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`server_id`".
- `launched_at` (String) Date-time when the server was launched
- `server_id` (String) The server ID.
- `updated_at` (String) Date-time when the server was updated

View file

@ -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
@ -22,7 +22,7 @@ resource "stackit_server_network_interface_attach" "attached_network_interface"
# Only use the import statement, if you want to import an existing server network interface attachment
import {
to = stackit_server_network_interface_attach.import-example
id = "${var.project_id},${var.server_id},${var.network_interface_id}"
id = "${var.project_id},${var.region},${var.server_id},${var.network_interface_id}"
}
```
@ -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`".

View file

@ -22,7 +22,7 @@ resource "stackit_server_service_account_attach" "attached_service_account" {
# Only use the import statement, if you want to import an existing server service account attachment
import {
to = stackit_server_service_account_attach.import-example
id = "${var.project_id},${var.server_id},${var.service_account_email}"
id = "${var.project_id},${var.region},${var.server_id},${var.service_account_email}"
}
```
@ -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`".
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`server_id`,`service_account_email`".

View file

@ -22,7 +22,7 @@ resource "stackit_server_volume_attach" "attached_volume" {
# Only use the import statement, if you want to import an existing server volume attachment
import {
to = stackit_server_volume_attach.import-example
id = "${var.project_id},${var.server_id},${var.volume_id}"
id = "${var.project_id},${var.region},${var.server_id},${var.volume_id}"
}
```
@ -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`".

View file

@ -26,7 +26,7 @@ resource "stackit_volume" "example" {
# Only use the import statement, if you want to import an existing volume
import {
to = stackit_volume.import-example
id = "${var.project_id},${var.volume_id}"
id = "${var.project_id},${var.region},${var.volume_id}"
}
```
@ -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/products/storage/block-storage/basics/service-plans/#currently-available-service-plans-performance-classes)
- `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.

View file

@ -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"
}

View file

@ -7,5 +7,5 @@ resource "stackit_affinity_group" "example" {
# Only use the import statement, if you want to import an existing affinity group
import {
to = stackit_affinity_group.import-example
id = "${var.project_id},${var.affinity_group_id}"
id = "${var.project_id},${var.region},${var.affinity_group_id}"
}

View file

@ -16,5 +16,5 @@ resource "stackit_image" "example_image" {
# }
import {
to = stackit_image.import-example
id = "${var.project_id},${var.image_id}"
id = "${var.project_id},${var.region},${var.image_id}"
}

View file

@ -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"
}
@ -30,5 +29,5 @@ resource "stackit_network" "example_non_routed_network" {
# These attributes cannot be configured together: [ipv4_prefix,ipv4_prefix_length,ipv4_gateway]
import {
to = stackit_network.import-example
id = "${var.project_id},${var.network_id}"
id = "${var.project_id},${var.region},${var.network_id}"
}

View file

@ -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"
}

View file

@ -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},${var.region}"
}

View file

@ -1,8 +1,14 @@
resource "stackit_network_area_route" "example" {
organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
prefix = "192.168.0.0/24"
next_hop = "192.168.0.0"
destination = {
type = "cidrv4"
value = "192.168.0.0/24"
}
next_hop = {
type = "ipv4"
value = "192.168.0.0"
}
labels = {
"key" = "value"
}
@ -11,5 +17,5 @@ resource "stackit_network_area_route" "example" {
# Only use the import statement, if you want to import an existing network area route
import {
to = stackit_network_area_route.import-example
id = "${var.organization_id},${var.network_area_id},${var.network_area_route_id}"
id = "${var.organization_id},${var.network_area_id},${var.region},${var.network_area_route_id}"
}

View file

@ -8,5 +8,5 @@ resource "stackit_network_interface" "example" {
# Only use the import statement, if you want to import an existing network interface
import {
to = stackit_network_interface.import-example
id = "${var.project_id},${var.network_id},${var.network_interface_id}"
id = "${var.project_id},${var.region},${var.network_id},${var.network_interface_id}"
}

View file

@ -9,5 +9,5 @@ resource "stackit_public_ip" "example" {
# Only use the import statement, if you want to import an existing public ip
import {
to = stackit_public_ip.import-example
id = "${var.project_id},${var.public_ip_id}"
id = "${var.project_id},${var.region},${var.public_ip_id}"
}

View file

@ -7,5 +7,5 @@ resource "stackit_public_ip_associate" "example" {
# Only use the import statement, if you want to import an existing public ip associate
import {
to = stackit_public_ip_associate.import-example
id = "${var.project_id},${var.public_ip_id},${var.network_interface_id}"
id = "${var.project_id},${var.region},${var.public_ip_id},${var.network_interface_id}"
}

View file

@ -23,5 +23,5 @@ resource "stackit_server" "example" {
# }
import {
to = stackit_server.import-example
id = "${var.project_id},${var.server_id}"
id = "${var.project_id},${var.region},${var.server_id}"
}

View file

@ -7,5 +7,5 @@ resource "stackit_server_network_interface_attach" "attached_network_interface"
# Only use the import statement, if you want to import an existing server network interface attachment
import {
to = stackit_server_network_interface_attach.import-example
id = "${var.project_id},${var.server_id},${var.network_interface_id}"
id = "${var.project_id},${var.region},${var.server_id},${var.network_interface_id}"
}

View file

@ -7,5 +7,5 @@ resource "stackit_server_service_account_attach" "attached_service_account" {
# Only use the import statement, if you want to import an existing server service account attachment
import {
to = stackit_server_service_account_attach.import-example
id = "${var.project_id},${var.server_id},${var.service_account_email}"
id = "${var.project_id},${var.region},${var.server_id},${var.service_account_email}"
}

View file

@ -7,5 +7,5 @@ resource "stackit_server_volume_attach" "attached_volume" {
# Only use the import statement, if you want to import an existing server volume attachment
import {
to = stackit_server_volume_attach.import-example
id = "${var.project_id},${var.server_id},${var.volume_id}"
id = "${var.project_id},${var.region},${var.server_id},${var.volume_id}"
}

View file

@ -11,5 +11,5 @@ resource "stackit_volume" "example" {
# Only use the import statement, if you want to import an existing volume
import {
to = stackit_volume.import-example
id = "${var.project_id},${var.volume_id}"
id = "${var.project_id},${var.region},${var.volume_id}"
}

6
go.mod
View file

@ -11,11 +11,11 @@ require (
github.com/hashicorp/terraform-plugin-go v0.29.0
github.com/hashicorp/terraform-plugin-log v0.10.0
github.com/hashicorp/terraform-plugin-testing v1.14.0
github.com/stackitcloud/stackit-sdk-go/core v0.20.0
github.com/stackitcloud/stackit-sdk-go/core v0.20.1
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.3.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
@ -29,7 +29,7 @@ require (
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1
github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1
github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.2
github.com/stackitcloud/stackit-sdk-go/services/scf v0.2.1
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2

12
go.sum
View file

@ -149,8 +149,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stackitcloud/stackit-sdk-go/core v0.20.0 h1:4rrUk6uT1g4nOn5/g1uXukP07Tux/o5xbMz/f/qE1rY=
github.com/stackitcloud/stackit-sdk-go/core v0.20.0/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ=
github.com/stackitcloud/stackit-sdk-go/core v0.20.1 h1:odiuhhRXmxvEvnVTeZSN9u98edvw2Cd3DcnkepncP3M=
github.com/stackitcloud/stackit-sdk-go/core v0.20.1/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ=
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 h1:7ZKd3b+E/R4TEVShLTXxx5FrsuDuJBOyuVOuKTMa4mo=
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0/go.mod h1:/FoXa6hF77Gv8brrvLBCKa5ie1Xy9xn39yfHwaln9Tw=
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0 h1:Q+qIdejeMsYMkbtVoI9BpGlKGdSVFRBhH/zj44SP8TM=
@ -159,8 +159,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.3.0 h1:U/x0tc487X9msMS5yZYjrBAAKrCx87Trmt0kh8JiARA=
github.com/stackitcloud/stackit-sdk-go/services/iaas v1.3.0/go.mod h1:6+5+RCDfU7eQN3+/SGdOtx7Bq9dEa2FrHz/jflgY1M4=
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=
@ -187,8 +187,8 @@ github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1 h1:ALrDCBih8Fu8
github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1/go.mod h1:+qGWSehoV0Js3FalgvT/bOgPj+UqW4I7lP5s8uAxP+o=
github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1 h1:8uPt82Ez34OYMOijjEYxB1zUW6kiybkt6veQKl0AL68=
github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1/go.mod h1:1Y2GEICmZDt+kr8aGnBx/sjYVAIYHmtfC8xYi9oxNEE=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1 h1:r7oaINTwLmIG31AaqKTuQHHFF8YNuYGzi+46DOuSjw4=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1/go.mod h1:ipcrPRbwfQXHH18dJVfY7K5ujHF5dTT6isoXgmA7YwQ=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.2 h1:VDIXOvRNmSYMeF0qQ2+w4/ez04YutVDz73hSMuuOJ54=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.2/go.mod h1:9zyEzPL4DnmU/SHq+SuMWTSO5BPxM1Z4g8Fp28n00ds=
github.com/stackitcloud/stackit-sdk-go/services/scf v0.2.1 h1:OdofRB6uj6lwN/TXLVHVrEOwNMG34MlFNwkiHD+eOts=
github.com/stackitcloud/stackit-sdk-go/services/scf v0.2.1/go.mod h1:5p7Xi8jadpJNDYr0t+07DXS104/RJLfhhA1r6P7PlGs=
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1 h1:WKFzlHllql3JsVcAq+Y1m5pSMkvwp1qH3Vf2N7i8CPg=

View file

@ -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,14 +124,16 @@ 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 = core.InitProviderContext(ctx)
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,
@ -142,7 +151,7 @@ func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadR
ctx = core.LogResponse(ctx)
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))
}

View file

@ -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,19 +195,21 @@ 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)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
// Create new affinityGroup
payload, err := toCreatePayload(&model)
if err != nil {
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
@ -176,7 +220,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
@ -199,14 +243,16 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -219,7 +265,7 @@ func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadReque
ctx = core.LogResponse(ctx)
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))
}
@ -247,15 +293,17 @@ 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 = core.InitProviderContext(ctx)
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
@ -269,21 +317,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")
}
@ -301,7 +348,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")
}
@ -319,7 +366,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)

View file

@ -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)
}

File diff suppressed because it is too large Load diff

View file

@ -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,23 +211,26 @@ 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 = core.InitProviderContext(ctx)
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,
@ -238,7 +249,7 @@ func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest,
ctx = core.LogResponse(ctx)
// 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
@ -252,7 +263,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")
}
@ -269,7 +280,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{}

View file

@ -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"),
description: "default_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
},
region: "eu01",
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
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"),
description: "simple_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Region: types.StringValue("eu01"),
},
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",
},
&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",
},
},
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{
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",
},
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"),
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
DataSourceModel{
Id: types.StringValue("pid,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
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)
}

View file

@ -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,11 +420,12 @@ 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)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
@ -391,7 +434,7 @@ 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
@ -401,15 +444,15 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest,
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
@ -430,7 +473,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 {
@ -439,7 +482,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
@ -454,7 +497,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)
@ -462,15 +505,18 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -484,7 +530,7 @@ func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp
ctx = core.LogResponse(ctx)
// 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
@ -507,12 +553,15 @@ 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 = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "image_id", imageId)
// Retrieve values from state
@ -530,7 +579,7 @@ 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
@ -538,7 +587,7 @@ func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest,
ctx = core.LogResponse(ctx)
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
@ -563,14 +612,15 @@ 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)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "image_id", imageId)
// 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
@ -578,7 +628,7 @@ func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest,
ctx = core.LogResponse(ctx)
_, 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
@ -588,29 +638,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")
}
@ -627,7 +676,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{}

View file

@ -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"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
},
region: "eu01",
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
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"),
description: "simple_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Region: types.StringValue("eu01"),
},
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",
},
&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",
},
},
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{
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",
},
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"),
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
Model{
Id: types.StringValue("pid,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
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)
}

View file

@ -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()
@ -373,6 +382,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest
ctx = core.InitProviderContext(ctx)
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)
@ -383,7 +393,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),
@ -409,7 +419,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
@ -457,7 +467,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
@ -473,7 +483,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")
}
@ -490,7 +500,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{}

View file

@ -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"),
description: "default_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
},
region: "eu01",
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
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"),
description: "simple_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Region: types.StringValue("eu01"),
},
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",
},
&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",
},
},
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{
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",
},
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"),
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
DataSourceModel{
Id: types.StringValue("pid,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
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)
}

View file

@ -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...)
@ -97,7 +97,7 @@ func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest
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,

View file

@ -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,15 +151,17 @@ 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 = core.InitProviderContext(ctx)
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()))
@ -187,7 +198,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
}
@ -199,7 +210,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")
}
@ -208,7 +219,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)

View file

@ -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)
}
})

View file

@ -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)
} else {
v2network.DatasourceRead(ctx, req, resp, d.alphaClient, d.providerData)
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 {
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
}

View file

@ -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,
},
}

View file

@ -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
@ -192,7 +191,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`network_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -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 := 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)
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 := 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)
// 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)
} else {
v2network.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 {
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) {

View file

@ -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)
}

View file

@ -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"`
}

View file

@ -1,208 +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
}
ctx = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
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
}

View file

@ -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)
}
}
})
}
}

View file

@ -1,558 +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 = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
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 = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
// 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 = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
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 = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
_, 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
}

View file

@ -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)
}
}
})
}
}

View file

@ -1,220 +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 = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
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
}

View file

@ -1,603 +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 = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
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 = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
// 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 = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
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 = core.InitProviderContext(ctx)
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
}
ctx = core.LogResponse(ctx)
_, 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
}

View file

@ -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),
@ -196,13 +207,32 @@ func (d *networkAreaDataSource) Read(ctx context.Context, req datasource.ReadReq
ctx = core.LogResponse(ctx)
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() {

View file

@ -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
}
@ -253,7 +314,7 @@ 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
@ -261,25 +322,66 @@ func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateReq
ctx = core.LogResponse(ctx)
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.
networkAreaRegionCreateResp, 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.
err = mapNetworkAreaRegionFields(ctx, networkAreaRegionCreateResp, &model) // map partial state - just in case anything goes wrong during the wait handler
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", 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 := 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
}
@ -289,11 +391,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()
@ -304,7 +406,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
@ -315,17 +418,53 @@ func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest
ctx = core.LogResponse(ctx)
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
}
@ -336,11 +475,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()
@ -351,8 +490,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
}
@ -360,8 +498,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
}
@ -373,7 +510,7 @@ 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
@ -381,39 +518,73 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq
ctx = core.LogResponse(ctx)
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)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Updating Network ranges: %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)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Calling API: %v", err))
return
}
networkAreaRanges := networkAreaResp.Ipv4.NetworkRanges
err = mapFields(ctx, waitResp, networkAreaRanges, &model)
err = mapFields(ctx, networkAreaUpdateResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
// 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
}
// 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
}
// 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)
}
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
@ -444,7 +615,29 @@ func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteReq
return
}
// Delete existing network
// 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 region", fmt.Sprintf("Calling API to list configured regions: %v", err))
return
}
// 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("Calling API: %v", err))
@ -453,12 +646,6 @@ func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteReq
ctx = core.LogResponse(ctx)
_, err = wait.DeleteNetworkAreaWaitHandler(ctx, r.client, organizationId, networkAreaId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Network area deletion waiting: %v", err))
return
}
tflog.Info(ctx, "Network area deleted")
}
@ -485,7 +672,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")
}
@ -496,18 +683,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)
@ -523,31 +733,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)
// 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)
}
}
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
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
@ -584,7 +791,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
}
}
@ -618,13 +825,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)
@ -632,24 +852,15 @@ 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)
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.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,
},
Labels: &labels,
}, nil
}
@ -658,6 +869,40 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map)
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.PartialUpdateNetworkAreaPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
}, nil
}
// 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)
@ -667,25 +912,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
@ -712,10 +942,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)
}
@ -739,13 +969,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)
}
@ -763,7 +993,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)
}

View file

@ -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,207 +116,53 @@ func TestMapFields(t *testing.T) {
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
},
true,
},
{
"model and response have ranges in different order",
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"),
}),
}),
},
&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"),
},
},
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),
"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),
},
true,
},
{
"default_nameservers_changed_outside_tf",
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"),
},
{
NetworkRangeId: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
},
Model{
Id: types.StringValue("oid,naid"),
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),
"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),
},
true,
},
{
"network_ranges_changed_outside_tf",
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"),
}),
}),
},
&iaas.NetworkArea{
AreaId: utils.Ptr("naid"),
Ipv4: &iaas.NetworkAreaIPv4{},
},
&[]iaas.NetworkRange{
{
NetworkRangeId: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
{
NetworkRangeId: utils.Ptr(testRangeId3),
Prefix: utils.Ptr("prefix-3"),
},
},
Model{
Id: types.StringValue("oid,naid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
DefaultNameservers: types.ListNull(types.StringType),
},
isValid: true,
},
{
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{
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"),
}),
}),
DefaultNameservers: types.ListNull(types.StringType),
},
input: &iaas.NetworkArea{
Id: utils.Ptr("naid"),
},
expected: Model{
Id: types.StringValue("oid,naid"),
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(testRangeId3),
"prefix": types.StringValue("prefix-3"),
"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),
Labels: types.MapNull(types.StringType),
DefaultNameservers: types.ListNull(types.StringType),
},
true,
},
{
"nil_network_ranges_list",
Model{},
&iaas.NetworkArea{},
nil,
Model{},
false,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
nil,
Model{},
false,
},
@ -369,14 +172,13 @@ func TestMapFields(t *testing.T) {
OrganizationId: types.StringValue("oid"),
},
&iaas.NetworkArea{},
&[]iaas.NetworkRange{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, tt.ListNetworkRanges, &tt.state)
err := mapFields(context.Background(), tt.input, &tt.state)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -393,6 +195,243 @@ func TestMapFields(t *testing.T) {
}
}
// 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),
},
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"),
},
},
},
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),
},
wantErr: false,
},
{
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(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),
},
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),
},
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"),
},
},
},
},
},
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(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),
},
wantErr: false,
},
{
name: "model is nil",
args: args{
model: nil,
networkAreaRegionResp: &iaas.RegionalArea{},
},
want: nil,
wantErr: true,
},
{
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,
},
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.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)
}
diff := cmp.Diff(tt.args.model, tt.want)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
@ -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(

View file

@ -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")
}

View file

@ -0,0 +1,728 @@
package networkarearegion
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils"
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
resourceManagerClient *resourcemanager.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
}
r.client = iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
r.resourceManagerClient = resourcemanagerUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
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)
ctx = core.InitProviderContext(ctx)
// 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
networkAreaRegion, 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
}
ctx = core.LogResponse(ctx)
// 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, networkAreaRegion, &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)
ctx = core.InitProviderContext(ctx)
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
}
}
ctx = core.LogResponse(ctx)
// 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)
ctx = core.InitProviderContext(ctx)
// 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
}
ctx = core.LogResponse(ctx)
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)
_, err := wait.ReadyForNetworkAreaDeletionWaitHandler(ctx, r.client, r.resourceManagerClient, organizationId, networkAreaId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Network area ready for deletion waiting: %v", err))
return
}
ctx = core.InitProviderContext(ctx)
// 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
}
ctx = core.LogResponse(ctx)
_, 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
}

File diff suppressed because it is too large Load diff

View file

@ -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,
@ -88,13 +95,33 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche
validate.NoSeparator(),
},
},
"next_hop": schema.StringAttribute{
Description: "The IP address of the routing system, that will route the prefix configured. Should be a valid IPv4 address.",
"destination": schema.SingleNestedAttribute{
Description: "Destination of the route.",
Computed: true,
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Description: fmt.Sprintf("CIDRV type. %s", utils.FormatPossibleValues("cidrv4", "cidrv6")),
Computed: true,
},
"value": schema.StringAttribute{
Description: "An CIDR string.",
Computed: true,
},
},
},
"prefix": schema.StringAttribute{
Description: "The network, that is reachable though the Next Hop. Should use CIDR notation.",
"next_hop": schema.SingleNestedAttribute{
Description: "Next hop destination.",
Computed: true,
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Description: "Type of the next hop. " + utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"),
Computed: true,
},
"value": schema.StringAttribute{
Description: "Either IPv4 or IPv6 (not set for blackhole and internet).",
Computed: true,
},
},
},
"labels": schema.MapAttribute{
Description: "Labels are key-value string pairs which can be attached to a resource container",
@ -107,23 +134,26 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche
// Read refreshes the Terraform state with the latest data.
func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
var model ModelV1
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
networkAreaRouteId := model.NetworkAreaRouteId.ValueString()
ctx = core.InitProviderContext(ctx)
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,
@ -141,11 +171,12 @@ func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.Re
ctx = core.LogResponse(ctx)
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() {

View file

@ -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"
@ -27,13 +28,28 @@ import (
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &networkAreaRouteResource{}
_ resource.ResourceWithConfigure = &networkAreaRouteResource{}
_ resource.ResourceWithImportState = &networkAreaRouteResource{}
_ resource.Resource = &networkAreaRouteResource{}
_ resource.ResourceWithConfigure = &networkAreaRouteResource{}
_ resource.ResourceWithImportState = &networkAreaRouteResource{}
_ resource.ResourceWithModifyPlan = &networkAreaRouteResource{}
_ resource.ResourceWithUpgradeState = &networkAreaRouteResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
// ModelV1 is the currently used model
type ModelV1 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 *NexthopModelV1 `tfsdk:"next_hop"`
Destination *DestinationModelV1 `tfsdk:"destination"`
Labels types.Map `tfsdk:"labels"`
}
// ModelV0 is the old model (only needed for state upgrade)
type ModelV0 struct {
Id types.String `tfsdk:"id"`
OrganizationId types.String `tfsdk:"organization_id"`
NetworkAreaId types.String `tfsdk:"network_area_id"`
NetworkAreaRouteId types.String `tfsdk:"network_area_route_id"`
@ -42,6 +58,18 @@ type Model struct {
Labels types.Map `tfsdk:"labels"`
}
// DestinationModelV1 maps the route destination data
type DestinationModelV1 struct {
Type types.String `tfsdk:"type"`
Value types.String `tfsdk:"value"`
}
// NexthopModelV1 maps the route nexthop data
type NexthopModelV1 struct {
Type types.String `tfsdk:"type"`
Value types.String `tfsdk:"value"`
}
// NewNetworkAreaRouteResource is a helper function to simplify the provider implementation.
func NewNetworkAreaRouteResource() resource.Resource {
return &networkAreaRouteResource{}
@ -49,7 +77,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 +86,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 ModelV1
// 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 ModelV1
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
}
@ -78,9 +138,10 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe
resp.Schema = schema.Schema{
Description: description,
MarkdownDescription: description,
Version: 1,
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 +158,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,
@ -121,24 +191,50 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe
validate.NoSeparator(),
},
},
"next_hop": schema.StringAttribute{
Description: "The IP address of the routing system, that will route the prefix configured. Should be a valid IPv4 address.",
"next_hop": schema.SingleNestedAttribute{
Description: "Next hop destination.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.IP(false),
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Description: fmt.Sprintf("Type of the next hop. %s %s", utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"), "Only `ipv4` supported currently."),
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"value": schema.StringAttribute{
Description: "Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported currently.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.IP(false),
},
},
},
},
"prefix": schema.StringAttribute{
Description: "The network, that is reachable though the Next Hop. Should use CIDR notation.",
"destination": schema.SingleNestedAttribute{
Description: "Destination of the route.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.CIDR(),
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Description: fmt.Sprintf("CIDRV type. %s %s", utils.FormatPossibleValues("cidrv4", "cidrv6"), "Only `cidrv4` is supported currently."),
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"value": schema.StringAttribute{
Description: "An CIDR string.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.CIDR(),
},
},
},
},
"labels": schema.MapAttribute{
@ -150,10 +246,91 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe
}
}
func (r *networkAreaRouteResource) UpgradeState(_ context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{
0: {
// This handles moving from version 0 to 1
PriorSchema: &schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"organization_id": schema.StringAttribute{
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"network_area_id": schema.StringAttribute{
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"network_area_route_id": schema.StringAttribute{
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"next_hop": schema.StringAttribute{
Required: true,
Validators: []validator.String{
validate.IP(false),
},
},
"prefix": schema.StringAttribute{
Required: true,
Validators: []validator.String{
validate.CIDR(),
},
},
"labels": schema.MapAttribute{
ElementType: types.StringType,
Optional: true,
},
},
},
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
var priorStateData ModelV0
resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...)
if resp.Diagnostics.HasError() {
return
}
nexthopValue := priorStateData.NextHop.ValueString()
prefixValue := priorStateData.Prefix.ValueString()
newStateData := ModelV1{
Id: priorStateData.Id,
OrganizationId: priorStateData.OrganizationId,
NetworkAreaId: priorStateData.NetworkAreaId,
NetworkAreaRouteId: priorStateData.NetworkAreaRouteId,
Labels: priorStateData.Labels,
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue(nexthopValue),
},
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue(prefixValue),
},
}
resp.Diagnostics.Append(resp.State.Set(ctx, newStateData)...)
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
var model ModelV1
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -163,8 +340,10 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea
ctx = core.InitProviderContext(ctx)
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
@ -175,7 +354,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
@ -196,12 +375,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
@ -217,7 +396,7 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea
// Read refreshes the Terraform state with the latest data.
func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
var model ModelV1
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -225,15 +404,17 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -247,7 +428,7 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe
ctx = core.LogResponse(ctx)
// 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
@ -264,7 +445,7 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe
// Delete deletes the resource and removes the Terraform state on success.
func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from state
var model Model
var model ModelV1
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -273,16 +454,18 @@ 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 = core.InitProviderContext(ctx)
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
@ -296,7 +479,7 @@ func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.Dele
// Update updates the resource and sets the updated Terraform state on success.
func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
var model ModelV1
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -305,16 +488,18 @@ 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 = core.InitProviderContext(ctx)
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
var stateModel Model
var stateModel ModelV1
diags = req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -328,7 +513,7 @@ 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
@ -336,7 +521,7 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda
ctx = core.LogResponse(ctx)
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
@ -354,28 +539,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 *ModelV1, region string) error {
if networkAreaRoute == nil {
return fmt.Errorf("response input is nil")
}
@ -386,13 +568,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 {
@ -400,13 +583,22 @@ 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
model.NextHop, err = mapRouteNextHop(networkAreaRoute)
if err != nil {
return err
}
model.Destination, err = mapRouteDestination(networkAreaRoute)
if err != nil {
return err
}
return nil
}
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaRoutePayload, error) {
func toCreatePayload(ctx context.Context, model *ModelV1) (*iaas.CreateNetworkAreaRoutePayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
@ -416,18 +608,28 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea
return nil, fmt.Errorf("converting to Go map: %w", err)
}
nextHopPayload, err := toNextHopPayload(model)
if err != nil {
return nil, err
}
destinationPayload, err := toDestinationPayload(model)
if err != nil {
return nil, err
}
return &iaas.CreateNetworkAreaRoutePayload{
Ipv4: &[]iaas.Route{
Items: &[]iaas.Route{
{
Prefix: conversion.StringValueToPointer(model.Prefix),
Nexthop: conversion.StringValueToPointer(model.NextHop),
Labels: &labels,
Destination: destinationPayload,
Labels: &labels,
Nexthop: nextHopPayload,
},
},
}, nil
}
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateNetworkAreaRoutePayload, error) {
func toUpdatePayload(ctx context.Context, model *ModelV1, currentLabels types.Map) (*iaas.UpdateNetworkAreaRoutePayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
@ -441,3 +643,97 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map)
Labels: &labels,
}, nil
}
func toNextHopPayload(model *ModelV1) (*iaas.RouteNexthop, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
} else if model.NextHop == nil {
return nil, fmt.Errorf("nexthop is nil in model")
}
switch model.NextHop.Type.ValueString() {
case "blackhole":
return sdkUtils.Ptr(iaas.NexthopBlackholeAsRouteNexthop(iaas.NewNexthopBlackhole("blackhole"))), nil
case "internet":
return sdkUtils.Ptr(iaas.NexthopInternetAsRouteNexthop(iaas.NewNexthopInternet("internet"))), nil
case "ipv4":
return sdkUtils.Ptr(iaas.NexthopIPv4AsRouteNexthop(iaas.NewNexthopIPv4("ipv4", model.NextHop.Value.ValueString()))), nil
case "ipv6":
return sdkUtils.Ptr(iaas.NexthopIPv6AsRouteNexthop(iaas.NewNexthopIPv6("ipv6", model.NextHop.Value.ValueString()))), nil
}
return nil, fmt.Errorf("unknown nexthop type: %s", model.NextHop.Type.ValueString())
}
func toDestinationPayload(model *ModelV1) (*iaas.RouteDestination, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
} else if model.Destination == nil {
return nil, fmt.Errorf("destination is nil in model")
}
switch model.Destination.Type.ValueString() {
case "cidrv4":
return sdkUtils.Ptr(iaas.DestinationCIDRv4AsRouteDestination(iaas.NewDestinationCIDRv4("cidrv4", model.Destination.Value.ValueString()))), nil
case "cidrv6":
return sdkUtils.Ptr(iaas.DestinationCIDRv6AsRouteDestination(iaas.NewDestinationCIDRv6("cidrv6", model.Destination.Value.ValueString()))), nil
}
return nil, fmt.Errorf("unknown destination type: %s", model.Destination.Type.ValueString())
}
func mapRouteNextHop(routeResp *iaas.Route) (*NexthopModelV1, error) {
if routeResp.Nexthop == nil {
return &NexthopModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
}, nil
}
switch i := routeResp.Nexthop.GetActualInstance().(type) {
case *iaas.NexthopIPv4:
return &NexthopModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringPointerValue(i.Value),
}, nil
case *iaas.NexthopIPv6:
return &NexthopModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringPointerValue(i.Value),
}, nil
case *iaas.NexthopBlackhole:
return &NexthopModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringNull(),
}, nil
case *iaas.NexthopInternet:
return &NexthopModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringNull(),
}, nil
default:
return nil, fmt.Errorf("unexpected nexthop type: %T", i)
}
}
func mapRouteDestination(routeResp *iaas.Route) (*DestinationModelV1, error) {
if routeResp.Destination == nil {
return &DestinationModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
}, nil
}
switch i := routeResp.Destination.GetActualInstance().(type) {
case *iaas.DestinationCIDRv4:
return &DestinationModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringPointerValue(i.Value),
}, nil
case *iaas.DestinationCIDRv6:
return &DestinationModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringPointerValue(i.Value),
}, nil
default:
return nil, fmt.Errorf("unexpected Destionation type: %T", i)
}
}

View file

@ -2,100 +2,133 @@ package networkarearoute
import (
"context"
"reflect"
"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 ModelV1
input *iaas.Route
region string
}
tests := []struct {
description string
state Model
input *iaas.Route
expected Model
args args
expected ModelV1
isValid bool
}{
{
"id_ok",
Model{
description: "id_ok",
args: args{
state: ModelV1{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
},
input: &iaas.Route{},
region: "eu01",
},
expected: ModelV1{
Id: types.StringValue("oid,naid,eu01,narid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
Destination: &DestinationModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
},
NextHop: &NexthopModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
},
Labels: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
&iaas.Route{},
Model{
Id: types.StringValue("oid,naid,narid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
Prefix: types.StringNull(),
NextHop: types.StringNull(),
Labels: types.MapNull(types.StringType),
},
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: ModelV1{
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: ModelV1{
Id: types.StringValue("oid,naid,eu02,narid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
Prefix: types.StringValue("prefix"),
NextHop: types.StringValue("hop"),
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue("prefix"),
},
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue("hop"),
},
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: ModelV1{
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 +136,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)
}
@ -115,24 +148,41 @@ func TestMapFields(t *testing.T) {
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
input *ModelV1
expected *iaas.CreateNetworkAreaRoutePayload
isValid bool
}{
{
description: "default_ok",
input: &Model{
Prefix: types.StringValue("prefix"),
NextHop: types.StringValue("hop"),
input: &ModelV1{
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue("prefix"),
},
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue("hop"),
},
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
},
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",
},
@ -164,13 +214,13 @@ func TestToCreatePayload(t *testing.T) {
func TestToUpdatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
input *ModelV1
expected *iaas.UpdateNetworkAreaRoutePayload
isValid bool
}{
{
"default_ok",
&Model{
&ModelV1{
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key1": types.StringValue("value1"),
"key2": types.StringValue("value2"),
@ -203,3 +253,371 @@ func TestToUpdatePayload(t *testing.T) {
})
}
}
func TestToNextHopPayload(t *testing.T) {
type args struct {
model *ModelV1
}
tests := []struct {
name string
args args
want *iaas.RouteNexthop
wantErr bool
}{
{
name: "ipv4",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue("10.20.30.40"),
},
},
},
want: &iaas.RouteNexthop{
NexthopIPv4: &iaas.NexthopIPv4{
Type: utils.Ptr("ipv4"),
Value: utils.Ptr("10.20.30.40"),
},
},
wantErr: false,
},
{
name: "ipv6",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv6"),
Value: types.StringValue("2001:db8:85a3:0:0:8a2e:370:7334"),
},
},
},
want: &iaas.RouteNexthop{
NexthopIPv6: &iaas.NexthopIPv6{
Type: utils.Ptr("ipv6"),
Value: utils.Ptr("2001:db8:85a3:0:0:8a2e:370:7334"),
},
},
wantErr: false,
},
{
name: "internet",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("internet"),
},
},
},
want: &iaas.RouteNexthop{
NexthopInternet: &iaas.NexthopInternet{
Type: utils.Ptr("internet"),
},
},
wantErr: false,
},
{
name: "blackhole",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("blackhole"),
},
},
},
want: &iaas.RouteNexthop{
NexthopBlackhole: &iaas.NexthopBlackhole{
Type: utils.Ptr("blackhole"),
},
},
wantErr: false,
},
{
name: "invalid type",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("foobar"),
},
},
},
wantErr: true,
},
{
name: "model is nil",
args: args{
model: nil,
},
wantErr: true,
},
{
name: "nexthop in model is nil",
args: args{
model: &ModelV1{
NextHop: nil,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toNextHopPayload(tt.args.model)
if (err != nil) != tt.wantErr {
t.Errorf("toNextHopPayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("toNextHopPayload() got = %v, want %v", got, tt.want)
}
})
}
}
func TestToDestinationPayload(t *testing.T) {
type args struct {
model *ModelV1
}
tests := []struct {
name string
args args
want *iaas.RouteDestination
wantErr bool
}{
{
name: "cidrv4",
args: args{
model: &ModelV1{
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue("192.168.1.0/24"),
},
},
},
want: &iaas.RouteDestination{
DestinationCIDRv4: &iaas.DestinationCIDRv4{
Type: utils.Ptr("cidrv4"),
Value: utils.Ptr("192.168.1.0/24"),
},
},
wantErr: false,
},
{
name: "cidrv6",
args: args{
model: &ModelV1{
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv6"),
Value: types.StringValue("2001:db8:1234::/48"),
},
},
},
want: &iaas.RouteDestination{
DestinationCIDRv6: &iaas.DestinationCIDRv6{
Type: utils.Ptr("cidrv6"),
Value: utils.Ptr("2001:db8:1234::/48"),
},
},
wantErr: false,
},
{
name: "invalid type",
args: args{
model: &ModelV1{
Destination: &DestinationModelV1{
Type: types.StringValue("foobar"),
},
},
},
wantErr: true,
},
{
name: "model is nil",
args: args{
model: nil,
},
wantErr: true,
},
{
name: "destination in model is nil",
args: args{
model: &ModelV1{
Destination: nil,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toDestinationPayload(tt.args.model)
if (err != nil) != tt.wantErr {
t.Errorf("toDestinationPayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("toDestinationPayload() got = %v, want %v", got, tt.want)
}
})
}
}
func TestMapRouteNextHop(t *testing.T) {
type args struct {
routeResp *iaas.Route
}
tests := []struct {
name string
args args
want *NexthopModelV1
wantErr bool
}{
{
name: "ipv4",
args: args{
routeResp: &iaas.Route{
Nexthop: &iaas.RouteNexthop{
NexthopIPv4: &iaas.NexthopIPv4{
Type: utils.Ptr("ipv4"),
Value: utils.Ptr("192.168.1.0/24"),
},
},
},
},
want: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue("192.168.1.0/24"),
},
},
{
name: "ipv6",
args: args{
routeResp: &iaas.Route{
Nexthop: &iaas.RouteNexthop{
NexthopIPv4: &iaas.NexthopIPv4{
Type: utils.Ptr("ipv6"),
Value: utils.Ptr("2001:db8:85a3:0:0:8a2e:370:7334"),
},
},
},
},
want: &NexthopModelV1{
Type: types.StringValue("ipv6"),
Value: types.StringValue("2001:db8:85a3:0:0:8a2e:370:7334"),
},
},
{
name: "blackhole",
args: args{
routeResp: &iaas.Route{
Nexthop: &iaas.RouteNexthop{
NexthopBlackhole: &iaas.NexthopBlackhole{
Type: utils.Ptr("blackhole"),
},
},
},
},
want: &NexthopModelV1{
Type: types.StringValue("blackhole"),
},
},
{
name: "internet",
args: args{
routeResp: &iaas.Route{
Nexthop: &iaas.RouteNexthop{
NexthopInternet: &iaas.NexthopInternet{
Type: utils.Ptr("internet"),
},
},
},
},
want: &NexthopModelV1{
Type: types.StringValue("internet"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mapRouteNextHop(tt.args.routeResp)
if (err != nil) != tt.wantErr {
t.Errorf("mapRouteNextHop() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mapRouteNextHop() got = %v, want %v", got, tt.want)
}
})
}
}
func TestMapRouteDestination(t *testing.T) {
type args struct {
routeResp *iaas.Route
}
tests := []struct {
name string
args args
want *DestinationModelV1
wantErr bool
}{
{
name: "cidrv4",
args: args{
routeResp: &iaas.Route{
Destination: &iaas.RouteDestination{
DestinationCIDRv4: &iaas.DestinationCIDRv4{
Type: utils.Ptr("cidrv4"),
Value: utils.Ptr("192.168.1.0/24"),
},
},
},
},
want: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue("192.168.1.0/24"),
},
},
{
name: "cidrv6",
args: args{
routeResp: &iaas.Route{
Destination: &iaas.RouteDestination{
DestinationCIDRv4: &iaas.DestinationCIDRv4{
Type: utils.Ptr("cidrv6"),
Value: utils.Ptr("2001:db8:1234::/48"),
},
},
},
},
want: &DestinationModelV1{
Type: types.StringValue("cidrv6"),
Value: types.StringValue("2001:db8:1234::/48"),
},
},
{
name: "destination in API response is nil",
args: args{
routeResp: &iaas.Route{
Destination: nil,
},
},
want: &DestinationModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mapRouteDestination(tt.args.routeResp)
if (err != nil) != tt.wantErr {
t.Errorf("mapRouteDestination() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mapRouteDestination() got = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -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,17 +148,20 @@ 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 = core.InitProviderContext(ctx)
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,
@ -169,7 +179,7 @@ func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.Re
ctx = core.LogResponse(ctx)
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

View file

@ -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,
@ -260,8 +283,10 @@ func (r *networkInterfaceResource) Create(ctx context.Context, req resource.Crea
ctx = core.InitProviderContext(ctx)
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
@ -272,7 +297,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
@ -285,7 +310,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
@ -308,16 +333,18 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -331,7 +358,7 @@ func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRe
ctx = core.LogResponse(ctx)
// 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
@ -355,12 +382,14 @@ 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 = core.InitProviderContext(ctx)
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)
@ -379,7 +408,7 @@ 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
@ -387,7 +416,7 @@ func (r *networkInterfaceResource) Update(ctx context.Context, req resource.Upda
ctx = core.LogResponse(ctx)
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
@ -411,17 +440,19 @@ 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 = core.InitProviderContext(ctx)
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
@ -437,28 +468,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")
}
@ -475,7 +503,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

View file

@ -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"),
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",
},
&iaas.NIC{
Id: utils.Ptr("nicid"),
},
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"),
},
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",
},
},
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{}),
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",
},
&iaas.NIC{
Id: utils.Ptr("nicid"),
AllowedAddresses: nil,
},
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)
}

View file

@ -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,
@ -133,14 +175,16 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc
ctx = core.InitProviderContext(ctx)
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
@ -148,7 +192,8 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc
ctx = core.LogResponse(ctx)
model.Id = utils.BuildInternalTerraformId(projectId, serverId, networkInterfaceId)
model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, networkInterfaceId)
model.Region = types.StringValue(region)
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
@ -171,13 +216,14 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.
ctx = core.InitProviderContext(ctx)
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 {
@ -200,12 +246,17 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.
if nic.Id == nil || (nic.Id != nil && *nic.Id != networkInterfaceId) {
continue
}
model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, networkInterfaceId)
model.Region = types.StringValue(region)
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network interface attachment read")
return
}
@ -233,14 +284,16 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc
ctx = core.InitProviderContext(ctx)
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
@ -256,23 +309,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")
}

View file

@ -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{
@ -170,8 +179,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")
}
@ -202,7 +211,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

View file

@ -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),

View file

@ -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,14 +117,16 @@ 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 = core.InitProviderContext(ctx)
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,
@ -135,7 +144,7 @@ func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadReques
ctx = core.LogResponse(ctx)
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

View file

@ -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,
@ -148,7 +190,9 @@ func (r *publicIpResource) Create(ctx context.Context, req resource.CreateReques
ctx = core.InitProviderContext(ctx)
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)
@ -159,7 +203,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
@ -170,7 +214,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
@ -193,14 +237,16 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -214,7 +260,7 @@ func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, r
ctx = core.LogResponse(ctx)
// 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
@ -238,11 +284,13 @@ 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 = core.InitProviderContext(ctx)
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
@ -260,7 +308,7 @@ 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
@ -268,7 +316,7 @@ func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateReques
ctx = core.LogResponse(ctx)
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
@ -292,15 +340,17 @@ 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 = core.InitProviderContext(ctx)
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
@ -316,25 +366,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")
}
@ -351,7 +400,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 {

View file

@ -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"),
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",
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
NetworkInterface: iaas.NewNullableString(nil),
},
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{}),
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",
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")),
},
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"),
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",
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
},
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)
}

View file

@ -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,12 +193,14 @@ 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 = core.InitProviderContext(ctx)
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)
@ -167,7 +211,7 @@ 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
@ -175,7 +219,7 @@ func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.Cre
ctx = core.LogResponse(ctx)
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
@ -197,16 +241,18 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -220,7 +266,7 @@ func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadR
ctx = core.LogResponse(ctx)
// 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
@ -250,12 +296,14 @@ 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 = core.InitProviderContext(ctx)
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)
@ -263,7 +311,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
@ -279,28 +327,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")
}
@ -324,8 +369,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)

View file

@ -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"),
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",
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")),
},
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)
}

View file

@ -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,14 +56,14 @@ 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,
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,
},
"project_id": schema.StringAttribute{
@ -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,14 +117,16 @@ 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 = core.InitProviderContext(ctx)
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,
@ -135,7 +144,7 @@ func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadR
ctx = core.LogResponse(ctx)
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

View file

@ -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,
@ -165,7 +207,9 @@ func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateR
ctx = core.InitProviderContext(ctx)
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)
@ -176,7 +220,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
@ -189,7 +233,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
@ -212,14 +256,16 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -233,7 +279,7 @@ func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadReque
ctx = core.LogResponse(ctx)
// 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
@ -257,11 +303,13 @@ 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 = core.InitProviderContext(ctx)
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
@ -279,7 +327,7 @@ 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
@ -287,7 +335,7 @@ func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateR
ctx = core.LogResponse(ctx)
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
@ -311,15 +359,17 @@ 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 = core.InitProviderContext(ctx)
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
@ -335,25 +385,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")
}
@ -370,7 +419,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 {

View file

@ -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"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
},
input: &iaas.SecurityGroup{
Id: utils.Ptr("sgid"),
},
region: "eu01",
},
&iaas.SecurityGroup{
Id: utils.Ptr("sgid"),
},
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"),
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",
},
&iaas.SecurityGroup{
Id: utils.Ptr("sgid"),
Labels: &map[string]interface{}{},
},
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)
}

View file

@ -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,16 +171,18 @@ 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 = core.InitProviderContext(ctx)
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,
@ -191,7 +200,7 @@ func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.R
ctx = core.LogResponse(ctx)
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

View file

@ -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,
@ -392,8 +436,10 @@ func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.Cre
ctx = core.InitProviderContext(ctx)
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
@ -434,7 +480,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
@ -445,7 +491,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
@ -468,16 +514,18 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -491,7 +539,7 @@ func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadR
ctx = core.LogResponse(ctx)
// 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
@ -522,17 +570,19 @@ 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 = core.InitProviderContext(ctx)
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
@ -548,28 +598,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")
}
@ -586,7 +633,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)

View file

@ -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(),
}),
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",
},
&iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Protocol: &fixtureProtocol,
},
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),
}),
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",
},
&iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Protocol: &fixtureProtocol,
},
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)
}

View file

@ -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,14 +84,14 @@ 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,
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -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,14 +192,16 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -212,7 +222,7 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
ctx = core.LogResponse(ctx)
// 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
@ -226,7 +236,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")
}
@ -243,7 +253,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 {

View file

@ -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"),
description: "default_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
},
input: &iaas.Server{
Id: utils.Ptr("sid"),
},
region: "eu01",
},
&iaas.Server{
Id: utils.Ptr("sid"),
},
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{}),
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",
},
&iaas.Server{
Id: utils.Ptr("sid"),
},
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)
}

View file

@ -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() {
@ -129,6 +162,10 @@ func (r serverResource) ValidateConfig(ctx context.Context, req resource.Validat
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring server", "You can only provide `delete_on_termination` for `source_type` `image`.")
}
}
if model.NetworkInterfaces.IsNull() || model.NetworkInterfaces.IsUnknown() || len(model.NetworkInterfaces.Elements()) < 1 {
core.LogAndAddWarning(ctx, &resp.Diagnostics, "No network interfaces configured", "You have no network interfaces configured for this server. This will be a problem when you want to (re-)create this server. Please note that modifying the network interfaces for an existing server will result in a replacement of the resource. We will provide a clear migration path soon.")
}
}
// ConfigValidators validates the resource configuration
@ -147,12 +184,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
}
@ -167,7 +205,7 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res
Description: "Server 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`,`server_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -184,6 +222,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,
@ -297,7 +344,7 @@ 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.",
Description: "The IDs of network interfaces which should be attached to the server. Updating it will recreate the server. **Required when (re-)creating servers. Still marked as optional in the schema to not introduce breaking changes. There will be a migration path for this field soon.**",
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
@ -428,11 +475,12 @@ 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)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
@ -442,7 +490,7 @@ 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
@ -451,7 +499,7 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
ctx = core.LogResponse(ctx)
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
@ -459,7 +507,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 {
@ -467,14 +515,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
}
@ -491,41 +539,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)
}
@ -533,7 +581,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
@ -542,52 +590,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
}
}
@ -598,7 +646,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)
@ -607,14 +655,16 @@ 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 = core.InitProviderContext(ctx)
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 {
@ -630,7 +680,7 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
ctx = core.LogResponse(ctx)
// 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
@ -644,7 +694,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 {
@ -655,7 +705,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)
}
@ -666,12 +716,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)
}
@ -691,11 +741,13 @@ 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 = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
// Retrieve values from state
@ -710,14 +762,14 @@ 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
@ -725,18 +777,18 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
ctx = core.LogResponse(ctx)
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
@ -746,7 +798,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 {
@ -754,7 +806,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
@ -779,15 +831,17 @@ 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 = core.InitProviderContext(ctx)
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
@ -795,7 +849,7 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest,
ctx = core.LogResponse(ctx)
_, 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
@ -809,25 +863,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")
}
@ -844,7 +897,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 {
@ -981,9 +1035,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{
@ -1005,22 +1059,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{

Some files were not shown because too many files have changed in this diff Show more