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..eb60caf37
--- /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 tunnel1
+
+~> 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 tunnel2
+
+~> 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))
+- `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` (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.
+- `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))
+- `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` (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.
+- `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..420a81559
--- /dev/null
+++ b/stackit/internal/services/vpn/connection/datasource.go
@@ -0,0 +1,309 @@
+package connection
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+
+ "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
+ vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
+ "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/validate"
+)
+
+var (
+ _ datasource.DataSource = (*vpnConnectionDataSource)(nil)
+ _ datasource.DataSourceWithConfigure = (*vpnConnectionDataSource)(nil)
+)
+
+type DataSourceTunnelModel struct {
+ RemoteAddress types.String `tfsdk:"remote_address"`
+ Phase1 *Phase1Model `tfsdk:"phase1"`
+ Phase2 *Phase2Model `tfsdk:"phase2"`
+ Peering *PeeringConfigModel `tfsdk:"peering"`
+ Bgp *BGPTunnelConfigModel `tfsdk:"bgp"`
+}
+
+type DataSourceModel struct {
+ CommonModel
+ Tunnel1 *DataSourceTunnelModel `tfsdk:"tunnel1"`
+ Tunnel2 *DataSourceTunnelModel `tfsdk:"tunnel2"`
+}
+
+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 DataSourceTunnelSchema() schema.SingleNestedAttribute {
+ return schema.SingleNestedAttribute{
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "remote_address": schema.StringAttribute{
+ Description: "Remote peer IPv4 address for this tunnel.",
+ Computed: true,
+ },
+ "phase1": schema.SingleNestedAttribute{
+ Description: "IKE Phase 1 configuration.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "dh_groups": schema.ListAttribute{
+ Description: "Diffie-Hellman groups.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "encryption_algorithms": schema.ListAttribute{
+ Description: "Encryption algorithms.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "integrity_algorithms": schema.ListAttribute{
+ Description: "Integrity/hash algorithms.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "rekey_time": schema.Int32Attribute{
+ Description: "IKE re-keying time in seconds.",
+ Computed: true,
+ },
+ },
+ },
+ "phase2": schema.SingleNestedAttribute{
+ Description: "IKE Phase 2 configuration.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "dh_groups": schema.ListAttribute{
+ Description: "Diffie-Hellman groups for PFS.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "encryption_algorithms": schema.ListAttribute{
+ Description: "Encryption algorithms.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "integrity_algorithms": schema.ListAttribute{
+ Description: "Integrity/hash algorithms.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "rekey_time": schema.Int32Attribute{
+ Description: "Child SA re-keying time in seconds.",
+ Computed: true,
+ },
+ "start_action": schema.StringAttribute{
+ Description: "Start action (none or start).",
+ Computed: true,
+ },
+ "dpd_action": schema.StringAttribute{
+ Description: "DPD timeout action (clear or restart).",
+ Computed: true,
+ },
+ },
+ },
+ "peering": schema.SingleNestedAttribute{
+ Description: "Tunnel interface peering configuration.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "local_address": schema.StringAttribute{
+ Description: "Local tunnel interface IPv4 address.",
+ Computed: true,
+ },
+ "remote_address": schema.StringAttribute{
+ Description: "Remote tunnel interface IPv4 address.",
+ Computed: true,
+ },
+ },
+ },
+ "bgp": schema.SingleNestedAttribute{
+ Description: "BGP configuration for this tunnel.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "remote_asn": schema.Int64Attribute{
+ Description: "Remote AS number.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *vpnConnectionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: fmt.Sprintf("VPN Connection data source schema. %s", core.DatasourceRegionFallbackDocstring),
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "Terraform's internal resource identifier. Structured as \"`project_id`,`region`,`gateway_id`,`connection_id`\".",
+ Computed: true,
+ },
+ "project_id": schema.StringAttribute{
+ Description: "STACKIT project ID.",
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "region": schema.StringAttribute{
+ Description: "STACKIT region.",
+ Computed: true,
+ },
+ "gateway_id": schema.StringAttribute{
+ Description: "The UUID of the parent VPN gateway.",
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "connection_id": schema.StringAttribute{
+ Description: "The server-generated UUID of the VPN connection.",
+ Required: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "display_name": schema.StringAttribute{
+ Description: "A user-friendly name for the connection.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ Description: "Whether this connection is enabled.",
+ Computed: true,
+ },
+ "remote_subnet": schema.ListAttribute{
+ Description: "List of remote IPv4 CIDRs accessible via this connection.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "local_subnet": schema.ListAttribute{
+ Description: "List of local IPv4 CIDRs to route through this connection.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "static_routes": schema.ListAttribute{
+ Description: "List of static routes (IPv4 CIDRs) for route-based VPN.",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "tunnel1": DataSourceTunnelSchema(),
+ "tunnel2": DataSourceTunnelSchema(),
+ "labels": schema.MapAttribute{
+ Description: "Map of custom 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 DataSourceModel
+ 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 = mapDataSourceFields(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,
+ })
+}
+
+func mapDataSourceFields(ctx context.Context, conn connectionResponse, model *DataSourceModel, region string) error {
+ err := mapCommonFields(ctx, conn, &model.CommonModel, region)
+ if err != nil {
+ return err
+ }
+
+ tunnel1 := conn.GetTunnel1()
+ if model.Tunnel1 == nil {
+ model.Tunnel1 = &DataSourceTunnelModel{}
+ }
+ err = mapTunnel(ctx, &tunnel1, model.Tunnel1)
+ if err != nil {
+ return fmt.Errorf("mapping tunnel1: %w", err)
+ }
+
+ tunnel2 := conn.GetTunnel2()
+ if model.Tunnel2 == nil {
+ model.Tunnel2 = &DataSourceTunnelModel{}
+ }
+ err = mapTunnel(ctx, &tunnel2, model.Tunnel2)
+ if err != nil {
+ return fmt.Errorf("mapping tunnel2: %w", err)
+ }
+
+ return nil
+}
diff --git a/stackit/internal/services/vpn/connection/datasource_test.go b/stackit/internal/services/vpn/connection/datasource_test.go
new file mode 100644
index 000000000..2eba462c3
--- /dev/null
+++ b/stackit/internal/services/vpn/connection/datasource_test.go
@@ -0,0 +1,234 @@
+package connection
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api"
+)
+
+func fixtureDataSourceTunnelModel(mods ...func(m *DataSourceTunnelModel)) *DataSourceTunnelModel {
+ resp := &DataSourceTunnelModel{
+ RemoteAddress: types.StringValue("203.0.113.1"),
+ Phase1: &Phase1Model{
+ BasePhaseModel: fixtureBasePhaseModel(),
+ },
+ Phase2: &Phase2Model{
+ BasePhaseModel: fixtureBasePhaseModel(func(m *BasePhaseModel) {
+ m.RekeyTime = types.Int32Value(3600)
+ }),
+ StartAction: types.StringValue("start"),
+ DpdAction: types.StringValue("restart"),
+ },
+ }
+ for _, mod := range mods {
+ mod(resp)
+ }
+ return resp
+}
+
+func fixtureDataSourceModel(mods ...func(m *DataSourceModel)) DataSourceModel {
+ resp := DataSourceModel{
+ CommonModel: CommonModel{
+ 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.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("123.45.67.89"),
+ }),
+ Labels: types.MapNull(types.StringType),
+ },
+ Tunnel1: fixtureDataSourceTunnelModel(),
+ Tunnel2: fixtureDataSourceTunnelModel(func(m *DataSourceTunnelModel) {
+ m.RemoteAddress = types.StringValue("203.0.113.2")
+ }),
+ }
+ for _, mod := range mods {
+ mod(&resp)
+ }
+ return resp
+}
+
+func TestMapDataSourceFields(t *testing.T) {
+ tests := []struct {
+ description string
+ input *vpn.ConnectionResponse
+ expected DataSourceModel
+ isValid bool
+ }{
+ {
+ description: "basic_connection",
+ input: fixtureConnectionResponse(),
+ expected: fixtureDataSourceModel(),
+ isValid: true,
+ },
+ {
+ description: "minimal_connection",
+ input: &vpn.ConnectionResponse{
+ Id: new("connection-id"),
+ },
+ expected: DataSourceModel{
+ CommonModel: CommonModel{
+ 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(""),
+ Enabled: types.BoolNull(),
+ RemoteSubnet: basetypes.NewListNull(basetypes.StringType{}),
+ LocalSubnet: basetypes.NewListNull(basetypes.StringType{}),
+ StaticRoutes: basetypes.NewListNull(basetypes.StringType{}),
+ Labels: basetypes.NewMapNull(basetypes.StringType{}),
+ },
+ Tunnel1: &DataSourceTunnelModel{
+ RemoteAddress: types.StringValue(""),
+ Phase1: &Phase1Model{
+ BasePhaseModel: BasePhaseModel{
+ DhGroups: basetypes.NewListNull(basetypes.StringType{}),
+ EncryptionAlgorithms: basetypes.NewListNull(basetypes.StringType{}),
+ IntegrityAlgorithms: basetypes.NewListNull(basetypes.StringType{}),
+ },
+ },
+ Phase2: &Phase2Model{
+ BasePhaseModel: BasePhaseModel{
+ DhGroups: basetypes.NewListNull(basetypes.StringType{}),
+ EncryptionAlgorithms: basetypes.NewListNull(basetypes.StringType{}),
+ IntegrityAlgorithms: basetypes.NewListNull(basetypes.StringType{}),
+ },
+ },
+ },
+ Tunnel2: &DataSourceTunnelModel{
+ RemoteAddress: types.StringValue(""),
+ Phase1: &Phase1Model{
+ BasePhaseModel: BasePhaseModel{
+ DhGroups: basetypes.NewListNull(basetypes.StringType{}),
+ EncryptionAlgorithms: basetypes.NewListNull(basetypes.StringType{}),
+ IntegrityAlgorithms: basetypes.NewListNull(basetypes.StringType{}),
+ },
+ },
+ Phase2: &Phase2Model{
+ BasePhaseModel: BasePhaseModel{
+ DhGroups: basetypes.NewListNull(basetypes.StringType{}),
+ EncryptionAlgorithms: basetypes.NewListNull(basetypes.StringType{}),
+ IntegrityAlgorithms: basetypes.NewListNull(basetypes.StringType{}),
+ },
+ },
+ },
+ },
+ isValid: true,
+ },
+ {
+ description: "connection_with_static_routes_and_bgp",
+ input: fixtureConnectionResponse(func(m *vpn.ConnectionResponse) {
+ m.StaticRoutes = []string{"10.0.0.0/8"}
+ m.Tunnel1.Bgp = &vpn.BGPTunnelConfig{
+ RemoteAsn: 65000,
+ }
+ }),
+ expected: fixtureDataSourceModel(func(m *DataSourceModel) {
+ m.StaticRoutes = types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("10.0.0.0/8"),
+ })
+ m.Tunnel1.Bgp = &BGPTunnelConfigModel{
+ RemoteAsn: types.Int64Value(65000),
+ }
+ }),
+ isValid: true,
+ },
+ {
+ description: "multiple_static_routes",
+ input: fixtureConnectionResponse(func(m *vpn.ConnectionResponse) {
+ m.StaticRoutes = []string{"10.0.0.0/8", "172.16.0.0/12"}
+ }),
+ expected: fixtureDataSourceModel(func(m *DataSourceModel) {
+ m.StaticRoutes = types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("10.0.0.0/8"),
+ types.StringValue("172.16.0.0/12"),
+ })
+ }),
+ isValid: true,
+ },
+ {
+ description: "empty_labels",
+ input: fixtureConnectionResponse(func(m *vpn.ConnectionResponse) {
+ m.Labels = &map[string]string{}
+ }),
+ expected: fixtureDataSourceModel(func(m *DataSourceModel) {
+ m.Labels = types.MapNull(types.StringType)
+ }),
+ isValid: true,
+ },
+ {
+ description: "peering",
+ input: fixtureConnectionResponse(func(m *vpn.ConnectionResponse) {
+ m.Tunnel1.Peering = &vpn.PeeringConfig{
+ LocalAddress: new("123.45.67.89"),
+ RemoteAddress: new("98.76.54.32"),
+ }
+ }),
+ expected: fixtureDataSourceModel(func(m *DataSourceModel) {
+ m.Tunnel1.Peering = &PeeringConfigModel{
+ LocalAddress: types.StringValue("123.45.67.89"),
+ RemoteAddress: types.StringValue("98.76.54.32"),
+ }
+ }),
+ isValid: true,
+ },
+ {
+ description: "nil_response",
+ input: nil,
+ isValid: false,
+ },
+ {
+ description: "nil_connection_id",
+ input: &vpn.ConnectionResponse{
+ Id: nil,
+ DisplayName: "test-connection",
+ },
+ isValid: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ state := &DataSourceModel{
+ CommonModel: CommonModel{
+ ProjectID: types.StringValue(projectId),
+ Region: types.StringValue(region),
+ GatewayID: types.StringValue(gatewayId),
+ },
+ Tunnel1: &DataSourceTunnelModel{},
+ Tunnel2: &DataSourceTunnelModel{},
+ }
+
+ err := mapDataSourceFields(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)
+ }
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/vpn/connection/resource.go b/stackit/internal/services/vpn/connection/resource.go
new file mode 100644
index 000000000..f07240410
--- /dev/null
+++ b/stackit/internal/services/vpn/connection/resource.go
@@ -0,0 +1,1113 @@
+package connection
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "regexp"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "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/listdefault"
+ "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 BasePhaseModel 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 Phase1Model struct {
+ BasePhaseModel
+}
+
+type Phase2Model struct {
+ StartAction types.String `tfsdk:"start_action"`
+ DpdAction types.String `tfsdk:"dpd_action"`
+ BasePhaseModel
+}
+
+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 {
+ DataSourceTunnelModel
+ PreSharedKey types.String `tfsdk:"pre_shared_key"`
+ PreSharedKeyWo types.String `tfsdk:"pre_shared_key_wo"`
+ PreSharedKeyWoVersion types.Int64 `tfsdk:"pre_shared_key_wo_version"`
+}
+
+// CommonModel is used in the resource and the datasource implementation to share most of the mapping logic
+type CommonModel 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"`
+ Labels types.Map `tfsdk:"labels"`
+}
+
+// Model is used for the resource implementation
+type Model struct {
+ CommonModel
+ Tunnel1 *TunnelModel `tfsdk:"tunnel1"`
+ Tunnel2 *TunnelModel `tfsdk:"tunnel2"`
+}
+
+type BasePhasePayload interface {
+ GetDhGroupsOk() ([]vpn.PhaseDhGroupsInner, bool)
+ GetEncryptionAlgorithmsOk() ([]vpn.PhaseEncryptionAlgorithmsInner, bool)
+ GetIntegrityAlgorithmsOk() ([]vpn.PhaseIntegrityAlgorithmsInner, bool)
+ GetRekeyTimeOk() (*int32, bool)
+
+ SetDhGroups([]vpn.PhaseDhGroupsInner)
+ SetEncryptionAlgorithms([]vpn.PhaseEncryptionAlgorithmsInner)
+ SetIntegrityAlgorithms([]vpn.PhaseIntegrityAlgorithmsInner)
+ SetRekeyTime(int32)
+}
+
+type connectionPayload interface {
+ SetDisplayName(string)
+ SetTunnel1(vpn.TunnelConfiguration)
+ SetTunnel2(vpn.TunnelConfiguration)
+ SetEnabled(bool)
+ SetRemoteSubnets([]string)
+ SetLocalSubnets([]string)
+ SetStaticRoutes([]string)
+ SetLabels(map[string]string)
+}
+
+type connectionResponse interface {
+ GetIdOk() (*string, bool)
+ GetDisplayName() string
+ GetTunnel1() vpn.TunnelConfiguration
+ GetTunnel2() vpn.TunnelConfiguration
+ GetEnabledOk() (*bool, bool)
+ GetRemoteSubnetsOk() ([]string, bool)
+ GetLocalSubnetsOk() ([]string, bool)
+ GetStaticRoutesOk() ([]string, bool)
+ GetLabelsOk() (*map[string]string, bool)
+}
+
+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)
+)
+
+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 := func(rootAttribute string) schema.SingleNestedAttribute {
+ return schema.SingleNestedAttribute{
+ Description: fmt.Sprintf("Configuration for the IPsec %s.", rootAttribute),
+ MarkdownDescription: fmt.Sprintf("Configuration for the IPsec %s \n\n~> 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).", rootAttribute),
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "pre_shared_key": schema.StringAttribute{
+ Description: "Pre-shared key for the IPsec tunnel. Minimum 20 characters. Write-only argument `pre_shared_key_wo` should be preferred.",
+ Optional: 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.MatchRoot(rootAttribute).AtName("pre_shared_key_wo")),
+ },
+ },
+ "pre_shared_key_wo": schema.StringAttribute{
+ Description: "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.",
+ Optional: true,
+ WriteOnly: true,
+ Sensitive: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(20),
+ stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("pre_shared_key")),
+ stringvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("pre_shared_key_wo_version")),
+ },
+ },
+ "pre_shared_key_wo_version": schema.Int64Attribute{
+ Description: "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.",
+ 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: "Remote IPv4 address for the tunnel endpoint.",
+ Required: true,
+ Validators: []validator.String{
+ validate.IP(true),
+ },
+ },
+ "phase1": schema.SingleNestedAttribute{
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "dh_groups": schema.ListAttribute{
+ Description: fmt.Sprintf("Diffie-Hellman groups for key exchange. %s", tfutils.FormatPossibleValues(dhGroupValues...)),
+ Optional: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.ValueStringsAre(
+ stringvalidator.OneOf(dhGroupValues...),
+ ),
+ },
+ },
+ "encryption_algorithms": schema.ListAttribute{
+ Description: fmt.Sprintf("Encryption algorithms for Phase 1. %s", tfutils.FormatPossibleValues(encryptionAlgorithmValues...)),
+ Required: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.ValueStringsAre(
+ stringvalidator.OneOf(encryptionAlgorithmValues...),
+ ),
+ },
+ },
+ "integrity_algorithms": schema.ListAttribute{
+ Description: fmt.Sprintf("Integrity algorithms for Phase 1. %s", tfutils.FormatPossibleValues(integrityAlgorithmValues...)),
+ Required: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.ValueStringsAre(
+ stringvalidator.OneOf(integrityAlgorithmValues...),
+ ),
+ },
+ },
+ "rekey_time": schema.Int32Attribute{
+ Description: "Time to schedule an IKE re-keying in seconds. Range: 900-28800. Default: 14400.",
+ 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: fmt.Sprintf("Diffie-Hellman groups for Phase 2. %s", tfutils.FormatPossibleValues(dhGroupValues...)),
+ Optional: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.ValueStringsAre(
+ stringvalidator.OneOf(dhGroupValues...),
+ ),
+ },
+ },
+ "encryption_algorithms": schema.ListAttribute{
+ Description: fmt.Sprintf("Encryption algorithms for Phase 2. %s", tfutils.FormatPossibleValues(encryptionAlgorithmValues...)),
+ Required: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.ValueStringsAre(
+ stringvalidator.OneOf(encryptionAlgorithmValues...),
+ ),
+ },
+ },
+ "integrity_algorithms": schema.ListAttribute{
+ Description: fmt.Sprintf("Integrity algorithms for Phase 2. %s", tfutils.FormatPossibleValues(integrityAlgorithmValues...)),
+ Required: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.ValueStringsAre(
+ stringvalidator.OneOf(integrityAlgorithmValues...),
+ ),
+ },
+ },
+ "rekey_time": schema.Int32Attribute{
+ Description: "Time to schedule a Child SA re-keying in seconds. Range: 900-3600. Default: 3600.",
+ Optional: true,
+ Computed: true,
+ Validators: []validator.Int32{
+ int32validator.Between(900, 3600),
+ },
+ },
+ "start_action": schema.StringAttribute{
+ Description: fmt.Sprintf("Action to perform after loading the connection configuration. Default: 'start'. %s", tfutils.FormatPossibleValues(startActionValues...)),
+ Optional: true,
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf(startActionValues...),
+ },
+ },
+ "dpd_action": schema.StringAttribute{
+ Description: fmt.Sprintf("Action to perform on DPD timeout. Default: 'restart'. %s", tfutils.FormatPossibleValues(dpdActionValues...)),
+ 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: "Local tunnel interface IPv4 address.",
+ Required: true,
+ Validators: []validator.String{
+ validate.IP(true),
+ },
+ },
+ "remote_address": schema.StringAttribute{
+ Description: "Remote tunnel interface IPv4 address.",
+ Required: true,
+ Validators: []validator.String{
+ validate.IP(true),
+ },
+ },
+ },
+ },
+ "bgp": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "remote_asn": schema.Int64Attribute{
+ Description: "Remote ASN for BGP peering (private ASN range, 64512-4294967294).",
+ 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: "Terraform's internal resource identifier. Structured as \"`project_id`,`region`,`gateway_id`,`connection_id`\".",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "connection_id": schema.StringAttribute{
+ Description: "The server-generated UUID of the VPN connection.",
+ Computed: true,
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: "STACKIT project ID.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "region": schema.StringAttribute{
+ Description: "STACKIT region.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "gateway_id": schema.StringAttribute{
+ Description: "The UUID of the parent VPN gateway.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ Validators: []validator.String{
+ validate.UUID(),
+ validate.NoSeparator(),
+ },
+ },
+ "display_name": schema.StringAttribute{
+ Description: "A user-friendly name for the connection. Must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long.",
+ 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: "Whether this connection is enabled. Defaults to true.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(true),
+ },
+ "remote_subnet": schema.ListAttribute{
+ Description: "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.",
+ Optional: true,
+ Computed: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.SizeBetween(1, 100),
+ listvalidator.ValueStringsAre(validate.CIDR()),
+ },
+ },
+ "local_subnet": schema.ListAttribute{
+ Description: "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.",
+ Optional: true,
+ Computed: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.SizeBetween(1, 100),
+ listvalidator.ValueStringsAre(validate.CIDR()),
+ },
+ },
+ "static_routes": schema.ListAttribute{
+ Description: "List of static routes (IPv4 CIDRs) for route-based VPN. Mandatory for ROUTE_BASED gateways.",
+ Optional: true,
+ Computed: true,
+ Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})),
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.ValueStringsAre(validate.CIDR()),
+ },
+ },
+ "tunnel1": tunnelSchema("tunnel1"),
+ "tunnel2": tunnelSchema("tunnel2"),
+ "labels": schema.MapAttribute{
+ Description: "Map of custom 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
+ // the regular plan model one always uses in the create implementation
+ var planModel Model
+ diags := req.Plan.Get(ctx, &planModel)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // The config model - this has to be used because Terraform doesn't include write-only field values in the
+ // plan and state models - for security measures. Write-only values should be only kept in the config model
+ // so that they never end up in the state (or plan).
+ var configModel Model
+ diags = req.Config.Get(ctx, &configModel)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ projectId := planModel.ProjectID.ValueString()
+ region := r.providerData.GetRegionWithOverride(planModel.Region)
+ gatewayId := planModel.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, &planModel, &configModel)
+ 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)
+
+ err = mapResourceFields(ctx, createResp, &planModel, 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, planModel)
+ 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 {
+ if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading VPN connection", err.Error())
+ return
+ }
+
+ ctx = core.LogResponse(ctx)
+
+ err = mapResourceFields(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
+ // the regular plan model one always uses in the update implementation
+ var planModel Model
+ diags := req.Plan.Get(ctx, &planModel)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // the state model - contains the "previous" values and is needed to compare the old write-only version field
+ // values to the new ones
+ var stateModel Model
+ diags = req.State.Get(ctx, &stateModel)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // The config model - this has to be used because Terraform doesn't include write-only field values in the
+ // plan and state models - for security measures. Write-only values should be only kept in the config model
+ // so that they never end up in the state (or plan).
+ var configModel Model
+ diags = req.Config.Get(ctx, &configModel)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ ctx = core.InitProviderContext(ctx)
+
+ projectId := planModel.ProjectID.ValueString()
+ region := r.providerData.GetRegionWithOverride(planModel.Region)
+ gatewayId := planModel.GatewayID.ValueString()
+ connectionId := planModel.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, &planModel, &stateModel, &configModel)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN connection", fmt.Sprintf("Creating API payload: %v", err))
+ return
+ }
+
+ connResp, 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)
+
+ err = mapResourceFields(ctx, connResp, &planModel, 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, planModel)
+ 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 {
+ if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && 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, planModel, configModel *Model) (*vpn.CreateGatewayConnectionPayload, error) {
+ if planModel == nil {
+ return nil, fmt.Errorf("nil plan model")
+ }
+ if configModel == nil {
+ return nil, fmt.Errorf("nil config model")
+ }
+
+ payload := &vpn.CreateGatewayConnectionPayload{}
+ err := toConnectionPayload(ctx, planModel, payload)
+ if err != nil {
+ return nil, err
+ }
+
+ // Terraform keeps the write-only field values in the config model - and they shouldn't leave this config model
+ // to make sure they don't end up being stored in the state. In the plan model the write-only field values are just
+ // empty. That's why we read everything from the plan model. Except for the field values which are marked as
+ // write-only in the schema. They are read from the config model.
+
+ // inline function to allow re-using the logic for tunnel1 & tunnel2 without being too confusing
+ getPresharedKey := func(planTunnelModel, configTunnelModel *TunnelModel) *string {
+ if !planTunnelModel.PreSharedKey.IsNull() && !planTunnelModel.PreSharedKey.IsUnknown() {
+ // handle the legacy fallback logic
+ return planTunnelModel.PreSharedKey.ValueStringPointer()
+ } else if (!configTunnelModel.PreSharedKeyWo.IsNull() && !configTunnelModel.PreSharedKeyWo.IsUnknown()) &&
+ (!planTunnelModel.PreSharedKeyWoVersion.IsNull() && !planTunnelModel.PreSharedKeyWoVersion.IsUnknown()) {
+ // the user is using the write-only field
+ return configTunnelModel.PreSharedKeyWo.ValueStringPointer()
+ }
+
+ return nil
+ }
+
+ payload.Tunnel1.PreSharedKey = getPresharedKey(planModel.Tunnel1, configModel.Tunnel1)
+ payload.Tunnel2.PreSharedKey = getPresharedKey(planModel.Tunnel2, configModel.Tunnel2)
+
+ return payload, nil
+}
+
+func toUpdatePayload(ctx context.Context, planModel, stateModel, configModel *Model) (*vpn.UpdateGatewayConnectionPayload, error) {
+ if planModel == nil {
+ return nil, fmt.Errorf("nil plan model")
+ }
+ if stateModel == nil {
+ return nil, fmt.Errorf("nil state model")
+ }
+ if configModel == nil {
+ return nil, fmt.Errorf("nil config model")
+ }
+
+ payload := &vpn.UpdateGatewayConnectionPayload{}
+ err := toConnectionPayload(ctx, planModel, payload)
+ if err != nil {
+ return nil, err
+ }
+
+ // Terraform keeps the write-only field values in the config model - and they shouldn't leave this config model
+ // to make sure they don't end up being stored in the state. In the plan model the write-only field values are just
+ // empty. That's why we read everything from the plan model. Except for the field values which are marked as
+ // write-only in the schema. They are read from the config model.
+
+ // inline function to allow re-using the logic for tunnel1 & tunnel2 without being too confusing
+ getPresharedKey := func(planTunnelModel, stateTunnelModel, configTunnelModel *TunnelModel) *string {
+ if !planTunnelModel.PreSharedKey.IsNull() {
+ // handle the legacy fallback logic
+ return planTunnelModel.PreSharedKey.ValueStringPointer()
+ } else if !configTunnelModel.PreSharedKeyWo.IsNull() {
+ // write-only field is set, handle the write-only and version logic
+
+ // check if the version changed between state (old) and plan (new)
+ if !planTunnelModel.PreSharedKeyWoVersion.Equal(stateTunnelModel.PreSharedKeyWoVersion) {
+ // the user bumped the version meaning we need to send the write-only field value to the API
+ return configTunnelModel.PreSharedKeyWo.ValueStringPointer()
+ }
+ }
+
+ return nil
+ }
+
+ payload.Tunnel1.PreSharedKey = getPresharedKey(planModel.Tunnel1, stateModel.Tunnel1, configModel.Tunnel1)
+ payload.Tunnel2.PreSharedKey = getPresharedKey(planModel.Tunnel2, stateModel.Tunnel2, configModel.Tunnel2)
+
+ return payload, nil
+}
+
+// toConnectionPayload builds the API payloads for create and update. It does NOT set presharedkey payload since it's
+// logic differs for create and update implementations
+func toConnectionPayload(ctx context.Context, model *Model, payload connectionPayload) error {
+ tunnel1, err := toTunnelPayload(model.Tunnel1)
+ if err != nil {
+ return fmt.Errorf("converting tunnel1: %w", err)
+ }
+
+ payload.SetTunnel1(*tunnel1)
+
+ tunnel2, err := toTunnelPayload(model.Tunnel2)
+ if err != nil {
+ return fmt.Errorf("converting tunnel2: %w", err)
+ }
+
+ payload.SetTunnel2(*tunnel2)
+
+ payload.SetDisplayName(model.DisplayName.ValueString())
+
+ if !tfutils.IsUndefined(model.Enabled) {
+ payload.SetEnabled(model.Enabled.ValueBool())
+ }
+
+ if !tfutils.IsUndefined(model.RemoteSubnet) {
+ remoteSubnets, err := tfutils.ListValueToStringSlice(model.RemoteSubnet)
+ if err != nil {
+ return fmt.Errorf("converting remote_subnet: %w", err)
+ }
+ payload.SetRemoteSubnets(remoteSubnets)
+ }
+
+ if !tfutils.IsUndefined(model.LocalSubnet) {
+ localSubnets, err := tfutils.ListValueToStringSlice(model.LocalSubnet)
+ if err != nil {
+ return fmt.Errorf("converting local_subnet: %w", err)
+ }
+ payload.SetLocalSubnets(localSubnets)
+ }
+
+ if !tfutils.IsUndefined(model.StaticRoutes) {
+ staticRoutes, err := tfutils.ListValueToStringSlice(model.StaticRoutes)
+ if err != nil {
+ return fmt.Errorf("converting static_routes: %w", err)
+ }
+ payload.SetStaticRoutes(staticRoutes)
+ }
+
+ labels, err := tfutils.LabelsToPayload(ctx, model.Labels)
+ if err != nil {
+ return err
+ }
+ payload.SetLabels(labels)
+
+ return nil
+}
+
+func toTunnelPayload(tunnel *TunnelModel) (*vpn.TunnelConfiguration, error) {
+ if tunnel == nil {
+ return nil, fmt.Errorf("nil tunnel model")
+ }
+
+ config := &vpn.TunnelConfiguration{
+ RemoteAddress: tunnel.RemoteAddress.ValueString(),
+ }
+
+ if tunnel.Phase1 != nil {
+ phase1 := vpn.TunnelConfigurationPhase1{}
+ err := toBasePhasePayload(&tunnel.Phase1.BasePhaseModel, &phase1)
+ if err != nil {
+ return nil, err
+ }
+ config.Phase1 = phase1
+ }
+
+ if tunnel.Phase2 != nil {
+ phase2 := vpn.TunnelConfigurationPhase2{
+ StartAction: conversion.StringValueToEnumPointer[vpn.TunnelConfigurationPhase2AllOfStartAction](tunnel.Phase2.StartAction),
+ DpdAction: conversion.StringValueToEnumPointer[vpn.TunnelConfigurationPhase2AllOfDpdAction](tunnel.Phase2.DpdAction),
+ }
+
+ err := toBasePhasePayload(&tunnel.Phase2.BasePhaseModel, &phase2)
+ if err != nil {
+ return nil, err
+ }
+
+ config.Phase2 = phase2
+ }
+
+ if tunnel.Peering != nil {
+ config.Peering = &vpn.PeeringConfig{
+ LocalAddress: new(tunnel.Peering.LocalAddress.ValueString()),
+ RemoteAddress: new(tunnel.Peering.RemoteAddress.ValueString()),
+ }
+ }
+
+ if tunnel.Bgp != nil {
+ config.Bgp = &vpn.BGPTunnelConfig{
+ RemoteAsn: tunnel.Bgp.RemoteAsn.ValueInt64(),
+ }
+ }
+
+ return config, nil
+}
+
+func toBasePhasePayload(phaseModel *BasePhaseModel, phasePayload BasePhasePayload) error {
+ if phaseModel == nil {
+ return nil
+ }
+
+ if !tfutils.IsUndefined(phaseModel.DhGroups) {
+ dhGroups, err := tfutils.ListValueToStringSlice(phaseModel.DhGroups)
+ if err != nil {
+ return fmt.Errorf("converting phase dh_groups: %w", err)
+ }
+ phasePayload.SetDhGroups(tfutils.Map(dhGroups, func(t string) vpn.PhaseDhGroupsInner {
+ return vpn.PhaseDhGroupsInner(t)
+ }))
+ }
+
+ encAlgs, err := tfutils.ListValueToStringSlice(phaseModel.EncryptionAlgorithms)
+ if err != nil {
+ return fmt.Errorf("converting phase encryption_algorithms: %w", err)
+ }
+ phasePayload.SetEncryptionAlgorithms(tfutils.Map(encAlgs, func(t string) vpn.PhaseEncryptionAlgorithmsInner {
+ return vpn.PhaseEncryptionAlgorithmsInner(t)
+ }))
+
+ intAlgs, err := tfutils.ListValueToStringSlice(phaseModel.IntegrityAlgorithms)
+ if err != nil {
+ return fmt.Errorf("converting phase integrity_algorithms: %w", err)
+ }
+ phasePayload.SetIntegrityAlgorithms(tfutils.Map(intAlgs, func(t string) vpn.PhaseIntegrityAlgorithmsInner {
+ return vpn.PhaseIntegrityAlgorithmsInner(t)
+ }))
+
+ if !tfutils.IsUndefined(phaseModel.RekeyTime) {
+ rekeyTime := phaseModel.RekeyTime.ValueInt32()
+ phasePayload.SetRekeyTime(rekeyTime)
+ }
+
+ return nil
+}
+
+func mapCommonFields(ctx context.Context, conn connectionResponse, model *CommonModel, 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 respConnectionId, _ := conn.GetIdOk(); respConnectionId != nil {
+ connectionId = *respConnectionId
+ } 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.GetDisplayName())
+ model.Region = types.StringValue(region)
+
+ enabled, _ := conn.GetEnabledOk()
+ model.Enabled = types.BoolPointerValue(enabled)
+
+ model.RemoteSubnet = types.ListNull(types.StringType)
+ if remoteSubnets, _ := conn.GetRemoteSubnetsOk(); remoteSubnets != nil {
+ list, diags := types.ListValueFrom(ctx, types.StringType, remoteSubnets)
+ if diags.HasError() {
+ return fmt.Errorf("mapping remote_subnet: %w", core.DiagsToError(diags))
+ }
+ model.RemoteSubnet = list
+ }
+
+ model.LocalSubnet = types.ListNull(types.StringType)
+ if localSubnets, _ := conn.GetLocalSubnetsOk(); localSubnets != nil {
+ list, diags := types.ListValueFrom(ctx, types.StringType, localSubnets)
+ if diags.HasError() {
+ return fmt.Errorf("mapping local_subnet: %w", core.DiagsToError(diags))
+ }
+ model.LocalSubnet = list
+ }
+
+ model.StaticRoutes = types.ListNull(types.StringType)
+ if staticRoutes, _ := conn.GetStaticRoutesOk(); staticRoutes != nil {
+ list, diags := types.ListValueFrom(ctx, types.StringType, staticRoutes)
+ if diags.HasError() {
+ return fmt.Errorf("mapping static_routes: %w", core.DiagsToError(diags))
+ }
+ model.StaticRoutes = list
+ }
+
+ respLabels, _ := conn.GetLabelsOk()
+ labels, err := tfutils.MapLabels(ctx, respLabels, model.Labels)
+ if err != nil {
+ return fmt.Errorf("mapping labels: %w", err)
+ }
+ model.Labels = labels
+
+ return nil
+}
+
+func mapResourceFields(ctx context.Context, conn connectionResponse, model *Model, region string) error {
+ err := mapCommonFields(ctx, conn, &model.CommonModel, region)
+ if err != nil {
+ return err
+ }
+
+ tunnel1 := conn.GetTunnel1()
+ if model.Tunnel1 == nil {
+ model.Tunnel1 = &TunnelModel{}
+ }
+ err = mapTunnel(ctx, &tunnel1, &model.Tunnel1.DataSourceTunnelModel)
+ if err != nil {
+ return fmt.Errorf("mapping tunnel1: %w", err)
+ }
+
+ tunnel2 := conn.GetTunnel2()
+ if model.Tunnel2 == nil {
+ model.Tunnel2 = &TunnelModel{}
+ }
+ err = mapTunnel(ctx, &tunnel2, &model.Tunnel2.DataSourceTunnelModel)
+ if err != nil {
+ return fmt.Errorf("mapping tunnel2: %w", err)
+ }
+
+ return nil
+}
+
+func mapTunnel(ctx context.Context, apiTunnel *vpn.TunnelConfiguration, tfTunnel *DataSourceTunnelModel) error {
+ if apiTunnel == nil {
+ return fmt.Errorf("apiTunnel can not be nil")
+ }
+ if tfTunnel == nil {
+ return fmt.Errorf("tfTunnel can not be nil")
+ }
+
+ tfTunnel.RemoteAddress = types.StringValue(apiTunnel.RemoteAddress)
+
+ basePhase1, err := mapBasePhase(ctx, &apiTunnel.Phase1)
+ if err != nil {
+ return err
+ }
+
+ tfTunnel.Phase1 = &Phase1Model{
+ BasePhaseModel: basePhase1,
+ }
+
+ basePhase2, err := mapBasePhase(ctx, &apiTunnel.Phase2)
+ if err != nil {
+ return err
+ }
+
+ tfTunnel.Phase2 = &Phase2Model{
+ BasePhaseModel: basePhase2,
+ StartAction: types.StringPointerValue((*string)(apiTunnel.Phase2.StartAction)),
+ DpdAction: types.StringPointerValue((*string)(apiTunnel.Phase2.DpdAction)),
+ }
+
+ tfTunnel.Peering = nil
+ if apiTunnel.Peering != nil {
+ tfTunnel.Peering = &PeeringConfigModel{
+ LocalAddress: types.StringPointerValue(apiTunnel.Peering.LocalAddress),
+ RemoteAddress: types.StringPointerValue(apiTunnel.Peering.RemoteAddress),
+ }
+ }
+
+ tfTunnel.Bgp = nil
+ if apiTunnel.Bgp != nil {
+ tfTunnel.Bgp = &BGPTunnelConfigModel{
+ RemoteAsn: types.Int64Value(apiTunnel.Bgp.RemoteAsn),
+ }
+ }
+
+ return nil
+}
+
+func mapBasePhase(ctx context.Context, apiPhase BasePhasePayload) (phase BasePhaseModel, err error) {
+ if apiPhase == nil {
+ return phase, fmt.Errorf("api phase can not be nil")
+ }
+
+ phase.DhGroups = types.ListNull(types.StringType)
+ if dhGroups, _ := apiPhase.GetDhGroupsOk(); dhGroups != nil {
+ list, diags := types.ListValueFrom(ctx, types.StringType, dhGroups)
+ if diags.HasError() {
+ return phase, fmt.Errorf("mapping base phase dh_groups: %w", core.DiagsToError(diags))
+ }
+ phase.DhGroups = list
+ }
+
+ phase.EncryptionAlgorithms = types.ListNull(types.StringType)
+ if encryptionAlgorithms, _ := apiPhase.GetEncryptionAlgorithmsOk(); encryptionAlgorithms != nil {
+ list, diags := types.ListValueFrom(ctx, types.StringType, encryptionAlgorithms)
+ if diags.HasError() {
+ return phase, fmt.Errorf("mapping base phase encryption_algorithms: %w", core.DiagsToError(diags))
+ }
+ phase.EncryptionAlgorithms = list
+ }
+
+ phase.IntegrityAlgorithms = types.ListNull(types.StringType)
+ if integrityAlgorithms, _ := apiPhase.GetIntegrityAlgorithmsOk(); integrityAlgorithms != nil {
+ list, diags := types.ListValueFrom(ctx, types.StringType, integrityAlgorithms)
+ if diags.HasError() {
+ return phase, fmt.Errorf("mapping base phase integrity_algorithms: %w", core.DiagsToError(diags))
+ }
+ phase.IntegrityAlgorithms = list
+ }
+
+ rekeyTime, _ := apiPhase.GetRekeyTimeOk()
+ phase.RekeyTime = types.Int32PointerValue(rekeyTime)
+
+ return phase, 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..40ca8a9d4
--- /dev/null
+++ b/stackit/internal/services/vpn/connection/resource_test.go
@@ -0,0 +1,914 @@
+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 fixtureTunnelResponse(mods ...func(m *vpn.TunnelConfiguration)) vpn.TunnelConfiguration {
+ resp := 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(),
+ },
+ }
+ for _, mod := range mods {
+ mod(&resp)
+ }
+ return resp
+}
+
+func fixtureConnectionResponse(mods ...func(m *vpn.ConnectionResponse)) *vpn.ConnectionResponse {
+ resp := &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"},
+ StaticRoutes: []string{"123.45.67.89"},
+ Tunnel1: fixtureTunnelResponse(),
+ Tunnel2: fixtureTunnelResponse(func(m *vpn.TunnelConfiguration) {
+ m.RemoteAddress = "203.0.113.2"
+ }),
+ }
+ for _, mod := range mods {
+ mod(resp)
+ }
+ return resp
+}
+
+func fixtureBasePhaseModel(mods ...func(m *BasePhaseModel)) BasePhaseModel {
+ resp := BasePhaseModel{
+ 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),
+ }
+ for _, mod := range mods {
+ mod(&resp)
+ }
+ return resp
+}
+
+func fixtureTunnelModel(mods ...func(m *TunnelModel)) *TunnelModel {
+ resp := &TunnelModel{
+ PreSharedKeyWoVersion: types.Int64Value(1),
+ DataSourceTunnelModel: DataSourceTunnelModel{
+ RemoteAddress: types.StringValue("203.0.113.1"),
+ Phase1: &Phase1Model{
+ BasePhaseModel: fixtureBasePhaseModel(),
+ },
+ Phase2: &Phase2Model{
+ BasePhaseModel: fixtureBasePhaseModel(func(m *BasePhaseModel) {
+ m.RekeyTime = types.Int32Value(3600)
+ }),
+ StartAction: types.StringValue("start"),
+ DpdAction: types.StringValue("restart"),
+ },
+ },
+ }
+ for _, mod := range mods {
+ mod(resp)
+ }
+ return resp
+}
+
+func fixtureModel(mods ...func(m *Model)) Model {
+ resp := Model{
+ CommonModel: CommonModel{
+ 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.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("123.45.67.89"),
+ }),
+ Labels: types.MapNull(types.StringType),
+ },
+ Tunnel1: fixtureTunnelModel(),
+ Tunnel2: fixtureTunnelModel(func(m *TunnelModel) {
+ m.RemoteAddress = types.StringValue("203.0.113.2")
+ }),
+ }
+ for _, mod := range mods {
+ mod(&resp)
+ }
+ return resp
+}
+
+func fixtureTunnelPayload(mods ...func(m *vpn.TunnelConfiguration)) vpn.TunnelConfiguration {
+ resp := vpn.TunnelConfiguration{
+ PreSharedKey: new("secret123-at-least-20-chars"),
+ 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(),
+ },
+ }
+ for _, mod := range mods {
+ mod(&resp)
+ }
+ return resp
+}
+
+func fixtureCreatePayload(mods ...func(m *vpn.CreateGatewayConnectionPayload)) *vpn.CreateGatewayConnectionPayload {
+ resp := &vpn.CreateGatewayConnectionPayload{
+ DisplayName: "test-connection",
+ RemoteSubnets: []string{
+ "10.0.0.0/16",
+ },
+ LocalSubnets: []string{
+ "192.168.0.0/24",
+ },
+ StaticRoutes: []string{
+ "123.45.67.89",
+ },
+ Tunnel1: fixtureTunnelPayload(),
+ Tunnel2: fixtureTunnelPayload(func(m *vpn.TunnelConfiguration) {
+ m.PreSharedKey = new("secret456-at-least-20-chars")
+ m.RemoteAddress = "203.0.113.2"
+ }),
+ Enabled: new(true),
+ Labels: &map[string]string{},
+ }
+ for _, mod := range mods {
+ mod(resp)
+ }
+ return resp
+}
+
+func fixtureUpdatePayload(mods ...func(m *vpn.UpdateGatewayConnectionPayload)) *vpn.UpdateGatewayConnectionPayload {
+ resp := &vpn.UpdateGatewayConnectionPayload{
+ DisplayName: "test-connection",
+ RemoteSubnets: []string{
+ "10.0.0.0/16",
+ },
+ LocalSubnets: []string{
+ "192.168.0.0/24",
+ },
+ StaticRoutes: []string{
+ "123.45.67.89",
+ },
+ Tunnel1: fixtureTunnelPayload(),
+ Tunnel2: fixtureTunnelPayload(func(m *vpn.TunnelConfiguration) {
+ m.PreSharedKey = new("secret456-at-least-20-chars")
+ m.RemoteAddress = "203.0.113.2"
+ }),
+ Enabled: new(true),
+ Labels: &map[string]string{},
+ }
+ for _, mod := range mods {
+ mod(resp)
+ }
+ return resp
+}
+
+func TestMapFields(t *testing.T) {
+ tests := []struct {
+ description string
+ input *vpn.ConnectionResponse
+ expected Model
+ isValid bool
+ }{
+ {
+ description: "basic_connection",
+ input: fixtureConnectionResponse(),
+ expected: fixtureModel(),
+ isValid: true,
+ },
+ {
+ description: "minimal_connection",
+ input: &vpn.ConnectionResponse{
+ Id: new("connection-id"),
+ Tunnel1: vpn.TunnelConfiguration{},
+ Tunnel2: vpn.TunnelConfiguration{},
+ },
+ expected: Model{
+ CommonModel: CommonModel{
+ 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(""),
+ RemoteSubnet: types.ListNull(types.StringType),
+ LocalSubnet: types.ListNull(types.StringType),
+ StaticRoutes: types.ListNull(types.StringType),
+ Labels: types.MapNull(types.StringType),
+ },
+ Tunnel1: &TunnelModel{
+ PreSharedKeyWoVersion: types.Int64Value(1),
+ DataSourceTunnelModel: DataSourceTunnelModel{
+ RemoteAddress: types.StringValue(""),
+ Phase1: &Phase1Model{
+ BasePhaseModel: BasePhaseModel{
+ DhGroups: types.ListNull(types.StringType),
+ EncryptionAlgorithms: types.ListNull(types.StringType),
+ IntegrityAlgorithms: types.ListNull(types.StringType),
+ },
+ },
+ Phase2: &Phase2Model{
+ BasePhaseModel: BasePhaseModel{
+ DhGroups: types.ListNull(types.StringType),
+ EncryptionAlgorithms: types.ListNull(types.StringType),
+ IntegrityAlgorithms: types.ListNull(types.StringType),
+ },
+ },
+ },
+ },
+ Tunnel2: &TunnelModel{
+ PreSharedKeyWoVersion: types.Int64Value(1),
+ DataSourceTunnelModel: DataSourceTunnelModel{
+ RemoteAddress: types.StringValue(""),
+ Phase1: &Phase1Model{
+ BasePhaseModel: BasePhaseModel{
+ DhGroups: types.ListNull(types.StringType),
+ EncryptionAlgorithms: types.ListNull(types.StringType),
+ IntegrityAlgorithms: types.ListNull(types.StringType),
+ },
+ },
+ Phase2: &Phase2Model{
+ BasePhaseModel: BasePhaseModel{
+ DhGroups: types.ListNull(types.StringType),
+ EncryptionAlgorithms: types.ListNull(types.StringType),
+ IntegrityAlgorithms: types.ListNull(types.StringType),
+ },
+ },
+ },
+ },
+ },
+ isValid: true,
+ },
+ {
+ description: "connection_with_static_routes_and_bgp",
+ input: fixtureConnectionResponse(func(m *vpn.ConnectionResponse) {
+ m.StaticRoutes = []string{"10.0.0.0/8"}
+ m.Tunnel1.Bgp = &vpn.BGPTunnelConfig{
+ RemoteAsn: 65000,
+ }
+ }),
+ expected: fixtureModel(func(m *Model) {
+ m.StaticRoutes = types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("10.0.0.0/8"),
+ })
+ m.Tunnel1.Bgp = &BGPTunnelConfigModel{
+ RemoteAsn: types.Int64Value(65000),
+ }
+ }),
+ isValid: true,
+ },
+ {
+ description: "multiple_static_routes",
+ input: fixtureConnectionResponse(func(m *vpn.ConnectionResponse) {
+ m.StaticRoutes = []string{"10.0.0.0/8", "172.16.0.0/12"}
+ }),
+ expected: fixtureModel(func(m *Model) {
+ m.StaticRoutes = types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("10.0.0.0/8"),
+ types.StringValue("172.16.0.0/12"),
+ })
+ }),
+ isValid: true,
+ },
+ {
+ description: "empty_labels",
+ input: fixtureConnectionResponse(func(m *vpn.ConnectionResponse) {
+ m.Labels = &map[string]string{}
+ }),
+ expected: fixtureModel(func(m *Model) {
+ m.Labels = types.MapNull(types.StringType)
+ }),
+ isValid: true,
+ },
+ {
+ description: "peering",
+ input: fixtureConnectionResponse(func(m *vpn.ConnectionResponse) {
+ m.Tunnel1.Peering = &vpn.PeeringConfig{
+ LocalAddress: new("123.45.67.89"),
+ RemoteAddress: new("98.76.54.32"),
+ }
+ }),
+ expected: fixtureModel(func(m *Model) {
+ m.Tunnel1.Peering = &PeeringConfigModel{
+ LocalAddress: types.StringValue("123.45.67.89"),
+ RemoteAddress: types.StringValue("98.76.54.32"),
+ }
+ }),
+ 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{
+ CommonModel: CommonModel{
+ 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 := mapResourceFields(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) {
+ type args struct {
+ planModel *Model
+ configModel *Model
+ }
+
+ tests := []struct {
+ description string
+ args args
+ expected *vpn.CreateGatewayConnectionPayload
+ isValid bool
+ }{
+ // NOTE: Before diving into these tests read the comments in the function implementation :)
+ {
+ description: "basic connection - no pre-shared key set",
+ args: args{
+ planModel: new(fixtureModel(func(m *Model) {
+ m.Tunnel1.PreSharedKey = types.StringNull()
+ m.Tunnel1.PreSharedKeyWo = types.StringNull()
+ m.Tunnel1.PreSharedKeyWoVersion = types.Int64Null()
+
+ m.Tunnel2.PreSharedKey = types.StringNull()
+ m.Tunnel2.PreSharedKeyWo = types.StringNull()
+ m.Tunnel2.PreSharedKeyWoVersion = types.Int64Null()
+ })),
+ configModel: &Model{
+ Tunnel1: &TunnelModel{},
+ Tunnel2: &TunnelModel{},
+ },
+ },
+ expected: fixtureCreatePayload(func(m *vpn.CreateGatewayConnectionPayload) {
+ m.Tunnel1.PreSharedKey = nil
+ m.Tunnel2.PreSharedKey = nil
+ }),
+ isValid: true,
+ },
+ {
+ description: "basic connection - pre-shared key set via legacy field",
+ args: args{
+ planModel: new(fixtureModel(func(m *Model) {
+ m.Tunnel1.PreSharedKey = types.StringValue("secret123-at-least-20-chars")
+ m.Tunnel1.PreSharedKeyWo = types.StringNull()
+ m.Tunnel1.PreSharedKeyWoVersion = types.Int64Null()
+
+ m.Tunnel2.PreSharedKey = types.StringValue("secret456-at-least-20-chars")
+ m.Tunnel2.PreSharedKeyWo = types.StringNull()
+ m.Tunnel2.PreSharedKeyWoVersion = types.Int64Null()
+ })),
+ configModel: &Model{},
+ },
+ expected: fixtureCreatePayload(func(m *vpn.CreateGatewayConnectionPayload) {
+ m.Tunnel1.PreSharedKey = new("secret123-at-least-20-chars")
+ m.Tunnel2.PreSharedKey = new("secret456-at-least-20-chars")
+ }),
+ isValid: true,
+ },
+ {
+ description: "basic connection - write only pre-shared key set together with write only version",
+ args: args{
+ planModel: new(fixtureModel(func(m *Model) {
+ m.Tunnel1.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel1.PreSharedKeyWo = types.StringNull()
+ m.Tunnel1.PreSharedKeyWoVersion = types.Int64Value(1)
+
+ m.Tunnel2.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel2.PreSharedKeyWo = types.StringNull()
+ m.Tunnel2.PreSharedKeyWoVersion = types.Int64Value(0)
+ })),
+ configModel: &Model{
+ Tunnel1: &TunnelModel{
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringValue("secret123-at-least-20-chars"),
+ },
+ Tunnel2: &TunnelModel{
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringValue("secret456-at-least-20-chars"),
+ },
+ },
+ },
+ expected: fixtureCreatePayload(func(m *vpn.CreateGatewayConnectionPayload) {
+ m.Tunnel1.PreSharedKey = new("secret123-at-least-20-chars")
+ m.Tunnel2.PreSharedKey = new("secret456-at-least-20-chars")
+ }),
+ isValid: true,
+ },
+ {
+ description: "basic connection - write only pre-shared key set but write only version not set",
+ args: args{
+ planModel: new(fixtureModel(func(m *Model) {
+ m.Tunnel1.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel1.PreSharedKeyWo = types.StringNull()
+ m.Tunnel1.PreSharedKeyWoVersion = types.Int64Null()
+
+ m.Tunnel2.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel2.PreSharedKeyWo = types.StringNull()
+ m.Tunnel2.PreSharedKeyWoVersion = types.Int64Null()
+ })),
+ configModel: &Model{
+ Tunnel1: &TunnelModel{
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringValue("secret123-at-least-20-chars"),
+ },
+ Tunnel2: &TunnelModel{
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringValue("secret456-at-least-20-chars"),
+ },
+ },
+ },
+ expected: fixtureCreatePayload(func(m *vpn.CreateGatewayConnectionPayload) {
+ // must be nil because the write only version is not set
+ m.Tunnel1.PreSharedKey = nil
+ m.Tunnel2.PreSharedKey = nil
+ }),
+ isValid: true,
+ },
+ {
+ description: "minimal_create",
+ args: args{
+ planModel: &Model{
+ Tunnel1: &TunnelModel{},
+ Tunnel2: &TunnelModel{},
+ },
+ configModel: &Model{
+ Tunnel1: &TunnelModel{},
+ Tunnel2: &TunnelModel{},
+ },
+ },
+ expected: &vpn.CreateGatewayConnectionPayload{
+ Labels: &map[string]string{},
+ },
+ isValid: true,
+ },
+ {
+ description: "plan model is nil",
+ args: args{
+ planModel: nil,
+ configModel: &Model{},
+ },
+ expected: nil,
+ isValid: false,
+ },
+ {
+ description: "config model is nil",
+ args: args{
+ planModel: &Model{},
+ configModel: nil,
+ },
+ expected: nil,
+ isValid: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ payload, err := toCreatePayload(context.Background(), tt.args.planModel, tt.args.configModel)
+
+ 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) {
+ type args struct {
+ planModel *Model
+ stateModel *Model
+ configModel *Model
+ }
+
+ tests := []struct {
+ description string
+ args args
+ expected *vpn.UpdateGatewayConnectionPayload
+ isValid bool
+ }{
+ // NOTE: Before diving into these tests read the comments in the function implementation :)
+ {
+ description: "basic update - update of pre-shared key legacy field",
+ args: args{
+ planModel: new(fixtureModel(func(m *Model) {
+ m.Tunnel1.PreSharedKey = types.StringValue("secret123-at-least-20-chars")
+ m.Tunnel1.PreSharedKeyWo = types.StringNull()
+ m.Tunnel1.PreSharedKeyWoVersion = types.Int64Value(1)
+
+ m.Tunnel2.PreSharedKey = types.StringValue("secret456-at-least-20-chars")
+ m.Tunnel2.PreSharedKeyWo = types.StringNull()
+ m.Tunnel2.PreSharedKeyWoVersion = types.Int64Value(1)
+ })),
+ stateModel: &Model{
+ Tunnel1: &TunnelModel{
+ PreSharedKey: types.StringValue("old-secret-123-foo-bar"),
+ PreSharedKeyWo: types.StringNull(),
+ PreSharedKeyWoVersion: types.Int64Null(),
+ },
+ Tunnel2: &TunnelModel{
+ PreSharedKey: types.StringValue("old-secret-456-foo-bar"),
+ PreSharedKeyWo: types.StringNull(),
+ PreSharedKeyWoVersion: types.Int64Null(),
+ },
+ },
+ configModel: &Model{},
+ },
+ expected: fixtureUpdatePayload(func(m *vpn.UpdateGatewayConnectionPayload) {
+ m.Tunnel1.PreSharedKey = new("secret123-at-least-20-chars")
+ m.Tunnel2.PreSharedKey = new("secret456-at-least-20-chars")
+ }),
+ isValid: true,
+ },
+ {
+ description: "basic update - from pre-shared key legacy field to write only field",
+ args: args{
+ planModel: new(fixtureModel(func(m *Model) {
+ m.Tunnel1.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel1.PreSharedKeyWo = types.StringNull()
+ m.Tunnel1.PreSharedKeyWoVersion = types.Int64Value(1)
+
+ m.Tunnel2.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel2.PreSharedKeyWo = types.StringNull()
+ m.Tunnel2.PreSharedKeyWoVersion = types.Int64Value(1)
+ })),
+ stateModel: &Model{
+ Tunnel1: &TunnelModel{
+ PreSharedKey: types.StringValue("old-secret-123-foo-bar"),
+ PreSharedKeyWo: types.StringNull(),
+ PreSharedKeyWoVersion: types.Int64Null(),
+ },
+ Tunnel2: &TunnelModel{
+ PreSharedKey: types.StringValue("old-secret-456-foo-bar"),
+ PreSharedKeyWo: types.StringNull(),
+ PreSharedKeyWoVersion: types.Int64Null(),
+ },
+ },
+ configModel: &Model{
+ Tunnel1: &TunnelModel{
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringValue("secret123-at-least-20-chars"),
+ },
+ Tunnel2: &TunnelModel{
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringValue("secret456-at-least-20-chars"),
+ },
+ },
+ },
+ expected: fixtureUpdatePayload(func(m *vpn.UpdateGatewayConnectionPayload) {
+ m.Tunnel1.PreSharedKey = new("secret123-at-least-20-chars")
+ m.Tunnel2.PreSharedKey = new("secret456-at-least-20-chars")
+ }),
+ isValid: true,
+ },
+ {
+ description: "basic update - pre-shared key was previously set via write-only field but version was not updated now",
+ args: args{
+ planModel: new(fixtureModel(func(m *Model) {
+ m.Tunnel1.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel1.PreSharedKeyWo = types.StringNull()
+ m.Tunnel1.PreSharedKeyWoVersion = types.Int64Value(1) // note: same version as in state model
+
+ m.Tunnel2.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel2.PreSharedKeyWo = types.StringNull()
+ m.Tunnel2.PreSharedKeyWoVersion = types.Int64Value(5) // note: same version as in state model
+ })),
+ stateModel: &Model{
+ Tunnel1: &TunnelModel{
+ PreSharedKey: types.StringNull(),
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringNull(),
+ PreSharedKeyWoVersion: types.Int64Value(1), // note: same version as in plan model
+ },
+ Tunnel2: &TunnelModel{
+ PreSharedKey: types.StringNull(),
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringNull(),
+ PreSharedKeyWoVersion: types.Int64Value(5), // note: same version as in plan model
+ },
+ },
+ configModel: &Model{
+ Tunnel1: &TunnelModel{
+ // new write-only field value is irrelevant because the version didn't change
+ PreSharedKeyWo: types.StringValue("foo-bar-something-new-123"),
+ },
+ Tunnel2: &TunnelModel{
+ // new write-only field value is irrelevant because the version didn't change
+ PreSharedKeyWo: types.StringValue("foo-bar-something-new-456"),
+ },
+ },
+ },
+ expected: fixtureUpdatePayload(func(m *vpn.UpdateGatewayConnectionPayload) {
+ // pre-shared key must be nil in the update payload since the WriteOnly Version didn't change between
+ // old (stateModel) and new (planModel)
+ m.Tunnel1.PreSharedKey = nil
+ m.Tunnel2.PreSharedKey = nil
+ }),
+ isValid: true,
+ },
+ {
+ description: "basic update - preshared key was previously set via write-only field and is updated now",
+ args: args{
+ planModel: new(fixtureModel(func(m *Model) {
+ m.Tunnel1.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel1.PreSharedKeyWo = types.StringNull()
+ m.Tunnel1.PreSharedKeyWoVersion = types.Int64Value(2)
+
+ m.Tunnel2.PreSharedKey = types.StringNull()
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ m.Tunnel2.PreSharedKeyWo = types.StringNull()
+ m.Tunnel2.PreSharedKeyWoVersion = types.Int64Value(4)
+ })),
+ stateModel: &Model{
+ Tunnel1: &TunnelModel{
+ PreSharedKey: types.StringNull(),
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringNull(),
+ PreSharedKeyWoVersion: types.Int64Value(1),
+ },
+ Tunnel2: &TunnelModel{
+ PreSharedKey: types.StringNull(),
+ // write-only fields are always null in the plan and struct model - they are/should only be present in the config model
+ PreSharedKeyWo: types.StringNull(),
+ PreSharedKeyWoVersion: types.Int64Value(5),
+ },
+ },
+ configModel: &Model{
+ Tunnel1: &TunnelModel{
+ PreSharedKeyWo: types.StringValue("foo-bar-something-new-123"),
+ },
+ Tunnel2: &TunnelModel{
+ PreSharedKeyWo: types.StringValue("foo-bar-something-new-456"),
+ },
+ },
+ },
+ expected: fixtureUpdatePayload(func(m *vpn.UpdateGatewayConnectionPayload) {
+ m.Tunnel1.PreSharedKey = new("foo-bar-something-new-123")
+ m.Tunnel2.PreSharedKey = new("foo-bar-something-new-456")
+ }),
+ isValid: true,
+ },
+ {
+ description: "minimal_update",
+ args: args{
+ planModel: &Model{
+ Tunnel1: &TunnelModel{},
+ Tunnel2: &TunnelModel{},
+ },
+ stateModel: &Model{},
+ configModel: &Model{
+ Tunnel1: &TunnelModel{},
+ Tunnel2: &TunnelModel{},
+ },
+ },
+ expected: &vpn.UpdateGatewayConnectionPayload{
+ Labels: &map[string]string{},
+ },
+ isValid: true,
+ },
+ {
+ description: "peering",
+ args: args{
+ planModel: new(fixtureModel(func(m *Model) {
+ m.Tunnel1.Peering = &PeeringConfigModel{
+ LocalAddress: types.StringValue("123.45.67.89"),
+ RemoteAddress: types.StringValue("98.76.54.32"),
+ }
+ })),
+ stateModel: &Model{},
+ configModel: &Model{
+ Tunnel1: &TunnelModel{},
+ Tunnel2: &TunnelModel{},
+ },
+ },
+ expected: fixtureUpdatePayload(func(m *vpn.UpdateGatewayConnectionPayload) {
+ m.Tunnel1.Peering = &vpn.PeeringConfig{
+ LocalAddress: new("123.45.67.89"),
+ RemoteAddress: new("98.76.54.32"),
+ }
+ m.Tunnel1.PreSharedKey = nil
+ m.Tunnel2.PreSharedKey = nil
+ }),
+ isValid: true,
+ },
+ {
+ description: "plan model is nil",
+ args: args{
+ planModel: nil,
+ stateModel: &Model{},
+ configModel: &Model{},
+ },
+ expected: nil,
+ isValid: false,
+ },
+ {
+ description: "state model is nil",
+ args: args{
+ planModel: &Model{},
+ stateModel: nil,
+ configModel: &Model{},
+ },
+ expected: nil,
+ isValid: false,
+ },
+ {
+ description: "config model is nil",
+ args: args{
+ planModel: &Model{},
+ stateModel: &Model{},
+ configModel: nil,
+ },
+ expected: nil,
+ isValid: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ payload, err := toUpdatePayload(context.Background(), tt.args.planModel, tt.args.stateModel, tt.args.configModel)
+
+ 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: fixtureTunnelModel(),
+ isValid: true,
+ },
+ {
+ description: "tunnel_with_bgp",
+ input: fixtureTunnelModel(func(m *TunnelModel) {
+ m.Bgp = &BGPTunnelConfigModel{
+ RemoteAsn: types.Int64Value(65000),
+ }
+ }),
+ isValid: true,
+ },
+ {
+ description: "empty_tunnel",
+ input: &TunnelModel{},
+ isValid: true,
+ },
+ {
+ description: "nil_tunnel",
+ input: nil,
+ isValid: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ config, err := toTunnelPayload(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..2fc8374eb
--- /dev/null
+++ b/stackit/internal/services/vpn/testdata/connection-max.tf
@@ -0,0 +1,87 @@
+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
+ # in the MIN test we use the legacy field, in the MAX test the write-only field
+ 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
+ # in the MIN test we use the legacy field, in the MAX test the write-only field
+ 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..e7e095c79
--- /dev/null
+++ b/stackit/internal/services/vpn/testdata/connection-min.tf
@@ -0,0 +1,48 @@
+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
+ # in the MIN test we use the legacy field, in the MAX test the write-only field
+ pre_shared_key = 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
+ # in the MIN test we use the legacy field, in the MAX test the write-only field
+ pre_shared_key = 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 387a5ed1f..3fa9693ba 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,71 @@ var gatewayMaxVarsUpdated2 = func() config.Variables {
return updated
}()
+var connectionMinVars = func() config.Variables {
+ vars := make(config.Variables, len(gatewayMinVars)+5)
+ maps.Copy(vars, gatewayMinVars)
+ 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
{
@@ -203,7 +274,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
{
@@ -351,40 +422,539 @@ 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"),
+ // 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"),
+ // 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", "tunnel2.pre_shared_key"},
+ },
+ },
+ })
+}
+
+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"),
+ // 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"),
+ // 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 {
+ if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && (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 {
+ if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && (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/internal/services/vpn/vpn_test.go b/stackit/internal/services/vpn/vpn_test.go
new file mode 100644
index 000000000..0f3d7bc4b
--- /dev/null
+++ b/stackit/internal/services/vpn/vpn_test.go
@@ -0,0 +1,84 @@
+package vpn
+
+import (
+ "fmt"
+ "regexp"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+
+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
+)
+
+// Test can't be moved to resource test file because of dependency cycle
+func TestConnectionResourceValidationExclusivePresharedKeyFields(t *testing.T) {
+ config := func(tunnel1PresharedKeyConfig string) string {
+ return fmt.Sprintf(`
+ provider "stackit" {
+ default_region = "eu01"
+ service_account_token = "mock-server-needs-no-auth"
+ }
+
+ resource "stackit_vpn_connection" "connection" {
+ project_id = "4e684f79-a12c-449d-aa89-bcd9d8aafaf2"
+ gateway_id = "3dee3fb9-59f0-4f97-8eeb-a4da37d05a00"
+ display_name = "foo"
+
+ tunnel1 = {
+ remote_address = "203.0.113.1"
+ %s
+ 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.2"
+ pre_shared_key = "secret-345-minimum-20-characters"
+ phase1 = {
+ dh_groups = ["ecp384"]
+ encryption_algorithms = ["aes256"]
+ integrity_algorithms = ["sha2_384"]
+ }
+ phase2 = {
+ dh_groups = ["ecp384"]
+ encryption_algorithms = ["aes256"]
+ integrity_algorithms = ["sha2_384"]
+ }
+ }
+ }
+`, tunnel1PresharedKeyConfig)
+ }
+
+ resource.UnitTest(t, resource.TestCase{
+ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ // FAIL - pre-shared key via write-only field without version
+ Config: config(`pre_shared_key_wo = "secret-123-minimum-20-characters"`),
+ ExpectError: regexp.MustCompile("Invalid Attribute Combination"),
+ },
+ {
+ // FAIL - pre-shared key via legacy field AND write-only field
+ Config: config(
+ `pre_shared_key = "secret-123-minimum-20-characters"
+ pre_shared_key_wo = "secret-123-minimum-20-characters"
+ pre_shared_key_wo_version = 1
+ `),
+ ExpectError: regexp.MustCompile("Invalid Attribute Combination"),
+ },
+ {
+ // FAIL - pre-shared key write-only field missing only version set
+ Config: config(`pre_shared_key_wo_version = 1`),
+ ExpectError: regexp.MustCompile("Invalid Attribute Combination"),
+ },
+ },
+ })
+}
diff --git a/stackit/provider.go b/stackit/provider.go
index 7c653f474..a195454d8 100644
--- a/stackit/provider.go
+++ b/stackit/provider.go
@@ -128,6 +128,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"
vpnGatewayStatus "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/gateway_status"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
@@ -758,6 +759,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
telemetryRouterDestination.NewTelemetryRouterDestinationDataSource,
telemetryLink.NewTelemetryLinkDataSource,
vpnGateway.NewVPNGatewayDataSource,
+ vpnConnection.NewVPNConnectionDataSource,
vpnGatewayStatus.NewVPNGatewayStatusDataSource,
}
dataSources = append(dataSources, customRole.NewCustomRoleDataSources()...)
@@ -858,6 +860,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()...)