diff --git a/docs/data-sources/vpn_connection.md b/docs/data-sources/vpn_connection.md
new file mode 100644
index 000000000..46476130e
--- /dev/null
+++ b/docs/data-sources/vpn_connection.md
@@ -0,0 +1,147 @@
+---
+# 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))
+- `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))
+- `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..9661d7d44
--- /dev/null
+++ b/docs/resources/vpn_connection.md
@@ -0,0 +1,218 @@
+---
+# 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) Configuration for the IPsec tunnel.
+
+-> **Note:** Write-Only argument `pre_shared_key_wo` is available to use in place of `pre_shared_key`. Write-Only arguments are supported in HashiCorp Terraform 1.11.0 and later. [Learn more](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments). (see [below for nested schema](#nestedatt--tunnel1))
+- `tunnel2` (Attributes) Configuration for the IPsec tunnel.
+
+-> **Note:** Write-Only argument `pre_shared_key_wo` is available to use in place of `pre_shared_key`. Write-Only arguments are supported in HashiCorp Terraform 1.11.0 and later. [Learn more](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments). (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` (String, Sensitive) Pre-shared key for the IPsec tunnel. Minimum 20 characters. Write-only argument `pre_shared_key_wo` should be preferred.
+- `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` (String, Sensitive) Pre-shared key for the IPsec tunnel. Minimum 20 characters. Write-only argument `pre_shared_key_wo` should be preferred.
+- `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..3ecf74a83
--- /dev/null
+++ b/stackit/internal/services/vpn/connection/datasource.go
@@ -0,0 +1,302 @@
+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{
+ "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, 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..2df2b8106
--- /dev/null
+++ b/stackit/internal/services/vpn/connection/resource.go
@@ -0,0 +1,1194 @@
+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/objectvalidator"
+ "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"
+
+ sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils"
+
+ "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 {
+ PreSharedKey types.String `tfsdk:"pre_shared_key"`
+ 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 = sdkUtils.EnumSliceToStringSlice(vpn.AllowedPhaseDhGroupsInnerEnumValues)
+ encryptionAlgorithmValues = sdkUtils.EnumSliceToStringSlice(vpn.AllowedPhaseEncryptionAlgorithmsInnerEnumValues)
+ integrityAlgorithmValues = sdkUtils.EnumSliceToStringSlice(vpn.AllowedPhaseIntegrityAlgorithmsInnerEnumValues)
+ startActionValues = sdkUtils.EnumSliceToStringSlice(vpn.AllowedTunnelConfigurationPhase2AllOfStartActionEnumValues)
+ dpdActionValues = sdkUtils.EnumSliceToStringSlice(vpn.AllowedTunnelConfigurationPhase2AllOfDpdActionEnumValues)
+)
+
+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{
+ "tunnel": "Configuration for the IPsec tunnel.",
+ "pre_shared_key": "Pre-shared key for the IPsec tunnel. Minimum 20 characters. Write-only argument `pre_shared_key_wo` should be preferred.",
+ "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{
+ Description: tunnelSchemaDescriptions["tunnel"],
+ MarkdownDescription: fmt.Sprintf("%s \n\n-> **Note:** Write-Only argument `pre_shared_key_wo` is available to use in place of `pre_shared_key`. Write-Only arguments are supported in HashiCorp Terraform 1.11.0 and later. [Learn more](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments).", tunnelSchemaDescriptions["tunnel"]),
+ Required: true,
+ Validators: []validator.Object{
+ objectvalidator.ExactlyOneOf(
+ path.MatchRelative().AtName("pre_shared_key"),
+ path.MatchRelative().AtName("pre_shared_key_wo"),
+ ),
+ },
+ Attributes: map[string]schema.Attribute{
+ "pre_shared_key": schema.StringAttribute{
+ Description: tunnelSchemaDescriptions["pre_shared_key"],
+ Required: true,
+ Sensitive: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(20),
+ stringvalidator.ConflictsWith(
+ path.MatchRelative().AtParent().AtName("pre_shared_key_wo"),
+ path.MatchRelative().AtParent().AtName("pre_shared_key_wo_version"),
+ ),
+ stringvalidator.PreferWriteOnlyAttribute(path.MatchRelative().AtParent().AtName("key_payload_base64_wo")),
+ },
+ },
+ "pre_shared_key_wo": schema.StringAttribute{
+ Description: tunnelSchemaDescriptions["pre_shared_key_wo"],
+ Required: true,
+ Sensitive: true,
+ WriteOnly: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(20),
+ stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("pre_shared_key")),
+ },
+ },
+ "pre_shared_key_wo_version": schema.Int64Attribute{
+ Description: tunnelSchemaDescriptions["pre_shared_key_wo_version"],
+ Optional: true,
+ Validators: []validator.Int64{
+ int64validator.AlsoRequires(path.MatchRelative().AtParent().AtName("pre_shared_key_wo")),
+ int64validator.ConflictsWith(path.MatchRelative().AtParent().AtName("pre_shared_key")),
+ },
+ },
+ "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.PreSharedKey = configModel.Tunnel1.PreSharedKey
+ model.Tunnel1.PreSharedKeyWo = configModel.Tunnel1.PreSharedKeyWo
+ model.Tunnel2.PreSharedKey = configModel.Tunnel2.PreSharedKey
+ 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, 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, 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, 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 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 !tfutils.IsUndefined(model.Tunnel1.PreSharedKeyWoVersion) {
+ 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 !tfutils.IsUndefined(model.Tunnel2.PreSharedKeyWoVersion) {
+ 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 {
+ // Secret must be read from Config, not Plan — write-only values are always null in plan.
+ 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, 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, 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, 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(ctx context.Context, model *Model) (*vpn.CreateGatewayConnectionPayload, error) {
+ if model == nil {
+ return nil, fmt.Errorf("nil model")
+ }
+
+ fields, err := toConnectionFields(ctx, model)
+ 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: &fields.labels,
+ }, nil
+}
+
+func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateGatewayConnectionPayload, error) {
+ if model == nil {
+ return nil, fmt.Errorf("nil model")
+ }
+
+ fields, err := toConnectionFields(ctx, model)
+ 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: &fields.labels,
+ }, nil
+}
+
+type connectionFields struct {
+ displayName string
+ tunnel1 vpn.TunnelConfiguration
+ tunnel2 vpn.TunnelConfiguration
+ enabled *bool
+ remoteSubnets []string
+ localSubnets []string
+ staticRoutes []string
+ labels map[string]string
+}
+
+func toConnectionFields(ctx context.Context, 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 !tfutils.IsUndefined(model.Enabled) {
+ enabled := model.Enabled.ValueBool()
+ fields.enabled = &enabled
+ }
+
+ if !tfutils.IsUndefined(model.RemoteSubnet) {
+ remoteSubnets, err := tfutils.ListValueToStringSlice(model.RemoteSubnet)
+ if err != nil {
+ return nil, fmt.Errorf("converting remote_subnet: %w", err)
+ }
+ fields.remoteSubnets = remoteSubnets
+ }
+
+ if !tfutils.IsUndefined(model.LocalSubnet) {
+ localSubnets, err := tfutils.ListValueToStringSlice(model.LocalSubnet)
+ if err != nil {
+ return nil, fmt.Errorf("converting local_subnet: %w", err)
+ }
+ fields.localSubnets = localSubnets
+ }
+
+ if !tfutils.IsUndefined(model.StaticRoutes) {
+ staticRoutes, err := tfutils.ListValueToStringSlice(model.StaticRoutes)
+ if err != nil {
+ return nil, fmt.Errorf("converting static_routes: %w", err)
+ }
+ fields.staticRoutes = staticRoutes
+ }
+
+ fields.labels, err = tfutils.LabelsToPayload(ctx, model.Labels)
+ if err != nil {
+ return nil, err
+ }
+
+ 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 !tfutils.IsUndefined(tunnel.PreSharedKeyWo) {
+ config.PreSharedKey = tunnel.PreSharedKeyWo.ValueStringPointer()
+ } else if !tfutils.IsUndefined(tunnel.PreSharedKey) {
+ config.PreSharedKey = tunnel.PreSharedKey.ValueStringPointer()
+ }
+
+ if tunnel.Phase1 != nil {
+ phase1 := vpn.TunnelConfigurationPhase1{}
+
+ if !tfutils.IsUndefined(tunnel.Phase1.DhGroups) {
+ dhGroups, err := tfutils.ListValueToStringSlice(tunnel.Phase1.DhGroups)
+ if err != nil {
+ return nil, fmt.Errorf("converting phase1 dh_groups: %w", err)
+ }
+ 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)
+ }
+ 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)
+ }
+ intAlgsInner := []vpn.PhaseIntegrityAlgorithmsInner{}
+ for _, item := range intAlgs {
+ intAlgsInner = append(intAlgsInner, vpn.PhaseIntegrityAlgorithmsInner(item))
+ }
+ phase1.IntegrityAlgorithms = intAlgsInner
+
+ if !tfutils.IsUndefined(tunnel.Phase1.RekeyTime) {
+ rekeyTime := tunnel.Phase1.RekeyTime.ValueInt32()
+ phase1.RekeyTime = &rekeyTime
+ }
+
+ config.Phase1 = phase1
+ }
+
+ if tunnel.Phase2 != nil {
+ phase2 := vpn.TunnelConfigurationPhase2{}
+ if !tfutils.IsUndefined(tunnel.Phase2.DhGroups) {
+ dhGroups, err := tfutils.ListValueToStringSlice(tunnel.Phase2.DhGroups)
+ if err != nil {
+ return nil, fmt.Errorf("converting phase2 dh_groups: %w", err)
+ }
+ 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)
+ }
+ 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)
+ }
+ intAlgsInner := []vpn.PhaseIntegrityAlgorithmsInner{}
+ for _, item := range intAlgs {
+ intAlgsInner = append(intAlgsInner, vpn.PhaseIntegrityAlgorithmsInner(item))
+ }
+ phase2.IntegrityAlgorithms = intAlgsInner
+ if !tfutils.IsUndefined(tunnel.Phase2.RekeyTime) {
+ rekeyTime := tunnel.Phase2.RekeyTime.ValueInt32()
+ phase2.RekeyTime = &rekeyTime
+ }
+ if !tfutils.IsUndefined(tunnel.Phase2.StartAction) {
+ startAction := tunnel.Phase2.StartAction.ValueString()
+ phase2.StartAction = vpn.TunnelConfigurationPhase2AllOfStartAction(startAction).Ptr()
+ }
+ if !tfutils.IsUndefined(tunnel.Phase2.DpdAction) {
+ dpdAction := tunnel.Phase2.DpdAction.ValueString()
+ phase2.DpdAction = vpn.TunnelConfigurationPhase2AllOfDpdAction(dpdAction).Ptr()
+ }
+ 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(string(*apiTunnel.Phase2.StartAction))
+ } else {
+ phase2.StartAction = types.StringNull()
+ }
+ if apiTunnel.Phase2.DpdAction != nil {
+ phase2.DpdAction = types.StringValue(string(*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..a1bf3c2f3
--- /dev/null
+++ b/stackit/internal/services/vpn/connection/resource_test.go
@@ -0,0 +1,1060 @@
+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"
+
+ tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
+)
+
+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: []vpn.PhaseDhGroupsInner{"modp2048"},
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ RekeyTime: new(int32(14400)),
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: []vpn.PhaseDhGroupsInner{"modp2048"},
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ RekeyTime: new(int32(3600)),
+ StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_START.Ptr(),
+ DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_RESTART.Ptr(),
+ },
+ },
+ Tunnel2: vpn.TunnelConfiguration{
+ RemoteAddress: "203.0.113.2",
+ Phase1: vpn.TunnelConfigurationPhase1{
+ DhGroups: []vpn.PhaseDhGroupsInner{"modp2048"},
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ RekeyTime: new(int32(14400)),
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: []vpn.PhaseDhGroupsInner{"modp2048"},
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ RekeyTime: new(int32(3600)),
+ StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_START.Ptr(),
+ DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_RESTART.Ptr(),
+ },
+ },
+ },
+ 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: []vpn.PhaseEncryptionAlgorithmsInner{"aes256gcm16"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ 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"),
+ RemoteAddress: new("169.254.0.2"),
+ },
+ Bgp: &vpn.BGPTunnelConfig{
+ RemoteAsn: 65000,
+ },
+ },
+ Tunnel2: vpn.TunnelConfiguration{
+ RemoteAddress: "203.0.113.11",
+ Phase1: vpn.TunnelConfigurationPhase1{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256gcm16"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256gcm16"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_NONE.Ptr(),
+ DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_CLEAR.Ptr(),
+ },
+ },
+ },
+ 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: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ },
+ },
+ Tunnel2: vpn.TunnelConfiguration{
+ RemoteAddress: "5.6.7.8",
+ Phase1: vpn.TunnelConfigurationPhase1{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"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: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ },
+ },
+ Tunnel2: vpn.TunnelConfiguration{
+ RemoteAddress: "5.6.7.8",
+ Phase1: vpn.TunnelConfigurationPhase1{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"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: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ RekeyTime: new(int32(7200)),
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ RekeyTime: new(int32(1800)),
+ StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_NONE.Ptr(),
+ DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_CLEAR.Ptr(),
+ },
+ },
+ Tunnel2: vpn.TunnelConfiguration{
+ RemoteAddress: "5.6.7.8",
+ Phase1: vpn.TunnelConfigurationPhase1{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"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: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ },
+ Tunnel2: vpn.TunnelConfiguration{
+ PreSharedKey: new("secret456-at-least-20-chars"),
+ RemoteAddress: "203.0.113.2",
+ Phase1: vpn.TunnelConfigurationPhase1{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ },
+ Enabled: new(true),
+ Labels: &map[string]string{},
+ },
+ 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: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ RekeyTime: new(int32(7200)),
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ RekeyTime: new(int32(1800)),
+ StartAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFSTARTACTION_NONE.Ptr(),
+ DpdAction: vpn.TUNNELCONFIGURATIONPHASE2ALLOFDPDACTION_CLEAR.Ptr(),
+ },
+ },
+ Tunnel2: vpn.TunnelConfiguration{
+ PreSharedKey: new("super-secret-key-at-least-20"),
+ RemoteAddress: "5.6.7.8",
+ Phase1: vpn.TunnelConfigurationPhase1{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ },
+ Labels: &map[string]string{},
+ 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: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ },
+ Tunnel2: vpn.TunnelConfiguration{
+ PreSharedKey: new("secret456-at-least-20-chars"),
+ RemoteAddress: "203.0.113.2",
+ Phase1: vpn.TunnelConfigurationPhase1{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ },
+ Labels: &map[string]string{},
+ 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: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ },
+ Tunnel2: vpn.TunnelConfiguration{
+ PreSharedKey: nil,
+ RemoteAddress: "203.0.113.2",
+ Phase1: vpn.TunnelConfigurationPhase1{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ Phase2: vpn.TunnelConfigurationPhase2{
+ DhGroups: nil,
+ EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"},
+ IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_384"},
+ },
+ },
+ Labels: &map[string]string{},
+ 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 !tfutils.IsUndefined(tt.input.PreSharedKeyWo) {
+ 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..62188c00c 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, 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, 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, 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..383152304 100644
--- a/stackit/provider.go
+++ b/stackit/provider.go
@@ -126,6 +126,7 @@ import (
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"
vpnGateway "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/gateway"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
)
@@ -746,6 +747,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()...)
@@ -843,6 +845,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
telemetryRouterInstance.NewTelemetryRouterInstanceResource,
telemetryRouterDestination.NewTelemetryRouterDestinationResource,
telemetryLink.NewTelemetryLinkResource,
+ vpnConnection.NewVpnConnectionResource,
vpnGateway.NewGatewayResource,
}
resources = append(resources, roleAssignements.NewRoleAssignmentResources()...)