From 460c18c202ac3f951b0e93d65eaa5270bb1c8194 Mon Sep 17 00:00:00 2001 From: mitterle-sit <195103606+mitterle-sit@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:12:15 +0100 Subject: [PATCH 1/2] fix(observability/instance): adjust drift (#1044) fixes #1003 --- .../observability/instance/resource.go | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/stackit/internal/services/observability/instance/resource.go b/stackit/internal/services/observability/instance/resource.go index ee89a5a9..3db875f1 100644 --- a/stackit/internal/services/observability/instance/resource.go +++ b/stackit/internal/services/observability/instance/resource.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" @@ -528,16 +529,25 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r Description: "Specifies for how many days the raw metrics are kept. Default is set to `90`.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, }, "metrics_retention_days_5m_downsampling": schema.Int64Attribute{ Description: "Specifies for how many days the 5m downsampled metrics are kept. must be less than the value of the general retention. Default is set to `90`.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, }, "metrics_retention_days_1h_downsampling": schema.Int64Attribute{ Description: "Specifies for how many days the 1h downsampled metrics are kept. must be less than the value of the 5m downsampling retention. Default is set to `90`.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, }, "metrics_url": schema.StringAttribute{ Description: "Specifies metrics URL.", @@ -659,6 +669,8 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "send_resolved": schema.BoolAttribute{ Description: "Whether to notify about resolved alerts.", Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), }, "smart_host": schema.StringAttribute{ Description: "The SMTP host through which emails are sent.", @@ -698,6 +710,8 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "send_resolved": schema.BoolAttribute{ Description: "Whether to notify about resolved alerts.", Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), }, }, }, @@ -733,6 +747,8 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "send_resolved": schema.BoolAttribute{ Description: "Whether to notify about resolved alerts.", Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), }, }, }, @@ -789,10 +805,18 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r Description: "The API key for OpsGenie.", Optional: true, Sensitive: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "opsgenie_api_url": schema.StringAttribute{ Description: "The host to send OpsGenie API requests to. Must be a valid URL", Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "resolve_timeout": schema.StringAttribute{ Description: "The default value used by alertmanager if the alert does not include EndsAt. After this time passes, it can declare the alert as resolved if it has not been updated. This has no impact on alerts from Prometheus, as they always include EndsAt.", @@ -805,24 +829,43 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "smtp_auth_identity": schema.StringAttribute{ Description: "SMTP authentication information. Must be a valid email address", Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "smtp_auth_password": schema.StringAttribute{ Description: "SMTP Auth using LOGIN and PLAIN.", Optional: true, Sensitive: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "smtp_auth_username": schema.StringAttribute{ Description: "SMTP Auth using CRAM-MD5, LOGIN and PLAIN. If empty, Alertmanager doesn't authenticate to the SMTP server.", Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "smtp_from": schema.StringAttribute{ Description: "The default SMTP From header field. Must be a valid email address", Optional: true, Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "smtp_smart_host": schema.StringAttribute{ Description: "The default SMTP smarthost used for sending emails, including port number in format `host:port` (eg. `smtp.example.com:587`). Port number usually is 25, or 587 for SMTP over TLS (sometimes referred to as STARTTLS).", Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, }, @@ -1798,6 +1841,8 @@ func mapGlobalConfigToAttributes(respGlobalConfigs *observability.Global, global smtpAuthIdentity := respGlobalConfigs.SmtpAuthIdentity smtpAuthPassword := respGlobalConfigs.SmtpAuthPassword smtpAuthUsername := respGlobalConfigs.SmtpAuthUsername + opsgenieApiKey := respGlobalConfigs.OpsgenieApiKey + opsgenieApiUrl := respGlobalConfigs.OpsgenieApiUrl if globalConfigsTF != nil { if respGlobalConfigs.SmtpSmarthost == nil && !globalConfigsTF.SmtpSmartHost.IsNull() && !globalConfigsTF.SmtpSmartHost.IsUnknown() { @@ -1815,11 +1860,17 @@ func mapGlobalConfigToAttributes(respGlobalConfigs *observability.Global, global !globalConfigsTF.SmtpAuthUsername.IsNull() && !globalConfigsTF.SmtpAuthUsername.IsUnknown() { smtpAuthUsername = sdkUtils.Ptr(globalConfigsTF.SmtpAuthUsername.ValueString()) } + if respGlobalConfigs.OpsgenieApiKey == nil { + opsgenieApiKey = sdkUtils.Ptr(globalConfigsTF.OpsgenieApiKey.ValueString()) + } + if respGlobalConfigs.OpsgenieApiUrl == nil { + opsgenieApiUrl = sdkUtils.Ptr(globalConfigsTF.OpsgenieApiUrl.ValueString()) + } } globalConfigObject, diags := types.ObjectValue(globalConfigurationTypes, map[string]attr.Value{ - "opsgenie_api_key": types.StringPointerValue(respGlobalConfigs.OpsgenieApiKey), - "opsgenie_api_url": types.StringPointerValue(respGlobalConfigs.OpsgenieApiUrl), + "opsgenie_api_key": types.StringPointerValue(opsgenieApiKey), + "opsgenie_api_url": types.StringPointerValue(opsgenieApiUrl), "resolve_timeout": types.StringPointerValue(respGlobalConfigs.ResolveTimeout), "smtp_from": types.StringPointerValue(respGlobalConfigs.SmtpFrom), "smtp_auth_identity": types.StringPointerValue(smtpAuthIdentity), From 53a36978504717f34b06f951bb8fc65eba69c3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20H=C3=B6nle?= Date: Wed, 17 Dec 2025 15:40:46 +0100 Subject: [PATCH 2/2] feat(iaas): support for v2 API (#1070) relates to STACKITTPR-313 --- docs/data-sources/affinity_group.md | 6 +- docs/data-sources/iaas_project.md | 3 +- docs/data-sources/image.md | 6 +- docs/data-sources/image_v2.md | 3 +- docs/data-sources/machine_type.md | 3 +- docs/data-sources/network_area.md | 12 +- docs/data-sources/network_area_region.md | 57 + docs/data-sources/network_area_route.md | 27 +- docs/data-sources/network_interface.md | 6 +- docs/data-sources/public_ip.md | 6 +- docs/data-sources/security_group.md | 6 +- docs/data-sources/security_group_rule.md | 6 +- docs/data-sources/server.md | 6 +- docs/data-sources/volume.md | 6 +- docs/resources/affinity_group.md | 12 +- docs/resources/image.md | 5 +- docs/resources/network.md | 21 +- docs/resources/network_area.md | 26 +- docs/resources/network_area_region.md | 77 + docs/resources/network_area_route.md | 78 +- docs/resources/network_interface.md | 5 +- docs/resources/public_ip.md | 5 +- docs/resources/public_ip_associate.md | 8 +- docs/resources/security_group.md | 3 +- docs/resources/security_group_rule.md | 3 +- docs/resources/server.md | 7 +- .../server_network_interface_attach.md | 12 +- .../server_service_account_attach.md | 8 +- docs/resources/server_volume_attach.md | 8 +- docs/resources/volume.md | 5 +- .../data-source.tf | 4 + .../stackit_affinity_group/resource.tf | 2 +- examples/resources/stackit_image/resource.tf | 2 +- .../resources/stackit_network/resource.tf | 15 +- .../stackit_network_area/resource.tf | 6 - .../stackit_network_area_region/resource.tf | 18 + .../stackit_network_area_route/resource.tf | 14 +- .../stackit_network_interface/resource.tf | 2 +- .../resources/stackit_public_ip/resource.tf | 2 +- .../stackit_public_ip_associate/resource.tf | 2 +- examples/resources/stackit_server/resource.tf | 2 +- .../resource.tf | 2 +- .../resource.tf | 2 +- .../stackit_server_volume_attach/resource.tf | 2 +- examples/resources/stackit_volume/resource.tf | 2 +- go.mod | 6 +- go.sum | 12 +- .../services/iaas/affinitygroup/datasource.go | 21 +- .../services/iaas/affinitygroup/resource.go | 94 +- .../iaas/affinitygroup/resource_test.go | 54 +- .../internal/services/iaas/iaas_acc_test.go | 1574 +++++++++-------- .../services/iaas/image/datasource.go | 34 +- .../services/iaas/image/datasource_test.go | 165 +- .../internal/services/iaas/image/resource.go | 118 +- .../services/iaas/image/resource_test.go | 163 +- .../services/iaas/imagev2/datasource.go | 31 +- .../services/iaas/imagev2/datasource_test.go | 163 +- .../services/iaas/keypair/datasource.go | 8 +- .../services/iaas/machinetype/datasource.go | 34 +- .../iaas/machinetype/datasource_test.go | 146 +- .../services/iaas/network/datasource.go | 259 ++- .../{utils/v2network => }/datasource_test.go | 84 +- .../services/iaas/network/resource.go | 644 ++++++- .../{utils/v2network => }/resource_test.go | 213 ++- .../iaas/network/utils/model/model.go | 53 - .../network/utils/v1network/datasource.go | 208 --- .../utils/v1network/datasource_test.go | 352 ---- .../iaas/network/utils/v1network/resource.go | 558 ------ .../network/utils/v1network/resource_test.go | 811 --------- .../network/utils/v2network/datasource.go | 220 --- .../iaas/network/utils/v2network/resource.go | 603 ------- .../services/iaas/networkarea/datasource.go | 66 +- .../services/iaas/networkarea/resource.go | 576 ++++-- .../iaas/networkarea/resource_test.go | 771 ++++---- .../iaas/networkarearegion/datasource.go | 181 ++ .../iaas/networkarearegion/resource.go | 728 ++++++++ .../iaas/networkarearegion/resource_test.go | 1052 +++++++++++ .../iaas/networkarearoute/datasource.go | 53 +- .../iaas/networkarearoute/resource.go | 418 ++++- .../iaas/networkarearoute/resource_test.go | 544 +++++- .../iaas/networkinterface/datasource.go | 24 +- .../iaas/networkinterface/resource.go | 79 +- .../iaas/networkinterface/resource_test.go | 195 +- .../iaas/networkinterfaceattach/resource.go | 104 +- .../services/iaas/project/datasource.go | 22 +- .../services/iaas/project/datasource_test.go | 18 +- .../services/iaas/publicip/datasource.go | 25 +- .../services/iaas/publicip/resource.go | 94 +- .../services/iaas/publicip/resource_test.go | 141 +- .../iaas/publicipassociate/resource.go | 92 +- .../iaas/publicipassociate/resource_test.go | 88 +- .../services/iaas/securitygroup/datasource.go | 23 +- .../services/iaas/securitygroup/resource.go | 94 +- .../iaas/securitygroup/resource_test.go | 116 +- .../iaas/securitygrouprule/datasource.go | 23 +- .../iaas/securitygrouprule/resource.go | 106 +- .../iaas/securitygrouprule/resource_test.go | 173 +- .../services/iaas/server/datasource.go | 33 +- .../services/iaas/server/datasource_test.go | 137 +- .../internal/services/iaas/server/resource.go | 210 ++- .../services/iaas/server/resource_test.go | 220 ++- .../iaas/serviceaccountattach/resource.go | 129 +- .../testdata/resource-network-area-max.tf | 16 +- .../testdata/resource-network-area-min.tf | 22 +- .../resource-network-area-region-max.tf | 33 + .../resource-network-area-region-min.tf | 23 + .../iaas/testdata/resource-network-max.tf | 85 + ...work-v1-min.tf => resource-network-min.tf} | 0 .../iaas/testdata/resource-network-v1-max.tf | 35 - .../iaas/testdata/resource-network-v2-max.tf | 43 - .../iaas/testdata/resource-network-v2-min.tf | 7 - .../iaas/testdata/resource-server-min.tf | 13 + .../iaas/testdata/resource-volume-max.tf | 5 +- stackit/internal/services/iaas/utils/util.go | 3 +- .../internal/services/iaas/utils/util_test.go | 1 - .../services/iaas/volume/datasource.go | 23 +- .../internal/services/iaas/volume/resource.go | 100 +- .../services/iaas/volume/resource_test.go | 125 +- .../services/iaas/volumeattach/resource.go | 101 +- .../iaasalpha/routingtable/route/resource.go | 6 + .../internal/services/logme/logme_acc_test.go | 2 +- .../serverupdate/serverupdate_acc_test.go | 8 +- stackit/provider.go | 3 + .../resources/network_area_route.md.tmpl | 54 + 124 files changed, 8342 insertions(+), 6042 deletions(-) create mode 100644 docs/data-sources/network_area_region.md create mode 100644 docs/resources/network_area_region.md create mode 100644 examples/data-sources/stackit_network_area_region/data-source.tf create mode 100644 examples/resources/stackit_network_area_region/resource.tf rename stackit/internal/services/iaas/network/{utils/v2network => }/datasource_test.go (87%) rename stackit/internal/services/iaas/network/{utils/v2network => }/resource_test.go (84%) delete mode 100644 stackit/internal/services/iaas/network/utils/model/model.go delete mode 100644 stackit/internal/services/iaas/network/utils/v1network/datasource.go delete mode 100644 stackit/internal/services/iaas/network/utils/v1network/datasource_test.go delete mode 100644 stackit/internal/services/iaas/network/utils/v1network/resource.go delete mode 100644 stackit/internal/services/iaas/network/utils/v1network/resource_test.go delete mode 100644 stackit/internal/services/iaas/network/utils/v2network/datasource.go delete mode 100644 stackit/internal/services/iaas/network/utils/v2network/resource.go create mode 100644 stackit/internal/services/iaas/networkarearegion/datasource.go create mode 100644 stackit/internal/services/iaas/networkarearegion/resource.go create mode 100644 stackit/internal/services/iaas/networkarearegion/resource_test.go create mode 100644 stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf create mode 100644 stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf create mode 100644 stackit/internal/services/iaas/testdata/resource-network-max.tf rename stackit/internal/services/iaas/testdata/{resource-network-v1-min.tf => resource-network-min.tf} (100%) delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-v1-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-v2-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-v2-min.tf create mode 100644 templates/resources/network_area_route.md.tmpl diff --git a/docs/data-sources/affinity_group.md b/docs/data-sources/affinity_group.md index 904746cd..63fc0629 100644 --- a/docs/data-sources/affinity_group.md +++ b/docs/data-sources/affinity_group.md @@ -27,9 +27,13 @@ data "stackit_affinity_group" "example" { - `affinity_group_id` (String) The affinity group ID. - `project_id` (String) STACKIT Project ID to which the affinity group is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only -- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`affinity_group_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`affinity_group_id`". - `members` (List of String) Affinity Group schema. Must have a `region` specified in the provider configuration. - `name` (String) The name of the affinity group. - `policy` (String) The policy of the affinity group. diff --git a/docs/data-sources/iaas_project.md b/docs/data-sources/iaas_project.md index 919318df..19aea853 100644 --- a/docs/data-sources/iaas_project.md +++ b/docs/data-sources/iaas_project.md @@ -31,5 +31,6 @@ data "stackit_iaas_project" "example" { - `created_at` (String) Date-time when the project was created. - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`". - `internet_access` (Boolean) Specifies if the project has internet_access -- `state` (String) Specifies the state of the project. +- `state` (String, Deprecated) Specifies the status of the project. +- `status` (String) Specifies the status of the project. - `updated_at` (String) Date-time when the project was last updated. diff --git a/docs/data-sources/image.md b/docs/data-sources/image.md index 23566526..34fa0c35 100644 --- a/docs/data-sources/image.md +++ b/docs/data-sources/image.md @@ -27,12 +27,16 @@ data "stackit_image" "example" { - `image_id` (String) The image ID. - `project_id` (String) STACKIT project ID to which the image is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `checksum` (Attributes) Representation of an image checksum. (see [below for nested schema](#nestedatt--checksum)) - `config` (Attributes) Properties to set hardware and scheduling settings for an image. (see [below for nested schema](#nestedatt--config)) - `disk_format` (String) The disk format of the image. -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`image_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `min_disk_size` (Number) The minimum disk size of the image in GB. - `min_ram` (Number) The minimum RAM of the image in MB. diff --git a/docs/data-sources/image_v2.md b/docs/data-sources/image_v2.md index 43f713ac..b417f17b 100644 --- a/docs/data-sources/image_v2.md +++ b/docs/data-sources/image_v2.md @@ -105,6 +105,7 @@ data "stackit_image_v2" "filter_distro_version" { - `image_id` (String) Image ID to fetch directly - `name` (String) Exact image name to match. Optionally applies a `filter` block to further refine results in case multiple images share the same name. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name_regex`. - `name_regex` (String) Regular expression to match against image names. Optionally applies a `filter` block to narrow down results when multiple image names match the regex. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name`. +- `region` (String) The resource region. If not defined, the provider region is used. - `sort_ascending` (Boolean) If set to `true`, images are sorted in ascending lexicographical order by image name (such as `Ubuntu 18.04`, `Ubuntu 20.04`, `Ubuntu 22.04`) before selecting the first match. Defaults to `false` (descending such as `Ubuntu 22.04`, `Ubuntu 20.04`, `Ubuntu 18.04`). ### Read-Only @@ -112,7 +113,7 @@ data "stackit_image_v2" "filter_distro_version" { - `checksum` (Attributes) Representation of an image checksum. (see [below for nested schema](#nestedatt--checksum)) - `config` (Attributes) Properties to set hardware and scheduling settings for an image. (see [below for nested schema](#nestedatt--config)) - `disk_format` (String) The disk format of the image. -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`image_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `min_disk_size` (Number) The minimum disk size of the image in GB. - `min_ram` (Number) The minimum RAM of the image in MB. diff --git a/docs/data-sources/machine_type.md b/docs/data-sources/machine_type.md index 10f80fc3..7a200ae0 100644 --- a/docs/data-sources/machine_type.md +++ b/docs/data-sources/machine_type.md @@ -63,6 +63,7 @@ stackit server machine-type list ### Optional +- `region` (String) The resource region. If not defined, the provider region is used. - `sort_ascending` (Boolean) Sort machine types by name ascending (`true`) or descending (`false`). Defaults to `false` ### Read-Only @@ -70,7 +71,7 @@ stackit server machine-type list - `description` (String) Machine type description. - `disk` (Number) Disk size in GB. - `extra_specs` (Map of String) Extra specs (e.g., CPU type, overcommit ratio). -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`image_id`". - `name` (String) Name of the machine type (e.g. 's1.2'). - `ram` (Number) RAM size in MB. - `vcpus` (Number) Number of vCPUs. diff --git a/docs/data-sources/network_area.md b/docs/data-sources/network_area.md index d561f3b3..86590676 100644 --- a/docs/data-sources/network_area.md +++ b/docs/data-sources/network_area.md @@ -29,16 +29,16 @@ data "stackit_network_area" "example" { ### Read-Only -- `default_nameservers` (List of String) List of DNS Servers/Nameservers. -- `default_prefix_length` (Number) The default prefix length for networks in the network area. +- `default_nameservers` (List of String, Deprecated) List of DNS Servers/Nameservers. +- `default_prefix_length` (Number, Deprecated) The default prefix length for networks in the network area. - `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container -- `max_prefix_length` (Number) The maximal prefix length for networks in the network area. -- `min_prefix_length` (Number) The minimal prefix length for networks in the network area. +- `max_prefix_length` (Number, Deprecated) The maximal prefix length for networks in the network area. +- `min_prefix_length` (Number, Deprecated) The minimal prefix length for networks in the network area. - `name` (String) The name of the network area. -- `network_ranges` (Attributes List) List of Network ranges. (see [below for nested schema](#nestedatt--network_ranges)) +- `network_ranges` (Attributes List, Deprecated) List of Network ranges. (see [below for nested schema](#nestedatt--network_ranges)) - `project_count` (Number) The amount of projects currently referencing this area. -- `transfer_network` (String) Classless Inter-Domain Routing (CIDR). +- `transfer_network` (String, Deprecated) Classless Inter-Domain Routing (CIDR). ### Nested Schema for `network_ranges` diff --git a/docs/data-sources/network_area_region.md b/docs/data-sources/network_area_region.md new file mode 100644 index 00000000..09ac1be3 --- /dev/null +++ b/docs/data-sources/network_area_region.md @@ -0,0 +1,57 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_network_area_region Data Source - stackit" +subcategory: "" +description: |- + Network area region data source schema. +--- + +# stackit_network_area_region (Data Source) + +Network area region data source schema. + +## Example Usage + +```terraform +data "stackit_network_area_region" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `network_area_id` (String) The network area ID. +- `organization_id` (String) STACKIT organization ID to which the network area is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`,`region`". +- `ipv4` (Attributes) The regional IPv4 config of a network area. (see [below for nested schema](#nestedatt--ipv4)) + + +### Nested Schema for `ipv4` + +Read-Only: + +- `default_nameservers` (List of String) List of DNS Servers/Nameservers. +- `default_prefix_length` (Number) The default prefix length for networks in the network area. +- `max_prefix_length` (Number) The maximal prefix length for networks in the network area. +- `min_prefix_length` (Number) The minimal prefix length for networks in the network area. +- `network_ranges` (Attributes List) List of Network ranges. (see [below for nested schema](#nestedatt--ipv4--network_ranges)) +- `transfer_network` (String) IPv4 Classless Inter-Domain Routing (CIDR). + + +### Nested Schema for `ipv4.network_ranges` + +Read-Only: + +- `network_range_id` (String) +- `prefix` (String) Classless Inter-Domain Routing (CIDR). diff --git a/docs/data-sources/network_area_route.md b/docs/data-sources/network_area_route.md index 688864a9..e17027d5 100644 --- a/docs/data-sources/network_area_route.md +++ b/docs/data-sources/network_area_route.md @@ -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)) + + +### Nested Schema for `destination` + +Read-Only: + +- `type` (String) CIDRV type. Possible values are: `cidrv4`, `cidrv6`. +- `value` (String) An CIDR string. + + + +### 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). diff --git a/docs/data-sources/network_interface.md b/docs/data-sources/network_interface.md index d6570aea..77e5d6ef 100644 --- a/docs/data-sources/network_interface.md +++ b/docs/data-sources/network_interface.md @@ -29,11 +29,15 @@ data "stackit_network_interface" "example" { - `network_interface_id` (String) The network interface ID. - `project_id` (String) STACKIT project ID to which the network interface is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `allowed_addresses` (List of String) The list of CIDR (Classless Inter-Domain Routing) notations. - `device` (String) The device UUID of the network interface. -- `id` (String) Terraform's internal data source ID. It is structured as "`project_id`,`network_id`,`network_interface_id`". +- `id` (String) Terraform's internal data source ID. It is structured as "`project_id`,`region`,`network_id`,`network_interface_id`". - `ipv4` (String) The IPv4 address. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a network interface. - `mac` (String) The MAC address of network interface. diff --git a/docs/data-sources/public_ip.md b/docs/data-sources/public_ip.md index a2db13a7..1f104878 100644 --- a/docs/data-sources/public_ip.md +++ b/docs/data-sources/public_ip.md @@ -27,9 +27,13 @@ data "stackit_public_ip" "example" { - `project_id` (String) STACKIT project ID to which the public IP is associated. - `public_ip_id` (String) The public IP ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only -- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`public_ip_id`". +- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`region`,`public_ip_id`". - `ip` (String) The IP address. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `network_interface_id` (String) Associates the public IP with a network interface or a virtual IP (ID). diff --git a/docs/data-sources/security_group.md b/docs/data-sources/security_group.md index 5a5af8a4..28c38fa7 100644 --- a/docs/data-sources/security_group.md +++ b/docs/data-sources/security_group.md @@ -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. diff --git a/docs/data-sources/security_group_rule.md b/docs/data-sources/security_group_rule.md index 749504de..d5871bd2 100644 --- a/docs/data-sources/security_group_rule.md +++ b/docs/data-sources/security_group_rule.md @@ -29,13 +29,17 @@ data "stackit_security_group_rule" "example" { - `security_group_id` (String) The security group ID. - `security_group_rule_id` (String) The security group rule ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `description` (String) The description of the security group rule. - `direction` (String) The direction of the traffic which the rule should match. Some of the possible values are: Possible values are: `ingress`, `egress`. - `ether_type` (String) The ethertype which the rule should match. - `icmp_parameters` (Attributes) ICMP Parameters. (see [below for nested schema](#nestedatt--icmp_parameters)) -- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`security_group_id`,`security_group_rule_id`". +- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`region`,`security_group_id`,`security_group_rule_id`". - `ip_range` (String) The remote IP range which the rule should match. - `port_range` (Attributes) The range of ports. (see [below for nested schema](#nestedatt--port_range)) - `protocol` (Attributes) The internet protocol which the rule should match. (see [below for nested schema](#nestedatt--protocol)) diff --git a/docs/data-sources/server.md b/docs/data-sources/server.md index 0b02736e..631a4ff9 100644 --- a/docs/data-sources/server.md +++ b/docs/data-sources/server.md @@ -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 diff --git a/docs/data-sources/volume.md b/docs/data-sources/volume.md index 97c8d51d..1b1e4064 100644 --- a/docs/data-sources/volume.md +++ b/docs/data-sources/volume.md @@ -27,11 +27,15 @@ data "stackit_volume" "example" { - `project_id` (String) STACKIT project ID to which the volume is associated. - `volume_id` (String) The volume ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `availability_zone` (String) The availability zone of the volume. - `description` (String) The description of the volume. -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`volume_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`volume_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container - `name` (String) The name of the volume. - `performance_class` (String) The performance class of the volume. Possible values are documented in [Service plans BlockStorage](https://docs.stackit.cloud/products/storage/block-storage/basics/service-plans/#currently-available-service-plans-performance-classes) diff --git a/docs/resources/affinity_group.md b/docs/resources/affinity_group.md index 2f1cbae6..3d8b8351 100644 --- a/docs/resources/affinity_group.md +++ b/docs/resources/affinity_group.md @@ -3,7 +3,7 @@ page_title: "stackit_affinity_group Resource - stackit" subcategory: "" description: |- - Affinity Group schema. Must have a region specified in the provider configuration. + Affinity Group schema. Usage with server resource "stackit_affinity_group" "affinity-group" { @@ -39,7 +39,7 @@ description: |- # stackit_affinity_group (Resource) -Affinity Group schema. Must have a `region` specified in the provider configuration. +Affinity Group schema. @@ -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. diff --git a/docs/resources/image.md b/docs/resources/image.md index abeeefc2..7dfb252f 100644 --- a/docs/resources/image.md +++ b/docs/resources/image.md @@ -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. diff --git a/docs/resources/network.md b/docs/resources/network.md index c11dad6b..6fe44131 100644 --- a/docs/resources/network.md +++ b/docs/resources/network.md @@ -34,12 +34,11 @@ resource "stackit_network" "example_routed_network" { } resource "stackit_network" "example_non_routed_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-non-routed-network" - ipv4_nameservers = ["1.2.3.4", "5.6.7.8"] - ipv4_prefix_length = 24 - ipv4_gateway = "10.1.2.3" - ipv4_prefix = "10.1.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-non-routed-network" + ipv4_nameservers = ["1.2.3.4", "5.6.7.8"] + ipv4_gateway = "10.1.2.3" + ipv4_prefix = "10.1.2.0/24" labels = { "key" = "value" } @@ -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. diff --git a/docs/resources/network_area.md b/docs/resources/network_area.md index 46c308d3..909784c3 100644 --- a/docs/resources/network_area.md +++ b/docs/resources/network_area.md @@ -3,12 +3,12 @@ page_title: "stackit_network_area Resource - stackit" subcategory: "" description: |- - Network area resource schema. Must have a region specified in the provider configuration. + Network area resource schema. --- # stackit_network_area (Resource) -Network area resource schema. Must have a `region` specified in the provider configuration. +Network area resource schema. ## Example Usage @@ -16,12 +16,6 @@ Network area resource schema. Must have a `region` specified in the provider con resource "stackit_network_area" "example" { organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-network-area" - network_ranges = [ - { - prefix = "192.168.0.0/24" - } - ] - transfer_network = "192.168.1.0/24" labels = { "key" = "value" } @@ -40,17 +34,17 @@ import { ### Required - `name` (String) The name of the network area. -- `network_ranges` (Attributes List) List of Network ranges. (see [below for nested schema](#nestedatt--network_ranges)) - `organization_id` (String) STACKIT organization ID to which the network area is associated. -- `transfer_network` (String) Classless Inter-Domain Routing (CIDR). ### Optional -- `default_nameservers` (List of String) List of DNS Servers/Nameservers. -- `default_prefix_length` (Number) The default prefix length for networks in the network area. +- `default_nameservers` (List of String, Deprecated) List of DNS Servers/Nameservers for configuration of network area for region `eu01`. +- `default_prefix_length` (Number, Deprecated) The default prefix length for networks in the network area for region `eu01`. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container -- `max_prefix_length` (Number) The maximal prefix length for networks in the network area. -- `min_prefix_length` (Number) The minimal prefix length for networks in the network area. +- `max_prefix_length` (Number, Deprecated) The maximal prefix length for networks in the network area for region `eu01`. +- `min_prefix_length` (Number, Deprecated) The minimal prefix length for networks in the network area for region `eu01`. +- `network_ranges` (Attributes List, Deprecated) List of Network ranges for configuration of network area for region `eu01`. (see [below for nested schema](#nestedatt--network_ranges)) +- `transfer_network` (String, Deprecated) Classless Inter-Domain Routing (CIDR) for configuration of network area for region `eu01`. ### Read-Only @@ -63,8 +57,8 @@ import { Required: -- `prefix` (String) Classless Inter-Domain Routing (CIDR). +- `prefix` (String, Deprecated) Classless Inter-Domain Routing (CIDR). Read-Only: -- `network_range_id` (String) +- `network_range_id` (String, Deprecated) diff --git a/docs/resources/network_area_region.md b/docs/resources/network_area_region.md new file mode 100644 index 00000000..050fdb35 --- /dev/null +++ b/docs/resources/network_area_region.md @@ -0,0 +1,77 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_network_area_region Resource - stackit" +subcategory: "" +description: |- + Network area region resource schema. +--- + +# stackit_network_area_region (Resource) + +Network area region resource schema. + +## Example Usage + +```terraform +resource "stackit_network_area_region" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + ipv4 = { + transfer_network = "10.1.2.0/24" + network_ranges = [ + { + prefix = "10.0.0.0/16" + } + ] + } +} + +# Only use the import statement, if you want to import an existing network area region +import { + to = stackit_network_area_region.import-example + id = "${var.organization_id},${var.network_area_id},${var.region}" +} +``` + + +## Schema + +### Required + +- `ipv4` (Attributes) The regional IPv4 config of a network area. (see [below for nested schema](#nestedatt--ipv4)) +- `network_area_id` (String) The network area ID. +- `organization_id` (String) STACKIT organization ID to which the network area is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`,`region`". + + +### Nested Schema for `ipv4` + +Required: + +- `network_ranges` (Attributes List) List of Network ranges. (see [below for nested schema](#nestedatt--ipv4--network_ranges)) +- `transfer_network` (String) IPv4 Classless Inter-Domain Routing (CIDR). + +Optional: + +- `default_nameservers` (List of String) List of DNS Servers/Nameservers. +- `default_prefix_length` (Number) The default prefix length for networks in the network area. +- `max_prefix_length` (Number) The maximal prefix length for networks in the network area. +- `min_prefix_length` (Number) The minimal prefix length for networks in the network area. + + +### Nested Schema for `ipv4.network_ranges` + +Required: + +- `prefix` (String) Classless Inter-Domain Routing (CIDR). + +Read-Only: + +- `network_range_id` (String) diff --git a/docs/resources/network_area_route.md b/docs/resources/network_area_route.md index a7f05460..5b9056d3 100644 --- a/docs/resources/network_area_route.md +++ b/docs/resources/network_area_route.md @@ -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. + + +### Nested Schema for `destination` + +Required: + +- `type` (String) CIDRV type. Possible values are: `cidrv4`, `cidrv6`. Only `cidrv4` is supported currently. +- `value` (String) An CIDR string. + + + +### 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. + diff --git a/docs/resources/network_interface.md b/docs/resources/network_interface.md index 4ef8a871..6c7156a5 100644 --- a/docs/resources/network_interface.md +++ b/docs/resources/network_interface.md @@ -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`. diff --git a/docs/resources/public_ip.md b/docs/resources/public_ip.md index fad2560d..f95b9314 100644 --- a/docs/resources/public_ip.md +++ b/docs/resources/public_ip.md @@ -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. diff --git a/docs/resources/public_ip_associate.md b/docs/resources/public_ip_associate.md index 098e6ff5..fd76fc36 100644 --- a/docs/resources/public_ip_associate.md +++ b/docs/resources/public_ip_associate.md @@ -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. diff --git a/docs/resources/security_group.md b/docs/resources/security_group.md index c4f9d06c..eec31aa0 100644 --- a/docs/resources/security_group.md +++ b/docs/resources/security_group.md @@ -40,9 +40,10 @@ import { - `description` (String) The description of the security group. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container +- `region` (String) The resource region. If not defined, the provider region is used. - `stateful` (Boolean) Configures if a security group is stateful or stateless. There can only be one type of security groups per network interface/server. ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`security_group_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`security_group_id`". - `security_group_id` (String) The security group ID. diff --git a/docs/resources/security_group_rule.md b/docs/resources/security_group_rule.md index 452f7c94..97e9fc65 100644 --- a/docs/resources/security_group_rule.md +++ b/docs/resources/security_group_rule.md @@ -52,11 +52,12 @@ import { - `ip_range` (String) The remote IP range which the rule should match. - `port_range` (Attributes) The range of ports. This should only be provided if the protocol is not ICMP. (see [below for nested schema](#nestedatt--port_range)) - `protocol` (Attributes) The internet protocol which the rule should match. (see [below for nested schema](#nestedatt--protocol)) +- `region` (String) The resource region. If not defined, the provider region is used. - `remote_security_group_id` (String) The remote security group which the rule should match. ### Read-Only -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`security_group_id`,`security_group_rule_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`security_group_id`,`security_group_rule_id`". - `security_group_rule_id` (String) The security group rule ID. diff --git a/docs/resources/server.md b/docs/resources/server.md index cdb31878..e7559dfc 100644 --- a/docs/resources/server.md +++ b/docs/resources/server.md @@ -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 diff --git a/docs/resources/server_network_interface_attach.md b/docs/resources/server_network_interface_attach.md index b6c99ce0..eab7c8c9 100644 --- a/docs/resources/server_network_interface_attach.md +++ b/docs/resources/server_network_interface_attach.md @@ -3,12 +3,12 @@ page_title: "stackit_server_network_interface_attach Resource - stackit" subcategory: "" description: |- - Network interface attachment resource schema. Attaches a network interface to a server. Must have a region specified in the provider configuration. The attachment only takes full effect after server reboot. + Network interface attachment resource schema. Attaches a network interface to a server. The attachment only takes full effect after server reboot. --- # stackit_server_network_interface_attach (Resource) -Network interface attachment resource schema. Attaches a network interface to a server. Must have a `region` specified in the provider configuration. The attachment only takes full effect after server reboot. +Network interface attachment resource schema. Attaches a network interface to a server. The attachment only takes full effect after server reboot. ## Example Usage @@ -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`". diff --git a/docs/resources/server_service_account_attach.md b/docs/resources/server_service_account_attach.md index 2b02b074..215c6f5f 100644 --- a/docs/resources/server_service_account_attach.md +++ b/docs/resources/server_service_account_attach.md @@ -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`". diff --git a/docs/resources/server_volume_attach.md b/docs/resources/server_volume_attach.md index 93c5862e..61710ce4 100644 --- a/docs/resources/server_volume_attach.md +++ b/docs/resources/server_volume_attach.md @@ -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`". diff --git a/docs/resources/volume.md b/docs/resources/volume.md index c41ad50d..0e61bb13 100644 --- a/docs/resources/volume.md +++ b/docs/resources/volume.md @@ -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. diff --git a/examples/data-sources/stackit_network_area_region/data-source.tf b/examples/data-sources/stackit_network_area_region/data-source.tf new file mode 100644 index 00000000..f673f587 --- /dev/null +++ b/examples/data-sources/stackit_network_area_region/data-source.tf @@ -0,0 +1,4 @@ +data "stackit_network_area_region" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/resources/stackit_affinity_group/resource.tf b/examples/resources/stackit_affinity_group/resource.tf index 7b4800a8..b0e506ab 100644 --- a/examples/resources/stackit_affinity_group/resource.tf +++ b/examples/resources/stackit_affinity_group/resource.tf @@ -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}" } \ No newline at end of file diff --git a/examples/resources/stackit_image/resource.tf b/examples/resources/stackit_image/resource.tf index a6449223..bf3bd692 100644 --- a/examples/resources/stackit_image/resource.tf +++ b/examples/resources/stackit_image/resource.tf @@ -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}" } \ No newline at end of file diff --git a/examples/resources/stackit_network/resource.tf b/examples/resources/stackit_network/resource.tf index dbf1876d..f5760a04 100644 --- a/examples/resources/stackit_network/resource.tf +++ b/examples/resources/stackit_network/resource.tf @@ -13,12 +13,11 @@ resource "stackit_network" "example_routed_network" { } resource "stackit_network" "example_non_routed_network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-non-routed-network" - ipv4_nameservers = ["1.2.3.4", "5.6.7.8"] - ipv4_prefix_length = 24 - ipv4_gateway = "10.1.2.3" - ipv4_prefix = "10.1.2.0/24" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-non-routed-network" + ipv4_nameservers = ["1.2.3.4", "5.6.7.8"] + ipv4_gateway = "10.1.2.3" + ipv4_prefix = "10.1.2.0/24" labels = { "key" = "value" } @@ -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}" -} \ No newline at end of file + id = "${var.project_id},${var.region},${var.network_id}" +} diff --git a/examples/resources/stackit_network_area/resource.tf b/examples/resources/stackit_network_area/resource.tf index e1cfbe0c..a699e7ca 100644 --- a/examples/resources/stackit_network_area/resource.tf +++ b/examples/resources/stackit_network_area/resource.tf @@ -1,12 +1,6 @@ resource "stackit_network_area" "example" { organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-network-area" - network_ranges = [ - { - prefix = "192.168.0.0/24" - } - ] - transfer_network = "192.168.1.0/24" labels = { "key" = "value" } diff --git a/examples/resources/stackit_network_area_region/resource.tf b/examples/resources/stackit_network_area_region/resource.tf new file mode 100644 index 00000000..bb876b86 --- /dev/null +++ b/examples/resources/stackit_network_area_region/resource.tf @@ -0,0 +1,18 @@ +resource "stackit_network_area_region" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + ipv4 = { + transfer_network = "10.1.2.0/24" + network_ranges = [ + { + prefix = "10.0.0.0/16" + } + ] + } +} + +# Only use the import statement, if you want to import an existing network area region +import { + to = stackit_network_area_region.import-example + id = "${var.organization_id},${var.network_area_id},${var.region}" +} diff --git a/examples/resources/stackit_network_area_route/resource.tf b/examples/resources/stackit_network_area_route/resource.tf index a18d26eb..91ea42d4 100644 --- a/examples/resources/stackit_network_area_route/resource.tf +++ b/examples/resources/stackit_network_area_route/resource.tf @@ -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}" -} \ No newline at end of file + id = "${var.organization_id},${var.network_area_id},${var.region},${var.network_area_route_id}" +} diff --git a/examples/resources/stackit_network_interface/resource.tf b/examples/resources/stackit_network_interface/resource.tf index 7f0ba90d..2ff598ff 100644 --- a/examples/resources/stackit_network_interface/resource.tf +++ b/examples/resources/stackit_network_interface/resource.tf @@ -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}" } \ No newline at end of file diff --git a/examples/resources/stackit_public_ip/resource.tf b/examples/resources/stackit_public_ip/resource.tf index ec63a744..691cfc34 100644 --- a/examples/resources/stackit_public_ip/resource.tf +++ b/examples/resources/stackit_public_ip/resource.tf @@ -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}" } \ No newline at end of file diff --git a/examples/resources/stackit_public_ip_associate/resource.tf b/examples/resources/stackit_public_ip_associate/resource.tf index 08f7c219..a025d0d7 100644 --- a/examples/resources/stackit_public_ip_associate/resource.tf +++ b/examples/resources/stackit_public_ip_associate/resource.tf @@ -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}" } diff --git a/examples/resources/stackit_server/resource.tf b/examples/resources/stackit_server/resource.tf index bacfb6fe..6fcb8ebc 100644 --- a/examples/resources/stackit_server/resource.tf +++ b/examples/resources/stackit_server/resource.tf @@ -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}" } \ No newline at end of file diff --git a/examples/resources/stackit_server_network_interface_attach/resource.tf b/examples/resources/stackit_server_network_interface_attach/resource.tf index 30b13c2b..054421dd 100644 --- a/examples/resources/stackit_server_network_interface_attach/resource.tf +++ b/examples/resources/stackit_server_network_interface_attach/resource.tf @@ -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}" } \ No newline at end of file diff --git a/examples/resources/stackit_server_service_account_attach/resource.tf b/examples/resources/stackit_server_service_account_attach/resource.tf index 1860dc4b..0658f55d 100644 --- a/examples/resources/stackit_server_service_account_attach/resource.tf +++ b/examples/resources/stackit_server_service_account_attach/resource.tf @@ -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}" } \ No newline at end of file diff --git a/examples/resources/stackit_server_volume_attach/resource.tf b/examples/resources/stackit_server_volume_attach/resource.tf index 0fae0fbe..a503eabe 100644 --- a/examples/resources/stackit_server_volume_attach/resource.tf +++ b/examples/resources/stackit_server_volume_attach/resource.tf @@ -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}" } \ No newline at end of file diff --git a/examples/resources/stackit_volume/resource.tf b/examples/resources/stackit_volume/resource.tf index 6bfd61f9..7a5c28ec 100644 --- a/examples/resources/stackit_volume/resource.tf +++ b/examples/resources/stackit_volume/resource.tf @@ -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}" } \ No newline at end of file diff --git a/go.mod b/go.mod index 28cba552..c938e177 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 7de9efc5..897ebf8d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/stackit/internal/services/iaas/affinitygroup/datasource.go b/stackit/internal/services/iaas/affinitygroup/datasource.go index 9a43e18d..4937f9c6 100644 --- a/stackit/internal/services/iaas/affinitygroup/datasource.go +++ b/stackit/internal/services/iaas/affinitygroup/datasource.go @@ -33,16 +33,18 @@ func NewAffinityGroupDatasource() datasource.DataSource { } type affinityGroupDatasource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } func (d *affinityGroupDatasource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -61,7 +63,7 @@ func (d *affinityGroupDatasource) Schema(_ context.Context, _ datasource.SchemaR MarkdownDescription: descriptionMain, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`affinity_group_id`\".", + Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`affinity_group_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -72,6 +74,11 @@ func (d *affinityGroupDatasource) Schema(_ context.Context, _ datasource.SchemaR validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "affinity_group_id": schema.StringAttribute{ Description: "The affinity group ID.", Required: true, @@ -117,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)) } diff --git a/stackit/internal/services/iaas/affinitygroup/resource.go b/stackit/internal/services/iaas/affinitygroup/resource.go index ff4a6e52..6597ff65 100644 --- a/stackit/internal/services/iaas/affinitygroup/resource.go +++ b/stackit/internal/services/iaas/affinitygroup/resource.go @@ -17,7 +17,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -33,12 +32,14 @@ var ( _ resource.Resource = &affinityGroupResource{} _ resource.ResourceWithConfigure = &affinityGroupResource{} _ resource.ResourceWithImportState = &affinityGroupResource{} + _ resource.ResourceWithModifyPlan = &affinityGroupResource{} ) // Model is the provider's internal model type Model struct { Id types.String `tfsdk:"id"` ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` AffinityGroupId types.String `tfsdk:"affinity_group_id"` Name types.String `tfsdk:"name"` Policy types.String `tfsdk:"policy"` @@ -51,7 +52,8 @@ func NewAffinityGroupResource() resource.Resource { // affinityGroupResource is the resource implementation. type affinityGroupResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -59,14 +61,45 @@ func (r *affinityGroupResource) Metadata(_ context.Context, req resource.Metadat resp.TypeName = req.ProviderTypeName + "_affinity_group" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *affinityGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *affinityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -75,13 +108,13 @@ func (r *affinityGroupResource) Configure(ctx context.Context, req resource.Conf } func (r *affinityGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Affinity Group schema. Must have a `region` specified in the provider configuration." + description := "Affinity Group schema." resp.Schema = schema.Schema{ Description: description, MarkdownDescription: description + "\n\n" + exampleUsageWithServer + policies, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`affinity_group_id`\".", + Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`affinity_group_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -98,6 +131,15 @@ func (r *affinityGroupResource) Schema(_ context.Context, _ resource.SchemaReque validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "affinity_group_id": schema.StringAttribute{ Description: "The affinity group ID.", Computed: true, @@ -153,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) diff --git a/stackit/internal/services/iaas/affinitygroup/resource_test.go b/stackit/internal/services/iaas/affinitygroup/resource_test.go index a4e20391..26f4bc05 100644 --- a/stackit/internal/services/iaas/affinitygroup/resource_test.go +++ b/stackit/internal/services/iaas/affinitygroup/resource_test.go @@ -11,52 +11,56 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.AffinityGroup + region string + } tests := []struct { description string - state Model - input *iaas.AffinityGroup + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - AffinityGroupId: types.StringValue("aid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + AffinityGroupId: types.StringValue("aid"), + }, + input: &iaas.AffinityGroup{ + Id: utils.Ptr("aid"), + }, + region: "eu01", }, - &iaas.AffinityGroup{ - Id: utils.Ptr("aid"), - }, - Model{ - Id: types.StringValue("pid,aid"), + expected: Model{ + Id: types.StringValue("pid,eu01,aid"), ProjectId: types.StringValue("pid"), AffinityGroupId: types.StringValue("aid"), Name: types.StringNull(), Policy: types.StringNull(), Members: types.ListNull(types.StringType), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_affinity_group_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_affinity_group_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.AffinityGroup{}, }, - &iaas.AffinityGroup{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -64,7 +68,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed") } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %v", diff) } diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go index dda89f70..a44aa75a 100644 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -13,6 +13,8 @@ import ( "sync" "testing" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -21,7 +23,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" waitAlpha "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -59,17 +60,17 @@ var ( //go:embed testdata/resource-network-area-max.tf resourceNetworkAreaMaxConfig string - //go:embed testdata/resource-network-v1-min.tf - resourceNetworkV1MinConfig string + //go:embed testdata/resource-network-area-region-min.tf + resourceNetworkAreaRegionMinConfig string - //go:embed testdata/resource-network-v1-max.tf - resourceNetworkV1MaxConfig string + //go:embed testdata/resource-network-area-region-max.tf + resourceNetworkAreaRegionMaxConfig string - //go:embed testdata/resource-network-v2-min.tf - resourceNetworkV2MinConfig string + //go:embed testdata/resource-network-min.tf + resourceNetworkMinConfig string - //go:embed testdata/resource-network-v2-max.tf - resourceNetworkV2MaxConfig string + //go:embed testdata/resource-network-max.tf + resourceNetworkMaxConfig string //go:embed testdata/resource-network-interface-min.tf resourceNetworkInterfaceMinConfig string @@ -101,13 +102,14 @@ var ( const ( keypairPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDsPd27M449akqCtdFg2+AmRVJz6eWio0oMP9dVg7XZ" - // TODO: create network area using terraform resource instead once it's out of experimental stage and GA - testNetworkAreaId = "25bbf23a-8134-4439-9f5e-1641caf8354e" ) +// SERVER - MIN + var testConfigServerVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), + "network_name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "machine_type": config.StringVariable("t1.1"), "image_id": config.StringVariable("a2c127b2-b1b5-4aee-986f-41cd11b41279"), } @@ -122,6 +124,8 @@ var testConfigServerVarsMinUpdated = func() config.Variables { return updatedConfig }() +// SERVER - MAX + var testConfigServerVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), @@ -162,17 +166,23 @@ var testConfigServerVarsMaxUpdatedDesiredStatus = func() config.Variables { return updatedConfig }() +// AFFINITY GROUP - MIN + var testConfigAffinityGroupVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "policy": config.StringVariable("hard-affinity"), } +// NETWORK INTERFACE - MIN + var testConfigNetworkInterfaceVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), } +// NETWORK INTERFACE - MAX + var testConfigNetworkInterfaceVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), @@ -195,6 +205,8 @@ var testConfigNetworkInterfaceVarsMaxUpdated = func() config.Variables { return updatedConfig }() +// VOLUME - MIN + var testConfigVolumeVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "availability_zone": config.StringVariable("eu01-1"), @@ -210,6 +222,8 @@ var testConfigVolumeVarsMinUpdated = func() config.Variables { return updatedConfig }() +// VOLUME - MAX + var testConfigVolumeVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "availability_zone": config.StringVariable("eu01-1"), @@ -232,28 +246,38 @@ var testConfigVolumeVarsMaxUpdated = func() config.Variables { return updatedConfig }() -var testConfigNetworkV1VarsMin = config.Variables{ +// NETWORK - MIN + +var testConfigNetworkVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), } -var testConfigNetworkV1VarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "ipv4_gateway": config.StringVariable("10.2.2.1"), - "ipv4_nameserver_0": config.StringVariable("10.2.2.2"), - "ipv4_nameserver_1": config.StringVariable("10.2.2.3"), - "ipv4_prefix": config.StringVariable("10.2.2.0/24"), - "ipv4_prefix_length": config.IntegerVariable(24), - "routed": config.BoolVariable(false), - "label": config.StringVariable("label"), +var testConfigNetworkVarsMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigNetworkVarsMin) + updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) + return updatedConfig +}() + +// NETWORK - MAX + +var testConfigNetworkVarsMax = config.Variables{ + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), + "ipv4_gateway": config.StringVariable("10.2.2.1"), + "ipv4_nameserver_0": config.StringVariable("10.2.2.2"), + "ipv4_nameserver_1": config.StringVariable("10.2.2.3"), + "ipv4_prefix": config.StringVariable("10.2.2.0/24"), + "ipv4_prefix_length": config.IntegerVariable(24), + "routed": config.BoolVariable(true), + "label": config.StringVariable("label"), + "organization_id": config.StringVariable(testutil.OrganizationId), + "service_account_mail": config.StringVariable(testutil.TestProjectServiceAccountEmail), } -var testConfigNetworkV1VarsMaxUpdated = func() config.Variables { +var testConfigNetworkVarsMaxUpdated = func() config.Variables { updatedConfig := config.Variables{} - for k, v := range testConfigNetworkV1VarsMax { - updatedConfig[k] = v - } + maps.Copy(updatedConfig, testConfigNetworkVarsMax) updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) updatedConfig["ipv4_gateway"] = config.StringVariable("") updatedConfig["ipv4_nameserver_0"] = config.StringVariable("10.2.2.10") @@ -261,49 +285,11 @@ var testConfigNetworkV1VarsMaxUpdated = func() config.Variables { return updatedConfig }() -var testConfigNetworkV2VarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), -} - -var testConfigNetworkV2VarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigNetworkV2VarsMin) - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - return updatedConfig -}() - -var testConfigNetworkV2VarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "ipv4_gateway": config.StringVariable("10.2.2.1"), - "ipv4_nameserver_0": config.StringVariable("10.2.2.2"), - "ipv4_nameserver_1": config.StringVariable("10.2.2.3"), - "ipv4_prefix": config.StringVariable("10.2.2.0/24"), - "ipv4_prefix_length": config.IntegerVariable(24), - "routed": config.BoolVariable(true), - "label": config.StringVariable("label"), - "organization_id": config.StringVariable(testutil.OrganizationId), - "network_area_id": config.StringVariable(testNetworkAreaId), -} - -var testConfigNetworkV2VarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigNetworkV2VarsMax) - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - updatedConfig["ipv4_gateway"] = config.StringVariable("") - updatedConfig["ipv4_nameserver_0"] = config.StringVariable("10.2.2.10") - updatedConfig["label"] = config.StringVariable("updated") - return updatedConfig -}() +// NETWORK AREA - MIN var testConfigNetworkAreaVarsMin = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "transfer_network": config.StringVariable("10.1.2.0/24"), - "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), - "route_prefix": config.StringVariable("1.1.1.0/24"), - "route_next_hop": config.StringVariable("1.1.1.1"), + "organization_id": config.StringVariable(testutil.OrganizationId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), } var testConfigNetworkAreaVarsMinUpdated = func() config.Variables { @@ -312,22 +298,25 @@ var testConfigNetworkAreaVarsMinUpdated = func() config.Variables { updatedConfig[k] = v } updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") return updatedConfig }() +// NETWORK AREA - MAX + var testConfigNetworkAreaVarsMax = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "transfer_network": config.StringVariable("10.1.2.0/24"), - "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), - "default_nameservers": config.StringVariable("1.1.1.1"), - "default_prefix_length": config.IntegerVariable(24), - "max_prefix_length": config.IntegerVariable(24), - "min_prefix_length": config.IntegerVariable(16), - "route_prefix": config.StringVariable("1.1.1.0/24"), - "route_next_hop": config.StringVariable("1.1.1.1"), - "label": config.StringVariable("label"), + "organization_id": config.StringVariable(testutil.OrganizationId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), + "transfer_network": config.StringVariable("10.1.2.0/24"), + "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), + "default_nameservers": config.StringVariable("1.1.1.1"), + "default_prefix_length": config.IntegerVariable(24), + "max_prefix_length": config.IntegerVariable(24), + "min_prefix_length": config.IntegerVariable(16), + "route_destination_type": config.StringVariable("cidrv4"), + "route_destination_value": config.StringVariable("1.1.1.0/24"), + "route_next_hop_type": config.StringVariable("ipv4"), + "route_next_hop_value": config.StringVariable("1.1.1.1"), + "label": config.StringVariable("label"), } var testConfigNetworkAreaVarsMaxUpdated = func() config.Variables { @@ -341,10 +330,61 @@ var testConfigNetworkAreaVarsMaxUpdated = func() config.Variables { updatedConfig["default_prefix_length"] = config.IntegerVariable(25) updatedConfig["max_prefix_length"] = config.IntegerVariable(25) updatedConfig["min_prefix_length"] = config.IntegerVariable(20) - updatedConfig["label"] = config.StringVariable("updated") + // TODO: enable once the IaaS API supports IPv6 + // updatedConfig["route_destination_type"] = config.StringVariable("cidrv6") + // updatedConfig["route_destination_value"] = config.StringVariable("2001:db8:3c4d:15::1a2b:3c4d/64") + // updatedConfig["route_next_hop_type"] = config.StringVariable("ipv6") + // updatedConfig["route_next_hop_value"] = config.StringVariable("2001:db8:3c4d:15::1a2b:3c4d") + // updatedConfig["label"] = config.StringVariable("updated") return updatedConfig }() +// NETWORK AREA REGION - MIN + +var testConfigNetworkAreaRegionVarsMin = config.Variables{ + "organization_id": config.StringVariable(testutil.OrganizationId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), + "transfer_network": config.StringVariable("10.1.2.0/24"), + "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), +} + +var testConfigNetworkAreaRegionVarsMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + for k, v := range testConfigNetworkAreaRegionVarsMin { + updatedConfig[k] = v + } + updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") + return updatedConfig +}() + +// NETWORK AREA REGION - MAX + +var testConfigNetworkAreaRegionVarsMax = config.Variables{ + "organization_id": config.StringVariable(testutil.OrganizationId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), + "transfer_network": config.StringVariable("10.1.2.0/24"), + "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), + "default_nameservers": config.StringVariable("1.1.1.1"), + "default_prefix_length": config.IntegerVariable(26), + "min_prefix_length": config.IntegerVariable(25), + "max_prefix_length": config.IntegerVariable(28), +} + +var testConfigNetworkAreaRegionVarsMaxUpdated = func() config.Variables { + updatedConfig := config.Variables{} + for k, v := range testConfigNetworkAreaRegionVarsMax { + updatedConfig[k] = v + } + updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") + updatedConfig["default_nameservers"] = config.StringVariable("8.8.8.8") + updatedConfig["default_prefix_length"] = config.IntegerVariable(27) + updatedConfig["min_prefix_length"] = config.IntegerVariable(26) + updatedConfig["max_prefix_length"] = config.IntegerVariable(28) + return updatedConfig +}() + +// SECURITY GROUP - MIN + var testConfigSecurityGroupsVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), @@ -360,6 +400,8 @@ func testConfigSecurityGroupsVarsMinUpdated() config.Variables { return updatedConfig } +// SECURITY GROUP - MAX + var testConfigSecurityGroupsVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), @@ -390,6 +432,8 @@ func testConfigSecurityGroupsVarsMaxUpdated() config.Variables { return updatedConfig } +// IMAGE - MIN + var testConfigImageVarsMin = func() config.Variables { localFilePath := testutil.TestImageLocalFilePath if localFilePath == "default" { @@ -417,6 +461,8 @@ var testConfigImageVarsMinUpdated = func() config.Variables { return updatedConfig }() +// IMAGE - MAX + var testConfigImageVarsMax = func() config.Variables { localFilePath := testutil.TestImageLocalFilePath if localFilePath == "default" { @@ -476,11 +522,15 @@ var testConfigImageVarsMaxUpdated = func() config.Variables { return updatedConfig }() +// KEYPAIR - MIN + var testConfigKeyPairMin = config.Variables{ "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), "public_key": config.StringVariable(keypairPublicKey), } +// KEYPAIR - MAX + var testConfigKeyPairMax = config.Variables{ "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), "public_key": config.StringVariable(keypairPublicKey), @@ -503,28 +553,31 @@ var testConfigMachineTypeVars = config.Variables{ // if no local file is provided the test should create a default file and work with this instead of failing var localFileForIaasImage os.File -func TestAccNetworkV1Min(t *testing.T) { - t.Logf("TestAccNetworkV1Min name: %s", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])) +func TestAccNetworkMin(t *testing.T) { + t.Logf("TestAccNetworkMin name: %s", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])) resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ // Creation { - ConfigVariables: testConfigNetworkV1VarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkV1MinConfig), + ConfigVariables: testConfigNetworkVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMinConfig), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "region", testutil.Region), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), + resource.TestCheckResourceAttrSet("stackit_network.network", "region"), + resource.TestCheckNoResourceAttr("stackit_network.network", "routing_table_id"), ), }, // Data source { - ConfigVariables: testConfigNetworkV1VarsMin, + ConfigVariables: testConfigNetworkVarsMin, Config: fmt.Sprintf(` %s %s @@ -534,21 +587,24 @@ func TestAccNetworkV1Min(t *testing.T) { network_id = stackit_network.network.network_id } `, - testutil.IaaSProviderConfig(), resourceNetworkV1MinConfig, + testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMinConfig, ), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])), + resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), + resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), + resource.TestCheckResourceAttr("data.stackit_network.network", "region", testutil.Region), resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("data.stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), + resource.TestCheckResourceAttrSet("data.stackit_network.network", "region"), + resource.TestCheckNoResourceAttr("data.stackit_network.network", "routing_table_id"), ), }, // Import { - ConfigVariables: testConfigNetworkV1VarsMin, + ConfigVariables: testConfigNetworkVarsMin, ResourceName: "stackit_network.network", ImportStateIdFunc: func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources["stackit_network.network"] @@ -559,70 +615,115 @@ func TestAccNetworkV1Min(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil }, ImportState: true, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMin["name"])), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), + resource.TestCheckResourceAttrSet("stackit_network.network", "region"), + resource.TestCheckNoResourceAttr("stackit_network.network", "routing_table_id"), + ), + }, + // Update + { + ConfigVariables: testConfigNetworkVarsMinUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), + resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMinUpdated["project_id"])), + resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMinUpdated["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), + resource.TestCheckResourceAttrSet("stackit_network.network", "region"), + resource.TestCheckNoResourceAttr("stackit_network.network", "routing_table_id"), ), }, - // In this minimal setup, no update can be performed // Deletion is done by the framework implicitly }, }) } -func TestAccNetworkV1Max(t *testing.T) { - t.Logf("TestAccNetworkV1Max name: %s", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])) +func TestAccNetworkMax(t *testing.T) { + t.Logf("TestAccNetworkMax name: %s", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])) resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ // Creation { - ConfigVariables: testConfigNetworkV1VarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkV1MaxConfig), + ConfigVariables: testConfigNetworkVarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( + // Network with prefix resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_gateway"])), + resource.TestCheckResourceAttrPair( + "stackit_resourcemanager_project.project", "project_id", + "stackit_network.network_prefix", "project_id", + ), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway"), resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "public_ip"), + // Network with prefix_length resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), + resource.TestCheckResourceAttrPair( + "stackit_resourcemanager_project.project", "project_id", + "stackit_network.network_prefix_length", "project_id", + ), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_gateway"), + // resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "public_ip"), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "public_ip"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), + + resource.TestCheckResourceAttrPair( + "stackit_network.network_prefix_length", "routing_table_id", + "stackit_routing_table.routing_table", "routing_table_id", + ), + + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_routing_table.routing_table", "network_area_id", + ), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), + resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), ), }, // Data source { - ConfigVariables: testConfigNetworkV1VarsMax, + ConfigVariables: testConfigNetworkVarsMax, Config: fmt.Sprintf(` %s %s @@ -632,319 +733,6 @@ func TestAccNetworkV1Max(t *testing.T) { network_id = stackit_network.network_prefix.network_id } - data "stackit_network" "network_prefix_length" { - project_id = stackit_network.network_prefix_length.project_id - network_id = stackit_network.network_prefix_length.network_id - } - `, - testutil.IaaSProviderConfig(), resourceNetworkV1MaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_gateway"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), - - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["name"])), - resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["label"])), - ), - }, - // Import - { - ConfigVariables: testConfigNetworkV1VarsMax, - ResourceName: "stackit_network.network_prefix", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network_prefix"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_gateway"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // nameservers may be returned in a randomized order, so we have to check them with a helper function - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - ), - }, - { - ConfigVariables: testConfigNetworkV1VarsMax, - ResourceName: "stackit_network.network_prefix_length", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network_prefix_length"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix_length") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["project_id"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - // nameservers may be returned in a randomized order, so we have to check them with a helper function - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMax["routed"])), - ), - }, - // Update - { - ConfigVariables: testConfigNetworkV1VarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkV1MaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["name"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), - - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["name"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["ipv4_prefix_length"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV1VarsMaxUpdated["label"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "public_ip"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkV2Min(t *testing.T) { - t.Logf("TestAccNetworkV2Min name: %s", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckNetworkV2Destroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkV2VarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - resource.TestCheckResourceAttrSet("stackit_network.network", "region"), - resource.TestCheckResourceAttrSet("stackit_network.network", "routing_table_id"), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkV2VarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network" "network" { - project_id = stackit_network.network.project_id - network_id = stackit_network.network.network_id - } - `, - testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "region"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "routing_table_id"), - ), - }, - - // Import - { - ConfigVariables: testConfigNetworkV2VarsMin, - ResourceName: "stackit_network.network", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network") - } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, networkId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMin["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - resource.TestCheckResourceAttrSet("stackit_network.network", "region"), - resource.TestCheckResourceAttrSet("stackit_network.network", "routing_table_id"), - ), - }, - // Update - { - ConfigVariables: testConfigNetworkV2VarsMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMinUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMinUpdated["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - resource.TestCheckResourceAttrSet("stackit_network.network", "region"), - resource.TestCheckResourceAttrSet("stackit_network.network", "routing_table_id"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkV2Max(t *testing.T) { - t.Logf("TestAccNetworkV2Max name: %s", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckNetworkV2Destroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkV2VarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // TODO: enable test cases for prefix option, when the API works again - // Network with prefix - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_gateway"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), - - // Network with prefix_length - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_gateway"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "public_ip"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), - - resource.TestCheckResourceAttrPair( - "stackit_network.network_prefix_length", "routing_table_id", - "stackit_routing_table.routing_table", "routing_table_id", - ), - - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), - resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkV2VarsMax, - Config: fmt.Sprintf(` - %s - %s - - //data "stackit_network" "network_prefix" { - // project_id = stackit_network.network_prefix.project_id - // network_id = stackit_network.network_prefix.network_id - //} - data "stackit_network" "network_prefix_length" { project_id = stackit_network.network_prefix_length.project_id network_id = stackit_network.network_prefix_length.network_id @@ -956,39 +744,44 @@ func TestAccNetworkV2Max(t *testing.T) { routing_table_id = stackit_routing_table.routing_table.routing_table_id } `, - testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MaxConfig, + testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMaxConfig, ), Check: resource.ComposeAggregateTestCheckFunc( - // TODO: enable test cases for prefix option, when the API works again // Network with prefix - // resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "network_id"), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_gateway"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - // resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - // resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "ipv6_prefixes.#"), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - // resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttrPair( + "stackit_resourcemanager_project.project", "project_id", + "data.stackit_network.network_prefix", "project_id", + ), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), // Network with prefix_length resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + resource.TestCheckResourceAttrPair( + "stackit_resourcemanager_project.project", "project_id", + "data.stackit_network.network_prefix_length", "project_id", + ), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), // resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv4_gateway"), resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["label"])), + resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "region", testutil.Region), resource.TestCheckResourceAttrPair( @@ -997,10 +790,13 @@ func TestAccNetworkV2Max(t *testing.T) { ), // Routing table - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["network_area_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "data.stackit_routing_table.routing_table", "network_area_id", + ), resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["name"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.%", "0"), resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "region", testutil.Region), resource.TestCheckNoResourceAttr("data.stackit_routing_table.routing_table", "description"), @@ -1010,106 +806,130 @@ func TestAccNetworkV2Max(t *testing.T) { ), }, // Import - // TODO: enable test cases for prefix option, when the API works again - //{ - // ConfigVariables: testConfigNetworkV2VarsMax, - // ResourceName: "stackit_network.network_prefix", - // ImportStateIdFunc: func(s *terraform.State) (string, error) { - // r, ok := s.RootModule().Resources["stackit_network.network_prefix"] - // if !ok { - // return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix") - // } - // networkId, ok := r.Primary.Attributes["network_id"] - // if !ok { - // return "", fmt.Errorf("couldn't find attribute network_id") - // } - // return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil - // }, - // ImportState: true, - // Check: resource.ComposeAggregateTestCheckFunc( - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_gateway"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // // nameservers may be returned in a randomized order, so we have to check them with a helper function - // resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - // resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), - // ), - // }, { - ConfigVariables: testConfigNetworkV2VarsMax, - ResourceName: "stackit_network.network_prefix_length", + ConfigVariables: testConfigNetworkVarsMax, + ResourceName: "stackit_network.network_prefix", ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network_prefix_length"] + projectResource, ok := s.RootModule().Resources["stackit_resourcemanager_project.project"] if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix_length") + return "", fmt.Errorf("couldn't find stackit_resourcemanager_project.project") } - region, ok := r.Primary.Attributes["region"] + projectId, ok := projectResource.Primary.Attributes["project_id"] if !ok { - return "", fmt.Errorf("couldn't find attribute region") + return "", fmt.Errorf("couldn't find attribute project_id") + } + + r, ok := s.RootModule().Resources["stackit_network.network_prefix"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix") } networkId, ok := r.Primary.Attributes["network_id"] if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, networkId), nil + return fmt.Sprintf("%s,%s,%s", projectId, testutil.Region, networkId), nil + }, + ImportState: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttrPair( + "stackit_resourcemanager_project.project", "project_id", + "stackit_network.network_prefix", "project_id", + ), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + // nameservers may be returned in a randomized order, so we have to check them with a helper function + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), + ), + }, + { + ConfigVariables: testConfigNetworkVarsMax, + ResourceName: "stackit_network.network_prefix_length", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + projectResource, ok := s.RootModule().Resources["stackit_resourcemanager_project.project"] + if !ok { + return "", fmt.Errorf("couldn't find stackit_resourcemanager_project.project") + } + projectId, ok := projectResource.Primary.Attributes["project_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute project_id") + } + + r, ok := s.RootModule().Resources["stackit_network.network_prefix_length"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix_length") + } + networkId, ok := r.Primary.Attributes["network_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_id") + } + return fmt.Sprintf("%s,%s,%s", projectId, testutil.Region, networkId), nil }, ImportState: true, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["project_id"])), + resource.TestCheckResourceAttrPair( + "stackit_resourcemanager_project.project", "project_id", + "stackit_network.network_prefix_length", "project_id", + ), // resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), // nameservers may be returned in a randomized order, so we have to check them with a helper function - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_nameserver_1"])), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), + resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMax["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), ), }, // Update { - ConfigVariables: testConfigNetworkV2VarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkV2MaxConfig), + ConfigVariables: testConfigNetworkVarsMaxUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( - // TODO: enable test cases for prefix option, when the API works again - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["project_id"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["name"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "ipv4_gateway"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), + resource.TestCheckResourceAttrPair( + "stackit_resourcemanager_project.project", "project_id", + "stackit_network.network_prefix", "project_id", + ), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv4_gateway"), // resource.TestCheckResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway", "true"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_0"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_1"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_prefix"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_prefix_length"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - // resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["routed"])), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["label"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "public_ip"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["label"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "public_ip"), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "project_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["name"])), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), + resource.TestCheckResourceAttrPair( + "stackit_resourcemanager_project.project", "project_id", + "stackit_network.network_prefix_length", "project_id", + ), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), + resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_gateway"), // resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["ipv4_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_0"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_1"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix_length"])), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["label"])), + resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv6_prefixes.#"), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["routed"])), + resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["label"])), resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "public_ip"), resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), @@ -1119,10 +939,13 @@ func TestAccNetworkV2Max(t *testing.T) { ), // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["network_area_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_routing_table.routing_table", "network_area_id", + ), resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkV2VarsMaxUpdated["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), @@ -1151,22 +974,7 @@ func TestAccNetworkAreaMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["organization_id"])), resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_ranges.0.network_range_id"), - - // Network Area Route - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "organization_id", - "stackit_network_area.network_area", "organization_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "network_area_id", - "stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["route_prefix"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["route_next_hop"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "0"), ), }, // Data source @@ -1180,12 +988,6 @@ func TestAccNetworkAreaMin(t *testing.T) { organization_id = stackit_network_area.network_area.organization_id network_area_id = stackit_network_area.network_area.network_area_id } - - data "stackit_network_area_route" "network_area_route" { - organization_id = stackit_network_area.network_area.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - network_area_route_id = stackit_network_area_route.network_area_route.network_area_route_id - } `, testutil.IaaSProviderConfig(), resourceNetworkAreaMinConfig, ), @@ -1198,26 +1000,7 @@ func TestAccNetworkAreaMin(t *testing.T) { "stackit_network_area.network_area", "network_area_id", ), resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["name"])), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("data.stackit_network_area.network_area", "network_ranges.0.network_range_id"), - - // Network Area Route - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_route.network_area_route", "organization_id", - "data.stackit_network_area.network_area", "organization_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_route.network_area_route", "network_area_id", - "data.stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_route.network_area_route", "network_area_route_id", - "stackit_network_area_route.network_area_route", "network_area_route_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["route_prefix"])), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "next_hop", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["route_next_hop"])), + resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "network_ranges.#", "0"), ), }, // Import @@ -1238,27 +1021,6 @@ func TestAccNetworkAreaMin(t *testing.T) { ImportState: true, ImportStateVerify: true, }, - { - ConfigVariables: testConfigNetworkAreaVarsMinUpdated, - ResourceName: "stackit_network_area_route.network_area_route", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_area_route.network_area_route"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_area_route.network_area_route") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - networkAreaRouteId, ok := r.Primary.Attributes["network_area_route_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_route_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, networkAreaRouteId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, // Update { ConfigVariables: testConfigNetworkAreaVarsMinUpdated, @@ -1268,22 +1030,7 @@ func TestAccNetworkAreaMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["organization_id"])), resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["name"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_ranges.0.network_range_id"), - - // Network Area Route - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "organization_id", - "stackit_network_area.network_area", "organization_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "network_area_id", - "stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["route_prefix"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["route_next_hop"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "0"), ), }, // Deletion is done by the framework implicitly @@ -1326,8 +1073,10 @@ func TestAccNetworkAreaMax(t *testing.T) { "stackit_network_area.network_area", "network_area_id", ), resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_prefix"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop"])), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "destination.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_destination_type"])), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "destination.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_destination_value"])), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop_type"])), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop_value"])), resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["label"])), ), }, @@ -1384,8 +1133,10 @@ func TestAccNetworkAreaMax(t *testing.T) { "stackit_network_area_route.network_area_route", "network_area_route_id", ), resource.TestCheckResourceAttrSet("data.stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_prefix"])), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "next_hop", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop"])), + resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "destination.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_destination_type"])), + resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "destination.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_destination_value"])), + resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "next_hop.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop_type"])), + resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "next_hop.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop_value"])), ), }, // Import @@ -1403,8 +1154,21 @@ func TestAccNetworkAreaMax(t *testing.T) { } return fmt.Sprintf("%s,%s", testutil.OrganizationId, networkAreaId), nil }, - ImportState: true, - ImportStateVerify: true, + ImportState: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["organization_id"])), + resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["name"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["label"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_nameservers"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["max_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area.network_area", "min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["min_prefix_length"])), + ), }, { ConfigVariables: testConfigNetworkAreaVarsMaxUpdated, @@ -1422,7 +1186,7 @@ func TestAccNetworkAreaMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_area_route_id") } - return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, networkAreaRouteId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.OrganizationId, networkAreaId, testutil.Region, networkAreaRouteId), nil }, ImportState: true, ImportStateVerify: true, @@ -1456,8 +1220,10 @@ func TestAccNetworkAreaMax(t *testing.T) { "stackit_network_area.network_area", "network_area_id", ), resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_prefix"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_next_hop"])), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "destination.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_destination_type"])), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "destination.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_destination_value"])), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_next_hop_type"])), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_next_hop_value"])), resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["label"])), ), }, @@ -1466,6 +1232,247 @@ func TestAccNetworkAreaMax(t *testing.T) { }) } +func TestAccNetworkAreaRegionMin(t *testing.T) { + t.Logf("TestAccNetworkAreaRegionMin name: %s", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["name"])) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigNetworkAreaRegionVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMinConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionCreate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + // Network Area + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["transfer_network"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckNoResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", "25"), // default value + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", "24"), // default value + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", "29"), // default value + ), + }, + // Data source + { + ConfigVariables: testConfigNetworkAreaRegionVarsMin, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_network_area_region" "network_area_region" { + organization_id = stackit_network_area_region.network_area_region.organization_id + network_area_id = stackit_network_area_region.network_area_region.network_area_id + } + `, + testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMinConfig, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionNoop), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["organization_id"])), + resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "network_area_id"), + resource.TestCheckResourceAttrPair( + "data.stackit_network_area_region.network_area_region", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["transfer_network"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", "25"), // default value + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", "24"), // default value + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", "29"), // default value + ), + }, + // Import + { + ConfigVariables: testConfigNetworkAreaRegionVarsMinUpdated, + ResourceName: "stackit_network_area_region.network_area_region", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_network_area_region.network_area_region"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network_area_region.network_area_region") + } + networkAreaId, ok := r.Primary.Attributes["network_area_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_area_id") + } + return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, testutil.Region), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigNetworkAreaRegionVarsMinUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMinConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + // Network Area + resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMinUpdated["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMinUpdated["transfer_network"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMinUpdated["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", "25"), // default value + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", "24"), // default value + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", "29"), // default value + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccNetworkAreaRegionMax(t *testing.T) { + t.Logf("TestAccNetworkAreaRegionMax name: %s", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["name"])) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigNetworkAreaRegionVarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMaxConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionCreate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + // Network Area + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["transfer_network"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_nameservers"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["min_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["max_prefix_length"])), + ), + }, + // Data source + { + ConfigVariables: testConfigNetworkAreaRegionVarsMax, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_network_area_region" "network_area_region" { + organization_id = stackit_network_area_region.network_area_region.organization_id + network_area_id = stackit_network_area_region.network_area_region.network_area_id + } + `, + testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMaxConfig, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionNoop), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["organization_id"])), + resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "network_area_id"), + resource.TestCheckResourceAttrPair( + "data.stackit_network_area_region.network_area_region", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["transfer_network"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#", "1"), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_nameservers"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_prefix_length"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["min_prefix_length"])), + resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["max_prefix_length"])), + ), + }, + // Import + { + ConfigVariables: testConfigNetworkAreaRegionVarsMaxUpdated, + ResourceName: "stackit_network_area_region.network_area_region", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_network_area_region.network_area_region"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_network_area_region.network_area_region") + } + networkAreaId, ok := r.Primary.Attributes["network_area_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_area_id") + } + return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, testutil.Region), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigNetworkAreaRegionVarsMaxUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMaxConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + // Network Area + resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["organization_id"])), + resource.TestCheckResourceAttrPair( + "stackit_network_area.network_area", "network_area_id", + "stackit_network_area_region.network_area_region", "network_area_id", + ), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["transfer_network"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["network_ranges_prefix"])), + resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#", "1"), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["default_nameservers"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["default_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["min_prefix_length"])), + resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["max_prefix_length"])), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + func TestAccVolumeMin(t *testing.T) { t.Logf("TestAccVolumeMin name: null") resource.ParallelTest(t, resource.TestCase{ @@ -1480,6 +1487,7 @@ func TestAccVolumeMin(t *testing.T) { // Volume size resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "performance_class"), @@ -1488,6 +1496,7 @@ func TestAccVolumeMin(t *testing.T) { // Volume source resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_source", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "performance_class"), @@ -1525,6 +1534,7 @@ func TestAccVolumeMin(t *testing.T) { "stackit_volume.volume_size", "volume_id", "data.stackit_volume.volume_size", "volume_id", ), + resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), resource.TestCheckResourceAttrSet("data.stackit_volume.volume_size", "performance_class"), resource.TestCheckNoResourceAttr("data.stackit_volume.volume_size", "server_id"), @@ -1560,7 +1570,7 @@ func TestAccVolumeMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -1577,7 +1587,7 @@ func TestAccVolumeMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -1590,6 +1600,7 @@ func TestAccVolumeMin(t *testing.T) { // Volume size resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["size"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "performance_class"), @@ -1598,6 +1609,7 @@ func TestAccVolumeMin(t *testing.T) { // Volume source resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_source", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["availability_zone"])), // Volume from source doesn't change size. So here the initial size will be used resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), @@ -1629,6 +1641,7 @@ func TestAccVolumeMax(t *testing.T) { // Volume size resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), resource.TestCheckResourceAttr("stackit_volume.volume_size", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"])), @@ -1641,6 +1654,7 @@ func TestAccVolumeMax(t *testing.T) { // Volume source resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["project_id"])), resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), + resource.TestCheckResourceAttr("stackit_volume.volume_source", "region", testutil.Region), resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), resource.TestCheckResourceAttr("stackit_volume.volume_source", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"])), @@ -1682,6 +1696,7 @@ func TestAccVolumeMax(t *testing.T) { "stackit_volume.volume_size", "volume_id", "data.stackit_volume.volume_size", "volume_id", ), + resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "region", testutil.Region), resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), resource.TestCheckNoResourceAttr("data.stackit_volume.volume_size", "server_id"), resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), @@ -1726,7 +1741,7 @@ func TestAccVolumeMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -1743,7 +1758,7 @@ func TestAccVolumeMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -1818,7 +1833,11 @@ func TestAccServerMin(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_server.server", "desired_status"), resource.TestCheckNoResourceAttr("stackit_server.server", "user_data"), resource.TestCheckNoResourceAttr("stackit_server.server", "keypair_name"), - resource.TestCheckNoResourceAttr("stackit_server.server", "network_interfaces"), + resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"), + resource.TestCheckResourceAttrPair( + "stackit_server.server", "network_interfaces.0", + "stackit_network_interface.nic", "network_interface_id", + ), resource.TestCheckResourceAttrSet("stackit_server.server", "created_at"), resource.TestCheckResourceAttrSet("stackit_server.server", "launched_at"), resource.TestCheckResourceAttrSet("stackit_server.server", "updated_at"), @@ -1866,7 +1885,11 @@ func TestAccServerMin(t *testing.T) { resource.TestCheckNoResourceAttr("data.stackit_server.server", "desired_status"), resource.TestCheckNoResourceAttr("data.stackit_server.server", "user_data"), resource.TestCheckNoResourceAttr("data.stackit_server.server", "keypair_name"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "network_interfaces"), + resource.TestCheckResourceAttr("data.stackit_server.server", "network_interfaces.#", "1"), + resource.TestCheckResourceAttrPair( + "data.stackit_server.server", "network_interfaces.0", + "stackit_network_interface.nic", "network_interface_id", + ), resource.TestCheckResourceAttrSet("data.stackit_server.server", "created_at"), resource.TestCheckResourceAttrSet("data.stackit_server.server", "launched_at"), resource.TestCheckResourceAttrSet("data.stackit_server.server", "updated_at"), @@ -1885,7 +1908,7 @@ func TestAccServerMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute server_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, serverId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, serverId), nil }, ImportState: true, ImportStateVerify: true, @@ -1915,7 +1938,11 @@ func TestAccServerMin(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_server.server", "desired_status"), resource.TestCheckNoResourceAttr("stackit_server.server", "user_data"), resource.TestCheckNoResourceAttr("stackit_server.server", "keypair_name"), - resource.TestCheckNoResourceAttr("stackit_server.server", "network_interfaces"), + resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"), + resource.TestCheckResourceAttrPair( + "stackit_server.server", "network_interfaces.0", + "stackit_network_interface.nic", "network_interface_id", + ), resource.TestCheckResourceAttrSet("stackit_server.server", "created_at"), resource.TestCheckResourceAttrSet("stackit_server.server", "launched_at"), resource.TestCheckResourceAttrSet("stackit_server.server", "updated_at"), @@ -2121,7 +2148,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute affinity_group_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, affinityGroupId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, affinityGroupId), nil }, ImportState: true, ImportStateVerify: true, @@ -2138,7 +2165,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -2155,7 +2182,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil }, ImportState: true, ImportStateVerify: true, @@ -2176,7 +2203,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, serverId, volumeId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, serverId, volumeId), nil }, ImportState: true, ImportStateVerify: false, @@ -2193,7 +2220,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil }, ImportState: true, ImportStateVerify: true, @@ -2215,7 +2242,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -2236,7 +2263,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -2257,7 +2284,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, serverId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, serverId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: false, @@ -2295,7 +2322,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, serverId, serviceAccountEmail), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, serverId, serviceAccountEmail), nil }, ImportState: true, ImportStateVerify: false, @@ -2312,7 +2339,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute server_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, serverId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, serverId), nil }, ImportState: true, ImportStateVerify: true, @@ -2587,7 +2614,7 @@ func TestAccAffinityGroupMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute affinity_group_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, affinityGroupId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, affinityGroupId), nil }, ImportState: true, ImportStateVerify: true, @@ -2684,7 +2711,7 @@ func TestAccIaaSSecurityGroupMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, securityGroupId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId), nil }, ImportState: true, ImportStateVerify: true, @@ -2705,7 +2732,7 @@ func TestAccIaaSSecurityGroupMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_rule_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, securityGroupId, securityGroupRuleId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId, securityGroupRuleId), nil }, ImportState: true, ImportStateVerify: true, @@ -2992,7 +3019,7 @@ func TestAccIaaSSecurityGroupMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, securityGroupId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId), nil }, ImportState: true, ImportStateVerify: true, @@ -3013,7 +3040,7 @@ func TestAccIaaSSecurityGroupMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_rule_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, securityGroupId, securityGroupRuleId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId, securityGroupRuleId), nil }, ImportState: true, ImportStateVerify: true, @@ -3127,7 +3154,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["project_id"])), resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), // Public ip @@ -3180,7 +3207,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["project_id"])), resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["name"])), resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("data.stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), // Public ip @@ -3215,7 +3242,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -3232,7 +3259,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil }, ImportState: true, ImportStateVerify: true, @@ -3249,7 +3276,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute public_ip_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, publicIpId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId), nil }, ImportState: true, ImportStateVerify: true, @@ -3298,7 +3325,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), // Public ip @@ -3407,7 +3434,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"])), resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("data.stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), // Public ip @@ -3472,7 +3499,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -3489,7 +3516,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil }, ImportState: true, ImportStateVerify: true, @@ -3506,7 +3533,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute public_ip_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, publicIpId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId), nil }, ImportState: true, ImportStateVerify: true, @@ -3527,7 +3554,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil }, ImportState: true, ImportStateVerify: true, @@ -3544,7 +3571,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute public_ip_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, publicIpId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId), nil }, ImportState: true, ImportStateVerify: true, @@ -3565,7 +3592,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, publicIpId, networkInterfaceId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId, networkInterfaceId), nil }, ImportState: true, Check: resource.ComposeAggregateTestCheckFunc( @@ -3603,7 +3630,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["name"])), resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv6_prefixes.#"), + resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), // Public ip @@ -3629,10 +3656,10 @@ func TestAccNetworkInterfaceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_public_ip.public_ip_simple", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "public_ip_id"), resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "ip"), - resource.TestCheckResourceAttrPair( - "stackit_public_ip.public_ip_simple", "network_interface_id", - "stackit_network_interface.network_interface_simple", "network_interface_id", - ), + // The network gets re-created, which triggers a re-create of the 'network_interface_simple' NIC, which leads the 'stackit_public_ip_associate' resource to update the + // networkInterfaceId of the public IP. All that without the public ip resource noticing. So the public ip resource will still hold the networkInterfaceId of the old NIC. + // So we can only check that *some* network interface ID is set here, but can't compare it with the networkInterfaceId of the NIC resource (old vs. new NIC id) + resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "network_interface_id"), resource.TestCheckResourceAttr("stackit_public_ip.public_ip_simple", "labels.%", "0"), // Nic and public ip attach @@ -3860,7 +3887,7 @@ func TestAccImageMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute image_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, imageId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, imageId), nil }, ImportState: true, ImportStateVerify: true, @@ -3990,7 +4017,7 @@ func TestAccImageMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute image_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, imageId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, imageId), nil }, ImportState: true, ImportStateVerify: true, @@ -4035,8 +4062,8 @@ func TestAccImageMax(t *testing.T) { }) } -func TestAccImageV2DatasourceSearchVariants(t *testing.T) { - t.Log("TestDataSource Image V2 Variants") +func TestAccImageDatasourceSearchVariants(t *testing.T) { + t.Log("TestDataSource Image Variants") resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ @@ -4205,6 +4232,7 @@ func TestAccProject(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "area_id"), resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "internet_access"), resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "state"), + resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "status"), resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "created_at"), resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "updated_at"), ), @@ -4256,9 +4284,6 @@ func TestAccMachineType(t *testing.T) { func testAccCheckDestroy(s *terraform.State) error { checkFunctions := []func(s *terraform.State) error{ - testAccCheckNetworkV1Destroy, - testAccCheckNetworkInterfaceDestroy, - testAccCheckNetworkAreaDestroy, testAccCheckIaaSVolumeDestroy, testAccCheckServerDestroy, testAccCheckAffinityGroupDestroy, @@ -4266,6 +4291,10 @@ func testAccCheckDestroy(s *terraform.State) error { testAccCheckIaaSPublicIpDestroy, testAccCheckIaaSKeyPairDestroy, testAccCheckIaaSImageDestroy, + testAccCheckNetworkDestroy, + testAccCheckNetworkInterfaceDestroy, + testAccCheckNetworkAreaRegionDestroy, + testAccCheckNetworkAreaDestroy, } var errs []error @@ -4285,50 +4314,7 @@ func testAccCheckDestroy(s *terraform.State) error { return errors.Join(errs...) } -func testAccCheckNetworkV1Destroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var errs []error - // networks - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_network" { - continue - } - networkId := strings.Split(rs.Primary.ID, core.Separator)[1] - err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, networkId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - continue - } - } - errs = append(errs, fmt.Errorf("cannot trigger network deletion %q: %w", networkId, err)) - } - _, err = wait.DeleteNetworkWaitHandler(ctx, client, testutil.ProjectId, networkId).WaitWithContext(ctx) - if err != nil { - errs = append(errs, fmt.Errorf("cannot delete network %q: %w", networkId, err)) - } - } - - return errors.Join(errs...) -} - -func testAccCheckNetworkV2Destroy(s *terraform.State) error { +func testAccCheckNetworkDestroy(s *terraform.State) error { ctx := context.Background() var client *iaasalpha.APIClient var err error @@ -4375,9 +4361,7 @@ func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4394,9 +4378,10 @@ func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { continue } ids := strings.Split(rs.Primary.ID, core.Separator) - networkId := ids[1] - networkInterfaceId := ids[2] - err := client.DeleteNicExecute(ctx, testutil.ProjectId, networkId, networkInterfaceId) + region := ids[1] + networkId := ids[2] + networkInterfaceId := ids[3] + err := client.DeleteNicExecute(ctx, testutil.ProjectId, region, networkId, networkInterfaceId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) { @@ -4414,14 +4399,57 @@ func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { return errors.Join(errs...) } +func testAccCheckNetworkAreaRegionDestroy(s *terraform.State) error { + ctx := context.Background() + var client *iaas.APIClient + var err error + if testutil.IaaSCustomEndpoint == "" { + client, err = iaas.NewAPIClient() + } else { + client, err = iaas.NewAPIClient( + stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + // network areas + networkAreasToDestroy := []string{} + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_network_area_region" { + continue + } + networkAreaId := strings.Split(rs.Primary.ID, core.Separator)[1] + networkAreasToDestroy = append(networkAreasToDestroy, networkAreaId) + } + + networkAreasResp, err := client.ListNetworkAreasExecute(ctx, testutil.OrganizationId) + if err != nil { + return fmt.Errorf("getting networkAreasResp: %w", err) + } + + networkAreas := *networkAreasResp.Items + for i := range networkAreas { + if networkAreas[i].Id == nil { + continue + } + if utils.Contains(networkAreasToDestroy, *networkAreas[i].Id) { + err := client.DeleteNetworkAreaRegionExecute(ctx, testutil.OrganizationId, *networkAreas[i].Id, testutil.Region) + if err != nil { + return fmt.Errorf("destroying network area %s during CheckDestroy: %w", *networkAreas[i].Id, err) + } + } + } + return nil +} + func testAccCheckNetworkAreaDestroy(s *terraform.State) error { ctx := context.Background() var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4448,13 +4476,13 @@ func testAccCheckNetworkAreaDestroy(s *terraform.State) error { networkAreas := *networkAreasResp.Items for i := range networkAreas { - if networkAreas[i].AreaId == nil { + if networkAreas[i].Id == nil { continue } - if utils.Contains(networkAreasToDestroy, *networkAreas[i].AreaId) { - err := client.DeleteNetworkAreaExecute(ctx, testutil.OrganizationId, *networkAreas[i].AreaId) + if utils.Contains(networkAreasToDestroy, *networkAreas[i].Id) { + err := client.DeleteNetworkAreaExecute(ctx, testutil.OrganizationId, *networkAreas[i].Id) if err != nil { - return fmt.Errorf("destroying network area %s during CheckDestroy: %w", *networkAreas[i].AreaId, err) + return fmt.Errorf("destroying network area %s during CheckDestroy: %w", *networkAreas[i].Id, err) } } } @@ -4466,9 +4494,7 @@ func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4488,7 +4514,7 @@ func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { volumesToDestroy = append(volumesToDestroy, volumeId) } - volumesResp, err := client.ListVolumesExecute(ctx, testutil.ProjectId) + volumesResp, err := client.ListVolumesExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting volumesResp: %w", err) } @@ -4499,7 +4525,7 @@ func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { continue } if utils.Contains(volumesToDestroy, *volumes[i].Id) { - err := client.DeleteVolumeExecute(ctx, testutil.ProjectId, *volumes[i].Id) + err := client.DeleteVolumeExecute(ctx, testutil.ProjectId, testutil.Region, *volumes[i].Id) if err != nil { return fmt.Errorf("destroying volume %s during CheckDestroy: %w", *volumes[i].Id, err) } @@ -4515,19 +4541,13 @@ func testAccCheckServerDestroy(s *terraform.State) error { var err error var alphaErr error if testutil.IaaSCustomEndpoint == "" { - alphaClient, alphaErr = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + alphaClient, alphaErr = iaas.NewAPIClient() + client, err = iaas.NewAPIClient() } else { alphaClient, alphaErr = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } if err != nil || alphaErr != nil { return fmt.Errorf("creating client: %w, %w", err, alphaErr) @@ -4540,12 +4560,12 @@ func testAccCheckServerDestroy(s *terraform.State) error { if rs.Type != "stackit_server" { continue } - // server terraform ID: "[project_id],[server_id]" - serverId := strings.Split(rs.Primary.ID, core.Separator)[1] + // server terraform ID: "[project_id],[region],[server_id]" + serverId := strings.Split(rs.Primary.ID, core.Separator)[2] serversToDestroy = append(serversToDestroy, serverId) } - serversResp, err := alphaClient.ListServersExecute(ctx, testutil.ProjectId) + serversResp, err := alphaClient.ListServersExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting serversResp: %w", err) } @@ -4556,7 +4576,7 @@ func testAccCheckServerDestroy(s *terraform.State) error { continue } if utils.Contains(serversToDestroy, *servers[i].Id) { - err := alphaClient.DeleteServerExecute(ctx, testutil.ProjectId, *servers[i].Id) + err := alphaClient.DeleteServerExecute(ctx, testutil.ProjectId, testutil.Region, *servers[i].Id) if err != nil { return fmt.Errorf("destroying server %s during CheckDestroy: %w", *servers[i].Id, err) } @@ -4575,20 +4595,20 @@ func testAccCheckServerDestroy(s *terraform.State) error { networksToDestroy = append(networksToDestroy, networkId) } - networksResp, err := client.ListNetworksExecute(ctx, testutil.ProjectId) + networksResp, err := client.ListNetworksExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting networksResp: %w", err) } networks := *networksResp.Items for i := range networks { - if networks[i].NetworkId == nil { + if networks[i].Id == nil { continue } - if utils.Contains(networksToDestroy, *networks[i].NetworkId) { - err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, *networks[i].NetworkId) + if utils.Contains(networksToDestroy, *networks[i].Id) { + err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, testutil.Region, *networks[i].Id) if err != nil { - return fmt.Errorf("destroying network %s during CheckDestroy: %w", *networks[i].NetworkId, err) + return fmt.Errorf("destroying network %s during CheckDestroy: %w", *networks[i].Id, err) } } } @@ -4601,9 +4621,7 @@ func testAccCheckAffinityGroupDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4618,12 +4636,12 @@ func testAccCheckAffinityGroupDestroy(s *terraform.State) error { if rs.Type != "stackit_affinity_group" { continue } - // affinity group terraform ID: "[project_id],[affinity_group_id]" - affinityGroupId := strings.Split(rs.Primary.ID, core.Separator)[1] + // affinity group terraform ID: "[project_id],[region],[affinity_group_id]" + affinityGroupId := strings.Split(rs.Primary.ID, core.Separator)[2] affinityGroupsToDestroy = append(affinityGroupsToDestroy, affinityGroupId) } - affinityGroupsResp, err := client.ListAffinityGroupsExecute(ctx, testutil.ProjectId) + affinityGroupsResp, err := client.ListAffinityGroupsExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting securityGroupsResp: %w", err) } @@ -4634,7 +4652,7 @@ func testAccCheckAffinityGroupDestroy(s *terraform.State) error { continue } if utils.Contains(affinityGroupsToDestroy, *affinityGroups[i].Id) { - err := client.DeleteAffinityGroupExecute(ctx, testutil.ProjectId, *affinityGroups[i].Id) + err := client.DeleteAffinityGroupExecute(ctx, testutil.ProjectId, testutil.Region, *affinityGroups[i].Id) if err != nil { return fmt.Errorf("destroying affinity group %s during CheckDestroy: %w", *affinityGroups[i].Id, err) } @@ -4648,9 +4666,7 @@ func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4665,12 +4681,12 @@ func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { if rs.Type != "stackit_security_group" { continue } - // security group terraform ID: "[project_id],[security_group_id]" - securityGroupId := strings.Split(rs.Primary.ID, core.Separator)[1] + // security group terraform ID: "[project_id],[region],[security_group_id]" + securityGroupId := strings.Split(rs.Primary.ID, core.Separator)[2] securityGroupsToDestroy = append(securityGroupsToDestroy, securityGroupId) } - securityGroupsResp, err := client.ListSecurityGroupsExecute(ctx, testutil.ProjectId) + securityGroupsResp, err := client.ListSecurityGroupsExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting securityGroupsResp: %w", err) } @@ -4681,7 +4697,7 @@ func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { continue } if utils.Contains(securityGroupsToDestroy, *securityGroups[i].Id) { - err := client.DeleteSecurityGroupExecute(ctx, testutil.ProjectId, *securityGroups[i].Id) + err := client.DeleteSecurityGroupExecute(ctx, testutil.ProjectId, testutil.Region, *securityGroups[i].Id) if err != nil { return fmt.Errorf("destroying security group %s during CheckDestroy: %w", *securityGroups[i].Id, err) } @@ -4695,9 +4711,7 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4712,12 +4726,12 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { if rs.Type != "stackit_public_ip" { continue } - // public IP terraform ID: "[project_id],[public_ip_id]" - publicIpId := strings.Split(rs.Primary.ID, core.Separator)[1] + // public IP terraform ID: "[project_id],[region],[public_ip_id]" + publicIpId := strings.Split(rs.Primary.ID, core.Separator)[2] publicIpsToDestroy = append(publicIpsToDestroy, publicIpId) } - publicIpsResp, err := client.ListPublicIPsExecute(ctx, testutil.ProjectId) + publicIpsResp, err := client.ListPublicIPsExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting publicIpsResp: %w", err) } @@ -4728,7 +4742,7 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { continue } if utils.Contains(publicIpsToDestroy, *publicIps[i].Id) { - err := client.DeletePublicIPExecute(ctx, testutil.ProjectId, *publicIps[i].Id) + err := client.DeletePublicIPExecute(ctx, testutil.ProjectId, testutil.Region, *publicIps[i].Id) if err != nil { return fmt.Errorf("destroying public IP %s during CheckDestroy: %w", *publicIps[i].Id, err) } @@ -4742,9 +4756,7 @@ func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error { var client *iaas.APIClient var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4789,9 +4801,7 @@ func testAccCheckIaaSImageDestroy(s *terraform.State) error { var err error if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = iaas.NewAPIClient() } else { client, err = iaas.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), @@ -4806,12 +4816,12 @@ func testAccCheckIaaSImageDestroy(s *terraform.State) error { if rs.Type != "stackit_image" { continue } - // Image terraform ID: "[project_id],[image_id]" - imageId := strings.Split(rs.Primary.ID, core.Separator)[1] + // Image terraform ID: "[project_id],[region],[image_id]" + imageId := strings.Split(rs.Primary.ID, core.Separator)[2] imagesToDestroy = append(imagesToDestroy, imageId) } - imagesResp, err := client.ListImagesExecute(ctx, testutil.ProjectId) + imagesResp, err := client.ListImagesExecute(ctx, testutil.ProjectId, testutil.Region) if err != nil { return fmt.Errorf("getting images: %w", err) } @@ -4822,7 +4832,7 @@ func testAccCheckIaaSImageDestroy(s *terraform.State) error { continue } if utils.Contains(imagesToDestroy, *images[i].Id) { - err := client.DeleteImageExecute(ctx, testutil.ProjectId, *images[i].Id) + err := client.DeleteImageExecute(ctx, testutil.ProjectId, testutil.Region, *images[i].Id) if err != nil { return fmt.Errorf("destroying image %s during CheckDestroy: %w", *images[i].Id, err) } diff --git a/stackit/internal/services/iaas/image/datasource.go b/stackit/internal/services/iaas/image/datasource.go index 1f398d9f..0d3e1aa2 100644 --- a/stackit/internal/services/iaas/image/datasource.go +++ b/stackit/internal/services/iaas/image/datasource.go @@ -30,6 +30,7 @@ var ( type DataSourceModel struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ImageId types.String `tfsdk:"image_id"` Name types.String `tfsdk:"name"` DiskFormat types.String `tfsdk:"disk_format"` @@ -49,7 +50,8 @@ func NewImageDataSource() datasource.DataSource { // imageDataSource is the data source implementation. type imageDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -58,12 +60,13 @@ func (d *imageDataSource) Metadata(_ context.Context, req datasource.MetadataReq } func (d *imageDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -72,14 +75,14 @@ func (d *imageDataSource) Configure(ctx context.Context, req datasource.Configur } // Schema defines the schema for the datasource. -func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Image datasource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -90,6 +93,11 @@ func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "image_id": schema.StringAttribute{ Description: "The image ID.", Required: true, @@ -203,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{} diff --git a/stackit/internal/services/iaas/image/datasource_test.go b/stackit/internal/services/iaas/image/datasource_test.go index a16120ac..37d81235 100644 --- a/stackit/internal/services/iaas/image/datasource_test.go +++ b/stackit/internal/services/iaas/image/datasource_test.go @@ -12,69 +12,81 @@ import ( ) func TestMapDataSourceFields(t *testing.T) { + type args struct { + state DataSourceModel + input *iaas.Image + region string + } tests := []struct { description string - state DataSourceModel - input *iaas.Image + args args expected DataSourceModel isValid bool }{ { - "default_values", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), + 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) } diff --git a/stackit/internal/services/iaas/image/resource.go b/stackit/internal/services/iaas/image/resource.go index a1e764cc..0699093f 100644 --- a/stackit/internal/services/iaas/image/resource.go +++ b/stackit/internal/services/iaas/image/resource.go @@ -15,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -40,11 +39,13 @@ var ( _ resource.Resource = &imageResource{} _ resource.ResourceWithConfigure = &imageResource{} _ resource.ResourceWithImportState = &imageResource{} + _ resource.ResourceWithModifyPlan = &imageResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ImageId types.String `tfsdk:"image_id"` Name types.String `tfsdk:"name"` DiskFormat types.String `tfsdk:"disk_format"` @@ -111,7 +112,8 @@ func NewImageResource() resource.Resource { // imageResource is the resource implementation. type imageResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -119,14 +121,45 @@ func (r *imageResource) Metadata(_ context.Context, req resource.MetadataRequest resp.TypeName = req.ProviderTypeName + "_image" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *imageResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *imageResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -140,7 +173,7 @@ func (r *imageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp Description: "Image resource schema. Must have a `region` specified in the provider configuration.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -157,6 +190,15 @@ func (r *imageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "image_id": schema.StringAttribute{ Description: "The image ID.", Computed: true, @@ -378,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{} diff --git a/stackit/internal/services/iaas/image/resource_test.go b/stackit/internal/services/iaas/image/resource_test.go index 23b894df..2040bdd6 100644 --- a/stackit/internal/services/iaas/image/resource_test.go +++ b/stackit/internal/services/iaas/image/resource_test.go @@ -17,69 +17,81 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.Image + region string + } tests := []struct { description string - state Model - input *iaas.Image + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), + 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) } diff --git a/stackit/internal/services/iaas/imagev2/datasource.go b/stackit/internal/services/iaas/imagev2/datasource.go index 88c5509b..01f3b8a2 100644 --- a/stackit/internal/services/iaas/imagev2/datasource.go +++ b/stackit/internal/services/iaas/imagev2/datasource.go @@ -36,6 +36,7 @@ var ( type DataSourceModel struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ImageId types.String `tfsdk:"image_id"` Name types.String `tfsdk:"name"` NameRegex types.String `tfsdk:"name_regex"` @@ -113,7 +114,8 @@ func NewImageV2DataSource() datasource.DataSource { // imageDataV2Source is the data source implementation. type imageDataV2Source struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -122,17 +124,18 @@ func (d *imageDataV2Source) Metadata(_ context.Context, req datasource.MetadataR } func (d *imageDataV2Source) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_image_v2", "datasource") + features.CheckBetaResourcesEnabled(ctx, &d.providerData, &resp.Diagnostics, "stackit_image_v2", "datasource") if resp.Diagnostics.HasError() { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -189,7 +192,7 @@ func (d *imageDataV2Source) Schema(_ context.Context, _ datasource.SchemaRequest Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -200,6 +203,11 @@ func (d *imageDataV2Source) Schema(_ context.Context, _ datasource.SchemaRequest validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "image_id": schema.StringAttribute{ Description: "Image ID to fetch directly", Optional: true, @@ -357,6 +365,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest } projectID := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) imageID := model.ImageId.ValueString() name := model.Name.ValueString() nameRegex := model.NameRegex.ValueString() @@ -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{} diff --git a/stackit/internal/services/iaas/imagev2/datasource_test.go b/stackit/internal/services/iaas/imagev2/datasource_test.go index 56b9930b..3d27ed4f 100644 --- a/stackit/internal/services/iaas/imagev2/datasource_test.go +++ b/stackit/internal/services/iaas/imagev2/datasource_test.go @@ -12,69 +12,81 @@ import ( ) func TestMapDataSourceFields(t *testing.T) { + type args struct { + state DataSourceModel + input *iaas.Image + region string + } tests := []struct { description string - state DataSourceModel - input *iaas.Image + args args expected DataSourceModel isValid bool }{ { - "default_values", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), + 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) } diff --git a/stackit/internal/services/iaas/keypair/datasource.go b/stackit/internal/services/iaas/keypair/datasource.go index d65ca6f1..455e043c 100644 --- a/stackit/internal/services/iaas/keypair/datasource.go +++ b/stackit/internal/services/iaas/keypair/datasource.go @@ -21,7 +21,7 @@ var ( _ datasource.DataSource = &keyPairDataSource{} ) -// NewVolumeDataSource is a helper function to simplify the provider implementation. +// NewKeyPairDataSource is a helper function to simplify the provider implementation. func NewKeyPairDataSource() datasource.DataSource { return &keyPairDataSource{} } @@ -51,7 +51,7 @@ func (d *keyPairDataSource) Configure(ctx context.Context, req datasource.Config } // Schema defines the schema for the resource. -func (r *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Key pair resource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ @@ -84,7 +84,7 @@ func (r *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest } // Read refreshes the Terraform state with the latest data. -func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -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, diff --git a/stackit/internal/services/iaas/machinetype/datasource.go b/stackit/internal/services/iaas/machinetype/datasource.go index 88ad2270..c4d89e91 100644 --- a/stackit/internal/services/iaas/machinetype/datasource.go +++ b/stackit/internal/services/iaas/machinetype/datasource.go @@ -7,10 +7,12 @@ import ( "sort" "strings" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" @@ -19,7 +21,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -28,6 +29,7 @@ var _ datasource.DataSource = &machineTypeDataSource{} type DataSourceModel struct { Id types.String `tfsdk:"id"` // required by Terraform to identify state ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` SortAscending types.Bool `tfsdk:"sort_ascending"` Filter types.String `tfsdk:"filter"` Description types.String `tfsdk:"description"` @@ -44,7 +46,8 @@ func NewMachineTypeDataSource() datasource.DataSource { } type machineTypeDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } func (d *machineTypeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -52,17 +55,18 @@ func (d *machineTypeDataSource) Metadata(_ context.Context, req datasource.Metad } func (d *machineTypeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_machine_type", "datasource") + features.CheckBetaResourcesEnabled(ctx, &d.providerData, &resp.Diagnostics, "stackit_machine_type", "datasource") if resp.Diagnostics.HasError() { return } - client := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + client := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -76,7 +80,7 @@ func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaReq MarkdownDescription: features.AddBetaDescription("Machine type data source.", core.Datasource), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -87,6 +91,11 @@ func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaReq validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "sort_ascending": schema.BoolAttribute{ Description: "Sort machine types by name ascending (`true`) or descending (`false`). Defaults to `false`", Optional: true, @@ -142,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) diff --git a/stackit/internal/services/iaas/machinetype/datasource_test.go b/stackit/internal/services/iaas/machinetype/datasource_test.go index 3fde4794..94918810 100644 --- a/stackit/internal/services/iaas/machinetype/datasource_test.go +++ b/stackit/internal/services/iaas/machinetype/datasource_test.go @@ -13,32 +13,39 @@ import ( ) func TestMapDataSourceFields(t *testing.T) { + type args struct { + initial DataSourceModel + input *iaas.MachineType + region string + } tests := []struct { name string - initial DataSourceModel - input *iaas.MachineType + args args expected DataSourceModel expectError bool }{ { name: "valid simple values", - initial: DataSourceModel{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("s1.2"), - Description: utils.Ptr("general-purpose small"), - Disk: utils.Ptr(int64(20)), - Ram: utils.Ptr(int64(2048)), - Vcpus: utils.Ptr(int64(2)), - ExtraSpecs: &map[string]interface{}{ - "cpu": "amd-epycrome-7702", - "overcommit": "1", - "environment": "general", + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("pid"), }, + input: &iaas.MachineType{ + Name: utils.Ptr("s1.2"), + Description: utils.Ptr("general-purpose small"), + Disk: utils.Ptr(int64(20)), + Ram: utils.Ptr(int64(2048)), + Vcpus: utils.Ptr(int64(2)), + ExtraSpecs: &map[string]interface{}{ + "cpu": "amd-epycrome-7702", + "overcommit": "1", + "environment": "general", + }, + }, + region: "eu01", }, expected: DataSourceModel{ - Id: types.StringValue("pid,s1.2"), + Id: types.StringValue("pid,eu01,s1.2"), ProjectId: types.StringValue("pid"), Name: types.StringValue("s1.2"), Description: types.StringValue("general-purpose small"), @@ -50,42 +57,50 @@ func TestMapDataSourceFields(t *testing.T) { "overcommit": types.StringValue("1"), "environment": types.StringValue("general"), }), + Region: types.StringValue("eu01"), }, expectError: false, }, { name: "missing name should fail", - initial: DataSourceModel{ - ProjectId: types.StringValue("pid-456"), - }, - input: &iaas.MachineType{ - Description: utils.Ptr("gp-medium"), + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("pid-456"), + }, + input: &iaas.MachineType{ + Description: utils.Ptr("gp-medium"), + }, }, expected: DataSourceModel{}, expectError: true, }, { - name: "nil machineType should fail", - initial: DataSourceModel{}, - input: nil, + name: "nil machineType should fail", + args: args{ + initial: DataSourceModel{}, + input: nil, + }, expected: DataSourceModel{}, expectError: true, }, { name: "empty extraSpecs should return null map", - initial: DataSourceModel{ - ProjectId: types.StringValue("pid-789"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("m1.noextras"), - Description: utils.Ptr("no extras"), - Disk: utils.Ptr(int64(10)), - Ram: utils.Ptr(int64(1024)), - Vcpus: utils.Ptr(int64(1)), - ExtraSpecs: &map[string]interface{}{}, + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("pid-789"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("m1.noextras"), + Description: utils.Ptr("no extras"), + Disk: utils.Ptr(int64(10)), + Ram: utils.Ptr(int64(1024)), + Vcpus: utils.Ptr(int64(1)), + ExtraSpecs: &map[string]interface{}{}, + }, + region: "eu01", }, expected: DataSourceModel{ - Id: types.StringValue("pid-789,m1.noextras"), + Id: types.StringValue("pid-789,eu01,m1.noextras"), ProjectId: types.StringValue("pid-789"), Name: types.StringValue("m1.noextras"), Description: types.StringValue("no extras"), @@ -93,24 +108,28 @@ func TestMapDataSourceFields(t *testing.T) { Ram: types.Int64Value(1024), Vcpus: types.Int64Value(1), ExtraSpecs: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, expectError: false, }, { name: "nil extrasSpecs should return null map", - initial: DataSourceModel{ - ProjectId: types.StringValue("pid-987"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("g1.nil"), - Description: utils.Ptr("missing extras"), - Disk: utils.Ptr(int64(40)), - Ram: utils.Ptr(int64(8096)), - Vcpus: utils.Ptr(int64(4)), - ExtraSpecs: nil, + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("pid-987"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("g1.nil"), + Description: utils.Ptr("missing extras"), + Disk: utils.Ptr(int64(40)), + Ram: utils.Ptr(int64(8096)), + Vcpus: utils.Ptr(int64(4)), + ExtraSpecs: nil, + }, + region: "eu01", }, expected: DataSourceModel{ - Id: types.StringValue("pid-987,g1.nil"), + Id: types.StringValue("pid-987,eu01,g1.nil"), ProjectId: types.StringValue("pid-987"), Name: types.StringValue("g1.nil"), Description: types.StringValue("missing extras"), @@ -118,24 +137,27 @@ func TestMapDataSourceFields(t *testing.T) { Ram: types.Int64Value(8096), Vcpus: types.Int64Value(4), ExtraSpecs: types.MapNull(types.StringType), + Region: types.StringValue("eu01"), }, expectError: false, }, { name: "invalid extraSpecs with non-string values", - initial: DataSourceModel{ - ProjectId: types.StringValue("test-err"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("invalid"), - Description: utils.Ptr("bad map"), - Disk: utils.Ptr(int64(10)), - Ram: utils.Ptr(int64(4096)), - Vcpus: utils.Ptr(int64(2)), - ExtraSpecs: &map[string]interface{}{ - "cpu": "intel", - "burst": true, // not a string - "gen": 8, // not a string + args: args{ + initial: DataSourceModel{ + ProjectId: types.StringValue("test-err"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("invalid"), + Description: utils.Ptr("bad map"), + Disk: utils.Ptr(int64(10)), + Ram: utils.Ptr(int64(4096)), + Vcpus: utils.Ptr(int64(2)), + ExtraSpecs: &map[string]interface{}{ + "cpu": "intel", + "burst": true, // not a string + "gen": 8, // not a string + }, }, }, expected: DataSourceModel{}, @@ -145,7 +167,7 @@ func TestMapDataSourceFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.input, &tt.initial) + err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.initial, tt.args.region) if tt.expectError { if err == nil { t.Errorf("expected error but got none") @@ -157,13 +179,13 @@ func TestMapDataSourceFields(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - diff := cmp.Diff(tt.expected, tt.initial) + diff := cmp.Diff(tt.expected, tt.args.initial) if diff != "" { t.Errorf("unexpected diff (-want +got):\n%s", diff) } // Extra sanity check for proper ID format - if id := tt.initial.Id.ValueString(); !strings.HasPrefix(id, tt.initial.ProjectId.ValueString()+",") { + if id := tt.args.initial.Id.ValueString(); !strings.HasPrefix(id, tt.args.initial.ProjectId.ValueString()+",") { t.Errorf("unexpected ID format: got %q", id) } }) diff --git a/stackit/internal/services/iaas/network/datasource.go b/stackit/internal/services/iaas/network/datasource.go index a78d11b9..4197ee1f 100644 --- a/stackit/internal/services/iaas/network/datasource.go +++ b/stackit/internal/services/iaas/network/datasource.go @@ -2,14 +2,11 @@ package network import ( "context" + "fmt" + "net" + "net/http" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -18,7 +15,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -27,6 +26,30 @@ var ( _ datasource.DataSource = &networkDataSource{} ) +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + NetworkId types.String `tfsdk:"network_id"` + Name types.String `tfsdk:"name"` + Nameservers types.List `tfsdk:"nameservers"` + IPv4Gateway types.String `tfsdk:"ipv4_gateway"` + IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` + IPv4Prefix types.String `tfsdk:"ipv4_prefix"` + IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` + Prefixes types.List `tfsdk:"prefixes"` + IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` + IPv6Gateway types.String `tfsdk:"ipv6_gateway"` + IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` + IPv6Prefix types.String `tfsdk:"ipv6_prefix"` + IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` + IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` + PublicIP types.String `tfsdk:"public_ip"` + Labels types.Map `tfsdk:"labels"` + Routed types.Bool `tfsdk:"routed"` + Region types.String `tfsdk:"region"` + RoutingTableID types.String `tfsdk:"routing_table_id"` +} + // NewNetworkDataSource is a helper function to simplify the provider implementation. func NewNetworkDataSource() datasource.DataSource { return &networkDataSource{} @@ -34,11 +57,8 @@ func NewNetworkDataSource() datasource.DataSource { // networkDataSource is the data source implementation. type networkDataSource struct { - client *iaas.APIClient - // alphaClient will be used in case the experimental flag "network" is set - alphaClient *iaasalpha.APIClient - isExperimental bool - providerData core.ProviderData + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -53,24 +73,11 @@ func (d *networkDataSource) Configure(ctx context.Context, req datasource.Config return } - d.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &d.providerData, features.NetworkExperiment, "stackit_network", core.Datasource, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - - if d.isExperimental { - alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.alphaClient = alphaApiClient - } else { - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - } + d.client = apiClient tflog.Info(ctx, "IaaS client configured") } @@ -197,9 +204,199 @@ func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest // Read refreshes the Terraform state with the latest data. func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - if !d.isExperimental { - v1network.DatasourceRead(ctx, req, resp, d.client) - } 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 } diff --git a/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go b/stackit/internal/services/iaas/network/datasource_test.go similarity index 87% rename from stackit/internal/services/iaas/network/utils/v2network/datasource_test.go rename to stackit/internal/services/iaas/network/datasource_test.go index eba9d411..c7c4d7f9 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go +++ b/stackit/internal/services/iaas/network/datasource_test.go @@ -1,15 +1,15 @@ -package v2network +package network import ( "context" "testing" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" ) const ( @@ -19,26 +19,26 @@ const ( func TestMapDataSourceFields(t *testing.T) { tests := []struct { description string - state networkModel.DataSourceModel - input *iaasalpha.Network + state DataSourceModel + input *iaas.Network region string - expected networkModel.DataSourceModel + expected DataSourceModel isValid bool }{ { "id_ok", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ - Gateway: iaasalpha.NewNullableString(nil), + Ipv4: &iaas.NetworkIPv4{ + Gateway: iaas.NewNullableString(nil), }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -64,14 +64,14 @@ func TestMapDataSourceFields(t *testing.T) { }, { "values_ok", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), Name: utils.Ptr("name"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Nameservers: &[]string{ "ns1", "ns2", @@ -81,9 +81,9 @@ func TestMapDataSourceFields(t *testing.T) { "10.100.10.0/16", }, PublicIp: utils.Ptr("publicIp"), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Nameservers: &[]string{ "ns1", "ns2", @@ -92,7 +92,7 @@ func TestMapDataSourceFields(t *testing.T) { "fd12:3456:789a:1::/64", "fd12:3456:789a:2::/64", }, - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, Labels: &map[string]interface{}{ "key": "value", @@ -100,7 +100,7 @@ func TestMapDataSourceFields(t *testing.T) { Routed: utils.Ptr(true), }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -146,7 +146,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv4_nameservers_changed_outside_tf", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -158,9 +158,9 @@ func TestMapDataSourceFields(t *testing.T) { types.StringValue("ns2"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Nameservers: &[]string{ "ns2", "ns3", @@ -168,7 +168,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -192,7 +192,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv6_nameservers_changed_outside_tf", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -200,9 +200,9 @@ func TestMapDataSourceFields(t *testing.T) { types.StringValue("ns2"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Nameservers: &[]string{ "ns2", "ns3", @@ -210,7 +210,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -231,7 +231,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv4_prefixes_changed_outside_tf", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -239,9 +239,9 @@ func TestMapDataSourceFields(t *testing.T) { types.StringValue("10.100.10.0/16"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Prefixes: &[]string{ "10.100.20.0/16", "10.100.10.0/16", @@ -249,7 +249,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -276,7 +276,7 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv6_prefixes_changed_outside_tf", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -284,9 +284,9 @@ func TestMapDataSourceFields(t *testing.T) { types.StringValue("fd12:3456:789a:2::/64"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Prefixes: &[]string{ "fd12:3456:789a:3::/64", "fd12:3456:789a:4::/64", @@ -294,7 +294,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -318,15 +318,15 @@ func TestMapDataSourceFields(t *testing.T) { }, { "ipv4_ipv6_gateway_nil", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), }, testRegion, - networkModel.DataSourceModel{ + DataSourceModel{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -350,20 +350,20 @@ func TestMapDataSourceFields(t *testing.T) { }, { "response_nil_fail", - networkModel.DataSourceModel{}, + DataSourceModel{}, nil, testRegion, - networkModel.DataSourceModel{}, + DataSourceModel{}, false, }, { "no_resource_id", - networkModel.DataSourceModel{ + DataSourceModel{ ProjectId: types.StringValue("pid"), }, - &iaasalpha.Network{}, + &iaas.Network{}, testRegion, - networkModel.DataSourceModel{}, + DataSourceModel{}, false, }, } diff --git a/stackit/internal/services/iaas/network/resource.go b/stackit/internal/services/iaas/network/resource.go index a1ea9e5d..0665a3bb 100644 --- a/stackit/internal/services/iaas/network/resource.go +++ b/stackit/internal/services/iaas/network/resource.go @@ -3,9 +3,13 @@ package network import ( "context" "fmt" + "net" + "net/http" + "strings" "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -18,16 +22,12 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -37,6 +37,7 @@ var ( _ resource.Resource = &networkResource{} _ resource.ResourceWithConfigure = &networkResource{} _ resource.ResourceWithImportState = &networkResource{} + _ resource.ResourceWithModifyPlan = &networkResource{} ) const ( @@ -46,6 +47,32 @@ const ( "In cases where `ipv4_nameservers` are defined within the resource, the existing behavior will remain unchanged." ) +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + NetworkId types.String `tfsdk:"network_id"` + Name types.String `tfsdk:"name"` + Nameservers types.List `tfsdk:"nameservers"` + IPv4Gateway types.String `tfsdk:"ipv4_gateway"` + IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` + IPv4Prefix types.String `tfsdk:"ipv4_prefix"` + IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` + Prefixes types.List `tfsdk:"prefixes"` + IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` + IPv6Gateway types.String `tfsdk:"ipv6_gateway"` + IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` + IPv6Prefix types.String `tfsdk:"ipv6_prefix"` + IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` + IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` + PublicIP types.String `tfsdk:"public_ip"` + Labels types.Map `tfsdk:"labels"` + Routed types.Bool `tfsdk:"routed"` + NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"` + NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"` + Region types.String `tfsdk:"region"` + RoutingTableID types.String `tfsdk:"routing_table_id"` +} + // NewNetworkResource is a helper function to simplify the provider implementation. func NewNetworkResource() resource.Resource { return &networkResource{} @@ -53,11 +80,8 @@ func NewNetworkResource() resource.Resource { // networkResource is the resource implementation. type networkResource struct { - client *iaas.APIClient - // alphaClient will be used in case the experimental flag "network" is set - alphaClient *iaasalpha.APIClient - isExperimental bool - providerData core.ProviderData + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -73,31 +97,18 @@ func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureR return } - r.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &r.providerData, features.NetworkExperiment, "stackit_network", core.Resource, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - - if r.isExperimental { - alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.alphaClient = alphaApiClient - } else { - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - } + r.client = apiClient tflog.Info(ctx, "IaaS client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel model.Model + var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return @@ -107,7 +118,7 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla return } - var planModel model.Model + var planModel Model resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return @@ -118,10 +129,6 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla addIPv4Warning(&resp.Diagnostics) } - // If the v1 api is used, it's not required to get the fallback region because it isn't used - if !r.isExperimental { - return - } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) if resp.Diagnostics.HasError() { return @@ -134,7 +141,7 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla } func (r *networkResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var resourceModel model.Model + var resourceModel Model resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) if resp.Diagnostics.HasError() { return @@ -143,14 +150,6 @@ func (r *networkResource) ValidateConfig(ctx context.Context, req resource.Valid if !resourceModel.Nameservers.IsUnknown() && !resourceModel.IPv4Nameservers.IsUnknown() && !resourceModel.Nameservers.IsNull() && !resourceModel.IPv4Nameservers.IsNull() { core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "You cannot provide both the `nameservers` and `ipv4_nameservers` fields simultaneously. Please remove the deprecated `nameservers` field, and use `ipv4_nameservers` to configure nameservers for IPv4.") } - if !r.isExperimental { - if !utils.IsUndefined(resourceModel.Region) { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the `region` is not supported yet. This can only be configured when the experiments `network` is set.") - } - if !utils.IsUndefined(resourceModel.RoutingTableID) { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the field `routing_table_id` is not supported yet. This can only be configured when the experiments `network` is set.") - } - } } // ConfigValidators validates the resource configuration @@ -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) { diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource_test.go b/stackit/internal/services/iaas/network/resource_test.go similarity index 84% rename from stackit/internal/services/iaas/network/utils/v2network/resource_test.go rename to stackit/internal/services/iaas/network/resource_test.go index 6f39b9a3..929424d6 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/resource_test.go +++ b/stackit/internal/services/iaas/network/resource_test.go @@ -1,4 +1,4 @@ -package v2network +package network import ( "context" @@ -8,34 +8,33 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) func TestMapFields(t *testing.T) { const testRegion = "region" tests := []struct { description string - state model.Model - input *iaasalpha.Network + state Model + input *iaas.Network region string - expected model.Model + expected Model isValid bool }{ { "id_ok", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ - Gateway: iaasalpha.NewNullableString(nil), + Ipv4: &iaas.NetworkIPv4{ + Gateway: iaas.NewNullableString(nil), }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -61,14 +60,14 @@ func TestMapFields(t *testing.T) { }, { "values_ok", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), Name: utils.Ptr("name"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Nameservers: utils.Ptr([]string{"ns1", "ns2"}), Prefixes: utils.Ptr( []string{ @@ -77,15 +76,15 @@ func TestMapFields(t *testing.T) { }, ), PublicIp: utils.Ptr("publicIp"), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Nameservers: utils.Ptr([]string{"ns1", "ns2"}), Prefixes: utils.Ptr([]string{ "fd12:3456:789a:1::/64", "fd12:3456:789b:1::/64", }), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, Labels: &map[string]interface{}{ "key": "value", @@ -93,7 +92,7 @@ func TestMapFields(t *testing.T) { Routed: utils.Ptr(true), }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -139,7 +138,7 @@ func TestMapFields(t *testing.T) { }, { "ipv4_nameservers_changed_outside_tf", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -151,9 +150,9 @@ func TestMapFields(t *testing.T) { types.StringValue("ns2"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Nameservers: utils.Ptr([]string{ "ns2", "ns3", @@ -161,7 +160,7 @@ func TestMapFields(t *testing.T) { }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -185,7 +184,7 @@ func TestMapFields(t *testing.T) { }, { "ipv6_nameservers_changed_outside_tf", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ @@ -193,9 +192,9 @@ func TestMapFields(t *testing.T) { types.StringValue("ns2"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Nameservers: utils.Ptr([]string{ "ns2", "ns3", @@ -203,7 +202,7 @@ func TestMapFields(t *testing.T) { }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -224,7 +223,7 @@ func TestMapFields(t *testing.T) { }, { "ipv4_prefixes_changed_outside_tf", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -232,9 +231,9 @@ func TestMapFields(t *testing.T) { types.StringValue("10.100.10.0/24"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv4: &iaasalpha.NetworkIPv4{ + Ipv4: &iaas.NetworkIPv4{ Prefixes: utils.Ptr( []string{ "192.168.54.0/24", @@ -244,7 +243,7 @@ func TestMapFields(t *testing.T) { }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -271,7 +270,7 @@ func TestMapFields(t *testing.T) { }, { "ipv6_prefixes_changed_outside_tf", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ @@ -279,9 +278,9 @@ func TestMapFields(t *testing.T) { types.StringValue("fd12:3456:789a:2::/64"), }), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), - Ipv6: &iaasalpha.NetworkIPv6{ + Ipv6: &iaas.NetworkIPv6{ Prefixes: utils.Ptr( []string{ "fd12:3456:789a:1::/64", @@ -291,7 +290,7 @@ func TestMapFields(t *testing.T) { }, }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -315,15 +314,15 @@ func TestMapFields(t *testing.T) { }, { "ipv4_ipv6_gateway_nil", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), }, - &iaasalpha.Network{ + &iaas.Network{ Id: utils.Ptr("nid"), }, testRegion, - model.Model{ + Model{ Id: types.StringValue("pid,region,nid"), ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), @@ -347,20 +346,20 @@ func TestMapFields(t *testing.T) { }, { "response_nil_fail", - model.Model{}, + Model{}, nil, testRegion, - model.Model{}, + Model{}, false, }, { "no_resource_id", - model.Model{ + Model{ ProjectId: types.StringValue("pid"), }, - &iaasalpha.Network{}, + &iaas.Network{}, testRegion, - model.Model{}, + Model{}, false, }, } @@ -386,13 +385,13 @@ func TestMapFields(t *testing.T) { func TestToCreatePayload(t *testing.T) { tests := []struct { description string - input *model.Model - expected *iaasalpha.CreateNetworkPayload + input *Model + expected *iaas.CreateNetworkPayload isValid bool }{ { "default_ok", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -405,11 +404,11 @@ func TestToCreatePayload(t *testing.T) { IPv4Gateway: types.StringValue("gateway"), IPv4Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv4: &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -426,7 +425,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv4_nameservers_okay", - &model.Model{ + &Model{ Name: types.StringValue("name"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -439,11 +438,11 @@ func TestToCreatePayload(t *testing.T) { IPv4Gateway: types.StringValue("gateway"), IPv4Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv4: &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -460,7 +459,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv6_default_ok", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -473,11 +472,11 @@ func TestToCreatePayload(t *testing.T) { IPv6Gateway: types.StringValue("gateway"), IPv6Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv6: &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -494,7 +493,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv6_nameserver_null", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListNull(types.StringType), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -504,12 +503,12 @@ func TestToCreatePayload(t *testing.T) { IPv6Gateway: types.StringValue("gateway"), IPv6Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ + Ipv6: &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ Nameservers: nil, - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Prefix: utils.Ptr("prefix"), }, }, @@ -522,7 +521,7 @@ func TestToCreatePayload(t *testing.T) { }, { "ipv6_nameserver_empty_list", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -532,12 +531,12 @@ func TestToCreatePayload(t *testing.T) { IPv6Gateway: types.StringValue("gateway"), IPv6Prefix: types.StringValue("prefix"), }, - &iaasalpha.CreateNetworkPayload{ + &iaas.CreateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{ + Ipv6: &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ Nameservers: utils.Ptr([]string{}), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Prefix: utils.Ptr("prefix"), }, }, @@ -559,7 +558,7 @@ func TestToCreatePayload(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) + diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -571,14 +570,14 @@ func TestToCreatePayload(t *testing.T) { func TestToUpdatePayload(t *testing.T) { tests := []struct { description string - input *model.Model - state model.Model - expected *iaasalpha.PartialUpdateNetworkPayload + input *Model + state Model + expected *iaas.PartialUpdateNetworkPayload isValid bool }{ { "default_ok", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -590,15 +589,15 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv4Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.UpdateNetworkIPv4Body{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv4: &iaas.UpdateNetworkIPv4Body{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -612,7 +611,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv4_nameservers_okay", - &model.Model{ + &Model{ Name: types.StringValue("name"), Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -624,15 +623,15 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv4Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.UpdateNetworkIPv4Body{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv4: &iaas.UpdateNetworkIPv4Body{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -646,7 +645,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv4_gateway_nil", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -657,14 +656,14 @@ func TestToUpdatePayload(t *testing.T) { }), Routed: types.BoolValue(true), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv4: &iaasalpha.UpdateNetworkIPv4Body{ + Ipv4: &iaas.UpdateNetworkIPv4Body{ Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -678,7 +677,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_default_ok", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -690,15 +689,15 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv6Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Ipv6: &iaas.UpdateNetworkIPv6Body{ + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -712,7 +711,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_gateway_nil", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("ns1"), @@ -723,14 +722,14 @@ func TestToUpdatePayload(t *testing.T) { }), Routed: types.BoolValue(true), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ + Ipv6: &iaas.UpdateNetworkIPv6Body{ Nameservers: utils.Ptr([]string{ "ns1", "ns2", @@ -744,7 +743,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_nameserver_null", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListNull(types.StringType), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -753,16 +752,16 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv6Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ + Ipv6: &iaas.UpdateNetworkIPv6Body{ Nameservers: nil, - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, Labels: &map[string]interface{}{ "key": "value", @@ -772,7 +771,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "ipv6_nameserver_empty_list", - &model.Model{ + &Model{ Name: types.StringValue("name"), IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -781,16 +780,16 @@ func TestToUpdatePayload(t *testing.T) { Routed: types.BoolValue(true), IPv6Gateway: types.StringValue("gateway"), }, - model.Model{ + Model{ ProjectId: types.StringValue("pid"), NetworkId: types.StringValue("nid"), Labels: types.MapNull(types.StringType), }, - &iaasalpha.PartialUpdateNetworkPayload{ + &iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("name"), - Ipv6: &iaasalpha.UpdateNetworkIPv6Body{ + Ipv6: &iaas.UpdateNetworkIPv6Body{ Nameservers: utils.Ptr([]string{}), - Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), }, Labels: &map[string]interface{}{ "key": "value", @@ -809,7 +808,7 @@ func TestToUpdatePayload(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) + diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/network/utils/model/model.go b/stackit/internal/services/iaas/network/utils/model/model.go deleted file mode 100644 index 73f994ec..00000000 --- a/stackit/internal/services/iaas/network/utils/model/model.go +++ /dev/null @@ -1,53 +0,0 @@ -package model - -import "github.com/hashicorp/terraform-plugin-framework/types" - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - NetworkId types.String `tfsdk:"network_id"` - Name types.String `tfsdk:"name"` - Nameservers types.List `tfsdk:"nameservers"` - IPv4Gateway types.String `tfsdk:"ipv4_gateway"` - IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` - IPv4Prefix types.String `tfsdk:"ipv4_prefix"` - IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` - Prefixes types.List `tfsdk:"prefixes"` - IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` - IPv6Gateway types.String `tfsdk:"ipv6_gateway"` - IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` - IPv6Prefix types.String `tfsdk:"ipv6_prefix"` - IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` - IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` - PublicIP types.String `tfsdk:"public_ip"` - Labels types.Map `tfsdk:"labels"` - Routed types.Bool `tfsdk:"routed"` - NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"` - NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"` - Region types.String `tfsdk:"region"` - RoutingTableID types.String `tfsdk:"routing_table_id"` -} - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - NetworkId types.String `tfsdk:"network_id"` - Name types.String `tfsdk:"name"` - Nameservers types.List `tfsdk:"nameservers"` - IPv4Gateway types.String `tfsdk:"ipv4_gateway"` - IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` - IPv4Prefix types.String `tfsdk:"ipv4_prefix"` - IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` - Prefixes types.List `tfsdk:"prefixes"` - IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` - IPv6Gateway types.String `tfsdk:"ipv6_gateway"` - IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` - IPv6Prefix types.String `tfsdk:"ipv6_prefix"` - IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` - IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` - PublicIP types.String `tfsdk:"public_ip"` - Labels types.Map `tfsdk:"labels"` - Routed types.Bool `tfsdk:"routed"` - Region types.String `tfsdk:"region"` - RoutingTableID types.String `tfsdk:"routing_table_id"` -} diff --git a/stackit/internal/services/iaas/network/utils/v1network/datasource.go b/stackit/internal/services/iaas/network/utils/v1network/datasource.go deleted file mode 100644 index ee4097af..00000000 --- a/stackit/internal/services/iaas/network/utils/v1network/datasource.go +++ /dev/null @@ -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 -} diff --git a/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go b/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go deleted file mode 100644 index 2ce9b5c9..00000000 --- a/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go +++ /dev/null @@ -1,352 +0,0 @@ -package v1network - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" -) - -func TestMapDataSourceFields(t *testing.T) { - tests := []struct { - description string - state networkModel.DataSourceModel - input *iaas.Network - expected networkModel.DataSourceModel - isValid bool - }{ - { - "id_ok", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Gateway: iaas.NewNullableString(nil), - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - IPv4Prefix: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefix: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - }, - true, - }, - { - "values_ok", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Name: utils.Ptr("name"), - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Prefixes: &[]string{ - "192.168.42.0/24", - "10.100.10.0/16", - }, - NameserversV6: &[]string{ - "ns1", - "ns2", - }, - PrefixesV6: &[]string{ - "fd12:3456:789a:1::/64", - "fd12:3456:789a:2::/64", - }, - PublicIp: utils.Ptr("publicIp"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(true), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Gatewayv6: iaas.NewNullableString(utils.Ptr("gateway")), - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefix: types.StringValue("192.168.42.0/24"), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - PublicIP: types.StringValue("publicIp"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - IPv6Gateway: types.StringValue("gateway"), - }, - true, - }, - { - "ipv4_nameservers_changed_outside_tf", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Nameservers: &[]string{ - "ns2", - "ns3", - }, - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "ipv6_nameservers_changed_outside_tf", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - NameserversV6: &[]string{ - "ns2", - "ns3", - }, - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "ipv4_prefixes_changed_outside_tf", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Prefixes: &[]string{ - "10.100.20.0/16", - "10.100.10.0/16", - }, - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Value(16), - IPv4Prefix: types.StringValue("10.100.20.0/16"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("10.100.20.0/16"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("10.100.20.0/16"), - types.StringValue("10.100.10.0/16"), - }), - }, - true, - }, - { - "ipv6_prefixes_changed_outside_tf", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - PrefixesV6: &[]string{ - "fd12:3456:789a:3::/64", - "fd12:3456:789a:4::/64", - }, - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefix: types.StringValue("fd12:3456:789a:3::/64"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:3::/64"), - types.StringValue("fd12:3456:789a:4::/64"), - }), - }, - true, - }, - { - "ipv4_ipv6_gateway_nil", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - }, - networkModel.DataSourceModel{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - }, - true, - }, - { - "response_nil_fail", - networkModel.DataSourceModel{}, - nil, - networkModel.DataSourceModel{}, - false, - }, - { - "no_resource_id", - networkModel.DataSourceModel{ - ProjectId: types.StringValue("pid"), - }, - &iaas.Network{}, - networkModel.DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/network/utils/v1network/resource.go b/stackit/internal/services/iaas/network/utils/v1network/resource.go deleted file mode 100644 index 4830d052..00000000 --- a/stackit/internal/services/iaas/network/utils/v1network/resource.go +++ /dev/null @@ -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 -} diff --git a/stackit/internal/services/iaas/network/utils/v1network/resource_test.go b/stackit/internal/services/iaas/network/utils/v1network/resource_test.go deleted file mode 100644 index 9a1f289a..00000000 --- a/stackit/internal/services/iaas/network/utils/v1network/resource_test.go +++ /dev/null @@ -1,811 +0,0 @@ -package v1network - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state model.Model - input *iaas.Network - expected model.Model - isValid bool - }{ - { - "id_ok", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Gateway: iaas.NewNullableString(nil), - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - IPv4Prefix: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefix: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - }, - true, - }, - { - "values_ok", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Name: utils.Ptr("name"), - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Prefixes: &[]string{ - "192.168.42.0/24", - "10.100.10.0/16", - }, - NameserversV6: &[]string{ - "ns1", - "ns2", - }, - PrefixesV6: &[]string{ - "fd12:3456:789a:1::/64", - "fd12:3456:789b:1::/64", - }, - PublicIp: utils.Ptr("publicIp"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(true), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Gatewayv6: iaas.NewNullableString(utils.Ptr("gateway")), - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefix: types.StringValue("192.168.42.0/24"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789b:1::/64"), - }), - IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), - PublicIP: types.StringValue("publicIp"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - IPv6Gateway: types.StringValue("gateway"), - }, - true, - }, - { - "ipv4_nameservers_changed_outside_tf", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Nameservers: &[]string{ - "ns2", - "ns3", - }, - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "ipv6_nameservers_changed_outside_tf", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - NameserversV6: &[]string{ - "ns2", - "ns3", - }, - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "ipv4_prefixes_changed_outside_tf", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/24"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - Prefixes: &[]string{ - "192.168.54.0/24", - "192.168.55.0/24", - }, - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Value(24), - IPv4Prefix: types.StringValue("192.168.54.0/24"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.54.0/24"), - types.StringValue("192.168.55.0/24"), - }), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.54.0/24"), - types.StringValue("192.168.55.0/24"), - }), - }, - true, - }, - { - "ipv6_prefixes_changed_outside_tf", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - PrefixesV6: &[]string{ - "fd12:3456:789a:1::/64", - "fd12:3456:789a:2::/64", - }, - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), - }, - true, - }, - { - "ipv4_ipv6_gateway_nil", - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - NetworkId: utils.Ptr("nid"), - }, - model.Model{ - Id: types.StringValue("pid,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - }, - true, - }, - { - "response_nil_fail", - model.Model{}, - nil, - model.Model{}, - false, - }, - { - "no_resource_id", - model.Model{ - ProjectId: types.StringValue("pid"), - }, - &iaas.Network{}, - model.Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *model.Model - expected *iaas.CreateNetworkPayload - isValid bool - }{ - { - "default_ok", - &model.Model{ - Name: types.StringValue("name"), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv4Gateway: types.StringValue("gateway"), - IPv4Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv4: &iaas.CreateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv4_nameservers_okay", - &model.Model{ - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv4Gateway: types.StringValue("gateway"), - IPv4Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv4: &iaas.CreateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv6_default_ok", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv6PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv6Gateway: types.StringValue("gateway"), - IPv6Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv6: &iaas.CreateNetworkIPv6Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv6_nameserver_null", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv6Gateway: types.StringValue("gateway"), - IPv6Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv6: &iaas.CreateNetworkIPv6Body{ - Nameservers: nil, - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv6_nameserver_empty_list", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), - IPv6PrefixLength: types.Int64Value(24), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv6Gateway: types.StringValue("gateway"), - IPv6Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv6: &iaas.CreateNetworkIPv6Body{ - Nameservers: utils.Ptr([]string{}), - PrefixLength: utils.Ptr(int64(24)), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *model.Model - state model.Model - expected *iaas.PartialUpdateNetworkPayload - isValid bool - }{ - { - "default_ok", - &model.Model{ - Name: types.StringValue("name"), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv4_nameservers_okay", - &model.Model{ - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv4_gateway_nil", - &model.Model{ - Name: types.StringValue("name"), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_default_ok", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv6Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_gateway_nil", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_nameserver_null", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListNull(types.StringType), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv6Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: nil, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_nameserver_empty_list", - &model.Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv6Gateway: types.StringValue("gateway"), - }, - model.Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: &[]string{}, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/network/utils/v2network/datasource.go b/stackit/internal/services/iaas/network/utils/v2network/datasource.go deleted file mode 100644 index c3acb3d4..00000000 --- a/stackit/internal/services/iaas/network/utils/v2network/datasource.go +++ /dev/null @@ -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 -} diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource.go b/stackit/internal/services/iaas/network/utils/v2network/resource.go deleted file mode 100644 index adf2ab8b..00000000 --- a/stackit/internal/services/iaas/network/utils/v2network/resource.go +++ /dev/null @@ -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 -} diff --git a/stackit/internal/services/iaas/networkarea/datasource.go b/stackit/internal/services/iaas/networkarea/datasource.go index f5ac30d9..75edfd2a 100644 --- a/stackit/internal/services/iaas/networkarea/datasource.go +++ b/stackit/internal/services/iaas/networkarea/datasource.go @@ -2,9 +2,15 @@ package networkarea import ( "context" + "errors" "fmt" "net/http" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" @@ -17,8 +23,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -58,6 +62,7 @@ func (d *networkAreaDataSource) Configure(ctx context.Context, req datasource.Co // Schema defines the schema for the data source. func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + deprecationMsg := "Deprecated because of the IaaS API v1 -> v2 migration. Will be removed in May 2026." description := "Network area datasource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ Description: description, @@ -99,13 +104,15 @@ func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaReq }, }, "default_nameservers": schema.ListAttribute{ - Description: "List of DNS Servers/Nameservers.", - Computed: true, - ElementType: types.StringType, + DeprecationMessage: deprecationMsg, + Description: "List of DNS Servers/Nameservers.", + Computed: true, + ElementType: types.StringType, }, "network_ranges": schema.ListNestedAttribute{ - Description: "List of Network ranges.", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "List of Network ranges.", + Computed: true, Validators: []validator.List{ listvalidator.SizeAtLeast(1), listvalidator.SizeAtMost(64), @@ -126,28 +133,32 @@ func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaReq }, }, "transfer_network": schema.StringAttribute{ - Description: "Classless Inter-Domain Routing (CIDR).", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "Classless Inter-Domain Routing (CIDR).", + Computed: true, }, "default_prefix_length": schema.Int64Attribute{ - Description: "The default prefix length for networks in the network area.", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The default prefix length for networks in the network area.", + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(24), int64validator.AtMost(29), }, }, "max_prefix_length": schema.Int64Attribute{ - Description: "The maximal prefix length for networks in the network area.", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The maximal prefix length for networks in the network area.", + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(24), int64validator.AtMost(29), }, }, "min_prefix_length": schema.Int64Attribute{ - Description: "The minimal prefix length for networks in the network area.", - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The minimal prefix length for networks in the network area.", + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(22), int64validator.AtMost(29), @@ -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() { diff --git a/stackit/internal/services/iaas/networkarea/resource.go b/stackit/internal/services/iaas/networkarea/resource.go index 621ad9b2..c4b5cb56 100644 --- a/stackit/internal/services/iaas/networkarea/resource.go +++ b/stackit/internal/services/iaas/networkarea/resource.go @@ -2,6 +2,7 @@ package networkarea import ( "context" + "errors" "fmt" "net/http" "strings" @@ -34,26 +35,55 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) +const ( + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + defaultValueDefaultPrefixLength = 25 + + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + defaultValueMinPrefixLength = 24 + + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + defaultValueMaxPrefixLength = 29 + + // Deprecated: Will be removed in May 2026. + deprecationWarningSummary = "Migration to new `stackit_network_area_region` resource needed" + // Deprecated: Will be removed in May 2026. + deprecationWarningDetails = "You're using deprecated features of the `stackit_network_area` resource. These will be removed in May 2026. Migrate to the new `stackit_network_area_region` resource instead." +) + // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &networkAreaResource{} - _ resource.ResourceWithConfigure = &networkAreaResource{} - _ resource.ResourceWithImportState = &networkAreaResource{} + _ resource.Resource = &networkAreaResource{} + _ resource.ResourceWithConfigure = &networkAreaResource{} + _ resource.ResourceWithImportState = &networkAreaResource{} + _ resource.ResourceWithValidateConfig = &networkAreaResource{} ) type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - Name types.String `tfsdk:"name"` - ProjectCount types.Int64 `tfsdk:"project_count"` - DefaultNameservers types.List `tfsdk:"default_nameservers"` - NetworkRanges types.List `tfsdk:"network_ranges"` - TransferNetwork types.String `tfsdk:"transfer_network"` - DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"` - MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"` - MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"` - Labels types.Map `tfsdk:"labels"` + Id types.String `tfsdk:"id"` // needed by TF + OrganizationId types.String `tfsdk:"organization_id"` + NetworkAreaId types.String `tfsdk:"network_area_id"` + Name types.String `tfsdk:"name"` + ProjectCount types.Int64 `tfsdk:"project_count"` + Labels types.Map `tfsdk:"labels"` + + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + DefaultNameservers types.List `tfsdk:"default_nameservers"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + NetworkRanges types.List `tfsdk:"network_ranges"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + TransferNetwork types.String `tfsdk:"transfer_network"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"` + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"` +} + +// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. LegacyMode checks if any of the deprecated fields are set which now relate to the network area region API resource. +func (model *Model) LegacyMode() bool { + return !model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown() || !model.TransferNetwork.IsNull() || model.TransferNetwork.IsUnknown() || !model.DefaultNameservers.IsNull() || model.DefaultNameservers.IsUnknown() || model.DefaultPrefixLength != types.Int64Value(int64(defaultValueDefaultPrefixLength)) || model.MinPrefixLength != types.Int64Value(int64(defaultValueMinPrefixLength)) || model.MaxPrefixLength != types.Int64Value(int64(defaultValueMaxPrefixLength)) } // Struct corresponding to Model.NetworkRanges[i] @@ -104,9 +134,27 @@ func (r *networkAreaResource) Configure(ctx context.Context, req resource.Config tflog.Info(ctx, "IaaS client configured") } +// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. +func (r *networkAreaResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var resourceModel Model + resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) + if resp.Diagnostics.HasError() { + return + } + + if resourceModel.NetworkRanges.IsNull() != resourceModel.TransferNetwork.IsNull() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network network area", "You have to either provide both the `network_ranges` and `transfer_network` fields simultaneously or none of them.") + } + + if (resourceModel.NetworkRanges.IsNull() || resourceModel.TransferNetwork.IsNull()) && (!resourceModel.DefaultNameservers.IsNull() || !resourceModel.DefaultPrefixLength.IsNull() || !resourceModel.MinPrefixLength.IsNull() || !resourceModel.MaxPrefixLength.IsNull()) { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network network area", "You have to provide both the `network_ranges` and `transfer_network` fields when providing one of these fields: `default_nameservers`, `default_prefix_length`, `max_prefix_length`, `min_prefix_length`") + } +} + // Schema defines the schema for the resource. func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Network area resource schema. Must have a `region` specified in the provider configuration." + deprecationMsg := "Deprecated because of the IaaS API v1 -> v2 migration. Will be removed in May 2026. Use the new `stackit_network_area_region` resource instead." + description := "Network area resource schema." resp.Schema = schema.Schema{ Description: description, MarkdownDescription: description, @@ -155,14 +203,18 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest int64validator.AtLeast(0), }, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "default_nameservers": schema.ListAttribute{ - Description: "List of DNS Servers/Nameservers.", - Optional: true, - ElementType: types.StringType, + Description: "List of DNS Servers/Nameservers for configuration of network area for region `eu01`.", + DeprecationMessage: deprecationMsg, + Optional: true, + ElementType: types.StringType, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "network_ranges": schema.ListNestedAttribute{ - Description: "List of Network ranges.", - Required: true, + Description: "List of Network ranges for configuration of network area for region `eu01`.", + DeprecationMessage: deprecationMsg, + Optional: true, Validators: []validator.List{ listvalidator.SizeAtLeast(1), listvalidator.SizeAtMost(64), @@ -170,55 +222,65 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "network_range_id": schema.StringAttribute{ - Computed: true, + DeprecationMessage: deprecationMsg, + Computed: true, Validators: []validator.String{ validate.UUID(), validate.NoSeparator(), }, }, "prefix": schema.StringAttribute{ - Description: "Classless Inter-Domain Routing (CIDR).", - Required: true, + DeprecationMessage: deprecationMsg, + Description: "Classless Inter-Domain Routing (CIDR).", + Required: true, }, }, }, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "transfer_network": schema.StringAttribute{ - Description: "Classless Inter-Domain Routing (CIDR).", - Required: true, + DeprecationMessage: deprecationMsg, + Description: "Classless Inter-Domain Routing (CIDR) for configuration of network area for region `eu01`.", + Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "default_prefix_length": schema.Int64Attribute{ - Description: "The default prefix length for networks in the network area.", - Optional: true, - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The default prefix length for networks in the network area for region `eu01`.", + Optional: true, + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(24), int64validator.AtMost(29), }, - Default: int64default.StaticInt64(25), + Default: int64default.StaticInt64(defaultValueDefaultPrefixLength), }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "max_prefix_length": schema.Int64Attribute{ - Description: "The maximal prefix length for networks in the network area.", - Optional: true, - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The maximal prefix length for networks in the network area for region `eu01`.", + Optional: true, + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(24), int64validator.AtMost(29), }, - Default: int64default.StaticInt64(29), + Default: int64default.StaticInt64(defaultValueMaxPrefixLength), }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "min_prefix_length": schema.Int64Attribute{ - Description: "The minimal prefix length for networks in the network area.", - Optional: true, - Computed: true, + DeprecationMessage: deprecationMsg, + Description: "The minimal prefix length for networks in the network area for region `eu01`.", + Optional: true, + Computed: true, Validators: []validator.Int64{ int64validator.AtLeast(8), int64validator.AtMost(29), }, - Default: int64default.StaticInt64(24), + Default: int64default.StaticInt64(defaultValueMinPrefixLength), }, "labels": schema.MapAttribute{ Description: "Labels are key-value string pairs which can be attached to a resource container", @@ -233,8 +295,7 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) if resp.Diagnostics.HasError() { return } @@ -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) } diff --git a/stackit/internal/services/iaas/networkarea/resource_test.go b/stackit/internal/services/iaas/networkarea/resource_test.go index d91044b9..dbcdfbb5 100644 --- a/stackit/internal/services/iaas/networkarea/resource_test.go +++ b/stackit/internal/services/iaas/networkarea/resource_test.go @@ -28,16 +28,15 @@ var testRangeId2Repeated = uuid.NewString() func TestMapFields(t *testing.T) { tests := []struct { - description string - state Model - input *iaas.NetworkArea - ListNetworkRanges *[]iaas.NetworkRange - expected Model - isValid bool + description string + state Model + input *iaas.NetworkArea + expected Model + isValid bool }{ { - "id_ok", - Model{ + description: "id_ok", + state: Model{ OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ @@ -50,32 +49,16 @@ func TestMapFields(t *testing.T) { "prefix": types.StringValue("prefix-2"), }), }), + DefaultNameservers: types.ListNull(types.StringType), }, - &iaas.NetworkArea{ - AreaId: utils.Ptr("naid"), - Ipv4: &iaas.NetworkAreaIPv4{}, + input: &iaas.NetworkArea{ + Id: utils.Ptr("naid"), }, - &[]iaas.NetworkRange{ - { - NetworkRangeId: utils.Ptr(testRangeId1), - Prefix: utils.Ptr("prefix-1"), - }, - { - NetworkRangeId: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - }, - - Model{ - Id: types.StringValue("oid,naid"), - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - Name: types.StringNull(), - DefaultNameservers: types.ListNull(types.StringType), - TransferNetwork: types.StringNull(), - DefaultPrefixLength: types.Int64Null(), - MaxPrefixLength: types.Int64Null(), - MinPrefixLength: types.Int64Null(), + expected: Model{ + Id: types.StringValue("oid,naid"), + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("naid"), + Name: types.StringNull(), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ "network_range_id": types.StringValue(testRangeId1), @@ -86,13 +69,14 @@ func TestMapFields(t *testing.T) { "prefix": types.StringValue("prefix-2"), }), }), - Labels: types.MapNull(types.StringType), + DefaultNameservers: types.ListNull(types.StringType), + Labels: types.MapNull(types.StringType), }, - true, + isValid: true, }, { - "values_ok", - Model{ + description: "values_ok", + state: Model{ OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ @@ -105,47 +89,20 @@ func TestMapFields(t *testing.T) { "prefix": types.StringValue("prefix-2"), }), }), + DefaultNameservers: types.ListNull(types.StringType), }, - &iaas.NetworkArea{ - AreaId: utils.Ptr("naid"), - Ipv4: &iaas.NetworkAreaIPv4{ - DefaultNameservers: &[]string{ - "nameserver1", - "nameserver2", - }, - TransferNetwork: utils.Ptr("network"), - DefaultPrefixLen: utils.Ptr(int64(20)), - MaxPrefixLen: utils.Ptr(int64(22)), - MinPrefixLen: utils.Ptr(int64(18)), - }, + input: &iaas.NetworkArea{ + Id: utils.Ptr("naid"), Name: utils.Ptr("name"), Labels: &map[string]interface{}{ "key": "value", }, }, - &[]iaas.NetworkRange{ - { - NetworkRangeId: utils.Ptr(testRangeId1), - Prefix: utils.Ptr("prefix-1"), - }, - { - NetworkRangeId: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - }, - Model{ + expected: Model{ Id: types.StringValue("oid,naid"), OrganizationId: types.StringValue("oid"), NetworkAreaId: types.StringValue("naid"), Name: types.StringValue("name"), - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("nameserver1"), - types.StringValue("nameserver2"), - }), - TransferNetwork: types.StringValue("network"), - DefaultPrefixLength: types.Int64Value(20), - MaxPrefixLength: types.Int64Value(22), - MinPrefixLength: types.Int64Value(18), NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ "network_range_id": types.StringValue(testRangeId1), @@ -159,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( diff --git a/stackit/internal/services/iaas/networkarearegion/datasource.go b/stackit/internal/services/iaas/networkarearegion/datasource.go new file mode 100644 index 00000000..efa9648a --- /dev/null +++ b/stackit/internal/services/iaas/networkarearegion/datasource.go @@ -0,0 +1,181 @@ +package networkarearegion + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &networkAreaRegionDataSource{} +) + +// NewNetworkAreaRegionDataSource is a helper function to simplify the provider implementation. +func NewNetworkAreaRegionDataSource() datasource.DataSource { + return &networkAreaRegionDataSource{} +} + +// networkAreaRegionDataSource is the data source implementation. +type networkAreaRegionDataSource struct { + client *iaas.APIClient + providerData core.ProviderData +} + +// Metadata returns the data source type name. +func (d *networkAreaRegionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_network_area_region" +} + +func (d *networkAreaRegionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") +} + +// Schema defines the schema for the resource. +func (d *networkAreaRegionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + description := "Network area region data source schema." + + resp.Schema = schema.Schema{ + MarkdownDescription: description, + Description: description, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`\".", + Computed: true, + }, + "organization_id": schema.StringAttribute{ + Description: "STACKIT organization ID to which the network area is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "network_area_id": schema.StringAttribute{ + Description: "The network area ID.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, + "ipv4": schema.SingleNestedAttribute{ + Computed: true, + Description: "The regional IPv4 config of a network area.", + Attributes: map[string]schema.Attribute{ + "default_nameservers": schema.ListAttribute{ + Description: "List of DNS Servers/Nameservers.", + Computed: true, + ElementType: types.StringType, + }, + "network_ranges": schema.ListNestedAttribute{ + Description: "List of Network ranges.", + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(64), + }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "network_range_id": schema.StringAttribute{ + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "prefix": schema.StringAttribute{ + Description: "Classless Inter-Domain Routing (CIDR).", + Computed: true, + }, + }, + }, + }, + "transfer_network": schema.StringAttribute{ + Description: "IPv4 Classless Inter-Domain Routing (CIDR).", + Computed: true, + }, + "default_prefix_length": schema.Int64Attribute{ + Description: "The default prefix length for networks in the network area.", + Computed: true, + }, + "max_prefix_length": schema.Int64Attribute{ + Description: "The maximal prefix length for networks in the network area.", + Computed: true, + }, + "min_prefix_length": schema.Int64Attribute{ + Description: "The minimal prefix length for networks in the network area.", + Computed: true, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *networkAreaRegionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) + + networkAreaRegionResp, err := d.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() + if err != nil { + utils.LogError(ctx, &resp.Diagnostics, err, "Reading network area region", fmt.Sprintf("Region configuration for %q for network area %q does not exist.", region, networkAreaId), nil) + resp.State.RemoveResource(ctx) + return + } + + // Map response body to schema + err = mapFields(ctx, networkAreaRegionResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network area region read") +} diff --git a/stackit/internal/services/iaas/networkarearegion/resource.go b/stackit/internal/services/iaas/networkarearegion/resource.go new file mode 100644 index 00000000..36dd3a1a --- /dev/null +++ b/stackit/internal/services/iaas/networkarearegion/resource.go @@ -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 +} diff --git a/stackit/internal/services/iaas/networkarearegion/resource_test.go b/stackit/internal/services/iaas/networkarearegion/resource_test.go new file mode 100644 index 00000000..978ca80a --- /dev/null +++ b/stackit/internal/services/iaas/networkarearegion/resource_test.go @@ -0,0 +1,1052 @@ +package networkarearegion + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gorilla/mux" + "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" +) + +var ( + organizationId = uuid.NewString() + networkAreaId = uuid.NewString() + + networkRangeId1 = uuid.NewString() + networkRangeId2 = uuid.NewString() + networkRangeId3 = uuid.NewString() + networkRangeId4 = uuid.NewString() + networkRangeId5 = uuid.NewString() + networkRangeId2Repeated = uuid.NewString() +) + +func Test_mapFields(t *testing.T) { + type args struct { + networkAreaRegion *iaas.RegionalArea + model *Model + region string + } + tests := []struct { + name string + args args + want *Model + wantErr bool + }{ + { + name: "default", + args: args{ + model: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + }, + networkAreaRegion: &iaas.RegionalArea{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: &[]string{ + "nameserver1", + "nameserver2", + }, + TransferNetwork: utils.Ptr("network"), + DefaultPrefixLen: utils.Ptr(int64(20)), + MaxPrefixLen: utils.Ptr(int64(22)), + MinPrefixLen: utils.Ptr(int64(18)), + NetworkRanges: &[]iaas.NetworkRange{ + { + Id: utils.Ptr(networkRangeId1), + Prefix: utils.Ptr("prefix-1"), + }, + { + Id: utils.Ptr(networkRangeId2), + Prefix: utils.Ptr("prefix-2"), + }, + }, + }, + }, + region: "eu01", + }, + want: &Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s,eu01", organizationId, networkAreaId)), + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Region: types.StringValue("eu01"), + + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("nameserver1"), + types.StringValue("nameserver2"), + }), + TransferNetwork: types.StringValue("network"), + DefaultPrefixLength: types.Int64Value(20), + MaxPrefixLength: types.Int64Value(22), + MinPrefixLength: types.Int64Value(18), + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("prefix-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + }, + }, + }, + wantErr: false, + }, + { + name: "model is nil", + args: args{ + model: nil, + networkAreaRegion: &iaas.RegionalArea{}, + }, + want: nil, + wantErr: true, + }, + { + name: "network area region response is nil", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListNull(types.StringType), + NetworkRanges: []networkRangeModel{}, + }, + }, + }, + want: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListNull(types.StringType), + NetworkRanges: []networkRangeModel{}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if err := mapFields(ctx, tt.args.networkAreaRegion, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { + t.Errorf("mapFields() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(tt.args.model, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_toCreatePayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want *iaas.CreateNetworkAreaRegionPayload + wantErr bool + }{ + { + name: "default_ok", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringUnknown(), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringUnknown(), + Prefix: types.StringValue("pr-2"), + }, + }, + TransferNetwork: types.StringValue("network"), + DefaultPrefixLength: types.Int64Value(20), + MaxPrefixLength: types.Int64Value(22), + MinPrefixLength: types.Int64Value(18), + }, + }, + }, + want: &iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: &[]string{ + "ns1", + "ns2", + }, + NetworkRanges: &[]iaas.NetworkRange{ + { + Prefix: utils.Ptr("pr-1"), + }, + { + Prefix: utils.Ptr("pr-2"), + }, + }, + TransferNetwork: utils.Ptr("network"), + DefaultPrefixLen: utils.Ptr(int64(20)), + MaxPrefixLen: utils.Ptr(int64(22)), + MinPrefixLen: utils.Ptr(int64(18)), + }, + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toCreatePayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toCreatePayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_toUpdatePayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want *iaas.UpdateNetworkAreaRegionPayload + wantErr bool + }{ + { + name: "default_ok", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + DefaultPrefixLength: types.Int64Value(22), + MaxPrefixLength: types.Int64Value(24), + MinPrefixLength: types.Int64Value(20), + }, + }, + }, + want: &iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: &[]string{ + "ns1", + "ns2", + }, + DefaultPrefixLen: utils.Ptr(int64(22)), + MaxPrefixLen: utils.Ptr(int64(24)), + MinPrefixLen: utils.Ptr(int64(20)), + }, + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toUpdatePayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toUpdatePayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_mapIpv4NetworkRanges(t *testing.T) { + type args struct { + networkAreaRangesList *[]iaas.NetworkRange + model *Model + } + tests := []struct { + name string + args args + want *Model + wantErr bool + }{ + { + name: "model and response have ranges in different order", + args: args{ + model: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListNull(types.StringType), + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("prefix-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + }, + }, + }, + networkAreaRangesList: &[]iaas.NetworkRange{ + { + Id: utils.Ptr(networkRangeId2), + Prefix: utils.Ptr("prefix-2"), + }, + { + Id: utils.Ptr(networkRangeId3), + Prefix: utils.Ptr("prefix-3"), + }, + { + Id: utils.Ptr(networkRangeId1), + Prefix: utils.Ptr("prefix-1"), + }, + }, + }, + want: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("prefix-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("prefix-3"), + }, + }, + DefaultNameservers: types.ListNull(types.StringType), + }, + }, + wantErr: false, + }, + { + name: "network_ranges_changed_outside_tf", + args: args{ + model: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("prefix-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + }, + DefaultNameservers: types.ListNull(types.StringType), + }, + }, + networkAreaRangesList: &[]iaas.NetworkRange{ + { + Id: utils.Ptr(networkRangeId2), + Prefix: utils.Ptr("prefix-2"), + }, + { + Id: utils.Ptr(networkRangeId3), + Prefix: utils.Ptr("prefix-3"), + }, + }, + }, + want: &Model{ + OrganizationId: types.StringValue(organizationId), + NetworkAreaId: types.StringValue(networkAreaId), + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("prefix-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("prefix-3"), + }, + }, + DefaultNameservers: types.ListNull(types.StringType), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := mapIpv4NetworkRanges(context.Background(), tt.args.networkAreaRangesList, tt.args.model); (err != nil) != tt.wantErr { + t.Errorf("mapIpv4NetworkRanges() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(tt.args.model, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_updateIpv4NetworkRanges(t *testing.T) { + getAllNetworkRangesResp := iaas.NetworkRangeListResponse{ + Items: &[]iaas.NetworkRange{ + { + Prefix: utils.Ptr("pr-1"), + Id: utils.Ptr(networkRangeId1), + }, + { + Prefix: utils.Ptr("pr-2"), + Id: utils.Ptr(networkRangeId2), + }, + { + Prefix: utils.Ptr("pr-3"), + Id: utils.Ptr(networkRangeId3), + }, + { + Prefix: utils.Ptr("pr-2"), + Id: utils.Ptr(networkRangeId2Repeated), + }, + }, + } + getAllNetworkRangesRespBytes, err := json.Marshal(getAllNetworkRangesResp) + if err != nil { + t.Fatalf("Failed to marshal get all network ranges response: %v", err) + } + + // This is the response used whenever an API returns a failure response + failureRespBytes := []byte("{\"message\": \"Something bad happened\"") + + type args struct { + networkRanges []networkRangeModel + } + tests := []struct { + description string + args args + + expectedNetworkRangesStates map[string]bool // Keys are prefix; value is true if prefix should exist at the end, false if should be deleted + isValid bool + + // mock control + createNetworkRangesFails bool + deleteNetworkRangesFails bool + getAllNetworkRangesFails bool + }{ + { + description: "no_changes", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": true, + "pr-3": true, + }, + isValid: true, + }, + { + description: "create_network_ranges", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": true, + "pr-3": true, + "pr-4": true, + }, + isValid: true, + }, + { + description: "delete_network_ranges", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": false, + "pr-3": true, + }, + isValid: true, + }, + { + description: "multiple_changes", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId5), + Prefix: types.StringValue("pr-5"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": false, + "pr-3": true, + "pr-4": true, + "pr-5": true, + }, + isValid: true, + }, + { + description: "multiple_changes_repetition", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId5), + Prefix: types.StringValue("pr-5"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId5), + Prefix: types.StringValue("pr-5"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": false, + "pr-3": true, + "pr-4": true, + "pr-5": true, + }, + isValid: true, + }, + { + description: "multiple_changes_2", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId5), + Prefix: types.StringValue("pr-5"), + }, + }, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": false, + "pr-2": false, + "pr-3": false, + "pr-4": true, + "pr-5": true, + }, + isValid: true, + }, + { + description: "multiple_changes_3", + args: args{ + networkRanges: []networkRangeModel{}, + }, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": false, + "pr-2": false, + "pr-3": false, + }, + isValid: true, + }, + { + description: "get_fails", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + }, + }, + getAllNetworkRangesFails: true, + isValid: false, + }, + { + description: "create_fails_1", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + }, + }, + createNetworkRangesFails: true, + isValid: false, + }, + { + description: "create_fails_2", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + }, + }, + createNetworkRangesFails: true, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": true, + "pr-3": false, + }, + isValid: true, + }, + { + description: "delete_fails_1", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + }, + }, + deleteNetworkRangesFails: true, + isValid: false, + }, + { + description: "delete_fails_2", + args: args{ + networkRanges: []networkRangeModel{ + { + NetworkRangeId: types.StringValue(networkRangeId1), + Prefix: types.StringValue("pr-1"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId2), + Prefix: types.StringValue("pr-2"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId3), + Prefix: types.StringValue("pr-3"), + }, + { + NetworkRangeId: types.StringValue(networkRangeId4), + Prefix: types.StringValue("pr-4"), + }, + }, + }, + deleteNetworkRangesFails: true, + expectedNetworkRangesStates: map[string]bool{ + "pr-1": true, + "pr-2": true, + "pr-3": true, + "pr-4": true, + }, + isValid: true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + // Will be compared to tt.expectedNetworkRangesStates at the end + networkRangesStates := make(map[string]bool) + networkRangesStates["pr-1"] = true + networkRangesStates["pr-2"] = true + networkRangesStates["pr-3"] = true + + // Handler for getting all network ranges + getAllNetworkRangesHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + if tt.getAllNetworkRangesFails { + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) + if err != nil { + t.Errorf("Get all network ranges handler: failed to write bad response: %v", err) + } + return + } + + _, err := w.Write(getAllNetworkRangesRespBytes) + if err != nil { + t.Errorf("Get all network ranges handler: failed to write response: %v", err) + } + }) + + // Handler for creating network range + createNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var payload iaas.CreateNetworkAreaRangePayload + err := decoder.Decode(&payload) + if err != nil { + t.Errorf("Create network range handler: failed to parse payload") + return + } + if payload.Ipv4 == nil { + t.Errorf("Create network range handler: nil Ipv4") + return + } + ipv4 := *payload.Ipv4 + + for _, networkRange := range ipv4 { + prefix := *networkRange.Prefix + if prefixExists, prefixWasCreated := networkRangesStates[prefix]; prefixWasCreated && prefixExists { + t.Errorf("Create network range handler: attempted to create range '%v' that already exists", *payload.Ipv4) + return + } + w.Header().Set("Content-Type", "application/json") + if tt.createNetworkRangesFails { + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) + if err != nil { + t.Errorf("Create network ranges handler: failed to write bad response: %v", err) + } + return + } + + resp := iaas.NetworkRange{ + Prefix: utils.Ptr("prefix"), + Id: utils.Ptr("id-range"), + } + respBytes, err := json.Marshal(resp) + if err != nil { + t.Errorf("Create network range handler: failed to marshal response: %v", err) + return + } + _, err = w.Write(respBytes) + if err != nil { + t.Errorf("Create network range handler: failed to write response: %v", err) + } + networkRangesStates[prefix] = true + } + }) + + // Handler for deleting Network range + deleteNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + networkRangeId, ok := vars["networkRangeId"] + if !ok { + t.Errorf("Delete network range handler: no range ID") + return + } + + var prefix string + for _, rangeItem := range *getAllNetworkRangesResp.Items { + if *rangeItem.Id == networkRangeId { + prefix = *rangeItem.Prefix + } + } + prefixExists, prefixWasCreated := networkRangesStates[prefix] + if !prefixWasCreated { + t.Errorf("Delete network range handler: attempted to delete range '%v' that wasn't created", prefix) + return + } + if prefixWasCreated && !prefixExists { + t.Errorf("Delete network range handler: attempted to delete range '%v' that was already deleted", prefix) + return + } + + w.Header().Set("Content-Type", "application/json") + if tt.deleteNetworkRangesFails { + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) + if err != nil { + t.Errorf("Delete network range handler: failed to write bad response: %v", err) + } + return + } + + _, err = w.Write([]byte("{}")) + if err != nil { + t.Errorf("Delete network range handler: failed to write response: %v", err) + } + networkRangesStates[prefix] = false + }) + + // Setup server and client + router := mux.NewRouter() + router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + getAllNetworkRangesHandler(w, r) + } else if r.Method == "POST" { + createNetworkRangeHandler(w, r) + } + }) + router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges/{networkRangeId}", deleteNetworkRangeHandler) + mockedServer := httptest.NewServer(router) + defer mockedServer.Close() + client, err := iaas.NewAPIClient( + config.WithEndpoint(mockedServer.URL), + config.WithoutAuthentication(), + ) + if err != nil { + t.Fatalf("Failed to initialize client: %v", err) + } + + // Run test + err = updateIpv4NetworkRanges(context.Background(), organizationId, networkAreaId, tt.args.networkRanges, client, testRegion) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(networkRangesStates, tt.expectedNetworkRangesStates) + if diff != "" { + t.Fatalf("Network range states do not match: %s", diff) + } + } + }) + } +} + +func Test_toDefaultNameserversPayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "values_ok", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("1.1.1.1"), + types.StringValue("8.8.8.8"), + types.StringValue("9.9.9.9"), + }), + }, + }, + }, + want: []string{ + "1.1.1.1", + "8.8.8.8", + "9.9.9.9", + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toDefaultNameserversPayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toDefaultNameserversPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("toDefaultNameserversPayload() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_toNetworkRangesPayload(t *testing.T) { + type args struct { + model *Model + } + tests := []struct { + name string + args args + want *[]iaas.NetworkRange + wantErr bool + }{ + { + name: "values_ok", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{ + { + Prefix: types.StringValue("prefix-1"), + }, + { + Prefix: types.StringValue("prefix-2"), + }, + }, + }, + }, + }, + want: &[]iaas.NetworkRange{ + { + Prefix: utils.Ptr("prefix-1"), + }, + { + Prefix: utils.Ptr("prefix-2"), + }, + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + }, + wantErr: true, + }, + { + name: "network ranges is nil", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + NetworkRanges: nil, + }, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "network ranges has length 0", + args: args{ + model: &Model{ + Ipv4: &ipv4Model{ + NetworkRanges: []networkRangeModel{}, + }, + }, + }, + want: nil, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toNetworkRangesPayload(context.Background(), tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toNetworkRangesPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/stackit/internal/services/iaas/networkarearoute/datasource.go b/stackit/internal/services/iaas/networkarearoute/datasource.go index dd76947b..924d90ca 100644 --- a/stackit/internal/services/iaas/networkarearoute/datasource.go +++ b/stackit/internal/services/iaas/networkarearoute/datasource.go @@ -31,7 +31,8 @@ func NewNetworkAreaRouteDataSource() datasource.DataSource { // networkDataSource is the data source implementation. type networkAreaRouteDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *networkAreaRouteDataSource) Metadata(_ context.Context, req datasource. } func (d *networkAreaRouteDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -61,7 +63,7 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche MarkdownDescription: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal data source ID. It is structured as \"`organization_id`,`network_area_id`,`network_area_route_id`\".", + Description: "Terraform's internal data source ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`network_area_route_id`\".", Computed: true, }, "organization_id": schema.StringAttribute{ @@ -80,6 +82,11 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "network_area_route_id": schema.StringAttribute{ Description: "The network area route ID.", Required: true, @@ -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() { diff --git a/stackit/internal/services/iaas/networkarearoute/resource.go b/stackit/internal/services/iaas/networkarearoute/resource.go index 4b74fdd5..f5ba3bd9 100644 --- a/stackit/internal/services/iaas/networkarearoute/resource.go +++ b/stackit/internal/services/iaas/networkarearoute/resource.go @@ -6,11 +6,12 @@ import ( "net/http" "strings" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -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) + } +} diff --git a/stackit/internal/services/iaas/networkarearoute/resource_test.go b/stackit/internal/services/iaas/networkarearoute/resource_test.go index d210e059..a0295cf3 100644 --- a/stackit/internal/services/iaas/networkarearoute/resource_test.go +++ b/stackit/internal/services/iaas/networkarearoute/resource_test.go @@ -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) + } + }) + } +} diff --git a/stackit/internal/services/iaas/networkinterface/datasource.go b/stackit/internal/services/iaas/networkinterface/datasource.go index 6ca52327..ad51f76a 100644 --- a/stackit/internal/services/iaas/networkinterface/datasource.go +++ b/stackit/internal/services/iaas/networkinterface/datasource.go @@ -24,14 +24,15 @@ var ( _ datasource.DataSource = &networkInterfaceDataSource{} ) -// NewNetworkDataSource is a helper function to simplify the provider implementation. +// NewNetworkInterfaceDataSource is a helper function to simplify the provider implementation. func NewNetworkInterfaceDataSource() datasource.DataSource { return &networkInterfaceDataSource{} } // networkInterfaceDataSource is the data source implementation. type networkInterfaceDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *networkInterfaceDataSource) Metadata(_ context.Context, req datasource. } func (d *networkInterfaceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -63,7 +65,7 @@ func (d *networkInterfaceDataSource) Schema(_ context.Context, _ datasource.Sche Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal data source ID. It is structured as \"`project_id`,`network_id`,`network_interface_id`\".", + Description: "Terraform's internal data source ID. It is structured as \"`project_id`,`region`,`network_id`,`network_interface_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -74,6 +76,11 @@ func (d *networkInterfaceDataSource) Schema(_ context.Context, _ datasource.Sche validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "network_id": schema.StringAttribute{ Description: "The network ID to which the network interface is associated.", Required: true, @@ -141,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 diff --git a/stackit/internal/services/iaas/networkinterface/resource.go b/stackit/internal/services/iaas/networkinterface/resource.go index 15ccb361..8ced0477 100644 --- a/stackit/internal/services/iaas/networkinterface/resource.go +++ b/stackit/internal/services/iaas/networkinterface/resource.go @@ -40,6 +40,7 @@ type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` NetworkId types.String `tfsdk:"network_id"` + Region types.String `tfsdk:"region"` NetworkInterfaceId types.String `tfsdk:"network_interface_id"` Name types.String `tfsdk:"name"` AllowedAddresses types.List `tfsdk:"allowed_addresses"` @@ -59,7 +60,8 @@ func NewNetworkInterfaceResource() resource.Resource { // networkResource is the resource implementation. type networkInterfaceResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // ModifyPlan implements resource.ResourceWithModifyPlan. @@ -92,6 +94,17 @@ func (r *networkInterfaceResource) ModifyPlan(ctx context.Context, req resource. if resp.Diagnostics.HasError() { return } + + // Use the modifier to set the effective region in the current plan. + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } } // Metadata returns the resource type name. @@ -101,12 +114,13 @@ func (r *networkInterfaceResource) Metadata(_ context.Context, req resource.Meta // Configure adds the provider configured client to the resource. func (r *networkInterfaceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -124,7 +138,7 @@ func (r *networkInterfaceResource) Schema(_ context.Context, _ resource.SchemaRe Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`,`network_interface_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`network_id`,`network_interface_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -164,6 +178,15 @@ func (r *networkInterfaceResource) Schema(_ context.Context, _ resource.SchemaRe validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "name": schema.StringAttribute{ Description: "The name of the network interface.", Optional: true, @@ -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 diff --git a/stackit/internal/services/iaas/networkinterface/resource_test.go b/stackit/internal/services/iaas/networkinterface/resource_test.go index 070c3c28..e549f7d3 100644 --- a/stackit/internal/services/iaas/networkinterface/resource_test.go +++ b/stackit/internal/services/iaas/networkinterface/resource_test.go @@ -12,25 +12,32 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.NIC + region string + } tests := []struct { description string - state Model - input *iaas.NIC + args args expected Model isValid bool }{ { - "id_ok", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), + 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) } diff --git a/stackit/internal/services/iaas/networkinterfaceattach/resource.go b/stackit/internal/services/iaas/networkinterfaceattach/resource.go index 73082a90..2b8d4240 100644 --- a/stackit/internal/services/iaas/networkinterfaceattach/resource.go +++ b/stackit/internal/services/iaas/networkinterfaceattach/resource.go @@ -11,7 +11,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -30,11 +29,13 @@ var ( _ resource.Resource = &networkInterfaceAttachResource{} _ resource.ResourceWithConfigure = &networkInterfaceAttachResource{} _ resource.ResourceWithImportState = &networkInterfaceAttachResource{} + _ resource.ResourceWithModifyPlan = &networkInterfaceAttachResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` NetworkInterfaceId types.String `tfsdk:"network_interface_id"` } @@ -46,7 +47,8 @@ func NewNetworkInterfaceAttachResource() resource.Resource { // networkInterfaceAttachResource is the resource implementation. type networkInterfaceAttachResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -54,14 +56,45 @@ func (r *networkInterfaceAttachResource) Metadata(_ context.Context, req resourc resp.TypeName = req.ProviderTypeName + "_server_network_interface_attach" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *networkInterfaceAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -71,13 +104,13 @@ func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req reso // Schema defines the schema for the resource. func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Network interface attachment resource schema. Attaches a network interface to a server. Must have a `region` specified in the provider configuration. The attachment only takes full effect after server reboot." + description := "Network interface attachment resource schema. Attaches a network interface to a server. The attachment only takes full effect after server reboot." resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`network_interface_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`network_interface_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -94,6 +127,15 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "server_id": schema.StringAttribute{ Description: "The server ID.", Required: true, @@ -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") } diff --git a/stackit/internal/services/iaas/project/datasource.go b/stackit/internal/services/iaas/project/datasource.go index 03d26fa6..ac2c0ec4 100644 --- a/stackit/internal/services/iaas/project/datasource.go +++ b/stackit/internal/services/iaas/project/datasource.go @@ -28,9 +28,12 @@ type DatasourceModel struct { ProjectId types.String `tfsdk:"project_id"` AreaId types.String `tfsdk:"area_id"` InternetAccess types.Bool `tfsdk:"internet_access"` - State types.String `tfsdk:"state"` + Status types.String `tfsdk:"status"` CreatedAt types.String `tfsdk:"created_at"` UpdatedAt types.String `tfsdk:"updated_at"` + + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. + State types.String `tfsdk:"state"` } // NewProjectDataSource is a helper function to simplify the provider implementation. @@ -70,7 +73,7 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest "project_id": "STACKIT project ID.", "area_id": "The area ID to which the project belongs to.", "internet_access": "Specifies if the project has internet_access", - "state": "Specifies the state of the project.", + "status": "Specifies the status of the project.", "created_at": "Date-time when the project was created.", "updated_at": "Date-time when the project was last updated.", } @@ -98,8 +101,14 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest Description: descriptions["internet_access"], Computed: true, }, + // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. "state": schema.StringAttribute{ - Description: descriptions["state"], + DeprecationMessage: "Deprecated: Will be removed in May 2026. Use the `status` field instead.", + Description: descriptions["status"], + Computed: true, + }, + "status": schema.StringAttribute{ + Description: descriptions["status"], Computed: true, }, "created_at": schema.StringAttribute{ @@ -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 diff --git a/stackit/internal/services/iaas/project/datasource_test.go b/stackit/internal/services/iaas/project/datasource_test.go index adbd5ec2..d2e57489 100644 --- a/stackit/internal/services/iaas/project/datasource_test.go +++ b/stackit/internal/services/iaas/project/datasource_test.go @@ -34,7 +34,7 @@ func TestMapDataSourceFields(t *testing.T) { ProjectId: types.StringValue(projectId), }, input: &iaas.Project{ - ProjectId: utils.Ptr(projectId), + Id: utils.Ptr(projectId), }, expected: &DatasourceModel{ Id: types.StringValue(projectId), @@ -48,13 +48,12 @@ func TestMapDataSourceFields(t *testing.T) { ProjectId: types.StringValue(projectId), }, input: &iaas.Project{ - AreaId: utils.Ptr(iaas.AreaId{String: utils.Ptr("aid")}), - CreatedAt: utils.Ptr(testTimestamp()), - InternetAccess: utils.Ptr(true), - OpenstackProjectId: utils.Ptr("oid"), - ProjectId: utils.Ptr(projectId), - State: utils.Ptr("CREATED"), - UpdatedAt: utils.Ptr(testTimestamp()), + AreaId: utils.Ptr(iaas.AreaId{String: utils.Ptr("aid")}), + CreatedAt: utils.Ptr(testTimestamp()), + InternetAccess: utils.Ptr(true), + Id: utils.Ptr(projectId), + Status: utils.Ptr("CREATED"), + UpdatedAt: utils.Ptr(testTimestamp()), }, expected: &DatasourceModel{ Id: types.StringValue(projectId), @@ -62,6 +61,7 @@ func TestMapDataSourceFields(t *testing.T) { AreaId: types.StringValue("aid"), InternetAccess: types.BoolValue(true), State: types.StringValue("CREATED"), + Status: types.StringValue("CREATED"), CreatedAt: types.StringValue(testTimestampValue), UpdatedAt: types.StringValue(testTimestampValue), }, @@ -76,7 +76,7 @@ func TestMapDataSourceFields(t *testing.T) { AreaId: utils.Ptr(iaas.AreaId{ StaticAreaID: iaas.STATICAREAID_PUBLIC.Ptr(), }), - ProjectId: utils.Ptr(projectId), + Id: utils.Ptr(projectId), }, expected: &DatasourceModel{ Id: types.StringValue(projectId), diff --git a/stackit/internal/services/iaas/publicip/datasource.go b/stackit/internal/services/iaas/publicip/datasource.go index 12e602ad..64b46425 100644 --- a/stackit/internal/services/iaas/publicip/datasource.go +++ b/stackit/internal/services/iaas/publicip/datasource.go @@ -24,14 +24,15 @@ var ( _ datasource.DataSource = &publicIpDataSource{} ) -// NewVolumeDataSource is a helper function to simplify the provider implementation. +// NewPublicIpDataSource is a helper function to simplify the provider implementation. func NewPublicIpDataSource() datasource.DataSource { return &publicIpDataSource{} } // publicIpDataSource is the data source implementation. type publicIpDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *publicIpDataSource) Metadata(_ context.Context, req datasource.Metadata } func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -54,14 +56,14 @@ func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.Confi } // Schema defines the schema for the resource. -func (r *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Public IP resource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`public_ip_id`\".", + Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`region`,`public_ip_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -72,6 +74,11 @@ func (r *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaReques validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "public_ip_id": schema.StringAttribute{ Description: "The public IP ID.", Required: true, @@ -110,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 diff --git a/stackit/internal/services/iaas/publicip/resource.go b/stackit/internal/services/iaas/publicip/resource.go index b1a40865..aa8ac637 100644 --- a/stackit/internal/services/iaas/publicip/resource.go +++ b/stackit/internal/services/iaas/publicip/resource.go @@ -10,7 +10,6 @@ import ( iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -30,11 +29,13 @@ var ( _ resource.Resource = &publicIpResource{} _ resource.ResourceWithConfigure = &publicIpResource{} _ resource.ResourceWithImportState = &publicIpResource{} + _ resource.ResourceWithModifyPlan = &publicIpResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` PublicIpId types.String `tfsdk:"public_ip_id"` Ip types.String `tfsdk:"ip"` NetworkInterfaceId types.String `tfsdk:"network_interface_id"` @@ -48,7 +49,8 @@ func NewPublicIpResource() resource.Resource { // publicIpResource is the resource implementation. type publicIpResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -56,14 +58,45 @@ func (r *publicIpResource) Metadata(_ context.Context, req resource.MetadataRequ resp.TypeName = req.ProviderTypeName + "_public_ip" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *publicIpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *publicIpResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -79,7 +112,7 @@ func (r *publicIpResource) Schema(_ context.Context, _ resource.SchemaRequest, r Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`public_ip_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`public_ip_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -96,6 +129,15 @@ func (r *publicIpResource) Schema(_ context.Context, _ resource.SchemaRequest, r validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "public_ip_id": schema.StringAttribute{ Description: "The public IP ID.", Computed: true, @@ -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 { diff --git a/stackit/internal/services/iaas/publicip/resource_test.go b/stackit/internal/services/iaas/publicip/resource_test.go index 1eda0e8d..d1797897 100644 --- a/stackit/internal/services/iaas/publicip/resource_test.go +++ b/stackit/internal/services/iaas/publicip/resource_test.go @@ -12,49 +12,61 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.PublicIp + region string + } tests := []struct { description string - state Model - input *iaas.PublicIp + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), + 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) } diff --git a/stackit/internal/services/iaas/publicipassociate/resource.go b/stackit/internal/services/iaas/publicipassociate/resource.go index 483a7762..66028381 100644 --- a/stackit/internal/services/iaas/publicipassociate/resource.go +++ b/stackit/internal/services/iaas/publicipassociate/resource.go @@ -10,7 +10,6 @@ import ( iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -30,11 +29,13 @@ var ( _ resource.Resource = &publicIpAssociateResource{} _ resource.ResourceWithConfigure = &publicIpAssociateResource{} _ resource.ResourceWithImportState = &publicIpAssociateResource{} + _ resource.ResourceWithModifyPlan = &publicIpAssociateResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` PublicIpId types.String `tfsdk:"public_ip_id"` Ip types.String `tfsdk:"ip"` NetworkInterfaceId types.String `tfsdk:"network_interface_id"` @@ -47,7 +48,8 @@ func NewPublicIpAssociateResource() resource.Resource { // publicIpAssociateResource is the resource implementation. type publicIpAssociateResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -55,14 +57,45 @@ func (r *publicIpAssociateResource) Metadata(_ context.Context, req resource.Met resp.TypeName = req.ProviderTypeName + "_public_ip_associate" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *publicIpAssociateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *publicIpAssociateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -88,7 +121,7 @@ func (r *publicIpAssociateResource) Schema(_ context.Context, _ resource.SchemaR Description: fmt.Sprintf("%s\n\n%s", descriptions["main"], descriptions["warning_message"]), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`public_ip_id`,`network_interface_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`public_ip_id`,`network_interface_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -105,6 +138,15 @@ func (r *publicIpAssociateResource) Schema(_ context.Context, _ resource.SchemaR validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "public_ip_id": schema.StringAttribute{ Description: "The public IP ID.", Required: true, @@ -151,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) diff --git a/stackit/internal/services/iaas/publicipassociate/resource_test.go b/stackit/internal/services/iaas/publicipassociate/resource_test.go index a15cf34b..f1c09f5a 100644 --- a/stackit/internal/services/iaas/publicipassociate/resource_test.go +++ b/stackit/internal/services/iaas/publicipassociate/resource_test.go @@ -10,74 +10,82 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.PublicIp + region string + } tests := []struct { description string - state Model - input *iaas.PublicIp + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - NetworkInterfaceId: types.StringValue("nicid"), + 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) } diff --git a/stackit/internal/services/iaas/securitygroup/datasource.go b/stackit/internal/services/iaas/securitygroup/datasource.go index bb117f75..d2b87e79 100644 --- a/stackit/internal/services/iaas/securitygroup/datasource.go +++ b/stackit/internal/services/iaas/securitygroup/datasource.go @@ -31,7 +31,8 @@ func NewSecurityGroupDataSource() datasource.DataSource { // securityGroupDataSource is the data source implementation. type securityGroupDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *securityGroupDataSource) Metadata(_ context.Context, req datasource.Met } func (d *securityGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -54,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 diff --git a/stackit/internal/services/iaas/securitygroup/resource.go b/stackit/internal/services/iaas/securitygroup/resource.go index 7641ee7b..07b510ac 100644 --- a/stackit/internal/services/iaas/securitygroup/resource.go +++ b/stackit/internal/services/iaas/securitygroup/resource.go @@ -12,7 +12,6 @@ import ( iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -33,11 +32,13 @@ var ( _ resource.Resource = &securityGroupResource{} _ resource.ResourceWithConfigure = &securityGroupResource{} _ resource.ResourceWithImportState = &securityGroupResource{} + _ resource.ResourceWithModifyPlan = &securityGroupResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` SecurityGroupId types.String `tfsdk:"security_group_id"` Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` @@ -52,7 +53,8 @@ func NewSecurityGroupResource() resource.Resource { // securityGroupResource is the resource implementation. type securityGroupResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -60,14 +62,45 @@ func (r *securityGroupResource) Metadata(_ context.Context, req resource.Metadat resp.TypeName = req.ProviderTypeName + "_security_group" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *securityGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *securityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -83,7 +116,7 @@ func (r *securityGroupResource) Schema(_ context.Context, _ resource.SchemaReque Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -100,6 +133,15 @@ func (r *securityGroupResource) Schema(_ context.Context, _ resource.SchemaReque validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "security_group_id": schema.StringAttribute{ Description: "The security group ID.", Computed: true, @@ -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 { diff --git a/stackit/internal/services/iaas/securitygroup/resource_test.go b/stackit/internal/services/iaas/securitygroup/resource_test.go index 2b4c1236..37498656 100644 --- a/stackit/internal/services/iaas/securitygroup/resource_test.go +++ b/stackit/internal/services/iaas/securitygroup/resource_test.go @@ -12,51 +12,62 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.SecurityGroup + region string + } tests := []struct { description string - state Model - input *iaas.SecurityGroup + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), + 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) } diff --git a/stackit/internal/services/iaas/securitygrouprule/datasource.go b/stackit/internal/services/iaas/securitygrouprule/datasource.go index 0861e9a6..fb675966 100644 --- a/stackit/internal/services/iaas/securitygrouprule/datasource.go +++ b/stackit/internal/services/iaas/securitygrouprule/datasource.go @@ -30,7 +30,8 @@ func NewSecurityGroupRuleDataSource() datasource.DataSource { // securityGroupRuleDataSource is the data source implementation. type securityGroupRuleDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -39,12 +40,13 @@ func (d *securityGroupRuleDataSource) Metadata(_ context.Context, req datasource } func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -53,7 +55,7 @@ func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasou } // Schema defines the schema for the resource. -func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { directionOptions := []string{"ingress", "egress"} description := "Security group datasource schema. Must have a `region` specified in the provider configuration." @@ -62,7 +64,7 @@ func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.Sch Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`security_group_id`,`security_group_rule_id`\".", + Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`region`,`security_group_id`,`security_group_rule_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -89,6 +91,11 @@ func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.Sch validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "direction": schema.StringAttribute{ Description: "The direction of the traffic which the rule should match. Some of the possible values are: " + utils.FormatPossibleValues(directionOptions...), Computed: true, @@ -164,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 diff --git a/stackit/internal/services/iaas/securitygrouprule/resource.go b/stackit/internal/services/iaas/securitygrouprule/resource.go index 56a15bd6..ab075b47 100644 --- a/stackit/internal/services/iaas/securitygrouprule/resource.go +++ b/stackit/internal/services/iaas/securitygrouprule/resource.go @@ -34,11 +34,13 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &securityGroupRuleResource{} - _ resource.ResourceWithConfigure = &securityGroupRuleResource{} - _ resource.ResourceWithImportState = &securityGroupRuleResource{} - icmpProtocols = []string{"icmp", "ipv6-icmp"} - protocolsPossibleValues = []string{ + _ resource.Resource = &securityGroupRuleResource{} + _ resource.ResourceWithConfigure = &securityGroupRuleResource{} + _ resource.ResourceWithImportState = &securityGroupRuleResource{} + _ resource.ResourceWithModifyPlan = &securityGroupRuleResource{} + + icmpProtocols = []string{"icmp", "ipv6-icmp"} + protocolsPossibleValues = []string{ "ah", "dccp", "egp", "esp", "gre", "icmp", "igmp", "ipip", "ipv6-encap", "ipv6-frag", "ipv6-icmp", "ipv6-nonxt", "ipv6-opts", "ipv6-route", "ospf", "pgm", "rsvp", "sctp", "tcp", "udp", "udplite", "vrrp", } @@ -47,6 +49,7 @@ var ( type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` SecurityGroupId types.String `tfsdk:"security_group_id"` SecurityGroupRuleId types.String `tfsdk:"security_group_rule_id"` Direction types.String `tfsdk:"direction"` @@ -99,7 +102,8 @@ func NewSecurityGroupRuleResource() resource.Resource { // securityGroupRuleResource is the resource implementation. type securityGroupRuleResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -107,14 +111,45 @@ func (r *securityGroupRuleResource) Metadata(_ context.Context, req resource.Met resp.TypeName = req.ProviderTypeName + "_security_group_rule" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *securityGroupRuleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *securityGroupRuleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -122,7 +157,7 @@ func (r *securityGroupRuleResource) Configure(ctx context.Context, req resource. tflog.Info(ctx, "iaas client configured") } -func (r securityGroupRuleResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { +func (r *securityGroupRuleResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var model Model resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) @@ -178,7 +213,7 @@ func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaR Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`,`security_group_rule_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`,`security_group_rule_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -196,6 +231,15 @@ func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaR validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "security_group_id": schema.StringAttribute{ Description: "The security group ID.", Required: true, @@ -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) diff --git a/stackit/internal/services/iaas/securitygrouprule/resource_test.go b/stackit/internal/services/iaas/securitygrouprule/resource_test.go index ef6dc006..dbf46f59 100644 --- a/stackit/internal/services/iaas/securitygrouprule/resource_test.go +++ b/stackit/internal/services/iaas/securitygrouprule/resource_test.go @@ -52,25 +52,32 @@ var fixtureCreateProtocol = iaas.CreateProtocol{ } func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.SecurityGroupRule + region string + } tests := []struct { description string - state Model - input *iaas.SecurityGroupRule + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + SecurityGroupRuleId: types.StringValue("sgrid"), + }, + input: &iaas.SecurityGroupRule{ + Id: utils.Ptr("sgrid"), + }, + region: "eu01", }, - &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - }, - Model{ - Id: types.StringValue("pid,sgid,sgrid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sgid,sgrid"), ProjectId: types.StringValue("pid"), SecurityGroupId: types.StringValue("sgid"), SecurityGroupRuleId: types.StringValue("sgrid"), @@ -82,29 +89,34 @@ func TestMapFields(t *testing.T) { IcmpParameters: types.ObjectNull(icmpParametersTypes), PortRange: types.ObjectNull(portRangeTypes), Protocol: types.ObjectNull(protocolTypes), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + SecurityGroupId: types.StringValue("sgid"), + SecurityGroupRuleId: types.StringValue("sgrid"), + Region: types.StringValue("eu01"), + }, + input: &iaas.SecurityGroupRule{ + Id: utils.Ptr("sgrid"), + Description: utils.Ptr("desc"), + Direction: utils.Ptr("ingress"), + Ethertype: utils.Ptr("ether"), + IpRange: utils.Ptr("iprange"), + RemoteSecurityGroupId: utils.Ptr("remote"), + IcmpParameters: &fixtureIcmpParameters, + PortRange: &fixturePortRange, + Protocol: &fixtureProtocol, + }, + region: "eu02", }, - &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - Description: utils.Ptr("desc"), - Direction: utils.Ptr("ingress"), - Ethertype: utils.Ptr("ether"), - IpRange: utils.Ptr("iprange"), - RemoteSecurityGroupId: utils.Ptr("remote"), - IcmpParameters: &fixtureIcmpParameters, - PortRange: &fixturePortRange, - Protocol: &fixtureProtocol, - }, - Model{ - Id: types.StringValue("pid,sgid,sgrid"), + expected: Model{ + Id: types.StringValue("pid,eu02,sgid,sgrid"), ProjectId: types.StringValue("pid"), SecurityGroupId: types.StringValue("sgid"), SecurityGroupRuleId: types.StringValue("sgrid"), @@ -116,26 +128,30 @@ func TestMapFields(t *testing.T) { IcmpParameters: fixtureModelIcmpParameters, PortRange: fixtureModelPortRange, Protocol: fixtureModelProtocol, + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "protocol_only_with_name", - Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{ - "name": types.StringValue("name"), - "number": types.Int64Null(), - }), + 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) } diff --git a/stackit/internal/services/iaas/server/datasource.go b/stackit/internal/services/iaas/server/datasource.go index ea0156fa..f68f5563 100644 --- a/stackit/internal/services/iaas/server/datasource.go +++ b/stackit/internal/services/iaas/server/datasource.go @@ -30,6 +30,7 @@ var ( type DataSourceModel struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` MachineType types.String `tfsdk:"machine_type"` Name types.String `tfsdk:"name"` @@ -58,7 +59,8 @@ func NewServerDataSource() datasource.DataSource { // serverDataSource is the data source implementation. type serverDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -67,12 +69,13 @@ func (d *serverDataSource) Metadata(_ context.Context, req datasource.MetadataRe } func (d *serverDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -81,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 { diff --git a/stackit/internal/services/iaas/server/datasource_test.go b/stackit/internal/services/iaas/server/datasource_test.go index bb709d15..56c2be53 100644 --- a/stackit/internal/services/iaas/server/datasource_test.go +++ b/stackit/internal/services/iaas/server/datasource_test.go @@ -12,24 +12,31 @@ import ( ) func TestMapDataSourceFields(t *testing.T) { + type args struct { + state DataSourceModel + input *iaas.Server + region string + } tests := []struct { description string - state DataSourceModel - input *iaas.Server + args args expected DataSourceModel isValid bool }{ { - "default_values", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), + 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) } diff --git a/stackit/internal/services/iaas/server/resource.go b/stackit/internal/services/iaas/server/resource.go index 0eac988a..5af72de8 100644 --- a/stackit/internal/services/iaas/server/resource.go +++ b/stackit/internal/services/iaas/server/resource.go @@ -42,6 +42,7 @@ var ( _ resource.Resource = &serverResource{} _ resource.ResourceWithConfigure = &serverResource{} _ resource.ResourceWithImportState = &serverResource{} + _ resource.ResourceWithModifyPlan = &serverResource{} supportedSourceTypes = []string{"volume", "image"} desiredStatusOptions = []string{modelStateActive, modelStateInactive, modelStateDeallocated} @@ -56,6 +57,7 @@ const ( type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` MachineType types.String `tfsdk:"machine_type"` Name types.String `tfsdk:"name"` @@ -100,7 +102,8 @@ func NewServerResource() resource.Resource { // serverResource is the resource implementation. type serverResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -108,7 +111,37 @@ func (r *serverResource) Metadata(_ context.Context, req resource.MetadataReques resp.TypeName = req.ProviderTypeName + "_server" } -func (r serverResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *serverResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var model Model resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) if resp.Diagnostics.HasError() { @@ -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{ diff --git a/stackit/internal/services/iaas/server/resource_test.go b/stackit/internal/services/iaas/server/resource_test.go index d9dac877..ad1c7074 100644 --- a/stackit/internal/services/iaas/server/resource_test.go +++ b/stackit/internal/services/iaas/server/resource_test.go @@ -26,24 +26,31 @@ func testTimestamp() time.Time { } func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.Server + region string + } tests := []struct { description string - state Model - input *iaas.Server + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + }, + input: &iaas.Server{ + Id: utils.Ptr("sid"), + }, + region: "eu01", }, - &iaas.Server{ - Id: utils.Ptr("sid"), - }, - Model{ - Id: types.StringValue("pid,sid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sid"), ProjectId: types.StringValue("pid"), ServerId: types.StringValue("sid"), Name: types.StringNull(), @@ -57,40 +64,45 @@ func TestMapFields(t *testing.T) { CreatedAt: types.StringNull(), UpdatedAt: types.StringNull(), LaunchedAt: types.StringNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - }, - &iaas.Server{ - Id: utils.Ptr("sid"), - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Region: types.StringValue("eu01"), }, - ImageId: utils.Ptr("image_id"), - Nics: &[]iaas.ServerNetwork{ - { - NicId: utils.Ptr("nic1"), + input: &iaas.Server{ + Id: utils.Ptr("sid"), + Name: utils.Ptr("name"), + AvailabilityZone: utils.Ptr("zone"), + Labels: &map[string]interface{}{ + "key": "value", }, - { - NicId: utils.Ptr("nic2"), + ImageId: utils.Ptr("image_id"), + Nics: &[]iaas.ServerNetwork{ + { + NicId: utils.Ptr("nic1"), + }, + { + NicId: utils.Ptr("nic2"), + }, }, + KeypairName: utils.Ptr("keypair_name"), + AffinityGroup: utils.Ptr("group_id"), + CreatedAt: utils.Ptr(testTimestamp()), + UpdatedAt: utils.Ptr(testTimestamp()), + LaunchedAt: utils.Ptr(testTimestamp()), + Status: utils.Ptr("active"), }, - KeypairName: utils.Ptr("keypair_name"), - AffinityGroup: utils.Ptr("group_id"), - CreatedAt: utils.Ptr(testTimestamp()), - UpdatedAt: utils.Ptr(testTimestamp()), - LaunchedAt: utils.Ptr(testTimestamp()), - Status: utils.Ptr("active"), + region: "eu02", }, - Model{ - Id: types.StringValue("pid,sid"), + expected: Model{ + Id: types.StringValue("pid,eu02,sid"), ProjectId: types.StringValue("pid"), ServerId: types.StringValue("sid"), Name: types.StringValue("name"), @@ -105,21 +117,25 @@ func TestMapFields(t *testing.T) { CreatedAt: types.StringValue(testTimestampValue), UpdatedAt: types.StringValue(testTimestampValue), LaunchedAt: types.StringValue(testTimestampValue), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - Model{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + description: "empty_labels", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + input: &iaas.Server{ + Id: utils.Ptr("sid"), + }, + region: "eu01", }, - &iaas.Server{ - Id: utils.Ptr("sid"), - }, - Model{ - Id: types.StringValue("pid,sid"), + expected: Model{ + Id: types.StringValue("pid,eu01,sid"), ProjectId: types.StringValue("pid"), ServerId: types.StringValue("sid"), Name: types.StringNull(), @@ -133,29 +149,26 @@ func TestMapFields(t *testing.T) { CreatedAt: types.StringNull(), UpdatedAt: types.StringNull(), LaunchedAt: types.StringNull(), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.Server{}, }, - &iaas.Server{}, - Model{}, - false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -163,7 +176,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -180,8 +193,8 @@ func TestToCreatePayload(t *testing.T) { isValid bool }{ { - "ok", - &Model{ + description: "ok", + input: &Model{ Name: types.StringValue("name"), AvailabilityZone: types.StringValue("zone"), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -199,14 +212,18 @@ func TestToCreatePayload(t *testing.T) { KeypairName: types.StringValue("keypair"), MachineType: types.StringValue("machine_type"), UserData: types.StringValue(userData), + NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("nic1"), + types.StringValue("nic2"), + }), }, - &iaas.CreateServerPayload{ + expected: &iaas.CreateServerPayload{ Name: utils.Ptr("name"), AvailabilityZone: utils.Ptr("zone"), Labels: &map[string]interface{}{ "key": "value", }, - BootVolume: &iaas.CreateServerPayloadBootVolume{ + BootVolume: &iaas.ServerBootVolume{ PerformanceClass: utils.Ptr("class"), Size: utils.Ptr(int64(1)), Source: &iaas.BootVolumeSource{ @@ -218,12 +235,17 @@ func TestToCreatePayload(t *testing.T) { KeypairName: utils.Ptr("keypair"), MachineType: utils.Ptr("machine_type"), UserData: utils.Ptr([]byte(base64EncodedUserData)), + Networking: &iaas.CreateServerPayloadAllOfNetworking{ + CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{ + NicIds: &[]string{"nic1", "nic2"}, + }, + }, }, - true, + isValid: true, }, { - "delete on termination is set to true", - &Model{ + description: "delete on termination is set to true", + input: &Model{ Name: types.StringValue("name"), AvailabilityZone: types.StringValue("zone"), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ @@ -241,14 +263,18 @@ func TestToCreatePayload(t *testing.T) { KeypairName: types.StringValue("keypair"), MachineType: types.StringValue("machine_type"), UserData: types.StringValue(userData), + NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("nic1"), + types.StringValue("nic2"), + }), }, - &iaas.CreateServerPayload{ + expected: &iaas.CreateServerPayload{ Name: utils.Ptr("name"), AvailabilityZone: utils.Ptr("zone"), Labels: &map[string]interface{}{ "key": "value", }, - BootVolume: &iaas.CreateServerPayloadBootVolume{ + BootVolume: &iaas.ServerBootVolume{ PerformanceClass: utils.Ptr("class"), Size: utils.Ptr(int64(1)), Source: &iaas.BootVolumeSource{ @@ -261,8 +287,13 @@ func TestToCreatePayload(t *testing.T) { KeypairName: utils.Ptr("keypair"), MachineType: utils.Ptr("machine_type"), UserData: utils.Ptr([]byte(base64EncodedUserData)), + Networking: &iaas.CreateServerPayloadAllOfNetworking{ + CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{ + NicIds: &[]string{"nic1", "nic2"}, + }, + }, }, - true, + isValid: true, }, } for _, tt := range tests { @@ -327,47 +358,47 @@ func TestToUpdatePayload(t *testing.T) { } } -var _ serverControlClient = (*mockServerControlClient)(nil) +var _ serverControlClient = &mockServerControlClient{} // mockServerControlClient mocks the [serverControlClient] interface with // pluggable functions type mockServerControlClient struct { wait.APIClientInterface startServerCalled int - startServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error + startServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error stopServerCalled int - stopServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error + stopServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error deallocateServerCalled int - deallocateServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error + deallocateServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error getServerCalled int - getServerExecute func(callNo int, ctx context.Context, projectId, serverId string) (*iaas.Server, error) + getServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) (*iaas.Server, error) } // DeallocateServerExecute implements serverControlClient. -func (t *mockServerControlClient) DeallocateServerExecute(ctx context.Context, projectId, serverId string) error { +func (t *mockServerControlClient) DeallocateServerExecute(ctx context.Context, projectId, region, serverId string) error { t.deallocateServerCalled++ - return t.deallocateServerExecute(t.deallocateServerCalled, ctx, projectId, serverId) + return t.deallocateServerExecute(t.deallocateServerCalled, ctx, projectId, region, serverId) } // GetServerExecute implements serverControlClient. -func (t *mockServerControlClient) GetServerExecute(ctx context.Context, projectId, serverId string) (*iaas.Server, error) { +func (t *mockServerControlClient) GetServerExecute(ctx context.Context, projectId, region, serverId string) (*iaas.Server, error) { t.getServerCalled++ - return t.getServerExecute(t.getServerCalled, ctx, projectId, serverId) + return t.getServerExecute(t.getServerCalled, ctx, projectId, region, serverId) } // StartServerExecute implements serverControlClient. -func (t *mockServerControlClient) StartServerExecute(ctx context.Context, projectId, serverId string) error { +func (t *mockServerControlClient) StartServerExecute(ctx context.Context, projectId, region, serverId string) error { t.startServerCalled++ - return t.startServerExecute(t.startServerCalled, ctx, projectId, serverId) + return t.startServerExecute(t.startServerCalled, ctx, projectId, region, serverId) } // StopServerExecute implements serverControlClient. -func (t *mockServerControlClient) StopServerExecute(ctx context.Context, projectId, serverId string) error { +func (t *mockServerControlClient) StopServerExecute(ctx context.Context, projectId, region, serverId string) error { t.stopServerCalled++ - return t.stopServerExecute(t.stopServerCalled, ctx, projectId, serverId) + return t.stopServerExecute(t.stopServerCalled, ctx, projectId, region, serverId) } func Test_serverResource_updateServerStatus(t *testing.T) { @@ -379,6 +410,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { type args struct { currentState *string model Model + region string } type want struct { err bool @@ -398,7 +430,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "no desired status", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: utils.Ptr(wait.ServerActiveStatus), @@ -422,7 +454,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "desired inactive state", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(no int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) { var state string if no <= 1 { state = wait.ServerActiveStatus @@ -434,7 +466,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { Status: &state, }, nil }, - stopServerExecute: func(_ int, _ context.Context, _, _ string) error { return nil }, + stopServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil }, }, }, args: args{ @@ -455,7 +487,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "desired deallocated state", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(no int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) { var state string switch no { case 1: @@ -470,7 +502,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { Status: &state, }, nil }, - deallocateServerExecute: func(_ int, _ context.Context, _, _ string) error { return nil }, + deallocateServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil }, }, }, args: args{ @@ -491,7 +523,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "don't call start if active", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: utils.Ptr(wait.ServerActiveStatus), @@ -516,7 +548,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "don't call stop if inactive", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: utils.Ptr(wait.ServerInactiveStatus), @@ -541,7 +573,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { name: "don't call dealloacate if deallocated", fields: fields{ client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) { + getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: utils.Ptr(wait.ServerDeallocatedStatus), @@ -566,7 +598,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model) + err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model, tt.args.region) if (err != nil) != tt.want.err { t.Errorf("inconsistent error, want %v and got %v", tt.want.err, err) } diff --git a/stackit/internal/services/iaas/serviceaccountattach/resource.go b/stackit/internal/services/iaas/serviceaccountattach/resource.go index bf7338ea..2063f15c 100644 --- a/stackit/internal/services/iaas/serviceaccountattach/resource.go +++ b/stackit/internal/services/iaas/serviceaccountattach/resource.go @@ -11,7 +11,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -27,41 +26,75 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &networkInterfaceAttachResource{} - _ resource.ResourceWithConfigure = &networkInterfaceAttachResource{} - _ resource.ResourceWithImportState = &networkInterfaceAttachResource{} + _ resource.Resource = &serviceAccountAttachResource{} + _ resource.ResourceWithConfigure = &serviceAccountAttachResource{} + _ resource.ResourceWithImportState = &serviceAccountAttachResource{} + _ resource.ResourceWithModifyPlan = &serviceAccountAttachResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` ServiceAccountEmail types.String `tfsdk:"service_account_email"` } // NewServiceAccountAttachResource is a helper function to simplify the provider implementation. func NewServiceAccountAttachResource() resource.Resource { - return &networkInterfaceAttachResource{} + return &serviceAccountAttachResource{} } -// networkInterfaceAttachResource is the resource implementation. -type networkInterfaceAttachResource struct { - client *iaas.APIClient +// serviceAccountAttachResource is the resource implementation. +type serviceAccountAttachResource struct { + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. -func (r *networkInterfaceAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *serviceAccountAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_server_service_account_attach" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *serviceAccountAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. -func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) +func (r *serviceAccountAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -70,14 +103,14 @@ func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req reso } // Schema defines the schema for the resource. -func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *serviceAccountAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { description := "Service account attachment resource schema. Attaches a service account to a server. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`service_account_email`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`service_account_email`\".", 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, @@ -117,7 +159,7 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc } // Create creates the resource and sets the initial Terraform state. -func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) @@ -129,14 +171,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) serviceAccountEmail := model.ServiceAccountEmail.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) // Create new service account attachment - _, err := r.client.AddServiceAccountToServer(ctx, projectId, serverId, serviceAccountEmail).Execute() + _, err := r.client.AddServiceAccountToServer(ctx, projectId, region, serverId, serviceAccountEmail).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching service account to server", fmt.Sprintf("Calling API: %v", err)) return @@ -144,7 +188,8 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc ctx = core.LogResponse(ctx) - model.Id = utils.BuildInternalTerraformId(projectId, serverId, serviceAccountEmail) + model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, serviceAccountEmail) + model.Region = types.StringValue(region) // Set state to fully populated data diags = resp.State.Set(ctx, model) @@ -156,7 +201,7 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc } // Read refreshes the Terraform state with the latest data. -func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -167,13 +212,15 @@ 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) serviceAccountEmail := model.ServiceAccountEmail.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) - serviceAccounts, err := r.client.ListServerServiceAccounts(ctx, projectId, serverId).Execute() + serviceAccounts, err := r.client.ListServerServiceAccounts(ctx, projectId, region, serverId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -196,6 +243,10 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. if mail != serviceAccountEmail { continue } + + model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, serviceAccountEmail) + model.Region = types.StringValue(region) + // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) @@ -212,12 +263,12 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. } // Update updates the resource and sets the updated Terraform state on success. -func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update is not supported, all fields require replace } // Delete deletes the resource and removes the Terraform state on success. -func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) @@ -229,14 +280,15 @@ 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) service_accountId := model.ServiceAccountEmail.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "service_account_email", service_accountId) // Remove service_account from server - _, err := r.client.RemoveServiceAccountFromServer(ctx, projectId, serverId, service_accountId).Execute() + _, err := r.client.RemoveServiceAccountFromServer(ctx, projectId, region, serverId, service_accountId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing service account from server", fmt.Sprintf("Calling API: %v", err)) return @@ -249,26 +301,23 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc // ImportState imports a resource into the Terraform state on success. // The expected format of the resource import identifier is: project_id,server_id -func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *serviceAccountAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing service_account attachment", - fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[service_account_email] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[service_account_email] Got: %q", req.ID), ) return } - projectId := idParts[0] - serverId := idParts[1] - service_accountId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "service_account_email", service_accountId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "server_id": idParts[2], + "service_account_email": idParts[3], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_account_email"), service_accountId)...) tflog.Info(ctx, "Service account attachment state imported") } diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-max.tf b/stackit/internal/services/iaas/testdata/resource-network-area-max.tf index 2da19e48..288fb0d0 100644 --- a/stackit/internal/services/iaas/testdata/resource-network-area-max.tf +++ b/stackit/internal/services/iaas/testdata/resource-network-area-max.tf @@ -8,8 +8,10 @@ variable "default_prefix_length" {} variable "max_prefix_length" {} variable "min_prefix_length" {} -variable "route_prefix" {} -variable "route_next_hop" {} +variable "route_destination_type" {} +variable "route_destination_value" {} +variable "route_next_hop_type" {} +variable "route_next_hop_value" {} variable "label" {} resource "stackit_network_area" "network_area" { @@ -33,8 +35,14 @@ resource "stackit_network_area" "network_area" { resource "stackit_network_area_route" "network_area_route" { organization_id = stackit_network_area.network_area.organization_id network_area_id = stackit_network_area.network_area.network_area_id - prefix = var.route_prefix - next_hop = var.route_next_hop + destination = { + type = var.route_destination_type + value = var.route_destination_value + } + next_hop = { + type = var.route_next_hop_type + value = var.route_next_hop_value + } labels = { "acc-test" : var.label } diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-min.tf b/stackit/internal/services/iaas/testdata/resource-network-area-min.tf index e1cfee28..5dde515d 100644 --- a/stackit/internal/services/iaas/testdata/resource-network-area-min.tf +++ b/stackit/internal/services/iaas/testdata/resource-network-area-min.tf @@ -1,26 +1,8 @@ variable "organization_id" {} variable "name" {} -variable "transfer_network" {} -variable "network_ranges_prefix" {} - -variable "route_prefix" {} -variable "route_next_hop" {} resource "stackit_network_area" "network_area" { - organization_id = var.organization_id - name = var.name - transfer_network = var.transfer_network - network_ranges = [ - { - prefix = var.network_ranges_prefix - } - ] + organization_id = var.organization_id + name = var.name } - -resource "stackit_network_area_route" "network_area_route" { - organization_id = stackit_network_area.network_area.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - prefix = var.route_prefix - next_hop = var.route_next_hop -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf b/stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf new file mode 100644 index 00000000..1d207e45 --- /dev/null +++ b/stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf @@ -0,0 +1,33 @@ +variable "organization_id" {} + +variable "name" {} +variable "transfer_network" {} +variable "network_ranges_prefix" {} +variable "default_prefix_length" {} +variable "min_prefix_length" {} +variable "max_prefix_length" {} +variable "default_nameservers" {} + +resource "stackit_network_area" "network_area" { + organization_id = var.organization_id + name = var.name +} + +resource "stackit_network_area_region" "network_area_region" { + organization_id = var.organization_id + network_area_id = stackit_network_area.network_area.network_area_id + ipv4 = { + transfer_network = var.transfer_network + network_ranges = [ + { + prefix = var.network_ranges_prefix + } + ] + default_prefix_length = var.default_prefix_length + min_prefix_length = var.min_prefix_length + max_prefix_length = var.max_prefix_length + default_nameservers = [ + var.default_nameservers + ] + } +} diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf b/stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf new file mode 100644 index 00000000..19ebe100 --- /dev/null +++ b/stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf @@ -0,0 +1,23 @@ +variable "organization_id" {} + +variable "name" {} +variable "transfer_network" {} +variable "network_ranges_prefix" {} + +resource "stackit_network_area" "network_area" { + organization_id = var.organization_id + name = var.name +} + +resource "stackit_network_area_region" "network_area_region" { + organization_id = var.organization_id + network_area_id = stackit_network_area.network_area.network_area_id + ipv4 = { + transfer_network = var.transfer_network + network_ranges = [ + { + prefix = var.network_ranges_prefix + } + ] + } +} diff --git a/stackit/internal/services/iaas/testdata/resource-network-max.tf b/stackit/internal/services/iaas/testdata/resource-network-max.tf new file mode 100644 index 00000000..2d86028a --- /dev/null +++ b/stackit/internal/services/iaas/testdata/resource-network-max.tf @@ -0,0 +1,85 @@ +variable "organization_id" {} +variable "name" {} +variable "ipv4_gateway" {} +variable "ipv4_nameserver_0" {} +variable "ipv4_nameserver_1" {} +variable "ipv4_prefix" {} +variable "ipv4_prefix_length" {} +variable "routed" {} +variable "label" {} +variable "service_account_mail" {} + +# no test candidate, just needed for the testing setup +resource "stackit_network_area" "network_area" { + organization_id = var.organization_id + name = var.name + labels = { + "preview/routingtables" = "true" + } +} + +resource "stackit_network_area_region" "network_area_region" { + organization_id = var.organization_id + network_area_id = stackit_network_area.network_area.network_area_id + ipv4 = { + network_ranges = [ + { + prefix = "10.0.0.0/16" + }, + { + prefix = "10.2.2.0/24" + } + ] + transfer_network = "10.1.2.0/24" + } +} + +# no test candidate, just needed for the testing setup +resource "stackit_resourcemanager_project" "project" { + parent_container_id = stackit_network_area.network_area.organization_id + name = var.name + labels = { + "networkArea" = stackit_network_area.network_area.network_area_id + } + owner_email = var.service_account_mail + + depends_on = [stackit_network_area_region.network_area_region] +} + +resource "stackit_network" "network_prefix" { + project_id = stackit_resourcemanager_project.project.project_id + name = var.name + # ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null + # no_ipv4_gateway = var.ipv4_gateway != "" ? null : true + ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] + ipv4_prefix = var.ipv4_prefix + routed = var.routed + labels = { + "acc-test" : var.label + } + + depends_on = [stackit_network_area_region.network_area_region] +} + +resource "stackit_network" "network_prefix_length" { + project_id = stackit_resourcemanager_project.project.project_id + name = var.name + # no_ipv4_gateway = true + ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] + ipv4_prefix_length = var.ipv4_prefix_length + routed = var.routed + labels = { + "acc-test" : var.label + } + routing_table_id = stackit_routing_table.routing_table.routing_table_id + + depends_on = [stackit_network.network_prefix, stackit_network_area_region.network_area_region] +} + +resource "stackit_routing_table" "routing_table" { + organization_id = var.organization_id + network_area_id = stackit_network_area.network_area.network_area_id + name = var.name + + depends_on = [stackit_network_area_region.network_area_region] +} diff --git a/stackit/internal/services/iaas/testdata/resource-network-v1-min.tf b/stackit/internal/services/iaas/testdata/resource-network-min.tf similarity index 100% rename from stackit/internal/services/iaas/testdata/resource-network-v1-min.tf rename to stackit/internal/services/iaas/testdata/resource-network-min.tf diff --git a/stackit/internal/services/iaas/testdata/resource-network-v1-max.tf b/stackit/internal/services/iaas/testdata/resource-network-v1-max.tf deleted file mode 100644 index cb56bc52..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-v1-max.tf +++ /dev/null @@ -1,35 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "ipv4_gateway" {} -variable "ipv4_nameserver_0" {} -variable "ipv4_nameserver_1" {} -variable "ipv4_prefix" {} -variable "ipv4_prefix_length" {} -variable "routed" {} -variable "label" {} - -resource "stackit_network" "network_prefix" { - project_id = var.project_id - name = var.name - ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null - no_ipv4_gateway = var.ipv4_gateway != "" ? null : true - ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] - ipv4_prefix = var.ipv4_prefix - routed = var.routed - labels = { - "acc-test" : var.label - } -} - - -resource "stackit_network" "network_prefix_length" { - project_id = var.project_id - name = var.name - no_ipv4_gateway = true - ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] - ipv4_prefix_length = var.ipv4_prefix_length - routed = var.routed - labels = { - "acc-test" : var.label - } -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-network-v2-max.tf b/stackit/internal/services/iaas/testdata/resource-network-v2-max.tf deleted file mode 100644 index 283ccdbe..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-v2-max.tf +++ /dev/null @@ -1,43 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "ipv4_gateway" {} -variable "ipv4_nameserver_0" {} -variable "ipv4_nameserver_1" {} -variable "ipv4_prefix" {} -variable "ipv4_prefix_length" {} -variable "routed" {} -variable "label" {} -variable "organization_id" {} -variable "network_area_id" {} - -# resource "stackit_network" "network_prefix" { -# project_id = var.project_id -# name = var.name -# # ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null -# # no_ipv4_gateway = var.ipv4_gateway != "" ? null : true -# ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] -# ipv4_prefix = var.ipv4_prefix -# routed = var.routed -# labels = { -# "acc-test" : var.label -# } -# } - -resource "stackit_network" "network_prefix_length" { - project_id = var.project_id - name = var.name - # no_ipv4_gateway = true - ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] - ipv4_prefix_length = var.ipv4_prefix_length - routed = var.routed - labels = { - "acc-test" : var.label - } - routing_table_id = stackit_routing_table.routing_table.routing_table_id -} - -resource "stackit_routing_table" "routing_table" { - organization_id = var.organization_id - network_area_id = var.network_area_id - name = var.name -} diff --git a/stackit/internal/services/iaas/testdata/resource-network-v2-min.tf b/stackit/internal/services/iaas/testdata/resource-network-v2-min.tf deleted file mode 100644 index e2748bdd..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-v2-min.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "project_id" {} -variable "name" {} - -resource "stackit_network" "network" { - project_id = var.project_id - name = var.name -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-server-min.tf b/stackit/internal/services/iaas/testdata/resource-server-min.tf index 0bf78dc9..6f3ba894 100644 --- a/stackit/internal/services/iaas/testdata/resource-server-min.tf +++ b/stackit/internal/services/iaas/testdata/resource-server-min.tf @@ -1,8 +1,18 @@ variable "project_id" {} variable "name" {} +variable "network_name" {} variable "machine_type" {} variable "image_id" {} +resource "stackit_network" "network" { + project_id = var.project_id + name = var.network_name +} + +resource "stackit_network_interface" "nic" { + project_id = var.project_id + network_id = stackit_network.network.network_id +} resource "stackit_server" "server" { project_id = var.project_id @@ -14,4 +24,7 @@ resource "stackit_server" "server" { source_id = var.image_id delete_on_termination = true } + network_interfaces = [ + stackit_network_interface.nic.network_interface_id + ] } diff --git a/stackit/internal/services/iaas/testdata/resource-volume-max.tf b/stackit/internal/services/iaas/testdata/resource-volume-max.tf index 8a85430e..54c590f6 100644 --- a/stackit/internal/services/iaas/testdata/resource-volume-max.tf +++ b/stackit/internal/services/iaas/testdata/resource-volume-max.tf @@ -23,8 +23,9 @@ resource "stackit_volume" "volume_source" { availability_zone = var.availability_zone name = var.name description = var.description - performance_class = var.performance_class - size = var.size + # TODO: keep commented until IaaS API bug is resolved + #performance_class = var.performance_class + size = var.size source = { id = stackit_volume.volume_size.volume_id type = "volume" diff --git a/stackit/internal/services/iaas/utils/util.go b/stackit/internal/services/iaas/utils/util.go index 7d7a2492..79368cf4 100644 --- a/stackit/internal/services/iaas/utils/util.go +++ b/stackit/internal/services/iaas/utils/util.go @@ -21,9 +21,8 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } if providerData.IaaSCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.IaaSCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := iaas.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/iaas/utils/util_test.go b/stackit/internal/services/iaas/utils/util_test.go index dce0d036..79af1174 100644 --- a/stackit/internal/services/iaas/utils/util_test.go +++ b/stackit/internal/services/iaas/utils/util_test.go @@ -49,7 +49,6 @@ func TestConfigureClient(t *testing.T) { }, expected: func() *iaas.APIClient { apiClient, err := iaas.NewAPIClient( - config.WithRegion("eu01"), utils.UserAgentConfigOption(testVersion), ) if err != nil { diff --git a/stackit/internal/services/iaas/volume/datasource.go b/stackit/internal/services/iaas/volume/datasource.go index a8613df4..5e36a395 100644 --- a/stackit/internal/services/iaas/volume/datasource.go +++ b/stackit/internal/services/iaas/volume/datasource.go @@ -31,7 +31,8 @@ func NewVolumeDataSource() datasource.DataSource { // volumeDataSource is the data source implementation. type volumeDataSource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -40,12 +41,13 @@ func (d *volumeDataSource) Metadata(_ context.Context, req datasource.MetadataRe } func (d *volumeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -54,14 +56,14 @@ func (d *volumeDataSource) Configure(ctx context.Context, req datasource.Configu } // Schema defines the schema for the resource. -func (r *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { description := "Volume resource schema. Must have a `region` specified in the provider configuration." resp.Schema = schema.Schema{ MarkdownDescription: description, Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`volume_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`volume_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -72,6 +74,11 @@ func (r *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, "volume_id": schema.StringAttribute{ Description: "The volume ID.", Required: true, @@ -140,14 +147,16 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, return } projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) volumeId := model.VolumeId.ValueString() ctx = core.InitProviderContext(ctx) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "volume_id", volumeId) - volumeResp, err := d.client.GetVolume(ctx, projectId, volumeId).Execute() + volumeResp, err := d.client.GetVolume(ctx, projectId, region, volumeId).Execute() if err != nil { utils.LogError( ctx, @@ -165,7 +174,7 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, ctx = core.LogResponse(ctx) - err = mapFields(ctx, volumeResp, &model) + err = mapFields(ctx, volumeResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/iaas/volume/resource.go b/stackit/internal/services/iaas/volume/resource.go index 54b8e7f1..0fc3a9e6 100644 --- a/stackit/internal/services/iaas/volume/resource.go +++ b/stackit/internal/services/iaas/volume/resource.go @@ -37,6 +37,7 @@ var ( _ resource.Resource = &volumeResource{} _ resource.ResourceWithConfigure = &volumeResource{} _ resource.ResourceWithImportState = &volumeResource{} + _ resource.ResourceWithModifyPlan = &volumeResource{} SupportedSourceTypes = []string{"volume", "image", "snapshot", "backup"} ) @@ -44,6 +45,7 @@ var ( type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` VolumeId types.String `tfsdk:"volume_id"` Name types.String `tfsdk:"name"` AvailabilityZone types.String `tfsdk:"availability_zone"` @@ -74,7 +76,8 @@ func NewVolumeResource() resource.Resource { // volumeResource is the resource implementation. type volumeResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -82,6 +85,36 @@ func (r *volumeResource) Metadata(_ context.Context, req resource.MetadataReques resp.TypeName = req.ProviderTypeName + "_volume" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *volumeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // ConfigValidators validates the resource configuration func (r *volumeResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ @@ -94,12 +127,13 @@ func (r *volumeResource) ConfigValidators(_ context.Context) []resource.ConfigVa // Configure adds the provider configured client to the resource. func (r *volumeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -115,7 +149,7 @@ func (r *volumeResource) Schema(_ context.Context, _ resource.SchemaRequest, res Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`volume_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`volume_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -132,6 +166,15 @@ func (r *volumeResource) Schema(_ context.Context, _ resource.SchemaRequest, res validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "volume_id": schema.StringAttribute{ Description: "The volume ID.", Computed: true, @@ -290,7 +333,9 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, 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) var source = &sourceModel{} if !(model.Source.IsNull() || model.Source.IsUnknown()) { @@ -310,7 +355,7 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, // Create new volume - volume, err := r.client.CreateVolume(ctx, projectId).CreateVolumePayload(*payload).Execute() + volume, err := r.client.CreateVolume(ctx, projectId, region).CreateVolumePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Calling API: %v", err)) return @@ -319,7 +364,7 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, ctx = core.LogResponse(ctx) volumeId := *volume.Id - volume, err = wait.CreateVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx) + volume, err = wait.CreateVolumeWaitHandler(ctx, r.client, projectId, region, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("volume creation waiting: %v", err)) return @@ -328,7 +373,7 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, ctx = tflog.SetField(ctx, "volume_id", volumeId) // Map response body to schema - err = mapFields(ctx, volume, &model) + err = mapFields(ctx, volume, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Processing API payload: %v", err)) return @@ -350,15 +395,18 @@ func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, res if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) volumeId := model.VolumeId.ValueString() ctx = core.InitProviderContext(ctx) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "volume_id", volumeId) - volumeResp, err := r.client.GetVolume(ctx, projectId, volumeId).Execute() + volumeResp, err := r.client.GetVolume(ctx, projectId, region, volumeId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -372,7 +420,7 @@ func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, res ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(ctx, volumeResp, &model) + err = mapFields(ctx, volumeResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err)) return @@ -399,8 +447,10 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) volumeId := model.VolumeId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "volume_id", volumeId) // Retrieve values from state @@ -418,7 +468,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, return } // Update existing volume - updatedVolume, err := r.client.UpdateVolume(ctx, projectId, volumeId).UpdateVolumePayload(*payload).Execute() + updatedVolume, err := r.client.UpdateVolume(ctx, projectId, region, volumeId).UpdateVolumePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Calling API: %v", err)) return @@ -436,7 +486,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, payload := iaas.ResizeVolumePayload{ Size: modelSize, } - err := r.client.ResizeVolume(ctx, projectId, volumeId).ResizeVolumePayload(payload).Execute() + err := r.client.ResizeVolume(ctx, projectId, region, volumeId).ResizeVolumePayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Resizing the volume, calling API: %v", err)) } @@ -444,7 +494,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, updatedVolume.Size = modelSize } } - err = mapFields(ctx, updatedVolume, &model) + err = mapFields(ctx, updatedVolume, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Processing API payload: %v", err)) return @@ -468,15 +518,17 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, } projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) volumeId := model.VolumeId.ValueString() ctx = core.InitProviderContext(ctx) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "volume_id", volumeId) // Delete existing volume - err := r.client.DeleteVolume(ctx, projectId, volumeId).Execute() + err := r.client.DeleteVolume(ctx, projectId, region, volumeId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("Calling API: %v", err)) return @@ -484,7 +536,7 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, ctx = core.LogResponse(ctx) - _, err = wait.DeleteVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx) + _, err = wait.DeleteVolumeWaitHandler(ctx, r.client, projectId, region, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("volume deletion waiting: %v", err)) return @@ -498,25 +550,24 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, func (r *volumeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing volume", - fmt.Sprintf("Expected import identifier with format: [project_id],[volume_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[volume_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - volumeId := idParts[1] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "volume_id", volumeId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "volume_id": idParts[2], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("volume_id"), volumeId)...) tflog.Info(ctx, "volume state imported") } -func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error { +func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model, region string) error { if volumeResp == nil { return fmt.Errorf("response input is nil") } @@ -533,7 +584,8 @@ func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error return fmt.Errorf("Volume id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), volumeId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, volumeId) + model.Region = types.StringValue(region) labels, err := iaasUtils.MapLabels(ctx, volumeResp.Labels, model.Labels) if err != nil { diff --git a/stackit/internal/services/iaas/volume/resource_test.go b/stackit/internal/services/iaas/volume/resource_test.go index 819d594e..14f456a7 100644 --- a/stackit/internal/services/iaas/volume/resource_test.go +++ b/stackit/internal/services/iaas/volume/resource_test.go @@ -12,24 +12,31 @@ import ( ) func TestMapFields(t *testing.T) { + type args struct { + state Model + input *iaas.Volume + region string + } tests := []struct { description string - state Model - input *iaas.Volume + args args expected Model isValid bool }{ { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), + description: "default_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + VolumeId: types.StringValue("nid"), + }, + input: &iaas.Volume{ + Id: utils.Ptr("nid"), + }, + region: "eu01", }, - &iaas.Volume{ - Id: utils.Ptr("nid"), - }, - Model{ - Id: types.StringValue("pid,nid"), + expected: Model{ + Id: types.StringValue("pid,eu01,nid"), ProjectId: types.StringValue("pid"), VolumeId: types.StringValue("nid"), Name: types.StringNull(), @@ -40,30 +47,35 @@ func TestMapFields(t *testing.T) { ServerId: types.StringNull(), Size: types.Int64Null(), Source: types.ObjectNull(sourceTypes), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - }, - &iaas.Volume{ - Id: utils.Ptr("nid"), - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", + description: "simple_values", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + VolumeId: types.StringValue("nid"), + Region: types.StringValue("eu01"), }, - Description: utils.Ptr("desc"), - PerformanceClass: utils.Ptr("class"), - ServerId: utils.Ptr("sid"), - Size: utils.Ptr(int64(1)), - Source: &iaas.VolumeSource{}, + input: &iaas.Volume{ + Id: utils.Ptr("nid"), + Name: utils.Ptr("name"), + AvailabilityZone: utils.Ptr("zone"), + Labels: &map[string]interface{}{ + "key": "value", + }, + Description: utils.Ptr("desc"), + PerformanceClass: utils.Ptr("class"), + ServerId: utils.Ptr("sid"), + Size: utils.Ptr(int64(1)), + Source: &iaas.VolumeSource{}, + }, + region: "eu02", }, - Model{ - Id: types.StringValue("pid,nid"), + expected: Model{ + Id: types.StringValue("pid,eu02,nid"), ProjectId: types.StringValue("pid"), VolumeId: types.StringValue("nid"), Name: types.StringValue("name"), @@ -79,21 +91,25 @@ func TestMapFields(t *testing.T) { "type": types.StringNull(), "id": types.StringNull(), }), + Region: types.StringValue("eu02"), }, - true, + isValid: true, }, { - "empty_labels", - Model{ - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + description: "empty_labels", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + VolumeId: types.StringValue("nid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + input: &iaas.Volume{ + Id: utils.Ptr("nid"), + }, + region: "eu01", }, - &iaas.Volume{ - Id: utils.Ptr("nid"), - }, - Model{ - Id: types.StringValue("pid,nid"), + expected: Model{ + Id: types.StringValue("pid,eu01,nid"), ProjectId: types.StringValue("pid"), VolumeId: types.StringValue("nid"), Name: types.StringNull(), @@ -104,29 +120,28 @@ func TestMapFields(t *testing.T) { ServerId: types.StringNull(), Size: types.Int64Null(), Source: types.ObjectNull(sourceTypes), + Region: types.StringValue("eu01"), }, - true, + isValid: true, }, { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, + description: "response_nil_fail", }, { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), + description: "no_resource_id", + args: args{ + state: Model{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.Volume{}, }, - &iaas.Volume{}, - Model{}, - false, + expected: Model{}, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -134,7 +149,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) + diff := cmp.Diff(tt.args.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/iaas/volumeattach/resource.go b/stackit/internal/services/iaas/volumeattach/resource.go index 30167c44..f297f16d 100644 --- a/stackit/internal/services/iaas/volumeattach/resource.go +++ b/stackit/internal/services/iaas/volumeattach/resource.go @@ -11,7 +11,6 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -32,11 +31,13 @@ var ( _ resource.Resource = &volumeAttachResource{} _ resource.ResourceWithConfigure = &volumeAttachResource{} _ resource.ResourceWithImportState = &volumeAttachResource{} + _ resource.ResourceWithModifyPlan = &volumeAttachResource{} ) type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` ServerId types.String `tfsdk:"server_id"` VolumeId types.String `tfsdk:"volume_id"` } @@ -48,7 +49,8 @@ func NewVolumeAttachResource() resource.Resource { // volumeAttachResource is the resource implementation. type volumeAttachResource struct { - client *iaas.APIClient + client *iaas.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -56,14 +58,45 @@ func (r *volumeAttachResource) Metadata(_ context.Context, req resource.Metadata resp.TypeName = req.ProviderTypeName + "_server_volume_attach" } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *volumeAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + // Configure adds the provider configured client to the resource. func (r *volumeAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -79,7 +112,7 @@ func (r *volumeAttachResource) Schema(_ context.Context, _ resource.SchemaReques Description: description, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`volume_id`\".", + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`volume_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -96,6 +129,15 @@ func (r *volumeAttachResource) Schema(_ context.Context, _ resource.SchemaReques validate.NoSeparator(), }, }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "server_id": schema.StringAttribute{ Description: "The server ID.", Required: true, @@ -135,10 +177,12 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe 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) volumeId := model.VolumeId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "volume_id", volumeId) // Create new Volume attachment @@ -146,7 +190,7 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe payload := iaas.AddVolumeToServerPayload{ DeleteOnTermination: sdkUtils.Ptr(false), } - _, err := r.client.AddVolumeToServer(ctx, projectId, serverId, volumeId).AddVolumeToServerPayload(payload).Execute() + _, err := r.client.AddVolumeToServer(ctx, projectId, region, serverId, volumeId).AddVolumeToServerPayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("Calling API: %v", err)) return @@ -154,13 +198,13 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe ctx = core.LogResponse(ctx) - _, err = wait.AddVolumeToServerWaitHandler(ctx, r.client, projectId, serverId, volumeId).WaitWithContext(ctx) + _, err = wait.AddVolumeToServerWaitHandler(ctx, r.client, projectId, region, serverId, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("volume attachment waiting: %v", err)) return } - model.Id = utils.BuildInternalTerraformId(projectId, serverId, volumeId) + model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, volumeId) // Set state to fully populated data diags = resp.State.Set(ctx, model) @@ -183,13 +227,15 @@ func (r *volumeAttachResource) Read(ctx context.Context, req resource.ReadReques 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) volumeId := model.VolumeId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "volume_id", volumeId) - _, err := r.client.GetAttachedVolume(ctx, projectId, serverId, volumeId).Execute() + _, err := r.client.GetAttachedVolume(ctx, projectId, region, serverId, volumeId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -229,14 +275,16 @@ func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRe 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) volumeId := model.VolumeId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "volume_id", volumeId) // Remove volume from server - err := r.client.RemoveVolumeFromServer(ctx, projectId, serverId, volumeId).Execute() + err := r.client.RemoveVolumeFromServer(ctx, projectId, region, serverId, volumeId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("Calling API: %v", err)) return @@ -244,7 +292,7 @@ func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRe ctx = core.LogResponse(ctx) - _, err = wait.RemoveVolumeFromServerWaitHandler(ctx, r.client, projectId, serverId, volumeId).WaitWithContext(ctx) + _, err = wait.RemoveVolumeFromServerWaitHandler(ctx, r.client, projectId, region, serverId, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("volume removal waiting: %v", err)) return @@ -258,23 +306,20 @@ func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRe func (r *volumeAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing volume attachment", - fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[volume_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[volume_id] Got: %q", req.ID), ) return } - projectId := idParts[0] - serverId := idParts[1] - volumeId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "volume_id", volumeId) + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "server_id": idParts[2], + "volume_id": idParts[3], + }) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("volume_id"), volumeId)...) tflog.Info(ctx, "Volume attachment state imported") } diff --git a/stackit/internal/services/iaasalpha/routingtable/route/resource.go b/stackit/internal/services/iaasalpha/routingtable/route/resource.go index c9f49690..4ca10104 100644 --- a/stackit/internal/services/iaasalpha/routingtable/route/resource.go +++ b/stackit/internal/services/iaasalpha/routingtable/route/resource.go @@ -176,6 +176,9 @@ func (r *routeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, + Validators: []validator.String{ + validate.CIDR(), + }, }, }, }, @@ -213,6 +216,9 @@ func (r *routeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, + Validators: []validator.String{ + validate.IP(false), + }, }, }, }, diff --git a/stackit/internal/services/logme/logme_acc_test.go b/stackit/internal/services/logme/logme_acc_test.go index d138380e..03cb3b4f 100644 --- a/stackit/internal/services/logme/logme_acc_test.go +++ b/stackit/internal/services/logme/logme_acc_test.go @@ -261,7 +261,7 @@ func TestAccLogMeMaxResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.0", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog1"])), resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.1", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog2"])), - // // Credential data + // Credential data resource.TestCheckResourceAttrPair( "stackit_logme_credential.credential", "project_id", "stackit_logme_instance.instance", "project_id", diff --git a/stackit/internal/services/serverupdate/serverupdate_acc_test.go b/stackit/internal/services/serverupdate/serverupdate_acc_test.go index 33a0253a..3d45a70a 100644 --- a/stackit/internal/services/serverupdate/serverupdate_acc_test.go +++ b/stackit/internal/services/serverupdate/serverupdate_acc_test.go @@ -121,7 +121,7 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "id"), ), }, - // // Import + // Import { ConfigVariables: testConfigVarsMin, ResourceName: "stackit_server_update_schedule.test_schedule", @@ -139,7 +139,7 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) { ImportState: true, ImportStateVerify: true, }, - // // Update + // Update { ConfigVariables: configVarsMinUpdated(), Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMinConfig, @@ -209,7 +209,7 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "id"), ), }, - // // Import + // Import { ConfigVariables: testConfigVarsMax, ResourceName: "stackit_server_update_schedule.test_schedule", @@ -227,7 +227,7 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) { ImportState: true, ImportStateVerify: true, }, - // // Update + // Update { ConfigVariables: configVarsMaxUpdated(), Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMaxConfig, diff --git a/stackit/provider.go b/stackit/provider.go index de212064..cebb5be0 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -33,6 +33,7 @@ import ( machineType "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/machinetype" iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network" iaasNetworkArea "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarea" + iaasNetworkAreaRegion "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearegion" iaasNetworkAreaRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearoute" iaasNetworkInterface "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterface" iaasNetworkInterfaceAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterfaceattach" @@ -501,6 +502,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource iaasImageV2.NewImageV2DataSource, iaasNetwork.NewNetworkDataSource, iaasNetworkArea.NewNetworkAreaDataSource, + iaasNetworkAreaRegion.NewNetworkAreaRegionDataSource, iaasNetworkAreaRoute.NewNetworkAreaRouteDataSource, iaasNetworkInterface.NewNetworkInterfaceDataSource, iaasVolume.NewVolumeDataSource, @@ -572,6 +574,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { iaasImage.NewImageResource, iaasNetwork.NewNetworkResource, iaasNetworkArea.NewNetworkAreaResource, + iaasNetworkAreaRegion.NewNetworkAreaRegionResource, iaasNetworkAreaRoute.NewNetworkAreaRouteResource, iaasNetworkInterface.NewNetworkInterfaceResource, iaasVolume.NewVolumeResource, diff --git a/templates/resources/network_area_route.md.tmpl b/templates/resources/network_area_route.md.tmpl new file mode 100644 index 00000000..48cd3b48 --- /dev/null +++ b/templates/resources/network_area_route.md.tmpl @@ -0,0 +1,54 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- + {{ .Description | trimspace }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name) }} + +## 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 + } +} +``` + +{{ .SchemaMarkdown | trimspace }} +