From 81f4a055414516c57df720dce73ee2b9f870531b Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Wed, 10 Jun 2026 18:01:15 +0200 Subject: [PATCH 1/2] feat(vpn): Onboarding VPN Connection relates to STACKITTPR-551 --- docs/data-sources/vpn_connection.md | 151 +++ docs/resources/vpn_connection.md | 212 +++ .../stackit_vpn_connection/data-source.tf | 5 + .../stackit_vpn_connection/resource.tf | 45 + .../services/vpn/connection/datasource.go | 309 +++++ .../services/vpn/connection/resource.go | 1150 +++++++++++++++++ .../services/vpn/connection/resource_test.go | 1054 +++++++++++++++ .../services/vpn/testdata/connection-max.tf | 85 ++ .../services/vpn/testdata/connection-min.tf | 46 + stackit/internal/services/vpn/vpn_acc_test.go | 611 ++++++++- stackit/provider.go | 9 + 11 files changed, 3660 insertions(+), 17 deletions(-) create mode 100644 docs/data-sources/vpn_connection.md create mode 100644 docs/resources/vpn_connection.md create mode 100644 examples/data-sources/stackit_vpn_connection/data-source.tf create mode 100644 examples/resources/stackit_vpn_connection/resource.tf create mode 100644 stackit/internal/services/vpn/connection/datasource.go create mode 100644 stackit/internal/services/vpn/connection/resource.go create mode 100644 stackit/internal/services/vpn/connection/resource_test.go create mode 100644 stackit/internal/services/vpn/testdata/connection-max.tf create mode 100644 stackit/internal/services/vpn/testdata/connection-min.tf diff --git a/docs/data-sources/vpn_connection.md b/docs/data-sources/vpn_connection.md new file mode 100644 index 000000000..48d60845e --- /dev/null +++ b/docs/data-sources/vpn_connection.md @@ -0,0 +1,151 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_vpn_connection Data Source - stackit" +subcategory: "" +description: |- + VPN Connection data source schema. Uses the default_region specified in the provider configuration as a fallback in case no region is defined on datasource level. +--- + +# stackit_vpn_connection (Data Source) + +VPN Connection data source schema. Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on datasource level. + +## Example Usage + +```terraform +data "stackit_vpn_connection" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + gateway_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + connection_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `connection_id` (String) The server-generated UUID of the VPN connection. +- `gateway_id` (String) The UUID of the parent VPN gateway. +- `project_id` (String) STACKIT project ID. + +### Read-Only + +- `display_name` (String) A user-friendly name for the connection. +- `enabled` (Boolean) Whether this connection is enabled. +- `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`gateway_id`,`connection_id`". +- `labels` (Map of String) Map of custom labels. +- `local_subnet` (List of String) List of local IPv4 CIDRs to route through this connection. +- `region` (String) STACKIT region. +- `remote_subnet` (List of String) List of remote IPv4 CIDRs accessible via this connection. +- `static_routes` (List of String) List of static routes (IPv4 CIDRs) for route-based VPN. +- `tunnel1` (Attributes) (see [below for nested schema](#nestedatt--tunnel1)) +- `tunnel2` (Attributes) (see [below for nested schema](#nestedatt--tunnel2)) + + +### Nested Schema for `tunnel1` + +Read-Only: + +- `bgp` (Attributes) BGP configuration for this tunnel. (see [below for nested schema](#nestedatt--tunnel1--bgp)) +- `peering` (Attributes) Tunnel interface peering configuration. (see [below for nested schema](#nestedatt--tunnel1--peering)) +- `phase1` (Attributes) IKE Phase 1 configuration. (see [below for nested schema](#nestedatt--tunnel1--phase1)) +- `phase2` (Attributes) IKE Phase 2 configuration. (see [below for nested schema](#nestedatt--tunnel1--phase2)) +- `pre_shared_key_wo` (String, Sensitive) +- `pre_shared_key_wo_version` (Number) +- `remote_address` (String) Remote peer IPv4 address for this tunnel. + + +### Nested Schema for `tunnel1.bgp` + +Read-Only: + +- `remote_asn` (Number) Remote AS number. + + + +### Nested Schema for `tunnel1.peering` + +Read-Only: + +- `local_address` (String) Local tunnel interface IPv4 address. +- `remote_address` (String) Remote tunnel interface IPv4 address. + + + +### Nested Schema for `tunnel1.phase1` + +Read-Only: + +- `dh_groups` (List of String) Diffie-Hellman groups. +- `encryption_algorithms` (List of String) Encryption algorithms. +- `integrity_algorithms` (List of String) Integrity/hash algorithms. +- `rekey_time` (Number) IKE re-keying time in seconds. + + + +### Nested Schema for `tunnel1.phase2` + +Read-Only: + +- `dh_groups` (List of String) Diffie-Hellman groups for PFS. +- `dpd_action` (String) DPD timeout action (clear or restart). +- `encryption_algorithms` (List of String) Encryption algorithms. +- `integrity_algorithms` (List of String) Integrity/hash algorithms. +- `rekey_time` (Number) Child SA re-keying time in seconds. +- `start_action` (String) Start action (none or start). + + + + +### Nested Schema for `tunnel2` + +Read-Only: + +- `bgp` (Attributes) BGP configuration for this tunnel. (see [below for nested schema](#nestedatt--tunnel2--bgp)) +- `peering` (Attributes) Tunnel interface peering configuration. (see [below for nested schema](#nestedatt--tunnel2--peering)) +- `phase1` (Attributes) IKE Phase 1 configuration. (see [below for nested schema](#nestedatt--tunnel2--phase1)) +- `phase2` (Attributes) IKE Phase 2 configuration. (see [below for nested schema](#nestedatt--tunnel2--phase2)) +- `pre_shared_key_wo` (String, Sensitive) +- `pre_shared_key_wo_version` (Number) +- `remote_address` (String) Remote peer IPv4 address for this tunnel. + + +### Nested Schema for `tunnel2.bgp` + +Read-Only: + +- `remote_asn` (Number) Remote AS number. + + + +### Nested Schema for `tunnel2.peering` + +Read-Only: + +- `local_address` (String) Local tunnel interface IPv4 address. +- `remote_address` (String) Remote tunnel interface IPv4 address. + + + +### Nested Schema for `tunnel2.phase1` + +Read-Only: + +- `dh_groups` (List of String) Diffie-Hellman groups. +- `encryption_algorithms` (List of String) Encryption algorithms. +- `integrity_algorithms` (List of String) Integrity/hash algorithms. +- `rekey_time` (Number) IKE re-keying time in seconds. + + + +### Nested Schema for `tunnel2.phase2` + +Read-Only: + +- `dh_groups` (List of String) Diffie-Hellman groups for PFS. +- `dpd_action` (String) DPD timeout action (clear or restart). +- `encryption_algorithms` (List of String) Encryption algorithms. +- `integrity_algorithms` (List of String) Integrity/hash algorithms. +- `rekey_time` (Number) Child SA re-keying time in seconds. +- `start_action` (String) Start action (none or start). diff --git a/docs/resources/vpn_connection.md b/docs/resources/vpn_connection.md new file mode 100644 index 000000000..4ca2d8d28 --- /dev/null +++ b/docs/resources/vpn_connection.md @@ -0,0 +1,212 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_vpn_connection Resource - stackit" +subcategory: "" +description: |- + VPN Connection resource schema. Uses the default_region specified in the provider configuration as a fallback in case no region is defined on resource level. +--- + +# stackit_vpn_connection (Resource) + +VPN Connection resource schema. Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on resource level. + +## Example Usage + +```terraform +resource "stackit_vpn_connection" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + gateway_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + display_name = "example-vpn-connection" + + tunnel1 = { + remote_address = "198.51.100.10" + pre_shared_key_wo = "example-super-secret-key-tunnel1" + + phase1 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + + phase2 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + } + + tunnel2 = { + remote_address = "203.0.113.10" + pre_shared_key_wo = "example-super-secret-key-tunnel2" + + phase1 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + + phase2 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + } +} + +# Only use the import statement, if you want to import an existing VPN connection +import { + to = stackit_vpn_connection.example + id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,eu01,xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `display_name` (String) A user-friendly name for the connection. Must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long. +- `gateway_id` (String) The UUID of the parent VPN gateway. +- `project_id` (String) STACKIT project ID. +- `tunnel1` (Attributes) (see [below for nested schema](#nestedatt--tunnel1)) +- `tunnel2` (Attributes) (see [below for nested schema](#nestedatt--tunnel2)) + +### Optional + +- `enabled` (Boolean) Whether this connection is enabled. Defaults to true. +- `labels` (Map of String) Map of custom labels. +- `local_subnet` (List of String) List of local IPv4 CIDRs to route through this connection. Optional for route-based and BGP configurations (defaults to 0.0.0.0/0). Mandatory for policy-based. +- `region` (String) STACKIT region. +- `remote_subnet` (List of String) List of remote IPv4 CIDRs accessible via this connection. Optional for route-based and BGP configurations (defaults to 0.0.0.0/0). Mandatory for policy-based. +- `static_routes` (List of String) List of static routes (IPv4 CIDRs) for route-based VPN. Mandatory for ROUTE_BASED gateways. + +### Read-Only + +- `connection_id` (String) The server-generated UUID of the VPN connection. +- `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`gateway_id`,`connection_id`". + + +### Nested Schema for `tunnel1` + +Required: + +- `phase1` (Attributes) (see [below for nested schema](#nestedatt--tunnel1--phase1)) +- `phase2` (Attributes) (see [below for nested schema](#nestedatt--tunnel1--phase2)) +- `pre_shared_key_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Pre-shared key for the IPsec tunnel. Minimum 20 characters. Write-only - never stored in state and never returned by the API. To rotate the key, update this value AND increment pre_shared_key_wo_version. Changing this field alone will NOT trigger an update. +- `remote_address` (String) Remote IPv4 address for the tunnel endpoint. + +Optional: + +- `bgp` (Attributes) (see [below for nested schema](#nestedatt--tunnel1--bgp)) +- `peering` (Attributes) (see [below for nested schema](#nestedatt--tunnel1--peering)) +- `pre_shared_key_wo_version` (Number) User-managed rotation counter for the pre-shared key. Must be incremented every time pre_shared_key_wo is changed. Terraform diffs this field to detect key rotations - changing pre_shared_key_wo alone will NOT trigger an update because it is write-only and never stored in state. + + +### Nested Schema for `tunnel1.phase1` + +Required: + +- `encryption_algorithms` (List of String) Encryption algorithms for Phase 1. Possible values are: `aes256`, `aes128gcm16`, `aes256gcm16`. +- `integrity_algorithms` (List of String) Integrity algorithms for Phase 1. Possible values are: `sha1`, `sha2_256`, `sha2_384`. + +Optional: + +- `dh_groups` (List of String) Diffie-Hellman groups for key exchange. Possible values are: `modp1024`, `modp2048`, `ecp256`, `ecp384`, `modp2048s256`. +- `rekey_time` (Number) Time to schedule an IKE re-keying in seconds. Range: 900-28800. Default: 14400. + + + +### Nested Schema for `tunnel1.phase2` + +Required: + +- `encryption_algorithms` (List of String) Encryption algorithms for Phase 2. Possible values are: `aes256`, `aes128gcm16`, `aes256gcm16`. +- `integrity_algorithms` (List of String) Integrity algorithms for Phase 2. Possible values are: `sha1`, `sha2_256`, `sha2_384`. + +Optional: + +- `dh_groups` (List of String) Diffie-Hellman groups for Phase 2. Possible values are: `modp1024`, `modp2048`, `ecp256`, `ecp384`, `modp2048s256`. +- `dpd_action` (String) Action to perform on DPD timeout. Default: 'restart'. Possible values are: `clear`, `restart`. +- `rekey_time` (Number) Time to schedule a Child SA re-keying in seconds. Range: 900-3600. Default: 3600. +- `start_action` (String) Action to perform after loading the connection configuration. Default: 'start'. Possible values are: `none`, `start`. + + + +### Nested Schema for `tunnel1.bgp` + +Required: + +- `remote_asn` (Number) Remote ASN for BGP peering (private ASN range, 64512-4294967294). + + + +### Nested Schema for `tunnel1.peering` + +Required: + +- `local_address` (String) Local tunnel interface IPv4 address. +- `remote_address` (String) Remote tunnel interface IPv4 address. + + + + +### Nested Schema for `tunnel2` + +Required: + +- `phase1` (Attributes) (see [below for nested schema](#nestedatt--tunnel2--phase1)) +- `phase2` (Attributes) (see [below for nested schema](#nestedatt--tunnel2--phase2)) +- `pre_shared_key_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Pre-shared key for the IPsec tunnel. Minimum 20 characters. Write-only - never stored in state and never returned by the API. To rotate the key, update this value AND increment pre_shared_key_wo_version. Changing this field alone will NOT trigger an update. +- `remote_address` (String) Remote IPv4 address for the tunnel endpoint. + +Optional: + +- `bgp` (Attributes) (see [below for nested schema](#nestedatt--tunnel2--bgp)) +- `peering` (Attributes) (see [below for nested schema](#nestedatt--tunnel2--peering)) +- `pre_shared_key_wo_version` (Number) User-managed rotation counter for the pre-shared key. Must be incremented every time pre_shared_key_wo is changed. Terraform diffs this field to detect key rotations - changing pre_shared_key_wo alone will NOT trigger an update because it is write-only and never stored in state. + + +### Nested Schema for `tunnel2.phase1` + +Required: + +- `encryption_algorithms` (List of String) Encryption algorithms for Phase 1. Possible values are: `aes256`, `aes128gcm16`, `aes256gcm16`. +- `integrity_algorithms` (List of String) Integrity algorithms for Phase 1. Possible values are: `sha1`, `sha2_256`, `sha2_384`. + +Optional: + +- `dh_groups` (List of String) Diffie-Hellman groups for key exchange. Possible values are: `modp1024`, `modp2048`, `ecp256`, `ecp384`, `modp2048s256`. +- `rekey_time` (Number) Time to schedule an IKE re-keying in seconds. Range: 900-28800. Default: 14400. + + + +### Nested Schema for `tunnel2.phase2` + +Required: + +- `encryption_algorithms` (List of String) Encryption algorithms for Phase 2. Possible values are: `aes256`, `aes128gcm16`, `aes256gcm16`. +- `integrity_algorithms` (List of String) Integrity algorithms for Phase 2. Possible values are: `sha1`, `sha2_256`, `sha2_384`. + +Optional: + +- `dh_groups` (List of String) Diffie-Hellman groups for Phase 2. Possible values are: `modp1024`, `modp2048`, `ecp256`, `ecp384`, `modp2048s256`. +- `dpd_action` (String) Action to perform on DPD timeout. Default: 'restart'. Possible values are: `clear`, `restart`. +- `rekey_time` (Number) Time to schedule a Child SA re-keying in seconds. Range: 900-3600. Default: 3600. +- `start_action` (String) Action to perform after loading the connection configuration. Default: 'start'. Possible values are: `none`, `start`. + + + +### Nested Schema for `tunnel2.bgp` + +Required: + +- `remote_asn` (Number) Remote ASN for BGP peering (private ASN range, 64512-4294967294). + + + +### Nested Schema for `tunnel2.peering` + +Required: + +- `local_address` (String) Local tunnel interface IPv4 address. +- `remote_address` (String) Remote tunnel interface IPv4 address. diff --git a/examples/data-sources/stackit_vpn_connection/data-source.tf b/examples/data-sources/stackit_vpn_connection/data-source.tf new file mode 100644 index 000000000..82d1f84ba --- /dev/null +++ b/examples/data-sources/stackit_vpn_connection/data-source.tf @@ -0,0 +1,5 @@ +data "stackit_vpn_connection" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + gateway_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + connection_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/resources/stackit_vpn_connection/resource.tf b/examples/resources/stackit_vpn_connection/resource.tf new file mode 100644 index 000000000..1fdf5673f --- /dev/null +++ b/examples/resources/stackit_vpn_connection/resource.tf @@ -0,0 +1,45 @@ +resource "stackit_vpn_connection" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + gateway_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + display_name = "example-vpn-connection" + + tunnel1 = { + remote_address = "198.51.100.10" + pre_shared_key_wo = "example-super-secret-key-tunnel1" + + phase1 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + + phase2 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + } + + tunnel2 = { + remote_address = "203.0.113.10" + pre_shared_key_wo = "example-super-secret-key-tunnel2" + + phase1 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + + phase2 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + } +} + +# Only use the import statement, if you want to import an existing VPN connection +import { + to = stackit_vpn_connection.example + id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,eu01,xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/stackit/internal/services/vpn/connection/datasource.go b/stackit/internal/services/vpn/connection/datasource.go new file mode 100644 index 000000000..e1e4c2c7f --- /dev/null +++ b/stackit/internal/services/vpn/connection/datasource.go @@ -0,0 +1,309 @@ +package connection + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "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" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/utils" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = (*vpnConnectionDataSource)(nil) + _ datasource.DataSourceWithConfigure = (*vpnConnectionDataSource)(nil) +) + +var datasourceSchemaDescriptions = map[string]string{ + "id": "Terraform's internal resource identifier. Structured as \"`project_id`,`region`,`gateway_id`,`connection_id`\".", + "project_id": "STACKIT project ID.", + "region": "STACKIT region.", + "gateway_id": "The UUID of the parent VPN gateway.", + "connection_id": "The server-generated UUID of the VPN connection.", + "display_name": "A user-friendly name for the connection.", + "enabled": "Whether this connection is enabled.", + "remote_subnet": "List of remote IPv4 CIDRs accessible via this connection.", + "local_subnet": "List of local IPv4 CIDRs to route through this connection.", + "static_routes": "List of static routes (IPv4 CIDRs) for route-based VPN.", + "labels": "Map of custom labels.", +} + +var datasourceTunnelSchemaDescriptions = map[string]string{ + "remote_address": "Remote peer IPv4 address for this tunnel.", + "phase1": "IKE Phase 1 configuration.", + "phase1_dh_groups": "Diffie-Hellman groups.", + "phase1_encryption_algorithms": "Encryption algorithms.", + "phase1_integrity_algorithms": "Integrity/hash algorithms.", + "phase1_rekey_time": "IKE re-keying time in seconds.", + "phase2": "IKE Phase 2 configuration.", + "phase2_dh_groups": "Diffie-Hellman groups for PFS.", + "phase2_encryption_algorithms": "Encryption algorithms.", + "phase2_integrity_algorithms": "Integrity/hash algorithms.", + "phase2_rekey_time": "Child SA re-keying time in seconds.", + "phase2_start_action": "Start action (none or start).", + "phase2_dpd_action": "DPD timeout action (clear or restart).", + "peering": "Tunnel interface peering configuration.", + "peering_local_address": "Local tunnel interface IPv4 address.", + "peering_remote_address": "Remote tunnel interface IPv4 address.", + "bgp": "BGP configuration for this tunnel.", + "bgp_remote_asn": "Remote AS number.", +} + +type vpnConnectionDataSource struct { + client *vpn.APIClient + providerData core.ProviderData +} + +func NewVPNConnectionDataSource() datasource.DataSource { + return &vpnConnectionDataSource{} +} + +func (d *vpnConnectionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := utils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + d.providerData = providerData + tflog.Info(ctx, "VPN connection data source configured") +} + +func (d *vpnConnectionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vpn_connection" +} + +func (d *vpnConnectionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + tunnelSchema := schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "pre_shared_key_wo": schema.StringAttribute{ + Computed: true, + Sensitive: true, + }, + "pre_shared_key_wo_version": schema.Int64Attribute{ + Computed: true, + }, + "remote_address": schema.StringAttribute{ + Description: datasourceTunnelSchemaDescriptions["remote_address"], + Computed: true, + }, + "phase1": schema.SingleNestedAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase1"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "dh_groups": schema.ListAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase1_dh_groups"], + Computed: true, + ElementType: types.StringType, + }, + "encryption_algorithms": schema.ListAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase1_encryption_algorithms"], + Computed: true, + ElementType: types.StringType, + }, + "integrity_algorithms": schema.ListAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase1_integrity_algorithms"], + Computed: true, + ElementType: types.StringType, + }, + "rekey_time": schema.Int32Attribute{ + Description: datasourceTunnelSchemaDescriptions["phase1_rekey_time"], + Computed: true, + }, + }, + }, + "phase2": schema.SingleNestedAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase2"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "dh_groups": schema.ListAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase2_dh_groups"], + Computed: true, + ElementType: types.StringType, + }, + "encryption_algorithms": schema.ListAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase2_encryption_algorithms"], + Computed: true, + ElementType: types.StringType, + }, + "integrity_algorithms": schema.ListAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase2_integrity_algorithms"], + Computed: true, + ElementType: types.StringType, + }, + "rekey_time": schema.Int32Attribute{ + Description: datasourceTunnelSchemaDescriptions["phase2_rekey_time"], + Computed: true, + }, + "start_action": schema.StringAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase2_start_action"], + Computed: true, + }, + "dpd_action": schema.StringAttribute{ + Description: datasourceTunnelSchemaDescriptions["phase2_dpd_action"], + Computed: true, + }, + }, + }, + "peering": schema.SingleNestedAttribute{ + Description: datasourceTunnelSchemaDescriptions["peering"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "local_address": schema.StringAttribute{ + Description: datasourceTunnelSchemaDescriptions["peering_local_address"], + Computed: true, + }, + "remote_address": schema.StringAttribute{ + Description: datasourceTunnelSchemaDescriptions["peering_remote_address"], + Computed: true, + }, + }, + }, + "bgp": schema.SingleNestedAttribute{ + Description: datasourceTunnelSchemaDescriptions["bgp"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "remote_asn": schema.Int64Attribute{ + Description: datasourceTunnelSchemaDescriptions["bgp_remote_asn"], + Computed: true, + }, + }, + }, + }, + } + + resp.Schema = schema.Schema{ + Description: fmt.Sprintf("VPN Connection data source schema. %s", core.DatasourceRegionFallbackDocstring), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: datasourceSchemaDescriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: datasourceSchemaDescriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: datasourceSchemaDescriptions["region"], + Computed: true, + }, + "gateway_id": schema.StringAttribute{ + Description: datasourceSchemaDescriptions["gateway_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "connection_id": schema.StringAttribute{ + Description: datasourceSchemaDescriptions["connection_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "display_name": schema.StringAttribute{ + Description: datasourceSchemaDescriptions["display_name"], + Computed: true, + }, + "enabled": schema.BoolAttribute{ + Description: datasourceSchemaDescriptions["enabled"], + Computed: true, + }, + "remote_subnet": schema.ListAttribute{ + Description: datasourceSchemaDescriptions["remote_subnet"], + Computed: true, + ElementType: types.StringType, + }, + "local_subnet": schema.ListAttribute{ + Description: datasourceSchemaDescriptions["local_subnet"], + Computed: true, + ElementType: types.StringType, + }, + "static_routes": schema.ListAttribute{ + Description: datasourceSchemaDescriptions["static_routes"], + Computed: true, + ElementType: types.StringType, + }, + "tunnel1": tunnelSchema, + "tunnel2": tunnelSchema, + "labels": schema.MapAttribute{ + Description: datasourceSchemaDescriptions["labels"], + Computed: true, + ElementType: types.StringType, + }, + }, + } +} + +func (d *vpnConnectionDataSource) 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...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + gatewayId := model.GatewayID.ValueString() + connectionId := model.ConnectionID.ValueString() + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "gateway_id", gatewayId) + ctx = tflog.SetField(ctx, "connection_id", connectionId) + + connResp, err := d.client.DefaultAPI.GetGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading VPN connection", fmt.Sprintf("Calling API: %v", err)) + return + } + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, connResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading VPN connection", fmt.Sprintf("Processing response: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "VPN connection read", map[string]any{ + "connection_id": connectionId, + }) +} diff --git a/stackit/internal/services/vpn/connection/resource.go b/stackit/internal/services/vpn/connection/resource.go new file mode 100644 index 000000000..26d5cf745 --- /dev/null +++ b/stackit/internal/services/vpn/connection/resource.go @@ -0,0 +1,1150 @@ +package connection + +import ( + "context" + "errors" + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "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" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "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/services/vpn/utils" + tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ resource.Resource = &vpnConnectionResource{} + _ resource.ResourceWithConfigure = &vpnConnectionResource{} + _ resource.ResourceWithImportState = &vpnConnectionResource{} + _ resource.ResourceWithModifyPlan = &vpnConnectionResource{} +) + +type Phase1Model struct { + DhGroups types.List `tfsdk:"dh_groups"` + EncryptionAlgorithms types.List `tfsdk:"encryption_algorithms"` + IntegrityAlgorithms types.List `tfsdk:"integrity_algorithms"` + RekeyTime types.Int32 `tfsdk:"rekey_time"` +} + +type Phase2Model struct { + DhGroups types.List `tfsdk:"dh_groups"` + EncryptionAlgorithms types.List `tfsdk:"encryption_algorithms"` + IntegrityAlgorithms types.List `tfsdk:"integrity_algorithms"` + RekeyTime types.Int32 `tfsdk:"rekey_time"` + StartAction types.String `tfsdk:"start_action"` + DpdAction types.String `tfsdk:"dpd_action"` +} + +type PeeringConfigModel struct { + LocalAddress types.String `tfsdk:"local_address"` + RemoteAddress types.String `tfsdk:"remote_address"` +} + +type BGPTunnelConfigModel struct { + RemoteAsn types.Int64 `tfsdk:"remote_asn"` +} + +type TunnelModel struct { + PreSharedKeyWo types.String `tfsdk:"pre_shared_key_wo"` + PreSharedKeyWoVersion types.Int64 `tfsdk:"pre_shared_key_wo_version"` + RemoteAddress types.String `tfsdk:"remote_address"` + Phase1 *Phase1Model `tfsdk:"phase1"` + Phase2 *Phase2Model `tfsdk:"phase2"` + Peering *PeeringConfigModel `tfsdk:"peering"` + Bgp *BGPTunnelConfigModel `tfsdk:"bgp"` +} + +type Model struct { + ID types.String `tfsdk:"id"` + ConnectionID types.String `tfsdk:"connection_id"` + ProjectID types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + GatewayID types.String `tfsdk:"gateway_id"` + DisplayName types.String `tfsdk:"display_name"` + Enabled types.Bool `tfsdk:"enabled"` + RemoteSubnet types.List `tfsdk:"remote_subnet"` + LocalSubnet types.List `tfsdk:"local_subnet"` + StaticRoutes types.List `tfsdk:"static_routes"` + Tunnel1 *TunnelModel `tfsdk:"tunnel1"` + Tunnel2 *TunnelModel `tfsdk:"tunnel2"` + Labels types.Map `tfsdk:"labels"` +} + +var ( + dhGroupValues = []string{"modp1024", "modp2048", "ecp256", "ecp384", "modp2048s256"} + encryptionAlgorithmValues = []string{"aes256", "aes128gcm16", "aes256gcm16"} + integrityAlgorithmValues = []string{"sha1", "sha2_256", "sha2_384"} + startActionValues = []string{"none", "start"} + dpdActionValues = []string{"clear", "restart"} +) + +var schemaDescriptions = map[string]string{ + "id": "Terraform's internal resource identifier. Structured as \"`project_id`,`region`,`gateway_id`,`connection_id`\".", + "connection_id": "The server-generated UUID of the VPN connection.", + "project_id": "STACKIT project ID.", + "region": "STACKIT region.", + "gateway_id": "The UUID of the parent VPN gateway.", + "display_name": "A user-friendly name for the connection. Must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long.", + "enabled": "Whether this connection is enabled. Defaults to true.", + "remote_subnet": "List of remote IPv4 CIDRs accessible via this connection. Optional for route-based and BGP configurations (defaults to 0.0.0.0/0). Mandatory for policy-based.", + "local_subnet": "List of local IPv4 CIDRs to route through this connection. Optional for route-based and BGP configurations (defaults to 0.0.0.0/0). Mandatory for policy-based.", + "static_routes": "List of static routes (IPv4 CIDRs) for route-based VPN. Mandatory for ROUTE_BASED gateways.", + "tunnel1": "Configuration for the first IPsec tunnel.", + "tunnel2": "Configuration for the second IPsec tunnel.", + "labels": "Map of custom labels.", +} + +var tunnelSchemaDescriptions = map[string]string{ + "pre_shared_key_wo": "Pre-shared key for the IPsec tunnel. Minimum 20 characters. Write-only - never stored in state and never returned by the API. To rotate the key, update this value AND increment pre_shared_key_wo_version. Changing this field alone will NOT trigger an update.", + "pre_shared_key_wo_version": "User-managed rotation counter for the pre-shared key. Must be incremented every time pre_shared_key_wo is changed. Terraform diffs this field to detect key rotations - changing pre_shared_key_wo alone will NOT trigger an update because it is write-only and never stored in state.", + "remote_address": "Remote IPv4 address for the tunnel endpoint.", + "phase1_dh_groups": fmt.Sprintf("Diffie-Hellman groups for key exchange. %s", tfutils.FormatPossibleValues(dhGroupValues...)), + "phase1_encryption_algorithms": fmt.Sprintf("Encryption algorithms for Phase 1. %s", tfutils.FormatPossibleValues(encryptionAlgorithmValues...)), + "phase1_integrity_algorithms": fmt.Sprintf("Integrity algorithms for Phase 1. %s", tfutils.FormatPossibleValues(integrityAlgorithmValues...)), + "phase1_rekey_time": "Time to schedule an IKE re-keying in seconds. Range: 900-28800. Default: 14400.", + "phase2_dh_groups": fmt.Sprintf("Diffie-Hellman groups for Phase 2. %s", tfutils.FormatPossibleValues(dhGroupValues...)), + "phase2_encryption_algorithms": fmt.Sprintf("Encryption algorithms for Phase 2. %s", tfutils.FormatPossibleValues(encryptionAlgorithmValues...)), + "phase2_integrity_algorithms": fmt.Sprintf("Integrity algorithms for Phase 2. %s", tfutils.FormatPossibleValues(integrityAlgorithmValues...)), + "phase2_rekey_time": "Time to schedule a Child SA re-keying in seconds. Range: 900-3600. Default: 3600.", + "phase2_start_action": fmt.Sprintf("Action to perform after loading the connection configuration. Default: 'start'. %s", tfutils.FormatPossibleValues(startActionValues...)), + "phase2_dpd_action": fmt.Sprintf("Action to perform on DPD timeout. Default: 'restart'. %s", tfutils.FormatPossibleValues(dpdActionValues...)), + "peering_local_address": "Local tunnel interface IPv4 address.", + "peering_remote_address": "Remote tunnel interface IPv4 address.", + "bgp_remote_asn": "Remote ASN for BGP peering (private ASN range, 64512-4294967294).", +} + +type vpnConnectionResource struct { + client *vpn.APIClient + providerData core.ProviderData +} + +func NewVpnConnectionResource() resource.Resource { + return &vpnConnectionResource{} +} + +func (r *vpnConnectionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := utils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + r.providerData = providerData + tflog.Info(ctx, "VPN client configured") +} + +func (r *vpnConnectionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vpn_connection" +} + +func (r *vpnConnectionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + tunnelSchema := schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "pre_shared_key_wo": schema.StringAttribute{ + Description: tunnelSchemaDescriptions["pre_shared_key_wo"], + Required: true, + Sensitive: true, + WriteOnly: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(20), + }, + }, + "pre_shared_key_wo_version": schema.Int64Attribute{ + Description: tunnelSchemaDescriptions["pre_shared_key_wo_version"], + Optional: true, + }, + "remote_address": schema.StringAttribute{ + Description: tunnelSchemaDescriptions["remote_address"], + Required: true, + Validators: []validator.String{ + validate.IP(true), + }, + }, + "phase1": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "dh_groups": schema.ListAttribute{ + Description: tunnelSchemaDescriptions["phase1_dh_groups"], + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf(dhGroupValues...), + ), + }, + }, + "encryption_algorithms": schema.ListAttribute{ + Description: tunnelSchemaDescriptions["phase1_encryption_algorithms"], + Required: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf(encryptionAlgorithmValues...), + ), + }, + }, + "integrity_algorithms": schema.ListAttribute{ + Description: tunnelSchemaDescriptions["phase1_integrity_algorithms"], + Required: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf(integrityAlgorithmValues...), + ), + }, + }, + "rekey_time": schema.Int32Attribute{ + Description: tunnelSchemaDescriptions["phase1_rekey_time"], + Optional: true, + Computed: true, + Validators: []validator.Int32{ + int32validator.Between(900, 28800), + }, + }, + }, + }, + "phase2": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "dh_groups": schema.ListAttribute{ + Description: tunnelSchemaDescriptions["phase2_dh_groups"], + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf(dhGroupValues...), + ), + }, + }, + "encryption_algorithms": schema.ListAttribute{ + Description: tunnelSchemaDescriptions["phase2_encryption_algorithms"], + Required: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf(encryptionAlgorithmValues...), + ), + }, + }, + "integrity_algorithms": schema.ListAttribute{ + Description: tunnelSchemaDescriptions["phase2_integrity_algorithms"], + Required: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf(integrityAlgorithmValues...), + ), + }, + }, + "rekey_time": schema.Int32Attribute{ + Description: tunnelSchemaDescriptions["phase2_rekey_time"], + Optional: true, + Computed: true, + Validators: []validator.Int32{ + int32validator.Between(900, 3600), + }, + }, + "start_action": schema.StringAttribute{ + Description: tunnelSchemaDescriptions["phase2_start_action"], + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf(startActionValues...), + }, + }, + "dpd_action": schema.StringAttribute{ + Description: tunnelSchemaDescriptions["phase2_dpd_action"], + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf(dpdActionValues...), + }, + }, + }, + }, + "peering": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "local_address": schema.StringAttribute{ + Description: tunnelSchemaDescriptions["peering_local_address"], + Required: true, + Validators: []validator.String{ + validate.IP(true), + }, + }, + "remote_address": schema.StringAttribute{ + Description: tunnelSchemaDescriptions["peering_remote_address"], + Required: true, + Validators: []validator.String{ + validate.IP(true), + }, + }, + }, + }, + "bgp": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "remote_asn": schema.Int64Attribute{ + Description: tunnelSchemaDescriptions["bgp_remote_asn"], + Required: true, + Validators: []validator.Int64{ + int64validator.Between(64512, 4294967294), + }, + }, + }, + }, + }, + } + + resp.Schema = schema.Schema{ + Description: fmt.Sprintf("VPN Connection resource schema. %s", core.ResourceRegionFallbackDocstring), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: schemaDescriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "connection_id": schema.StringAttribute{ + Description: schemaDescriptions["connection_id"], + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: schemaDescriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: schemaDescriptions["region"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "gateway_id": schema.StringAttribute{ + Description: schemaDescriptions["gateway_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "display_name": schema.StringAttribute{ + Description: schemaDescriptions["display_name"], + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`), + "must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long", + ), + }, + }, + "enabled": schema.BoolAttribute{ + Description: schemaDescriptions["enabled"], + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "remote_subnet": schema.ListAttribute{ + Description: schemaDescriptions["remote_subnet"], + Optional: true, + Computed: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.SizeBetween(1, 100), + listvalidator.ValueStringsAre(validate.CIDR()), + }, + }, + "local_subnet": schema.ListAttribute{ + Description: schemaDescriptions["local_subnet"], + Optional: true, + Computed: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.SizeBetween(1, 100), + listvalidator.ValueStringsAre(validate.CIDR()), + }, + }, + "static_routes": schema.ListAttribute{ + Description: schemaDescriptions["static_routes"], + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre(validate.CIDR()), + }, + }, + "tunnel1": tunnelSchema, + "tunnel2": tunnelSchema, + "labels": schema.MapAttribute{ + Description: schemaDescriptions["labels"], + Optional: true, + ElementType: types.StringType, + Validators: validate.LabelValidators(), + }, + }, + } +} + +func (r *vpnConnectionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + 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 + } + + tfutils.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 *vpnConnectionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing VPN connection", + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[gateway_id],[connection_id] Got: %q", req.ID), + ) + return + } + + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "gateway_id": idParts[2], + "connection_id": idParts[3], + }) + tflog.Info(ctx, "VPN connection state imported") +} + +func (r *vpnConnectionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var configModel Model + diags = req.Config.Get(ctx, &configModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + model.Tunnel1.PreSharedKeyWo = configModel.Tunnel1.PreSharedKeyWo + model.Tunnel2.PreSharedKeyWo = configModel.Tunnel2.PreSharedKeyWo + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + gatewayId := model.GatewayID.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "gateway_id", gatewayId) + + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN connection", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + createResp, err := r.client.DefaultAPI.CreateGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId).CreateGatewayConnectionPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN connection", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + if createResp.Id == nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN connection", "Got empty connection id") + return + } + connectionId := *createResp.Id + + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + "gateway_id": gatewayId, + "connection_id": connectionId, + }) + if resp.Diagnostics.HasError() { + return + } + + connResp, err := r.client.DefaultAPI.GetGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN connection", fmt.Sprintf("Reading created connection: %v", err)) + return + } + + err = mapFields(ctx, connResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN connection", 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, "VPN connection created") +} + +func (r *vpnConnectionResource) 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...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + gatewayId := model.GatewayID.ValueString() + connectionId := model.ConnectionID.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "gateway_id", gatewayId) + ctx = tflog.SetField(ctx, "connection_id", connectionId) + + connResp, err := r.client.DefaultAPI.GetGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).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 VPN connection", err.Error()) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, connResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading VPN connection", 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, "VPN connection read") +} + +func (r *vpnConnectionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var configModel Model + diags = req.Config.Get(ctx, &configModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var stateModel Model + diags = req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // tunnel1 PSK rotation + if !model.Tunnel1.PreSharedKeyWoVersion.IsNull() { + pv := model.Tunnel1.PreSharedKeyWoVersion.ValueInt64() + sv := stateModel.Tunnel1.PreSharedKeyWoVersion.ValueInt64() + if pv < sv { + resp.Diagnostics.AddAttributeError( + path.Root("tunnel1").AtName("pre_shared_key_wo_version"), + "Version must not decrease", + fmt.Sprintf( + "`pre_shared_key_wo_version` must be incremented to rotate the pre-shared key, got %d (current: %d).", + pv, sv, + ), + ) + return + } + if pv > sv { + // Secret must be read from Config, not Plan — write-only values are always null in plan. + model.Tunnel1.PreSharedKeyWo = configModel.Tunnel1.PreSharedKeyWo + } + } + + // tunnel2 PSK rotation + if !model.Tunnel2.PreSharedKeyWoVersion.IsNull() { + pv := model.Tunnel2.PreSharedKeyWoVersion.ValueInt64() + sv := stateModel.Tunnel2.PreSharedKeyWoVersion.ValueInt64() + if pv < sv { + resp.Diagnostics.AddAttributeError( + path.Root("tunnel2").AtName("pre_shared_key_wo_version"), + "Version must not decrease", + fmt.Sprintf( + "`pre_shared_key_wo_version` must be incremented to rotate the pre-shared key, got %d (current: %d).", + pv, sv, + ), + ) + return + } + if pv > sv { + model.Tunnel2.PreSharedKeyWo = configModel.Tunnel2.PreSharedKeyWo + } + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + gatewayId := model.GatewayID.ValueString() + connectionId := model.ConnectionID.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "gateway_id", gatewayId) + ctx = tflog.SetField(ctx, "connection_id", connectionId) + + payload, err := toUpdatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN connection", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + _, err = r.client.DefaultAPI.UpdateGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).UpdateGatewayConnectionPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN connection", err.Error()) + return + } + + ctx = core.LogResponse(ctx) + + connResp, err := r.client.DefaultAPI.GetGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN connection", fmt.Sprintf("Reading updated connection: %v", err)) + return + } + + err = mapFields(ctx, connResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN connection", 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, "VPN connection updated") +} + +func (r *vpnConnectionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + gatewayId := model.GatewayID.ValueString() + connectionId := model.ConnectionID.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "gateway_id", gatewayId) + ctx = tflog.SetField(ctx, "connection_id", connectionId) + + err := r.client.DefaultAPI.DeleteGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting VPN connection", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + tflog.Info(ctx, "VPN connection deleted") +} + +func toCreatePayload(_ context.Context, model *Model) (*vpn.CreateGatewayConnectionPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + fields, err := toConnectionFields(model) + if err != nil { + return nil, err + } + + // The spec's Connection schema `#/components/schemas/Connection` (used for create) has no labels field, + // unlike ConnectionResponse which does. The API does accept labels on create despite the omission, but because + // CreateGatewayConnectionPayload is generated from Connection, there is no Labels field on the + // struct. Once the spec is corrected and the SDK regenerated, uncomment the block below. + + // nolint:gocritic // commented-out code (blocked by spec/SDK gap — see comment above) + // labels, err := tfutils.LabelsToPayload(ctx, model.Labels) + // if err != nil { + // return nil, err + // } + + return &vpn.CreateGatewayConnectionPayload{ + DisplayName: fields.displayName, + Tunnel1: fields.tunnel1, + Tunnel2: fields.tunnel2, + Enabled: fields.enabled, + RemoteSubnets: fields.remoteSubnets, + LocalSubnets: fields.localSubnets, + StaticRoutes: fields.staticRoutes, + // Labels: &labels, // blocked by spec/SDK gap — see comment above + }, nil +} + +func toUpdatePayload(_ context.Context, model *Model) (*vpn.UpdateGatewayConnectionPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + fields, err := toConnectionFields(model) + if err != nil { + return nil, err + } + + // The spec's Connection schema `#/components/schemas/Connection` (used for update) has no labels field, + // unlike ConnectionResponse which does. The API does accept labels on update despite the omission, but because + // UpdateGatewayConnectionPayload is generated from Connection, there is no Labels field on the + // struct. Once the spec is corrected and the SDK regenerated, uncomment the block below. + + // nolint:gocritic // commented-out code (blocked by spec/SDK gap — see comment above) + // labels, err := tfutils.LabelsToPayload(ctx, model.Labels) + // if err != nil { + // return nil, err + // } + + return &vpn.UpdateGatewayConnectionPayload{ + DisplayName: fields.displayName, + Tunnel1: fields.tunnel1, + Tunnel2: fields.tunnel2, + Enabled: fields.enabled, + RemoteSubnets: fields.remoteSubnets, + LocalSubnets: fields.localSubnets, + StaticRoutes: fields.staticRoutes, + // Labels: &labels, // blocked by spec/SDK gap — see comment above + }, nil +} + +type connectionFields struct { + displayName string + tunnel1 vpn.TunnelConfiguration + tunnel2 vpn.TunnelConfiguration + enabled *bool + remoteSubnets []string + localSubnets []string + staticRoutes []string +} + +func toConnectionFields(model *Model) (*connectionFields, error) { + tunnel1, err := toTunnelConfiguration(model.Tunnel1) + if err != nil { + return nil, fmt.Errorf("converting tunnel1: %w", err) + } + + tunnel2, err := toTunnelConfiguration(model.Tunnel2) + if err != nil { + return nil, fmt.Errorf("converting tunnel2: %w", err) + } + + fields := &connectionFields{ + displayName: model.DisplayName.ValueString(), + tunnel1: *tunnel1, + tunnel2: *tunnel2, + } + + if !model.Enabled.IsNull() && !model.Enabled.IsUnknown() { + enabled := model.Enabled.ValueBool() + fields.enabled = &enabled + } + + if !model.RemoteSubnet.IsNull() && !model.RemoteSubnet.IsUnknown() { + remoteSubnets, err := tfutils.ListValueToStringSlice(model.RemoteSubnet) + if err != nil { + return nil, fmt.Errorf("converting remote_subnet: %w", err) + } + fields.remoteSubnets = remoteSubnets + } + + if !model.LocalSubnet.IsNull() && !model.LocalSubnet.IsUnknown() { + localSubnets, err := tfutils.ListValueToStringSlice(model.LocalSubnet) + if err != nil { + return nil, fmt.Errorf("converting local_subnet: %w", err) + } + fields.localSubnets = localSubnets + } + + if !model.StaticRoutes.IsNull() { + staticRoutes, err := tfutils.ListValueToStringSlice(model.StaticRoutes) + if err != nil { + return nil, fmt.Errorf("converting static_routes: %w", err) + } + fields.staticRoutes = staticRoutes + } + + return fields, nil +} + +func toTunnelConfiguration(tunnel *TunnelModel) (*vpn.TunnelConfiguration, error) { + if tunnel == nil { + return nil, fmt.Errorf("nil tunnel model") + } + + config := &vpn.TunnelConfiguration{ + RemoteAddress: tunnel.RemoteAddress.ValueString(), + } + if !tunnel.PreSharedKeyWo.IsNull() { + preSharedKey := tunnel.PreSharedKeyWo.ValueString() + config.PreSharedKey = &preSharedKey + } + + if tunnel.Phase1 != nil { + phase1 := vpn.TunnelConfigurationPhase1{} + if !tunnel.Phase1.DhGroups.IsNull() { + dhGroups, err := tfutils.ListValueToStringSlice(tunnel.Phase1.DhGroups) + if err != nil { + return nil, fmt.Errorf("converting phase1 dh_groups: %w", err) + } + phase1.DhGroups = dhGroups + } + encAlgs, err := tfutils.ListValueToStringSlice(tunnel.Phase1.EncryptionAlgorithms) + if err != nil { + return nil, fmt.Errorf("converting phase1 encryption_algorithms: %w", err) + } + phase1.EncryptionAlgorithms = encAlgs + intAlgs, err := tfutils.ListValueToStringSlice(tunnel.Phase1.IntegrityAlgorithms) + if err != nil { + return nil, fmt.Errorf("converting phase1 integrity_algorithms: %w", err) + } + phase1.IntegrityAlgorithms = intAlgs + if !tunnel.Phase1.RekeyTime.IsNull() && !tunnel.Phase1.RekeyTime.IsUnknown() { + rekeyTime := tunnel.Phase1.RekeyTime.ValueInt32() + phase1.RekeyTime = &rekeyTime + } + config.Phase1 = phase1 + } + + if tunnel.Phase2 != nil { + phase2 := vpn.TunnelConfigurationPhase2{} + if !tunnel.Phase2.DhGroups.IsNull() { + dhGroups, err := tfutils.ListValueToStringSlice(tunnel.Phase2.DhGroups) + if err != nil { + return nil, fmt.Errorf("converting phase2 dh_groups: %w", err) + } + phase2.DhGroups = dhGroups + } + encAlgs, err := tfutils.ListValueToStringSlice(tunnel.Phase2.EncryptionAlgorithms) + if err != nil { + return nil, fmt.Errorf("converting phase2 encryption_algorithms: %w", err) + } + phase2.EncryptionAlgorithms = encAlgs + intAlgs, err := tfutils.ListValueToStringSlice(tunnel.Phase2.IntegrityAlgorithms) + if err != nil { + return nil, fmt.Errorf("converting phase2 integrity_algorithms: %w", err) + } + phase2.IntegrityAlgorithms = intAlgs + if !tunnel.Phase2.RekeyTime.IsNull() && !tunnel.Phase2.RekeyTime.IsUnknown() { + rekeyTime := tunnel.Phase2.RekeyTime.ValueInt32() + phase2.RekeyTime = &rekeyTime + } + if !tunnel.Phase2.StartAction.IsNull() && !tunnel.Phase2.StartAction.IsUnknown() { + startAction := tunnel.Phase2.StartAction.ValueString() + phase2.StartAction = &startAction + } + if !tunnel.Phase2.DpdAction.IsNull() && !tunnel.Phase2.DpdAction.IsUnknown() { + dpdAction := tunnel.Phase2.DpdAction.ValueString() + phase2.DpdAction = &dpdAction + } + config.Phase2 = phase2 + } + + if tunnel.Peering != nil { + localAddr := tunnel.Peering.LocalAddress.ValueString() + remoteAddr := tunnel.Peering.RemoteAddress.ValueString() + config.Peering = &vpn.PeeringConfig{ + LocalAddress: &localAddr, + RemoteAddress: &remoteAddr, + } + } + + if tunnel.Bgp != nil { + remoteAsn := tunnel.Bgp.RemoteAsn.ValueInt64() + config.Bgp = &vpn.BGPTunnelConfig{ + RemoteAsn: remoteAsn, + } + } + + return config, nil +} + +func mapFields(ctx context.Context, conn *vpn.ConnectionResponse, model *Model, region string) error { + if conn == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var connectionId string + if conn.Id != nil { + connectionId = *conn.Id + } else if model.ConnectionID.ValueString() != "" { + connectionId = model.ConnectionID.ValueString() + } else { + return fmt.Errorf("connection id not present") + } + + model.ID = tfutils.BuildInternalTerraformId(model.ProjectID.ValueString(), region, model.GatewayID.ValueString(), connectionId) + model.ConnectionID = types.StringValue(connectionId) + model.DisplayName = types.StringValue(conn.DisplayName) + model.Region = types.StringValue(region) + + if conn.Enabled != nil { + model.Enabled = types.BoolValue(*conn.Enabled) + } else { + model.Enabled = types.BoolValue(true) + } + + if conn.RemoteSubnets != nil { + list, diags := types.ListValueFrom(ctx, types.StringType, conn.RemoteSubnets) + if diags.HasError() { + return fmt.Errorf("mapping remote_subnet: %w", core.DiagsToError(diags)) + } + model.RemoteSubnet = list + } else { + model.RemoteSubnet = types.ListNull(types.StringType) + } + + if conn.LocalSubnets != nil { + list, diags := types.ListValueFrom(ctx, types.StringType, conn.LocalSubnets) + if diags.HasError() { + return fmt.Errorf("mapping local_subnet: %w", core.DiagsToError(diags)) + } + model.LocalSubnet = list + } else { + model.LocalSubnet = types.ListNull(types.StringType) + } + + if conn.StaticRoutes != nil { + list, diags := types.ListValueFrom(ctx, types.StringType, conn.StaticRoutes) + if diags.HasError() { + return fmt.Errorf("mapping static_routes: %w", core.DiagsToError(diags)) + } + model.StaticRoutes = list + } else { + model.StaticRoutes = types.ListNull(types.StringType) + } + + tunnel1, err := mapTunnel(ctx, &conn.Tunnel1, model.Tunnel1) + if err != nil { + return fmt.Errorf("mapping tunnel1: %w", err) + } + model.Tunnel1 = tunnel1 + + tunnel2, err := mapTunnel(ctx, &conn.Tunnel2, model.Tunnel2) + if err != nil { + return fmt.Errorf("mapping tunnel2: %w", err) + } + model.Tunnel2 = tunnel2 + + labels, err := tfutils.MapLabels(ctx, conn.Labels, model.Labels) + if err != nil { + return fmt.Errorf("mapping labels: %w", err) + } + model.Labels = labels + + return nil +} + +func mapTunnel(ctx context.Context, apiTunnel *vpn.TunnelConfiguration, cuurrentTunnel *TunnelModel) (*TunnelModel, error) { + tunnel := &TunnelModel{ + RemoteAddress: types.StringValue(string(apiTunnel.RemoteAddress)), + } + phase1 := &Phase1Model{} + if len(apiTunnel.Phase1.DhGroups) > 0 { + list, diags := types.ListValueFrom(ctx, types.StringType, apiTunnel.Phase1.DhGroups) + if diags.HasError() { + return nil, fmt.Errorf("mapping phase1 dh_groups: %w", core.DiagsToError(diags)) + } + phase1.DhGroups = list + } else { + phase1.DhGroups = types.ListNull(types.StringType) + } + if len(apiTunnel.Phase1.EncryptionAlgorithms) > 0 { + list, diags := types.ListValueFrom(ctx, types.StringType, apiTunnel.Phase1.EncryptionAlgorithms) + if diags.HasError() { + return nil, fmt.Errorf("mapping phase1 encryption_algorithms: %w", core.DiagsToError(diags)) + } + phase1.EncryptionAlgorithms = list + } else { + phase1.EncryptionAlgorithms = types.ListNull(types.StringType) + } + if len(apiTunnel.Phase1.IntegrityAlgorithms) > 0 { + list, diags := types.ListValueFrom(ctx, types.StringType, apiTunnel.Phase1.IntegrityAlgorithms) + if diags.HasError() { + return nil, fmt.Errorf("mapping phase1 integrity_algorithms: %w", core.DiagsToError(diags)) + } + phase1.IntegrityAlgorithms = list + } else { + phase1.IntegrityAlgorithms = types.ListNull(types.StringType) + } + if apiTunnel.Phase1.RekeyTime != nil { + phase1.RekeyTime = types.Int32Value(*apiTunnel.Phase1.RekeyTime) + } else { + phase1.RekeyTime = types.Int32Null() + } + tunnel.Phase1 = phase1 + + phase2 := &Phase2Model{} + if len(apiTunnel.Phase2.DhGroups) > 0 { + list, diags := types.ListValueFrom(ctx, types.StringType, apiTunnel.Phase2.DhGroups) + if diags.HasError() { + return nil, fmt.Errorf("mapping phase2 dh_groups: %w", core.DiagsToError(diags)) + } + phase2.DhGroups = list + } else { + phase2.DhGroups = types.ListNull(types.StringType) + } + if len(apiTunnel.Phase2.EncryptionAlgorithms) > 0 { + list, diags := types.ListValueFrom(ctx, types.StringType, apiTunnel.Phase2.EncryptionAlgorithms) + if diags.HasError() { + return nil, fmt.Errorf("mapping phase2 encryption_algorithms: %w", core.DiagsToError(diags)) + } + phase2.EncryptionAlgorithms = list + } else { + phase2.EncryptionAlgorithms = types.ListNull(types.StringType) + } + if len(apiTunnel.Phase2.IntegrityAlgorithms) > 0 { + list, diags := types.ListValueFrom(ctx, types.StringType, apiTunnel.Phase2.IntegrityAlgorithms) + if diags.HasError() { + return nil, fmt.Errorf("mapping phase2 integrity_algorithms: %w", core.DiagsToError(diags)) + } + phase2.IntegrityAlgorithms = list + } else { + phase2.IntegrityAlgorithms = types.ListNull(types.StringType) + } + if apiTunnel.Phase2.RekeyTime != nil { + phase2.RekeyTime = types.Int32Value(*apiTunnel.Phase2.RekeyTime) + } else { + phase2.RekeyTime = types.Int32Null() + } + if apiTunnel.Phase2.StartAction != nil { + phase2.StartAction = types.StringValue(*apiTunnel.Phase2.StartAction) + } else { + phase2.StartAction = types.StringNull() + } + if apiTunnel.Phase2.DpdAction != nil { + phase2.DpdAction = types.StringValue(*apiTunnel.Phase2.DpdAction) + } else { + phase2.DpdAction = types.StringNull() + } + tunnel.Phase2 = phase2 + + if apiTunnel.Peering != nil { + peering := &PeeringConfigModel{} + if apiTunnel.Peering.LocalAddress != nil { + peering.LocalAddress = types.StringValue(*apiTunnel.Peering.LocalAddress) + } else { + peering.LocalAddress = types.StringNull() + } + if apiTunnel.Peering.RemoteAddress != nil { + peering.RemoteAddress = types.StringValue(*apiTunnel.Peering.RemoteAddress) + } else { + peering.RemoteAddress = types.StringNull() + } + tunnel.Peering = peering + } + + if apiTunnel.Bgp != nil { + tunnel.Bgp = &BGPTunnelConfigModel{ + RemoteAsn: types.Int64Value(int64(apiTunnel.Bgp.RemoteAsn)), + } + } + + // could be nil for Read after a terraform import + if cuurrentTunnel != nil { + tunnel.PreSharedKeyWoVersion = cuurrentTunnel.PreSharedKeyWoVersion + } else { + tunnel.PreSharedKeyWoVersion = types.Int64Null() + } + + return tunnel, nil +} diff --git a/stackit/internal/services/vpn/connection/resource_test.go b/stackit/internal/services/vpn/connection/resource_test.go new file mode 100644 index 000000000..0477ffced --- /dev/null +++ b/stackit/internal/services/vpn/connection/resource_test.go @@ -0,0 +1,1054 @@ +package connection + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" +) + +var ( + projectId = uuid.NewString() + gatewayId = uuid.NewString() + region = "eu01" +) + +func TestMapFields(t *testing.T) { + tests := []struct { + description string + input *vpn.ConnectionResponse + expected Model + isValid bool + }{ + { + description: "basic_connection", + input: &vpn.ConnectionResponse{ + Id: new("connection-id"), + DisplayName: "test-connection", + Enabled: new(true), + RemoteSubnets: []string{"10.0.0.0/16"}, + LocalSubnets: []string{"192.168.0.0/24"}, + Tunnel1: vpn.TunnelConfiguration{ + RemoteAddress: "203.0.113.1", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: []string{"modp2048"}, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + RekeyTime: new(int32(14400)), + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: []string{"modp2048"}, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + RekeyTime: new(int32(3600)), + StartAction: new("start"), + DpdAction: new("restart"), + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + RemoteAddress: "203.0.113.2", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: []string{"modp2048"}, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + RekeyTime: new(int32(14400)), + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: []string{"modp2048"}, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + RekeyTime: new(int32(3600)), + StartAction: new("start"), + DpdAction: new("restart"), + }, + }, + }, + expected: Model{ + ID: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, region, gatewayId, "connection-id")), + ConnectionID: types.StringValue("connection-id"), + ProjectID: types.StringValue(projectId), + Region: types.StringValue(region), + GatewayID: types.StringValue(gatewayId), + DisplayName: types.StringValue("test-connection"), + Enabled: types.BoolValue(true), + RemoteSubnet: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("10.0.0.0/16"), + }), + LocalSubnet: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("192.168.0.0/24"), + }), + StaticRoutes: types.ListNull(types.StringType), + Tunnel1: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("203.0.113.1"), + Phase1: &Phase1Model{ + DhGroups: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("modp2048"), + }), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("aes256"), + }), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("sha2_256"), + }), + RekeyTime: types.Int32Value(14400), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("modp2048"), + }), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("aes256"), + }), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("sha2_256"), + }), + RekeyTime: types.Int32Value(3600), + StartAction: types.StringValue("start"), + DpdAction: types.StringValue("restart"), + }, + }, + Tunnel2: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("203.0.113.2"), + Phase1: &Phase1Model{ + DhGroups: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("modp2048"), + }), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("aes256"), + }), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("sha2_256"), + }), + RekeyTime: types.Int32Value(14400), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("modp2048"), + }), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("aes256"), + }), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("sha2_256"), + }), + RekeyTime: types.Int32Value(3600), + StartAction: types.StringValue("start"), + DpdAction: types.StringValue("restart"), + }, + }, + Labels: types.MapNull(types.StringType), + }, + isValid: true, + }, + { + description: "connection_with_static_routes_and_bgp", + input: &vpn.ConnectionResponse{ + Id: new("conn-id-2"), + DisplayName: "bgp-connection", + Enabled: new(false), + StaticRoutes: []string{"10.0.0.0/8"}, + Tunnel1: vpn.TunnelConfiguration{ + RemoteAddress: "203.0.113.10", + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []string{"aes256gcm16"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []string{"aes256gcm16"}, + IntegrityAlgorithms: []string{"sha2_384"}, + DpdAction: new("clear"), + StartAction: new("none"), + }, + Peering: &vpn.PeeringConfig{ + LocalAddress: new("169.254.0.1"), + RemoteAddress: new("169.254.0.2"), + }, + Bgp: &vpn.BGPTunnelConfig{ + RemoteAsn: 65000, + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + RemoteAddress: "203.0.113.11", + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []string{"aes256gcm16"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []string{"aes256gcm16"}, + IntegrityAlgorithms: []string{"sha2_384"}, + DpdAction: new("clear"), + StartAction: new("none"), + }, + }, + }, + expected: Model{ + ID: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, region, gatewayId, "conn-id-2")), + ConnectionID: types.StringValue("conn-id-2"), + ProjectID: types.StringValue(projectId), + Region: types.StringValue(region), + GatewayID: types.StringValue(gatewayId), + DisplayName: types.StringValue("bgp-connection"), + Enabled: types.BoolValue(false), + RemoteSubnet: types.ListNull(types.StringType), + LocalSubnet: types.ListNull(types.StringType), + StaticRoutes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("10.0.0.0/8"), + }), + Tunnel1: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("203.0.113.10"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256gcm16")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256gcm16")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringValue("none"), + DpdAction: types.StringValue("clear"), + }, + Peering: &PeeringConfigModel{ + LocalAddress: types.StringValue("169.254.0.1"), + RemoteAddress: types.StringValue("169.254.0.2"), + }, + Bgp: &BGPTunnelConfigModel{ + RemoteAsn: types.Int64Value(65000), + }, + }, + Tunnel2: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("203.0.113.11"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256gcm16")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256gcm16")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringValue("none"), + DpdAction: types.StringValue("clear"), + }, + }, + Labels: types.MapNull(types.StringType), + }, + isValid: true, + }, + { + description: "multiple_static_routes", + input: &vpn.ConnectionResponse{ + Id: new("conn-id-3"), + DisplayName: "static-routes-connection", + StaticRoutes: []string{"10.0.0.0/8", "172.16.0.0/12"}, + Tunnel1: vpn.TunnelConfiguration{ + RemoteAddress: "1.2.3.4", + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + RemoteAddress: "5.6.7.8", + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + }, + }, + expected: Model{ + ID: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, region, gatewayId, "conn-id-3")), + ConnectionID: types.StringValue("conn-id-3"), + ProjectID: types.StringValue(projectId), + Region: types.StringValue(region), + GatewayID: types.StringValue(gatewayId), + DisplayName: types.StringValue("static-routes-connection"), + Enabled: types.BoolValue(true), + RemoteSubnet: types.ListNull(types.StringType), + LocalSubnet: types.ListNull(types.StringType), + StaticRoutes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("10.0.0.0/8"), + types.StringValue("172.16.0.0/12"), + }), + Tunnel1: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("1.2.3.4"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + Tunnel2: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("5.6.7.8"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + Labels: types.MapNull(types.StringType), + }, + isValid: true, + }, + { + description: "empty_labels", + input: &vpn.ConnectionResponse{ + Id: new("conn-id-4"), + DisplayName: "empty-labels-connection", + Labels: &map[string]string{}, + Tunnel1: vpn.TunnelConfiguration{ + RemoteAddress: "1.2.3.4", + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + RemoteAddress: "5.6.7.8", + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + }, + }, + expected: Model{ + ID: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, region, gatewayId, "conn-id-4")), + ConnectionID: types.StringValue("conn-id-4"), + ProjectID: types.StringValue(projectId), + Region: types.StringValue(region), + GatewayID: types.StringValue(gatewayId), + DisplayName: types.StringValue("empty-labels-connection"), + Enabled: types.BoolValue(true), + RemoteSubnet: types.ListNull(types.StringType), + LocalSubnet: types.ListNull(types.StringType), + StaticRoutes: types.ListNull(types.StringType), + Tunnel1: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("1.2.3.4"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + Tunnel2: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("5.6.7.8"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + Labels: types.MapNull(types.StringType), + }, + isValid: true, + }, + { + description: "asymmetric_phase_fields", + input: &vpn.ConnectionResponse{ + Id: new("conn-id-5"), + DisplayName: "asymmetric-connection", + Tunnel1: vpn.TunnelConfiguration{ + RemoteAddress: "1.2.3.4", + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + RekeyTime: new(int32(7200)), + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + RekeyTime: new(int32(1800)), + StartAction: new("none"), + DpdAction: new("clear"), + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + RemoteAddress: "5.6.7.8", + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_256"}, + }, + }, + }, + expected: Model{ + ID: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, region, gatewayId, "conn-id-5")), + ConnectionID: types.StringValue("conn-id-5"), + ProjectID: types.StringValue(projectId), + Region: types.StringValue(region), + GatewayID: types.StringValue(gatewayId), + DisplayName: types.StringValue("asymmetric-connection"), + Enabled: types.BoolValue(true), + RemoteSubnet: types.ListNull(types.StringType), + LocalSubnet: types.ListNull(types.StringType), + StaticRoutes: types.ListNull(types.StringType), + Tunnel1: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("1.2.3.4"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Value(7200), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Value(1800), + StartAction: types.StringValue("none"), + DpdAction: types.StringValue("clear"), + }, + }, + Tunnel2: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + RemoteAddress: types.StringValue("5.6.7.8"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_256")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + Labels: types.MapNull(types.StringType), + }, + isValid: true, + }, + { + description: "nil_response", + input: nil, + expected: Model{}, + isValid: false, + }, + { + description: "nil_connection_id", + input: &vpn.ConnectionResponse{ + Id: nil, + DisplayName: "test-connection", + }, + expected: Model{}, + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &Model{ + ProjectID: types.StringValue(projectId), + Region: types.StringValue(region), + GatewayID: types.StringValue(gatewayId), + Tunnel1: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + }, + Tunnel2: &TunnelModel{ + PreSharedKeyWoVersion: types.Int64Value(1), + }, + } + + err := mapFields(context.Background(), tt.input, state, region) + + if !tt.isValid && err == nil { + t.Fatalf("expected error, got none") + } + if tt.isValid && err != nil { + t.Fatalf("expected no error, got %v", err) + } + if tt.isValid { + if diff := cmp.Diff(&tt.expected, state); diff != "" { + t.Fatalf("Data mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *vpn.CreateGatewayConnectionPayload + isValid bool + }{ + { + description: "basic_connection", + input: &Model{ + DisplayName: types.StringValue("test-connection"), + Enabled: types.BoolValue(true), + RemoteSubnet: types.ListNull(types.StringType), + LocalSubnet: types.ListNull(types.StringType), + StaticRoutes: types.ListNull(types.StringType), + Tunnel1: &TunnelModel{ + RemoteAddress: types.StringValue("203.0.113.1"), + PreSharedKeyWo: types.StringValue("secret123-at-least-20-chars"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + Tunnel2: &TunnelModel{ + RemoteAddress: types.StringValue("203.0.113.2"), + PreSharedKeyWo: types.StringValue("secret456-at-least-20-chars"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + }, + expected: &vpn.CreateGatewayConnectionPayload{ + DisplayName: "test-connection", + Tunnel1: vpn.TunnelConfiguration{ + PreSharedKey: new("secret123-at-least-20-chars"), + RemoteAddress: "203.0.113.1", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + PreSharedKey: new("secret456-at-least-20-chars"), + RemoteAddress: "203.0.113.2", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + }, + Enabled: new(true), + }, + isValid: true, + }, + { + description: "with_phase2_fields", + input: &Model{ + DisplayName: types.StringValue("test"), + Enabled: types.BoolValue(true), + RemoteSubnet: types.ListNull(types.StringType), + LocalSubnet: types.ListNull(types.StringType), + StaticRoutes: types.ListNull(types.StringType), + Tunnel1: &TunnelModel{ + RemoteAddress: types.StringValue("1.2.3.4"), + PreSharedKeyWo: types.StringValue("super-secret-key-at-least-20"), + PreSharedKeyWoVersion: types.Int64Null(), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Value(7200), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Value(1800), + StartAction: types.StringValue("none"), + DpdAction: types.StringValue("clear"), + }, + }, + Tunnel2: &TunnelModel{ + RemoteAddress: types.StringValue("5.6.7.8"), + PreSharedKeyWo: types.StringValue("super-secret-key-at-least-20"), + PreSharedKeyWoVersion: types.Int64Null(), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + }, + expected: &vpn.CreateGatewayConnectionPayload{ + DisplayName: "test", + Tunnel1: vpn.TunnelConfiguration{ + PreSharedKey: new("super-secret-key-at-least-20"), + RemoteAddress: "1.2.3.4", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + RekeyTime: new(int32(7200)), + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + RekeyTime: new(int32(1800)), + StartAction: new("none"), + DpdAction: new("clear"), + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + PreSharedKey: new("super-secret-key-at-least-20"), + RemoteAddress: "5.6.7.8", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + }, + Enabled: new(true), + }, + isValid: true, + }, + { + description: "nil_model", + input: nil, + expected: nil, + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + payload, 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(tt.expected, payload) + if diff != "" { + t.Fatalf("Data does not match (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *vpn.UpdateGatewayConnectionPayload + isValid bool + }{ + { + description: "basic_update", + input: &Model{ + DisplayName: types.StringValue("updated-connection"), + Enabled: types.BoolValue(false), + RemoteSubnet: types.ListNull(types.StringType), + LocalSubnet: types.ListNull(types.StringType), + StaticRoutes: types.ListNull(types.StringType), + Tunnel1: &TunnelModel{ + RemoteAddress: types.StringValue("203.0.113.1"), + PreSharedKeyWo: types.StringValue("secret123-at-least-20-chars"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + Tunnel2: &TunnelModel{ + RemoteAddress: types.StringValue("203.0.113.2"), + PreSharedKeyWo: types.StringValue("secret456-at-least-20-chars"), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + }, + expected: &vpn.UpdateGatewayConnectionPayload{ + DisplayName: "updated-connection", + Tunnel1: vpn.TunnelConfiguration{ + PreSharedKey: new("secret123-at-least-20-chars"), + RemoteAddress: "203.0.113.1", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + PreSharedKey: new("secret456-at-least-20-chars"), + RemoteAddress: "203.0.113.2", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + }, + Enabled: new(false), + }, + isValid: true, + }, + { + description: "update_without_psk", + input: &Model{ + DisplayName: types.StringValue("updated-connection"), + Enabled: types.BoolValue(false), + RemoteSubnet: types.ListNull(types.StringType), + LocalSubnet: types.ListNull(types.StringType), + StaticRoutes: types.ListNull(types.StringType), + Tunnel1: &TunnelModel{ + RemoteAddress: types.StringValue("203.0.113.1"), + PreSharedKeyWo: types.StringNull(), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + Tunnel2: &TunnelModel{ + RemoteAddress: types.StringValue("203.0.113.2"), + PreSharedKeyWo: types.StringNull(), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + }, + expected: &vpn.UpdateGatewayConnectionPayload{ + DisplayName: "updated-connection", + Tunnel1: vpn.TunnelConfiguration{ + PreSharedKey: nil, + RemoteAddress: "203.0.113.1", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + PreSharedKey: nil, + RemoteAddress: "203.0.113.2", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: nil, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + }, + }, + Enabled: new(false), + }, + isValid: true, + }, + { + description: "nil_model", + input: nil, + expected: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + payload, err := toUpdatePayload(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(tt.expected, payload) + if diff != "" { + t.Fatalf("Data does not match (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestToTunnelConfiguration(t *testing.T) { + tests := []struct { + description string + input *TunnelModel + isValid bool + }{ + { + description: "valid_tunnel", + input: &TunnelModel{ + RemoteAddress: types.StringValue("203.0.113.1"), + PreSharedKeyWo: types.StringValue("secret123-at-least-20-chars"), + PreSharedKeyWoVersion: types.Int64Null(), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + isValid: true, + }, + { + description: "tunnel_with_bgp", + input: &TunnelModel{ + RemoteAddress: types.StringValue("203.0.113.1"), + PreSharedKeyWo: types.StringValue("secret123-at-least-20-chars"), + PreSharedKeyWoVersion: types.Int64Null(), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + Bgp: &BGPTunnelConfigModel{ + RemoteAsn: types.Int64Value(65000), + }, + }, + isValid: true, + }, + { + description: "tunnel_without_psk", + input: &TunnelModel{ + RemoteAddress: types.StringValue("203.0.113.1"), + PreSharedKeyWo: types.StringNull(), + PreSharedKeyWoVersion: types.Int64Null(), + Phase1: &Phase1Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + }, + Phase2: &Phase2Model{ + DhGroups: types.ListNull(types.StringType), + EncryptionAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("aes256")}), + IntegrityAlgorithms: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("sha2_384")}), + RekeyTime: types.Int32Null(), + StartAction: types.StringNull(), + DpdAction: types.StringNull(), + }, + }, + isValid: true, + }, + { + description: "nil_tunnel", + input: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + config, err := toTunnelConfiguration(tt.input) + + if !tt.isValid && err == nil { + t.Fatalf("expected error, got none") + } + if tt.isValid && err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !tt.isValid { + return + } + + if config.RemoteAddress != tt.input.RemoteAddress.ValueString() { + t.Errorf("RemoteAddress mismatch: got %v, want %v", config.RemoteAddress, tt.input.RemoteAddress.ValueString()) + } + if !tt.input.PreSharedKeyWo.IsNull() && !tt.input.PreSharedKeyWo.IsUnknown() { + if config.PreSharedKey == nil || *config.PreSharedKey != tt.input.PreSharedKeyWo.ValueString() { + t.Errorf("PreSharedKey mismatch") + } + } else if config.PreSharedKey != nil { + t.Errorf("PreSharedKey should be omitted") + } + + if tt.input.Bgp != nil { + if config.Bgp == nil { + t.Errorf("expected BGP config, got nil") + } else if config.Bgp.RemoteAsn != tt.input.Bgp.RemoteAsn.ValueInt64() { + t.Errorf("RemoteAsn mismatch: got %v, want %v", config.Bgp.RemoteAsn, tt.input.Bgp.RemoteAsn.ValueInt64()) + } + } + }) + } +} diff --git a/stackit/internal/services/vpn/testdata/connection-max.tf b/stackit/internal/services/vpn/testdata/connection-max.tf new file mode 100644 index 000000000..599f0bce0 --- /dev/null +++ b/stackit/internal/services/vpn/testdata/connection-max.tf @@ -0,0 +1,85 @@ +variable "connection_display_name" {} +variable "tunnel1_remote_address" {} +variable "tunnel1_psk" {} +variable "tunnel1_psk_version" {} +variable "tunnel1_bgp_remote_asn" {} +variable "tunnel2_remote_address" {} +variable "tunnel2_psk" {} +variable "tunnel2_psk_version" {} +variable "tunnel2_bgp_remote_asn" {} +variable "remote_subnet" {} +variable "local_subnet" {} +variable "tunnel1_local_peering" {} +variable "tunnel1_remote_peering" {} +variable "tunnel2_local_peering" {} +variable "tunnel2_remote_peering" {} + +resource "stackit_vpn_connection" "connection" { + project_id = stackit_vpn_gateway.gateway.project_id + region = stackit_vpn_gateway.gateway.region + gateway_id = stackit_vpn_gateway.gateway.gateway_id + display_name = var.connection_display_name + + remote_subnet = [var.remote_subnet] + local_subnet = [var.local_subnet] + + tunnel1 = { + remote_address = var.tunnel1_remote_address + pre_shared_key_wo = var.tunnel1_psk + pre_shared_key_wo_version = var.tunnel1_psk_version + + phase1 = { + dh_groups = ["modp2048", "ecp256"] + encryption_algorithms = ["aes256", "aes128gcm16"] + integrity_algorithms = ["sha2_256", "sha2_384"] + rekey_time = 25920 + } + + phase2 = { + dh_groups = ["modp2048", "ecp256"] + encryption_algorithms = ["aes256", "aes128gcm16"] + integrity_algorithms = ["sha2_256", "sha2_384"] + rekey_time = 3240 + start_action = "start" + } + + peering = { + local_address = var.tunnel1_local_peering + remote_address = var.tunnel1_remote_peering + } + + bgp = { + remote_asn = var.tunnel1_bgp_remote_asn + } + } + + tunnel2 = { + remote_address = var.tunnel2_remote_address + pre_shared_key_wo = var.tunnel2_psk + pre_shared_key_wo_version = var.tunnel2_psk_version + + phase1 = { + dh_groups = ["modp2048", "ecp256"] + encryption_algorithms = ["aes256", "aes128gcm16"] + integrity_algorithms = ["sha2_256", "sha2_384"] + rekey_time = 25920 + } + + phase2 = { + dh_groups = ["modp2048", "ecp256"] + encryption_algorithms = ["aes256", "aes128gcm16"] + integrity_algorithms = ["sha2_256", "sha2_384"] + rekey_time = 3240 + start_action = "start" + } + + peering = { + local_address = var.tunnel2_local_peering + remote_address = var.tunnel2_remote_peering + } + + bgp = { + remote_asn = var.tunnel2_bgp_remote_asn + } + } +} diff --git a/stackit/internal/services/vpn/testdata/connection-min.tf b/stackit/internal/services/vpn/testdata/connection-min.tf new file mode 100644 index 000000000..23953c2d8 --- /dev/null +++ b/stackit/internal/services/vpn/testdata/connection-min.tf @@ -0,0 +1,46 @@ +variable "connection_display_name" {} +variable "tunnel1_remote_address" {} +variable "tunnel1_psk" {} +variable "tunnel2_remote_address" {} +variable "tunnel2_psk" {} + +resource "stackit_vpn_connection" "connection" { + project_id = stackit_vpn_gateway.gateway.project_id + region = stackit_vpn_gateway.gateway.region + gateway_id = stackit_vpn_gateway.gateway.gateway_id + display_name = var.connection_display_name + + tunnel1 = { + remote_address = var.tunnel1_remote_address + pre_shared_key_wo = var.tunnel1_psk + + phase1 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + + phase2 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + } + + tunnel2 = { + remote_address = var.tunnel2_remote_address + pre_shared_key_wo = var.tunnel2_psk + + phase1 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + + phase2 = { + dh_groups = ["ecp384"] + encryption_algorithms = ["aes256"] + integrity_algorithms = ["sha2_384"] + } + } +} diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go index 8a54ad3b5..6d8401260 100644 --- a/stackit/internal/services/vpn/vpn_acc_test.go +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -3,8 +3,11 @@ package vpn_test import ( "context" _ "embed" + "errors" "fmt" "maps" + "net/http" + "slices" "strings" "testing" @@ -12,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -24,6 +28,12 @@ var gatewayMinConfig string //go:embed testdata/gateway-max.tf var gatewayMaxConfig string +//go:embed testdata/connection-min.tf +var connectionMinConfig string + +//go:embed testdata/connection-max.tf +var connectionMaxConfig string + var gatewayMinVars = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "display_name": config.StringVariable("vpn-gw-acc-test-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), @@ -79,10 +89,72 @@ var gatewayMaxVarsUpdated2 = func() config.Variables { return updated }() +var connectionMinVars = func() config.Variables { + vars := make(config.Variables, len(gatewayMinVars)+5) + maps.Copy(vars, gatewayMinVars) + // vars["plan_id"] = config.StringVariable("p500") + vars["connection_display_name"] = config.StringVariable("vpn-conn-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)) + vars["tunnel1_remote_address"] = config.StringVariable("203.0.113.1") + vars["tunnel1_psk"] = config.StringVariable("Super.Secret_$hared3Key_1") + vars["tunnel2_remote_address"] = config.StringVariable("203.0.113.2") + vars["tunnel2_psk"] = config.StringVariable("Super.Secret_$hared3Key_2") + return vars +}() + +var connectionMinVarsUpdated = func() config.Variables { + updated := make(config.Variables, len(connectionMinVars)) + maps.Copy(updated, connectionMinVars) + updated["connection_display_name"] = config.StringVariable("vpn-conn-updated-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)) + return updated +}() + +var connectionMaxVars = func() config.Variables { + vars := make(config.Variables) + maps.Copy(vars, gatewayMaxVars) // BGP_ROUTE_BASED gateway with local_asn, labels, etc. + vars["connection_display_name"] = config.StringVariable("vpn-conn-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)) + vars["tunnel1_remote_address"] = config.StringVariable("203.0.113.1") + vars["tunnel1_psk"] = config.StringVariable("Super.Secret_$hared3Key_1") + vars["tunnel1_psk_version"] = config.IntegerVariable(1) + vars["tunnel1_bgp_remote_asn"] = config.IntegerVariable(65001) + vars["tunnel2_remote_address"] = config.StringVariable("203.0.113.2") + vars["tunnel2_psk"] = config.StringVariable("Super.Secret_$hared3Key_2") + vars["tunnel2_psk_version"] = config.IntegerVariable(1) + vars["tunnel2_bgp_remote_asn"] = config.IntegerVariable(65002) + vars["remote_subnet"] = config.StringVariable("10.10.10.0/24") + vars["local_subnet"] = config.StringVariable("192.168.0.0/24") + vars["tunnel1_local_peering"] = config.StringVariable("192.168.0.1") + vars["tunnel1_remote_peering"] = config.StringVariable("10.10.10.1") + vars["tunnel2_local_peering"] = config.StringVariable("192.168.0.2") + vars["tunnel2_remote_peering"] = config.StringVariable("10.10.10.2") + return vars +}() + +// connectionMaxVarsUpdated changes non-PSK mutable fields to exercise updates. +var connectionMaxVarsUpdated = func() config.Variables { + updated := make(config.Variables) + maps.Copy(updated, connectionMaxVars) + updated["connection_display_name"] = config.StringVariable("vpn-conn-updated-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)) + updated["tunnel1_bgp_remote_asn"] = config.IntegerVariable(65003) + updated["tunnel2_bgp_remote_asn"] = config.IntegerVariable(65004) + return updated +}() + +// connectionMaxVarsPskRotated exercises the write-only PSK rotation workflow: +// both tunnel PSKs are replaced and their versions incremented from 1 → 2. +var connectionMaxVarsPskRotated = func() config.Variables { + rotated := make(config.Variables) + maps.Copy(rotated, connectionMaxVarsUpdated) + rotated["tunnel1_psk"] = config.StringVariable("Super.Secret_Rotated_$hared3Key_1!") + rotated["tunnel1_psk_version"] = config.IntegerVariable(2) + rotated["tunnel2_psk"] = config.StringVariable("Super.Secret_Rotated_$hared3Key_2!") + rotated["tunnel2_psk_version"] = config.IntegerVariable(2) + return rotated +}() + func TestAccVpnGatewayResourceMin(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckVpnGatewayDestroy, + CheckDestroy: testAccCheckVpnResourcesDestroy, Steps: []resource.TestStep{ // Creation { @@ -174,7 +246,7 @@ func TestAccVpnGatewayResourceMin(t *testing.T) { func TestAccVpnGatewayResourceMax(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckVpnGatewayDestroy, + CheckDestroy: testAccCheckVpnResourcesDestroy, Steps: []resource.TestStep{ // Creation { @@ -294,40 +366,545 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { }) } -func testAccCheckVpnGatewayDestroy(s *terraform.State) error { +func TestAccVpnConnectionResourceMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckVpnResourcesDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: connectionMinVars, + Config: fmt.Sprintf("%s\n%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMinConfig, connectionMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Gateway + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "region", testutil.Region), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(connectionMinVars["display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(connectionMinVars["plan_id"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "routing_type", testutil.ConvertConfigVariable(connectionMinVars["routing_type"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "availability_zones.tunnel1", testutil.ConvertConfigVariable(connectionMinVars["az_tunnel1"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "availability_zones.tunnel2", testutil.ConvertConfigVariable(connectionMinVars["az_tunnel2"])), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "state"), + // Connection – identity & top-level + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "region", testutil.Region), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "display_name", testutil.ConvertConfigVariable(connectionMinVars["connection_display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "enabled", "true"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "connection_id"), + resource.TestCheckResourceAttrPair("stackit_vpn_connection.connection", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), + // Connection – tunnel1 + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.remote_address", testutil.ConvertConfigVariable(connectionMinVars["tunnel1_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.0", "sha2_384"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase1.rekey_time"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.dh_groups.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.encryption_algorithms.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.integrity_algorithms.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.integrity_algorithms.0", "sha2_384"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase2.rekey_time"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase2.start_action"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase2.dpd_action"), + // Connection – tunnel2 + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.remote_address", testutil.ConvertConfigVariable(connectionMinVars["tunnel2_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.dh_groups.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.encryption_algorithms.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.integrity_algorithms.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.integrity_algorithms.0", "sha2_384"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase1.rekey_time"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.dh_groups.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.encryption_algorithms.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.integrity_algorithms.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.integrity_algorithms.0", "sha2_384"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase2.rekey_time"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase2.start_action"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase2.dpd_action"), + ), + }, + // Data source + { + ConfigVariables: connectionMinVars, + Config: fmt.Sprintf(` + %s + %s + %s + + data "stackit_vpn_connection" "connection" { + project_id = stackit_vpn_connection.connection.project_id + gateway_id = stackit_vpn_connection.connection.gateway_id + connection_id = stackit_vpn_connection.connection.connection_id + } + `, + testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMinConfig, connectionMinConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "region", testutil.Region), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "display_name", testutil.ConvertConfigVariable(connectionMinVars["connection_display_name"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "enabled", "true"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.remote_address", testutil.ConvertConfigVariable(connectionMinVars["tunnel1_remote_address"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.#", "1"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.0", "sha2_384"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.remote_address", testutil.ConvertConfigVariable(connectionMinVars["tunnel2_remote_address"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.phase1.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.phase1.integrity_algorithms.0", "sha2_384"), + + resource.TestCheckResourceAttrSet("data.stackit_vpn_connection.connection", "connection_id"), + resource.TestCheckResourceAttrSet("data.stackit_vpn_connection.connection", "gateway_id"), + + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "project_id", "stackit_vpn_connection.connection", "project_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "region", "stackit_vpn_connection.connection", "region"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "gateway_id", "stackit_vpn_connection.connection", "gateway_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "connection_id", "stackit_vpn_connection.connection", "connection_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "display_name", "stackit_vpn_connection.connection", "display_name"), + ), + }, + // Update + { + ConfigVariables: connectionMinVarsUpdated, + Config: fmt.Sprintf("%s\n%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMinConfig, connectionMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Gateway unchanged + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "region", testutil.Region), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(connectionMinVarsUpdated["display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(connectionMinVarsUpdated["plan_id"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "routing_type", testutil.ConvertConfigVariable(connectionMinVarsUpdated["routing_type"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "availability_zones.tunnel1", testutil.ConvertConfigVariable(connectionMinVarsUpdated["az_tunnel1"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "availability_zones.tunnel2", testutil.ConvertConfigVariable(connectionMinVarsUpdated["az_tunnel2"])), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "state"), + // Connection – all fields + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "region", testutil.Region), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "display_name", testutil.ConvertConfigVariable(connectionMinVarsUpdated["connection_display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "enabled", "true"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "connection_id"), + resource.TestCheckResourceAttrPair("stackit_vpn_connection.connection", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.remote_address", testutil.ConvertConfigVariable(connectionMinVarsUpdated["tunnel1_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.0", "sha2_384"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase1.rekey_time"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.integrity_algorithms.0", "sha2_384"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase2.rekey_time"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase2.start_action"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase2.dpd_action"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.remote_address", testutil.ConvertConfigVariable(connectionMinVarsUpdated["tunnel2_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.integrity_algorithms.0", "sha2_384"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase1.rekey_time"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.dh_groups.0", "ecp384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.integrity_algorithms.0", "sha2_384"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase2.rekey_time"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase2.start_action"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase2.dpd_action"), + ), + }, + // Import + { + ConfigVariables: connectionMinVars, + ResourceName: "stackit_vpn_connection.connection", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_vpn_connection.connection"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_vpn_connection.connection") + } + connectionId, ok := r.Primary.Attributes["connection_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute connection_id") + } + gatewayId, ok := r.Primary.Attributes["gateway_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute gateway_id") + } + return fmt.Sprintf("%s,%s,%s,%s", + testutil.ProjectId, + testutil.Region, + gatewayId, + connectionId, + ), nil + }, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"tunnel1.pre_shared_key_wo", "tunnel2.pre_shared_key_wo"}, + }, + }, + }) +} + +func TestAccVpnConnectionResourceMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckVpnResourcesDestroy, + Steps: []resource.TestStep{ + // Creation – BGP_ROUTE_BASED gateway + full connection config including BGP tunnel peers + { + ConfigVariables: connectionMaxVars, + Config: fmt.Sprintf("%s\n%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig, connectionMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Gateway + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "region", testutil.ConvertConfigVariable(connectionMaxVars["region"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(connectionMaxVars["display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(connectionMaxVars["plan_id"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "routing_type", testutil.ConvertConfigVariable(connectionMaxVars["routing_type"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "availability_zones.tunnel1", testutil.ConvertConfigVariable(connectionMaxVars["az_tunnel1"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "availability_zones.tunnel2", testutil.ConvertConfigVariable(connectionMaxVars["az_tunnel2"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(connectionMaxVars["local_asn"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.override_advertised_routes.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.override_advertised_routes.0", testutil.ConvertConfigVariable(connectionMaxVars["advertised_route_1"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.override_advertised_routes.1", testutil.ConvertConfigVariable(connectionMaxVars["advertised_route_2"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(connectionMaxVars["label_key"]), testutil.ConvertConfigVariable(connectionMaxVars["label_value"])), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "state"), + // Connection – identity & top-level + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "region", testutil.ConvertConfigVariable(connectionMaxVars["region"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "display_name", testutil.ConvertConfigVariable(connectionMaxVars["connection_display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "enabled", "true"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "remote_subnet.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "remote_subnet.0", testutil.ConvertConfigVariable(connectionMaxVars["remote_subnet"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "local_subnet.#", "1"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "local_subnet.0", testutil.ConvertConfigVariable(connectionMaxVars["local_subnet"])), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "connection_id"), + resource.TestCheckResourceAttrPair("stackit_vpn_connection.connection", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), + // Connection – tunnel1 + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.remote_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel1_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.pre_shared_key_wo_version", testutil.ConvertConfigVariable(connectionMaxVars["tunnel1_psk_version"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.0", "modp2048"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.1", "ecp256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.1", "aes128gcm16"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.0", "sha2_256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.1", "sha2_384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.rekey_time", "25920"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.dh_groups.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.dh_groups.0", "modp2048"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.dh_groups.1", "ecp256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.encryption_algorithms.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.encryption_algorithms.1", "aes128gcm16"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.integrity_algorithms.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.integrity_algorithms.0", "sha2_256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.integrity_algorithms.1", "sha2_384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.rekey_time", "3240"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.start_action", "start"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase2.dpd_action"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.peering.local_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel1_local_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.peering.remote_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel1_remote_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.bgp.remote_asn", testutil.ConvertConfigVariable(connectionMaxVars["tunnel1_bgp_remote_asn"])), + // Connection – tunnel2 + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.remote_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel2_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.pre_shared_key_wo_version", testutil.ConvertConfigVariable(connectionMaxVars["tunnel2_psk_version"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.dh_groups.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.dh_groups.0", "modp2048"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.dh_groups.1", "ecp256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.encryption_algorithms.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.encryption_algorithms.1", "aes128gcm16"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.integrity_algorithms.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.integrity_algorithms.0", "sha2_256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.integrity_algorithms.1", "sha2_384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.rekey_time", "25920"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.dh_groups.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.dh_groups.0", "modp2048"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.dh_groups.1", "ecp256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.encryption_algorithms.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.encryption_algorithms.1", "aes128gcm16"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.integrity_algorithms.#", "2"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.integrity_algorithms.0", "sha2_256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.integrity_algorithms.1", "sha2_384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.rekey_time", "3240"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.start_action", "start"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase2.dpd_action"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.peering.local_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel2_local_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.peering.remote_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel2_remote_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.bgp.remote_asn", testutil.ConvertConfigVariable(connectionMaxVars["tunnel2_bgp_remote_asn"])), + ), + }, + // Data source + { + ConfigVariables: connectionMaxVars, + Config: fmt.Sprintf(` + %s + %s + %s + + data "stackit_vpn_connection" "connection" { + project_id = stackit_vpn_connection.connection.project_id + gateway_id = stackit_vpn_connection.connection.gateway_id + connection_id = stackit_vpn_connection.connection.connection_id + } + `, + testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig, connectionMaxConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "region", testutil.ConvertConfigVariable(connectionMaxVars["region"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "display_name", testutil.ConvertConfigVariable(connectionMaxVars["connection_display_name"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "enabled", "true"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "remote_subnet.#", "1"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "remote_subnet.0", testutil.ConvertConfigVariable(connectionMaxVars["remote_subnet"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "local_subnet.#", "1"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "local_subnet.0", testutil.ConvertConfigVariable(connectionMaxVars["local_subnet"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.remote_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel1_remote_address"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.0", "modp2048"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.1", "ecp256"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.1", "aes128gcm16"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.0", "sha2_256"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.1", "sha2_384"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase1.rekey_time", "25920"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase2.rekey_time", "3240"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.phase2.start_action", "start"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.peering.local_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel1_local_peering"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.peering.remote_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel1_remote_peering"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel1.bgp.remote_asn", testutil.ConvertConfigVariable(connectionMaxVars["tunnel1_bgp_remote_asn"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.remote_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel2_remote_address"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.phase1.rekey_time", "25920"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.phase2.rekey_time", "3240"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.phase2.start_action", "start"), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.peering.local_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel2_local_peering"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.peering.remote_address", testutil.ConvertConfigVariable(connectionMaxVars["tunnel2_remote_peering"])), + resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "tunnel2.bgp.remote_asn", testutil.ConvertConfigVariable(connectionMaxVars["tunnel2_bgp_remote_asn"])), + + resource.TestCheckResourceAttrSet("data.stackit_vpn_connection.connection", "connection_id"), + resource.TestCheckResourceAttrSet("data.stackit_vpn_connection.connection", "gateway_id"), + + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "project_id", "stackit_vpn_connection.connection", "project_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "region", "stackit_vpn_connection.connection", "region"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "gateway_id", "stackit_vpn_connection.connection", "gateway_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "connection_id", "stackit_vpn_connection.connection", "connection_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "display_name", "stackit_vpn_connection.connection", "display_name"), + ), + }, + // Update – change display name and BGP remote ASNs; verify no other drift + { + ConfigVariables: connectionMaxVarsUpdated, + Config: fmt.Sprintf("%s\n%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig, connectionMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Gateway unchanged + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "region", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["region"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["plan_id"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "routing_type", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["routing_type"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["local_asn"])), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "state"), + // Connection + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "region", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["region"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "display_name", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["connection_display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "enabled", "true"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "remote_subnet.0", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["remote_subnet"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "local_subnet.0", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["local_subnet"])), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "connection_id"), + resource.TestCheckResourceAttrPair("stackit_vpn_connection.connection", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), + // tunnel1 + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.remote_address", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel1_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.pre_shared_key_wo_version", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel1_psk_version"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.0", "modp2048"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.dh_groups.1", "ecp256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.encryption_algorithms.1", "aes128gcm16"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.0", "sha2_256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.integrity_algorithms.1", "sha2_384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.rekey_time", "25920"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.rekey_time", "3240"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.start_action", "start"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase2.dpd_action"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.peering.local_address", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel1_local_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.peering.remote_address", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel1_remote_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.bgp.remote_asn", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel1_bgp_remote_asn"])), + // tunnel2 + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.remote_address", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel2_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.pre_shared_key_wo_version", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel2_psk_version"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.dh_groups.0", "modp2048"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.dh_groups.1", "ecp256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.encryption_algorithms.0", "aes256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.encryption_algorithms.1", "aes128gcm16"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.integrity_algorithms.0", "sha2_256"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.integrity_algorithms.1", "sha2_384"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.rekey_time", "25920"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.rekey_time", "3240"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.start_action", "start"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase2.dpd_action"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.peering.local_address", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel2_local_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.peering.remote_address", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel2_remote_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.bgp.remote_asn", testutil.ConvertConfigVariable(connectionMaxVarsUpdated["tunnel2_bgp_remote_asn"])), + ), + }, + // PSK rotation – increment pre_shared_key_wo_version 1 → 2 on both tunnels. + // The write-only pre_shared_key_wo values are replaced; the provider reads the + // version from state to detect the rotation and re-sends the new key to the API. + // Verifying the new version value in state (and no unintended plan diff) is the + // observable signal that the rotation was applied correctly. + { + ConfigVariables: connectionMaxVarsPskRotated, + Config: fmt.Sprintf("%s\n%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig, connectionMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Rotated version counters must be persisted in state + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.pre_shared_key_wo_version", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel1_psk_version"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.pre_shared_key_wo_version", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel2_psk_version"])), + // All other fields must be unchanged – catches unintended drift + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "region", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["region"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "display_name", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["connection_display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "enabled", "true"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "remote_subnet.0", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["remote_subnet"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "local_subnet.0", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["local_subnet"])), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "connection_id"), + resource.TestCheckResourceAttrPair("stackit_vpn_connection.connection", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.remote_address", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel1_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase1.rekey_time", "25920"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.rekey_time", "3240"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.phase2.start_action", "start"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel1.phase2.dpd_action"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.peering.local_address", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel1_local_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.peering.remote_address", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel1_remote_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel1.bgp.remote_asn", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel1_bgp_remote_asn"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.remote_address", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel2_remote_address"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase1.rekey_time", "25920"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.rekey_time", "3240"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.phase2.start_action", "start"), + resource.TestCheckResourceAttrSet("stackit_vpn_connection.connection", "tunnel2.phase2.dpd_action"), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.peering.local_address", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel2_local_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.peering.remote_address", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel2_remote_peering"])), + resource.TestCheckResourceAttr("stackit_vpn_connection.connection", "tunnel2.bgp.remote_asn", testutil.ConvertConfigVariable(connectionMaxVarsPskRotated["tunnel2_bgp_remote_asn"])), + ), + }, + // Import + { + ConfigVariables: connectionMaxVars, + ResourceName: "stackit_vpn_connection.connection", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_vpn_connection.connection"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_vpn_connection.connection") + } + connectionId, ok := r.Primary.Attributes["connection_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute connection_id") + } + gatewayId, ok := r.Primary.Attributes["gateway_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute gateway_id") + } + return fmt.Sprintf("%s,%s,%s,%s", + testutil.ProjectId, + testutil.Region, + gatewayId, + connectionId, + ), nil + }, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"tunnel1.pre_shared_key_wo", "tunnel2.pre_shared_key_wo", "tunnel1.pre_shared_key_wo_version", "tunnel2.pre_shared_key_wo_version"}, + }, + }, + }) +} + +func testAccCheckVpnResourcesDestroy(s *terraform.State) error { ctx := context.Background() client, err := vpn.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.VpnCustomEndpoint, false)...) if err != nil { return fmt.Errorf("creating client: %w", err) } - gatewaysToDestroy := []string{} + gatewayIdsToDestroy := []string{} for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_vpn_gateway" { + var gatewayId string + switch rs.Type { + case "stackit_vpn_gateway": + // gateway terraform ID: "[project_id],[region],[gateway_id]" + parts := strings.Split(rs.Primary.ID, core.Separator) + if len(parts) > 2 { + gatewayId = parts[2] + } else if attrId, ok := rs.Primary.Attributes["gateway_id"]; ok && attrId != "" { + gatewayId = attrId + } + case "stackit_vpn_connection": + // connection terraform ID: "[project_id],[region],[gateway_id],[connection_id]" + parts := strings.Split(rs.Primary.ID, core.Separator) + if len(parts) > 2 { + gatewayId = parts[2] + } else if attrId, ok := rs.Primary.Attributes["gateway_id"]; ok && attrId != "" { + gatewayId = attrId + } + default: + continue + } + if gatewayId == "" { continue } - // gateway terraform ID: "[project_id],[region],[gateway_id]" - gatewayId := strings.Split(rs.Primary.ID, core.Separator)[2] - gatewaysToDestroy = append(gatewaysToDestroy, gatewayId) + if !slices.Contains(gatewayIdsToDestroy, gatewayId) { + gatewayIdsToDestroy = append(gatewayIdsToDestroy, gatewayId) + } + } + + if len(gatewayIdsToDestroy) == 0 { + return nil } gatewaysResp, err := client.DefaultAPI.ListGateways(ctx, testutil.ProjectId, testutil.Region).Execute() if err != nil { - return fmt.Errorf("getting gateways: %w", err) + return fmt.Errorf("listing gateways during CheckDestroy: %w", err) } - gateways := gatewaysResp.Gateways - for _, gateway := range gateways { - if gateway.Id == nil { + for _, gateway := range gatewaysResp.Gateways { + if gateway.Id == nil || !slices.Contains(gatewayIdsToDestroy, *gateway.Id) { continue } - for _, gatewayId := range gatewaysToDestroy { - if *gateway.Id == gatewayId { - err := client.DefaultAPI.DeleteGateway(ctx, testutil.ProjectId, testutil.Region, *gateway.Id).Execute() - if err != nil { - return fmt.Errorf("destroying gateway %s during CheckDestroy: %w", gatewayId, err) + + connectionsResp, err := client.DefaultAPI.ListGatewayConnections(ctx, testutil.ProjectId, vpn.Region(testutil.Region), *gateway.Id).Execute() + if err != nil { + return fmt.Errorf("listing connections for gateway %s during CheckDestroy: %w", *gateway.Id, err) + } + for _, conn := range connectionsResp.Connections { + if conn.Id == nil { + continue + } + err := client.DefaultAPI.DeleteGatewayConnection(ctx, testutil.ProjectId, vpn.Region(testutil.Region), *gateway.Id, *conn.Id).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { + continue } + return fmt.Errorf("destroying connection %s during CheckDestroy: %w", *conn.Id, err) + } + } + + err = client.DefaultAPI.DeleteGateway(ctx, testutil.ProjectId, vpn.Region(testutil.Region), *gateway.Id).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { + continue } + return fmt.Errorf("destroying gateway %s during CheckDestroy: %w", *gateway.Id, err) } } return nil diff --git a/stackit/provider.go b/stackit/provider.go index f62e157e2..dbe8be8db 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -122,10 +122,14 @@ import ( skeMachineImages "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/provideroptions/machineimages" sqlServerFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/instance" sqlServerFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/user" +<<<<<<< HEAD telemetryLink "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetrylink/link" telemetryRouterAccessToken "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/accesstoken" telemetryRouterDestination "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/destination" telemetryRouterInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/instance" +======= + vpnConnection "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/connection" +>>>>>>> 2fb59bdf (feat(vpn): Onboarding VPN Connection) vpnGateway "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/gateway" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -746,6 +750,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource telemetryRouterDestination.NewTelemetryRouterDestinationDataSource, telemetryLink.NewTelemetryLinkDataSource, vpnGateway.NewVPNGatewayDataSource, + vpnConnection.NewVPNConnectionDataSource, } dataSources = append(dataSources, customRole.NewCustomRoleDataSources()...) dataSources = append(dataSources, iamRoleBindingsV1.NewRoleBindingsDatasources()...) @@ -839,10 +844,14 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { compliancelock.NewComplianceLockResource, serverBackupEnable.NewServerBackupEnableResource, serverUpdateEnable.NewServerUpdateEnableResource, +<<<<<<< HEAD telemetryRouterAccessToken.NewTelemetryRouterAccessTokenResource, telemetryRouterInstance.NewTelemetryRouterInstanceResource, telemetryRouterDestination.NewTelemetryRouterDestinationResource, telemetryLink.NewTelemetryLinkResource, +======= + vpnConnection.NewVpnConnectionResource, +>>>>>>> 2fb59bdf (feat(vpn): Onboarding VPN Connection) vpnGateway.NewGatewayResource, } resources = append(resources, roleAssignements.NewRoleAssignmentResources()...) From ae56f797d37d7edc061d97d46a5eeabba32c26a1 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Thu, 11 Jun 2026 10:12:10 +0200 Subject: [PATCH 2/2] upgrade vpn sdk version --- .../services/vpn/connection/datasource.go | 2 +- .../services/vpn/connection/resource.go | 56 ++++-- .../services/vpn/connection/resource_test.go | 176 +++++++++--------- stackit/internal/services/vpn/vpn_acc_test.go | 6 +- stackit/provider.go | 6 - 5 files changed, 132 insertions(+), 114 deletions(-) diff --git a/stackit/internal/services/vpn/connection/datasource.go b/stackit/internal/services/vpn/connection/datasource.go index e1e4c2c7f..a7651c1dc 100644 --- a/stackit/internal/services/vpn/connection/datasource.go +++ b/stackit/internal/services/vpn/connection/datasource.go @@ -279,7 +279,7 @@ func (d *vpnConnectionDataSource) Read(ctx context.Context, req datasource.ReadR ctx = tflog.SetField(ctx, "gateway_id", gatewayId) ctx = tflog.SetField(ctx, "connection_id", connectionId) - connResp, err := d.client.DefaultAPI.GetGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).Execute() + connResp, err := d.client.DefaultAPI.GetGatewayConnection(ctx, projectId, region, gatewayId, connectionId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError ok := errors.As(err, &oapiErr) diff --git a/stackit/internal/services/vpn/connection/resource.go b/stackit/internal/services/vpn/connection/resource.go index 26d5cf745..36d80ef69 100644 --- a/stackit/internal/services/vpn/connection/resource.go +++ b/stackit/internal/services/vpn/connection/resource.go @@ -506,7 +506,7 @@ func (r *vpnConnectionResource) Create(ctx context.Context, req resource.CreateR return } - createResp, err := r.client.DefaultAPI.CreateGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId).CreateGatewayConnectionPayload(*payload).Execute() + createResp, err := r.client.DefaultAPI.CreateGatewayConnection(ctx, projectId, region, gatewayId).CreateGatewayConnectionPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN connection", fmt.Sprintf("Calling API: %v", err)) return @@ -530,7 +530,7 @@ func (r *vpnConnectionResource) Create(ctx context.Context, req resource.CreateR return } - connResp, err := r.client.DefaultAPI.GetGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).Execute() + connResp, err := r.client.DefaultAPI.GetGatewayConnection(ctx, projectId, region, gatewayId, connectionId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN connection", fmt.Sprintf("Reading created connection: %v", err)) return @@ -569,7 +569,7 @@ func (r *vpnConnectionResource) Read(ctx context.Context, req resource.ReadReque ctx = tflog.SetField(ctx, "gateway_id", gatewayId) ctx = tflog.SetField(ctx, "connection_id", connectionId) - connResp, err := r.client.DefaultAPI.GetGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).Execute() + connResp, err := r.client.DefaultAPI.GetGatewayConnection(ctx, projectId, region, gatewayId, connectionId).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 { @@ -676,7 +676,7 @@ func (r *vpnConnectionResource) Update(ctx context.Context, req resource.UpdateR return } - _, err = r.client.DefaultAPI.UpdateGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).UpdateGatewayConnectionPayload(*payload).Execute() + _, err = r.client.DefaultAPI.UpdateGatewayConnection(ctx, projectId, region, gatewayId, connectionId).UpdateGatewayConnectionPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN connection", err.Error()) return @@ -684,7 +684,7 @@ func (r *vpnConnectionResource) Update(ctx context.Context, req resource.UpdateR ctx = core.LogResponse(ctx) - connResp, err := r.client.DefaultAPI.GetGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).Execute() + connResp, err := r.client.DefaultAPI.GetGatewayConnection(ctx, projectId, region, gatewayId, connectionId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN connection", fmt.Sprintf("Reading updated connection: %v", err)) return @@ -723,7 +723,7 @@ func (r *vpnConnectionResource) Delete(ctx context.Context, req resource.DeleteR ctx = tflog.SetField(ctx, "gateway_id", gatewayId) ctx = tflog.SetField(ctx, "connection_id", connectionId) - err := r.client.DefaultAPI.DeleteGatewayConnection(ctx, projectId, vpn.Region(region), gatewayId, connectionId).Execute() + err := r.client.DefaultAPI.DeleteGatewayConnection(ctx, projectId, region, gatewayId, connectionId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { @@ -883,18 +883,30 @@ func toTunnelConfiguration(tunnel *TunnelModel) (*vpn.TunnelConfiguration, error if err != nil { return nil, fmt.Errorf("converting phase1 dh_groups: %w", err) } - phase1.DhGroups = dhGroups + dhGroupsInner := []vpn.PhaseDhGroupsInner{} + for _, item := range dhGroups { + dhGroupsInner = append(dhGroupsInner, vpn.PhaseDhGroupsInner(item)) + } + phase1.DhGroups = dhGroupsInner } encAlgs, err := tfutils.ListValueToStringSlice(tunnel.Phase1.EncryptionAlgorithms) if err != nil { return nil, fmt.Errorf("converting phase1 encryption_algorithms: %w", err) } - phase1.EncryptionAlgorithms = encAlgs + encAlgsInner := []vpn.PhaseEncryptionAlgorithmsInner{} + for _, item := range encAlgs { + encAlgsInner = append(encAlgsInner, vpn.PhaseEncryptionAlgorithmsInner(item)) + } + phase1.EncryptionAlgorithms = encAlgsInner intAlgs, err := tfutils.ListValueToStringSlice(tunnel.Phase1.IntegrityAlgorithms) if err != nil { return nil, fmt.Errorf("converting phase1 integrity_algorithms: %w", err) } - phase1.IntegrityAlgorithms = intAlgs + intAlgsInner := []vpn.PhaseIntegrityAlgorithmsInner{} + for _, item := range intAlgs { + intAlgsInner = append(intAlgsInner, vpn.PhaseIntegrityAlgorithmsInner(item)) + } + phase1.IntegrityAlgorithms = intAlgsInner if !tunnel.Phase1.RekeyTime.IsNull() && !tunnel.Phase1.RekeyTime.IsUnknown() { rekeyTime := tunnel.Phase1.RekeyTime.ValueInt32() phase1.RekeyTime = &rekeyTime @@ -909,29 +921,41 @@ func toTunnelConfiguration(tunnel *TunnelModel) (*vpn.TunnelConfiguration, error if err != nil { return nil, fmt.Errorf("converting phase2 dh_groups: %w", err) } - phase2.DhGroups = dhGroups + dhGroupsInner := []vpn.PhaseDhGroupsInner{} + for _, item := range dhGroups { + dhGroupsInner = append(dhGroupsInner, vpn.PhaseDhGroupsInner(item)) + } + phase2.DhGroups = dhGroupsInner } encAlgs, err := tfutils.ListValueToStringSlice(tunnel.Phase2.EncryptionAlgorithms) if err != nil { return nil, fmt.Errorf("converting phase2 encryption_algorithms: %w", err) } - phase2.EncryptionAlgorithms = encAlgs + encAlgsInner := []vpn.PhaseEncryptionAlgorithmsInner{} + for _, item := range encAlgs { + encAlgsInner = append(encAlgsInner, vpn.PhaseEncryptionAlgorithmsInner(item)) + } + phase2.EncryptionAlgorithms = encAlgsInner intAlgs, err := tfutils.ListValueToStringSlice(tunnel.Phase2.IntegrityAlgorithms) if err != nil { return nil, fmt.Errorf("converting phase2 integrity_algorithms: %w", err) } - phase2.IntegrityAlgorithms = intAlgs + intAlgsInner := []vpn.PhaseIntegrityAlgorithmsInner{} + for _, item := range intAlgs { + intAlgsInner = append(intAlgsInner, vpn.PhaseIntegrityAlgorithmsInner(item)) + } + phase2.IntegrityAlgorithms = intAlgsInner if !tunnel.Phase2.RekeyTime.IsNull() && !tunnel.Phase2.RekeyTime.IsUnknown() { rekeyTime := tunnel.Phase2.RekeyTime.ValueInt32() phase2.RekeyTime = &rekeyTime } if !tunnel.Phase2.StartAction.IsNull() && !tunnel.Phase2.StartAction.IsUnknown() { startAction := tunnel.Phase2.StartAction.ValueString() - phase2.StartAction = &startAction + phase2.StartAction = vpn.TunnelConfigurationPhase2AllOfStartAction(startAction).Ptr() } if !tunnel.Phase2.DpdAction.IsNull() && !tunnel.Phase2.DpdAction.IsUnknown() { dpdAction := tunnel.Phase2.DpdAction.ValueString() - phase2.DpdAction = &dpdAction + phase2.DpdAction = vpn.TunnelConfigurationPhase2AllOfDpdAction(dpdAction).Ptr() } config.Phase2 = phase2 } @@ -1107,12 +1131,12 @@ func mapTunnel(ctx context.Context, apiTunnel *vpn.TunnelConfiguration, cuurrent phase2.RekeyTime = types.Int32Null() } if apiTunnel.Phase2.StartAction != nil { - phase2.StartAction = types.StringValue(*apiTunnel.Phase2.StartAction) + phase2.StartAction = types.StringValue(string(*apiTunnel.Phase2.StartAction)) } else { phase2.StartAction = types.StringNull() } if apiTunnel.Phase2.DpdAction != nil { - phase2.DpdAction = types.StringValue(*apiTunnel.Phase2.DpdAction) + phase2.DpdAction = types.StringValue(string(*apiTunnel.Phase2.DpdAction)) } else { phase2.DpdAction = types.StringNull() } diff --git a/stackit/internal/services/vpn/connection/resource_test.go b/stackit/internal/services/vpn/connection/resource_test.go index 0477ffced..b158c5a04 100644 --- a/stackit/internal/services/vpn/connection/resource_test.go +++ b/stackit/internal/services/vpn/connection/resource_test.go @@ -36,35 +36,35 @@ func TestMapFields(t *testing.T) { Tunnel1: vpn.TunnelConfiguration{ RemoteAddress: "203.0.113.1", Phase1: vpn.TunnelConfigurationPhase1{ - DhGroups: []string{"modp2048"}, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + DhGroups: []vpn.PhaseDhGroupsInner{"modp2048"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, RekeyTime: new(int32(14400)), }, Phase2: vpn.TunnelConfigurationPhase2{ - DhGroups: []string{"modp2048"}, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + DhGroups: []vpn.PhaseDhGroupsInner{"modp2048"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, RekeyTime: new(int32(3600)), - StartAction: new("start"), - DpdAction: new("restart"), + StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_START.Ptr(), + DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_RESTART.Ptr(), }, }, Tunnel2: vpn.TunnelConfiguration{ RemoteAddress: "203.0.113.2", Phase1: vpn.TunnelConfigurationPhase1{ - DhGroups: []string{"modp2048"}, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + DhGroups: []vpn.PhaseDhGroupsInner{"modp2048"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, RekeyTime: new(int32(14400)), }, Phase2: vpn.TunnelConfigurationPhase2{ - DhGroups: []string{"modp2048"}, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + DhGroups: []vpn.PhaseDhGroupsInner{"modp2048"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, RekeyTime: new(int32(3600)), - StartAction: new("start"), - DpdAction: new("restart"), + StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_START.Ptr(), + DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_RESTART.Ptr(), }, }, }, @@ -157,14 +157,14 @@ func TestMapFields(t *testing.T) { Tunnel1: vpn.TunnelConfiguration{ RemoteAddress: "203.0.113.10", Phase1: vpn.TunnelConfigurationPhase1{ - EncryptionAlgorithms: []string{"aes256gcm16"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256gcm16"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, Phase2: vpn.TunnelConfigurationPhase2{ - EncryptionAlgorithms: []string{"aes256gcm16"}, - IntegrityAlgorithms: []string{"sha2_384"}, - DpdAction: new("clear"), - StartAction: new("none"), + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256gcm16"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, + StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_NONE.Ptr(), + DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_CLEAR.Ptr(), }, Peering: &vpn.PeeringConfig{ LocalAddress: new("169.254.0.1"), @@ -177,14 +177,14 @@ func TestMapFields(t *testing.T) { Tunnel2: vpn.TunnelConfiguration{ RemoteAddress: "203.0.113.11", Phase1: vpn.TunnelConfigurationPhase1{ - EncryptionAlgorithms: []string{"aes256gcm16"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256gcm16"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, Phase2: vpn.TunnelConfigurationPhase2{ - EncryptionAlgorithms: []string{"aes256gcm16"}, - IntegrityAlgorithms: []string{"sha2_384"}, - DpdAction: new("clear"), - StartAction: new("none"), + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256gcm16"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, + StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_NONE.Ptr(), + DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_CLEAR.Ptr(), }, }, }, @@ -257,23 +257,23 @@ func TestMapFields(t *testing.T) { Tunnel1: vpn.TunnelConfiguration{ RemoteAddress: "1.2.3.4", Phase1: vpn.TunnelConfigurationPhase1{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, Phase2: vpn.TunnelConfigurationPhase2{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, }, Tunnel2: vpn.TunnelConfiguration{ RemoteAddress: "5.6.7.8", Phase1: vpn.TunnelConfigurationPhase1{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, Phase2: vpn.TunnelConfigurationPhase2{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, }, }, @@ -340,23 +340,23 @@ func TestMapFields(t *testing.T) { Tunnel1: vpn.TunnelConfiguration{ RemoteAddress: "1.2.3.4", Phase1: vpn.TunnelConfigurationPhase1{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, Phase2: vpn.TunnelConfigurationPhase2{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, }, Tunnel2: vpn.TunnelConfiguration{ RemoteAddress: "5.6.7.8", Phase1: vpn.TunnelConfigurationPhase1{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, Phase2: vpn.TunnelConfigurationPhase2{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, }, }, @@ -419,27 +419,27 @@ func TestMapFields(t *testing.T) { Tunnel1: vpn.TunnelConfiguration{ RemoteAddress: "1.2.3.4", Phase1: vpn.TunnelConfigurationPhase1{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, RekeyTime: new(int32(7200)), }, Phase2: vpn.TunnelConfigurationPhase2{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, RekeyTime: new(int32(1800)), - StartAction: new("none"), - DpdAction: new("clear"), + StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_NONE.Ptr(), + DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_CLEAR.Ptr(), }, }, Tunnel2: vpn.TunnelConfiguration{ RemoteAddress: "5.6.7.8", Phase1: vpn.TunnelConfigurationPhase1{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, Phase2: vpn.TunnelConfigurationPhase2{ - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_256"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, }, }, }, @@ -600,13 +600,13 @@ func TestToCreatePayload(t *testing.T) { RemoteAddress: "203.0.113.1", Phase1: vpn.TunnelConfigurationPhase1{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, Phase2: vpn.TunnelConfigurationPhase2{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, }, Tunnel2: vpn.TunnelConfiguration{ @@ -614,13 +614,13 @@ func TestToCreatePayload(t *testing.T) { RemoteAddress: "203.0.113.2", Phase1: vpn.TunnelConfigurationPhase1{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, Phase2: vpn.TunnelConfigurationPhase2{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, }, Enabled: new(true), @@ -681,17 +681,17 @@ func TestToCreatePayload(t *testing.T) { RemoteAddress: "1.2.3.4", Phase1: vpn.TunnelConfigurationPhase1{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, RekeyTime: new(int32(7200)), }, Phase2: vpn.TunnelConfigurationPhase2{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, RekeyTime: new(int32(1800)), - StartAction: new("none"), - DpdAction: new("clear"), + StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_NONE.Ptr(), + DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_CLEAR.Ptr(), }, }, Tunnel2: vpn.TunnelConfiguration{ @@ -699,13 +699,13 @@ func TestToCreatePayload(t *testing.T) { RemoteAddress: "5.6.7.8", Phase1: vpn.TunnelConfigurationPhase1{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, Phase2: vpn.TunnelConfigurationPhase2{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, }, Enabled: new(true), @@ -798,13 +798,13 @@ func TestToUpdatePayload(t *testing.T) { RemoteAddress: "203.0.113.1", Phase1: vpn.TunnelConfigurationPhase1{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, Phase2: vpn.TunnelConfigurationPhase2{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, }, Tunnel2: vpn.TunnelConfiguration{ @@ -812,13 +812,13 @@ func TestToUpdatePayload(t *testing.T) { RemoteAddress: "203.0.113.2", Phase1: vpn.TunnelConfigurationPhase1{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, Phase2: vpn.TunnelConfigurationPhase2{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, }, Enabled: new(false), @@ -877,13 +877,13 @@ func TestToUpdatePayload(t *testing.T) { RemoteAddress: "203.0.113.1", Phase1: vpn.TunnelConfigurationPhase1{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, Phase2: vpn.TunnelConfigurationPhase2{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, }, Tunnel2: vpn.TunnelConfiguration{ @@ -891,13 +891,13 @@ func TestToUpdatePayload(t *testing.T) { RemoteAddress: "203.0.113.2", Phase1: vpn.TunnelConfigurationPhase1{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, Phase2: vpn.TunnelConfigurationPhase2{ DhGroups: nil, - EncryptionAlgorithms: []string{"aes256"}, - IntegrityAlgorithms: []string{"sha2_384"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"}, }, }, Enabled: new(false), diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go index 6d8401260..62188c00c 100644 --- a/stackit/internal/services/vpn/vpn_acc_test.go +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -880,7 +880,7 @@ func testAccCheckVpnResourcesDestroy(s *terraform.State) error { continue } - connectionsResp, err := client.DefaultAPI.ListGatewayConnections(ctx, testutil.ProjectId, vpn.Region(testutil.Region), *gateway.Id).Execute() + connectionsResp, err := client.DefaultAPI.ListGatewayConnections(ctx, testutil.ProjectId, testutil.Region, *gateway.Id).Execute() if err != nil { return fmt.Errorf("listing connections for gateway %s during CheckDestroy: %w", *gateway.Id, err) } @@ -888,7 +888,7 @@ func testAccCheckVpnResourcesDestroy(s *terraform.State) error { if conn.Id == nil { continue } - err := client.DefaultAPI.DeleteGatewayConnection(ctx, testutil.ProjectId, vpn.Region(testutil.Region), *gateway.Id, *conn.Id).Execute() + err := client.DefaultAPI.DeleteGatewayConnection(ctx, testutil.ProjectId, testutil.Region, *gateway.Id, *conn.Id).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { @@ -898,7 +898,7 @@ func testAccCheckVpnResourcesDestroy(s *terraform.State) error { } } - err = client.DefaultAPI.DeleteGateway(ctx, testutil.ProjectId, vpn.Region(testutil.Region), *gateway.Id).Execute() + err = client.DefaultAPI.DeleteGateway(ctx, testutil.ProjectId, testutil.Region, *gateway.Id).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { diff --git a/stackit/provider.go b/stackit/provider.go index dbe8be8db..383152304 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -122,14 +122,11 @@ import ( skeMachineImages "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/provideroptions/machineimages" sqlServerFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/instance" sqlServerFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/user" -<<<<<<< HEAD telemetryLink "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetrylink/link" telemetryRouterAccessToken "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/accesstoken" telemetryRouterDestination "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/destination" telemetryRouterInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/instance" -======= vpnConnection "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/connection" ->>>>>>> 2fb59bdf (feat(vpn): Onboarding VPN Connection) vpnGateway "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/gateway" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -844,14 +841,11 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { compliancelock.NewComplianceLockResource, serverBackupEnable.NewServerBackupEnableResource, serverUpdateEnable.NewServerUpdateEnableResource, -<<<<<<< HEAD telemetryRouterAccessToken.NewTelemetryRouterAccessTokenResource, telemetryRouterInstance.NewTelemetryRouterInstanceResource, telemetryRouterDestination.NewTelemetryRouterDestinationResource, telemetryLink.NewTelemetryLinkResource, -======= vpnConnection.NewVpnConnectionResource, ->>>>>>> 2fb59bdf (feat(vpn): Onboarding VPN Connection) vpnGateway.NewGatewayResource, } resources = append(resources, roleAssignements.NewRoleAssignmentResources()...)