From 9ee9804065033e303b0631693a1fc374c86238b3 Mon Sep 17 00:00:00 2001 From: Jonas Schlecht Date: Thu, 10 Sep 2026 09:45:53 +0200 Subject: [PATCH 1/2] feat(vpn): add networkConfig --- .../internal/services/vpn/gateway/resource.go | 103 +++++++++++++++++- .../services/vpn/gateway/resource_test.go | 82 ++++++++++++++ .../services/vpn/testdata/gateway-max.tf | 5 + stackit/internal/services/vpn/vpn_acc_test.go | 6 + 4 files changed, 195 insertions(+), 1 deletion(-) diff --git a/stackit/internal/services/vpn/gateway/resource.go b/stackit/internal/services/vpn/gateway/resource.go index 8a93bbef9..a60381ed5 100644 --- a/stackit/internal/services/vpn/gateway/resource.go +++ b/stackit/internal/services/vpn/gateway/resource.go @@ -20,6 +20,7 @@ import ( "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-framework/types/basetypes" "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" @@ -54,6 +55,16 @@ type BGPGatewayConfigModel struct { OverrideAdvertisedRoutes types.List `tfsdk:"override_advertised_routes"` } +type NetworkConfigModel struct { + PredefinedNetworkPrefix types.String `tfsdk:"predefined_network_prefix"` + RoutingTableId types.String `tfsdk:"routing_table_id"` +} + +var networkConfigTypes = map[string]attr.Type{ + "predefined_network_prefix": basetypes.StringType{}, + "routing_table_id": basetypes.StringType{}, +} + type Model struct { Id types.String `tfsdk:"id"` // needed by TF GatewayId types.String `tfsdk:"gateway_id"` @@ -64,6 +75,7 @@ type Model struct { RoutingType types.String `tfsdk:"routing_type"` AvailabilityZones *AvailabilityZonesModel `tfsdk:"availability_zones"` Bgp *BGPGatewayConfigModel `tfsdk:"bgp"` + NetworkConfig types.Object `tfsdk:"network_config"` Labels types.Map `tfsdk:"labels"` } @@ -81,7 +93,10 @@ var schemaDescriptions = map[string]string{ "bgp": fmt.Sprintf("BGP configuration. Only applicable when routing_type is %s.", vpn.ROUTINGTYPE_BGP_ROUTE_BASED), "bgp_local_asn": "Local ASN for BGP (private ASN range, 64512-4294967294).", "bgp_override_advertised_routes": "List of IPv4 CIDRs to advertise via BGP. If omitted, SNA network ranges are advertised.", - "labels": "Map of custom labels (key-value string pairs).", + "network_config": "Network configuration for the VPN gateway.", + "network_config_predefined_network_prefix": "The IPv4 network prefix (CIDR notation) allocated for the VPN gateway. Must have a prefix length of /28 or larger. Cannot be changed after the gateway is created.", + "network_config_routing_table_id": "Custom routing table ID for the VPN gateway. If omitted, a default routing table is assigned.", + "labels": "Map of custom labels (key-value string pairs).", } type gatewayResource struct { @@ -215,6 +230,34 @@ func (r *gatewayResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, }, + "network_config": schema.SingleNestedAttribute{ + Description: schemaDescriptions["network_config"], + Optional: true, + Attributes: map[string]schema.Attribute{ + "predefined_network_prefix": schema.StringAttribute{ + Description: schemaDescriptions["network_config_predefined_network_prefix"], + Optional: true, + Validators: []validator.String{ + validate.CIDR(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "routing_table_id": schema.StringAttribute{ + Description: schemaDescriptions["network_config_routing_table_id"], + Optional: true, + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, "labels": schema.MapAttribute{ Description: schemaDescriptions["labels"], Optional: true, @@ -525,6 +568,14 @@ func toCreatePayload(ctx context.Context, model *Model) (*vpn.CreateGatewayPaylo payload.Bgp = bgpConfig } + if !tfutils.IsUndefined(model.NetworkConfig) { + networkConfig, err := getNetworkConfigPayload(ctx, model) + if err != nil { + return nil, err + } + payload.NetworkConfig = &networkConfig + } + labels, err := tfutils.LabelsToPayload(ctx, model.Labels) if err != nil { return nil, err @@ -566,6 +617,14 @@ func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateGatewayPaylo payload.Bgp = bgpConfig } + if !tfutils.IsUndefined(model.NetworkConfig) { + networkConfig, err := getNetworkConfigPayload(ctx, model) + if err != nil { + return nil, err + } + payload.NetworkConfig = &networkConfig + } + labels, err := tfutils.LabelsToPayload(ctx, model.Labels) if err != nil { return nil, err @@ -575,6 +634,25 @@ func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateGatewayPaylo return payload, nil } +func getNetworkConfigPayload(ctx context.Context, model *Model) (vpn.NetworkConfig, error) { + var networkConfigModel NetworkConfigModel + diags := model.NetworkConfig.As(ctx, &networkConfigModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return vpn.NetworkConfig{}, core.DiagsToError(diags) + } + networkConfig := vpn.NetworkConfig{} + + if !tfutils.IsUndefined(networkConfigModel.PredefinedNetworkPrefix) { + networkConfig.PredefinedNetworkPrefix = networkConfigModel.PredefinedNetworkPrefix.ValueStringPointer() + } + + if !tfutils.IsUndefined(networkConfigModel.RoutingTableId) { + networkConfig.RoutingTableId = networkConfigModel.RoutingTableId.ValueStringPointer() + } + + return networkConfig, nil +} + func mapFields(ctx context.Context, gateway *vpn.GatewayResponse, model *Model, region string) error { if gateway == nil { return fmt.Errorf("response input is nil") @@ -617,6 +695,29 @@ func mapFields(ctx context.Context, gateway *vpn.GatewayResponse, model *Model, model.Bgp = bgpModel } + if gateway.NetworkConfig == nil { + model.NetworkConfig = types.ObjectNull(networkConfigTypes) + } else { + predefinedNetworkPrefix := types.StringNull() + if gateway.NetworkConfig.PredefinedNetworkPrefix != nil { + predefinedNetworkPrefix = types.StringValue(*gateway.NetworkConfig.PredefinedNetworkPrefix) + } + + routingTableId := types.StringNull() + if gateway.NetworkConfig.RoutingTableId != nil { + routingTableId = types.StringValue(*gateway.NetworkConfig.RoutingTableId) + } + + networkConfigObject, diags := types.ObjectValue(networkConfigTypes, map[string]attr.Value{ + "predefined_network_prefix": predefinedNetworkPrefix, + "routing_table_id": routingTableId, + }) + if diags.HasError() { + return fmt.Errorf("mapping network config: %w", core.DiagsToError(diags)) + } + model.NetworkConfig = networkConfigObject + } + labels, err := tfutils.MapLabels(ctx, gateway.Labels, model.Labels) if err != nil { return fmt.Errorf("mapping labels: %w", err) diff --git a/stackit/internal/services/vpn/gateway/resource_test.go b/stackit/internal/services/vpn/gateway/resource_test.go index 54571eb9d..d1ac89fda 100644 --- a/stackit/internal/services/vpn/gateway/resource_test.go +++ b/stackit/internal/services/vpn/gateway/resource_test.go @@ -182,6 +182,88 @@ func TestMapFields(t *testing.T) { expected: Model{}, isValid: false, }, + { + description: "with_network_config", + args: args{ + state: Model{ + ProjectId: types.StringValue(projectId), + }, + input: &vpn.GatewayResponse{ + Id: new("gateway-id"), + DisplayName: "test-gateway", + PlanId: "p500", + RoutingType: vpn.ROUTINGTYPE_ROUTE_BASED, + AvailabilityZones: vpn.GatewayAvailabilityZones{ + Tunnel1: "eu01-1", + Tunnel2: "eu01-2", + }, + NetworkConfig: &vpn.NetworkConfig{ + PredefinedNetworkPrefix: new("10.20.0.0/28"), + RoutingTableId: new("routing-table-id"), + }, + }, + }, + expected: Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s,%s", projectId, region, "gateway-id")), + ProjectId: types.StringValue(projectId), + Region: types.StringValue(region), + GatewayId: types.StringValue("gateway-id"), + DisplayName: types.StringValue("test-gateway"), + PlanId: types.StringValue("p500"), + RoutingType: types.StringValue("ROUTE_BASED"), + AvailabilityZones: &AvailabilityZonesModel{ + Tunnel1: types.StringValue("eu01-1"), + Tunnel2: types.StringValue("eu01-2"), + }, + NetworkConfig: types.ObjectValueMust(networkConfigTypes, map[string]attr.Value{ + "predefined_network_prefix": types.StringValue("10.20.0.0/28"), + "routing_table_id": types.StringValue("routing-table-id"), + }), + Labels: types.MapNull(types.StringType), + }, + isValid: true, + }, + { + description: "network_config_without_routing_table_id", + args: args{ + state: Model{ + ProjectId: types.StringValue(projectId), + }, + input: &vpn.GatewayResponse{ + Id: new("gateway-id"), + DisplayName: "test-gateway", + PlanId: "p500", + RoutingType: vpn.ROUTINGTYPE_ROUTE_BASED, + AvailabilityZones: vpn.GatewayAvailabilityZones{ + Tunnel1: "eu01-1", + Tunnel2: "eu01-2", + }, + NetworkConfig: &vpn.NetworkConfig{ + PredefinedNetworkPrefix: new("10.20.0.0/28"), + }, + }, + }, + expected: Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s,%s", projectId, region, "gateway-id")), + ProjectId: types.StringValue(projectId), + Region: types.StringValue(region), + GatewayId: types.StringValue("gateway-id"), + DisplayName: types.StringValue("test-gateway"), + PlanId: types.StringValue("p500"), + RoutingType: types.StringValue("ROUTE_BASED"), + AvailabilityZones: &AvailabilityZonesModel{ + Tunnel1: types.StringValue("eu01-1"), + Tunnel2: types.StringValue("eu01-2"), + }, + NetworkConfig: types.ObjectValueMust(networkConfigTypes, map[string]attr.Value{ + "predefined_network_prefix": types.StringValue("10.20.0.0/28"), + "routing_table_id": types.StringNull(), + }), + + Labels: types.MapNull(types.StringType), + }, + isValid: true, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { diff --git a/stackit/internal/services/vpn/testdata/gateway-max.tf b/stackit/internal/services/vpn/testdata/gateway-max.tf index 486122d01..6283c1140 100644 --- a/stackit/internal/services/vpn/testdata/gateway-max.tf +++ b/stackit/internal/services/vpn/testdata/gateway-max.tf @@ -9,6 +9,7 @@ variable "local_asn" {} variable "override_advertised_routes" {} variable "label_key" {} variable "label_value" {} +variable "network_config_prefix" {} resource "stackit_vpn_gateway" "gateway" { project_id = var.project_id @@ -22,6 +23,10 @@ resource "stackit_vpn_gateway" "gateway" { tunnel2 = var.az_tunnel2 } + network_config = { + predefined_network_prefix = [var.network_config_prefix] + } + bgp = { local_asn = var.local_asn override_advertised_routes = var.override_advertised_routes diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go index 18946237d..0024477b6 100644 --- a/stackit/internal/services/vpn/vpn_acc_test.go +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -61,6 +61,7 @@ var gatewayMaxVars = config.Variables{ "az_tunnel2": config.StringVariable("eu01-2"), "local_asn": config.IntegerVariable(65000), "override_advertised_routes": config.ListVariable(config.StringVariable("10.0.0.0/16"), config.StringVariable("192.168.0.0/24")), + "network_config_prefix": config.StringVariable("10.20.0.0/28"), "label_key": config.StringVariable("env"), "label_value": config.StringVariable("test"), } @@ -286,6 +287,8 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(gatewayMaxVars["local_asn"])), testutil.CheckListAttr("stackit_vpn_gateway.gateway", "bgp.override_advertised_routes", gatewayMaxVars["override_advertised_routes"]), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(gatewayMaxVars["label_key"]), testutil.ConvertConfigVariable(gatewayMaxVars["label_value"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix.0", testutil.ConvertConfigVariable(gatewayMaxVars["network_config_prefix"])), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "network_config.routing_table_id"), resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), ), }, @@ -314,6 +317,7 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { testutil.CheckListAttr("data.stackit_vpn_gateway.gateway", "bgp.override_advertised_routes", gatewayMaxVars["override_advertised_routes"]), resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(gatewayMaxVars["label_key"]), testutil.ConvertConfigVariable(gatewayMaxVars["label_value"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix.0", testutil.ConvertConfigVariable(gatewayMaxVars["network_config_prefix"])), resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway.gateway", "gateway_id"), resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "region", "stackit_vpn_gateway.gateway", "region"), @@ -367,6 +371,7 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["local_asn"])), testutil.CheckListAttr("stackit_vpn_gateway.gateway", "bgp.override_advertised_routes", gatewayMaxVarsUpdated["override_advertised_routes"]), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["label_key"]), testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["label_value"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix.0", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["network_config_prefix"])), resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), ), }, @@ -385,6 +390,7 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["local_asn"])), testutil.CheckListAttr("stackit_vpn_gateway.gateway", "bgp.override_advertised_routes", gatewayMaxVarsUpdated2["override_advertised_routes"]), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels.#", "0"), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix.0", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["network_config_prefix"])), resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), ), }, From d70130efdc6ceaa36360deb1c769721dacecbed5 Mon Sep 17 00:00:00 2001 From: Jonas Schlecht Date: Fri, 11 Sep 2026 08:59:24 +0200 Subject: [PATCH 2/2] test(vpn): add acceptance tests for new fields --- docs/data-sources/vpn_gateway.md | 10 +++ docs/resources/vpn_gateway.md | 10 +++ go.mod | 1 - go.sum | 2 - .../services/vpn/gateway/datasource.go | 14 +++ .../internal/services/vpn/gateway/resource.go | 16 +++- .../services/vpn/gateway/resource_test.go | 87 +++++++++++++++++++ .../services/vpn/testdata/gateway-max.tf | 54 +++++++++++- stackit/internal/services/vpn/vpn_acc_test.go | 84 +++++++++++------- 9 files changed, 240 insertions(+), 38 deletions(-) diff --git a/docs/data-sources/vpn_gateway.md b/docs/data-sources/vpn_gateway.md index ee514ae05..4809647b9 100644 --- a/docs/data-sources/vpn_gateway.md +++ b/docs/data-sources/vpn_gateway.md @@ -34,6 +34,7 @@ data "stackit_vpn_gateway" "example" { - `display_name` (String) A user-friendly name for the VPN gateway. - `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`gateway_id`". - `labels` (Map of String) Map of custom labels (key-value string pairs). +- `network_config` (Attributes) Network configuration for the VPN gateway. (see [below for nested schema](#nestedatt--network_config)) - `plan_id` (String) The service plan identifier (e.g. `p500`). For guidance on finding available plans, see [List available service plans](https://docs.stackit.cloud/products/network/connectivity-hybrid-multi-cloud/vpn/getting-started/gateway-create/#list-available-service-plans). - `region` (String) STACKIT region name the resource is located in. If not defined, the provider region is used. - `routing_type` (String) Routing architecture. Possible values are: `POLICY_BASED`, `ROUTE_BASED`, `BGP_ROUTE_BASED`. @@ -54,3 +55,12 @@ Read-Only: - `local_asn` (Number) Local ASN for BGP (private ASN range, 64512-4294967294). - `override_advertised_routes` (List of String) List of IPv4 CIDRs to advertise via BGP. If omitted, SNA network ranges are advertised. + + + +### Nested Schema for `network_config` + +Read-Only: + +- `predefined_network_prefix` (String) The IPv4 network prefix (CIDR notation) allocated for the VPN gateway. Must have a prefix length of /28 or larger. Cannot be changed after the gateway is created. +- `routing_table_id` (String) Custom routing table ID for the VPN gateway. If omitted, a default routing table is assigned. diff --git a/docs/resources/vpn_gateway.md b/docs/resources/vpn_gateway.md index bbd557e1b..17ce01cb3 100644 --- a/docs/resources/vpn_gateway.md +++ b/docs/resources/vpn_gateway.md @@ -41,6 +41,7 @@ resource "stackit_vpn_gateway" "example" { - `bgp` (Attributes) BGP configuration. Only applicable when routing_type is BGP_ROUTE_BASED. (see [below for nested schema](#nestedatt--bgp)) - `labels` (Map of String) Map of custom labels (key-value string pairs). +- `network_config` (Attributes) Network configuration for the VPN gateway. (see [below for nested schema](#nestedatt--network_config)) - `region` (String) STACKIT region name the resource is located in. If not defined, the provider region is used. ### Read-Only @@ -68,6 +69,15 @@ Optional: - `override_advertised_routes` (List of String) List of IPv4 CIDRs to advertise via BGP. If omitted, SNA network ranges are advertised. + + +### Nested Schema for `network_config` + +Optional: + +- `predefined_network_prefix` (String) The IPv4 network prefix (CIDR notation) allocated for the VPN gateway. Must have a prefix length of /28 or larger. Cannot be changed after the gateway is created. +- `routing_table_id` (String) Custom routing table ID for the VPN gateway. If omitted, a default routing table is assigned. + ## Import Import is supported using the following syntax: diff --git a/go.mod b/go.mod index f1f0c1466..447b6ba8e 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/hashicorp/terraform-plugin-log v0.11.0 github.com/hashicorp/terraform-plugin-testing v1.16.0 github.com/stackitcloud/stackit-sdk-go/core v0.27.0 - github.com/stackitcloud/stackit-sdk-go/experimental v0.2.0 github.com/stackitcloud/stackit-sdk-go/services/alb v0.17.2 github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.4 github.com/stackitcloud/stackit-sdk-go/services/automation v0.1.1 diff --git a/go.sum b/go.sum index fc720ea03..9644d3f20 100644 --- a/go.sum +++ b/go.sum @@ -155,8 +155,6 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/stackitcloud/stackit-sdk-go/core v0.27.0 h1:7lc6qStcFDFf8zHP4ORPa9joybw+Sa2LtB6BVjKQ+ek= github.com/stackitcloud/stackit-sdk-go/core v0.27.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= -github.com/stackitcloud/stackit-sdk-go/experimental v0.2.0 h1:xRgrDL0jZ9otmF+tgSJQEcMc8b7WUJJ9UdFtX9GBQSY= -github.com/stackitcloud/stackit-sdk-go/experimental v0.2.0/go.mod h1:ebvgQJYWApj5Ktk3QfPzvDpKdghrtZcKMpWA7R3NvPs= github.com/stackitcloud/stackit-sdk-go/services/alb v0.17.2 h1:OAMCll/+6FTB65+H+BVyqJDrELUGdkMXggnveBl1O7I= github.com/stackitcloud/stackit-sdk-go/services/alb v0.17.2/go.mod h1:zEx3JFbwg1VLB9uKa2P4BA2DdypghX1FT8mbJU+msjE= github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.4 h1:KNEcAPj66Asb00V7f6v3Ku2d6ybP8PL1IwFQubUuxc0= diff --git a/stackit/internal/services/vpn/gateway/datasource.go b/stackit/internal/services/vpn/gateway/datasource.go index eebfd3904..055c197df 100644 --- a/stackit/internal/services/vpn/gateway/datasource.go +++ b/stackit/internal/services/vpn/gateway/datasource.go @@ -128,6 +128,20 @@ func (d *vpnGatewayDataSource) Schema(_ context.Context, _ datasource.SchemaRequ Computed: true, ElementType: types.StringType, }, + "network_config": schema.SingleNestedAttribute{ + Description: schemaDescriptions["network_config"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "predefined_network_prefix": schema.StringAttribute{ + Description: schemaDescriptions["network_config_predefined_network_prefix"], + Computed: true, + }, + "routing_table_id": schema.StringAttribute{ + Description: schemaDescriptions["network_config_routing_table_id"], + Computed: true, + }, + }, + }, }, } } diff --git a/stackit/internal/services/vpn/gateway/resource.go b/stackit/internal/services/vpn/gateway/resource.go index a60381ed5..96f07a4b8 100644 --- a/stackit/internal/services/vpn/gateway/resource.go +++ b/stackit/internal/services/vpn/gateway/resource.go @@ -7,6 +7,7 @@ import ( "net/http" "regexp" "strings" + "time" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" @@ -35,6 +36,11 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) +const ( + updateGatewayAttempts = 6 + updateGatewayRetryDelay = 10 * time.Second +) + var ( _ resource.Resource = &gatewayResource{} _ resource.ResourceWithConfigure = &gatewayResource{} @@ -247,7 +253,7 @@ func (r *gatewayResource) Schema(_ context.Context, _ resource.SchemaRequest, re "routing_table_id": schema.StringAttribute{ Description: schemaDescriptions["network_config_routing_table_id"], Optional: true, - Computed: true, + // Computed: true, Validators: []validator.String{ validate.UUID(), validate.NoSeparator(), @@ -474,7 +480,13 @@ func (r *gatewayResource) Update(ctx context.Context, req resource.UpdateRequest return } - _, err = r.client.DefaultAPI.UpdateGateway(ctx, projectId, region, gatewayId).UpdateGatewayPayload(*payload).Execute() + retryConfig := tfutils.RetryConfig{ + Attempts: updateGatewayAttempts, + Delay: updateGatewayRetryDelay, + RetryStatusCodes: []int{http.StatusConflict}, + } + + _, err = tfutils.RetryRequest(ctx, r.client.DefaultAPI.UpdateGateway(ctx, projectId, region, gatewayId).UpdateGatewayPayload(*payload).Execute, retryConfig) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN gateway", err.Error()) return diff --git a/stackit/internal/services/vpn/gateway/resource_test.go b/stackit/internal/services/vpn/gateway/resource_test.go index d1ac89fda..fda619a5a 100644 --- a/stackit/internal/services/vpn/gateway/resource_test.go +++ b/stackit/internal/services/vpn/gateway/resource_test.go @@ -3,13 +3,19 @@ package gateway import ( "context" "fmt" + "net/http" "testing" + "testing/synctest" + "time" "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) var ( @@ -441,6 +447,37 @@ func TestToUpdatePayload(t *testing.T) { }, isValid: true, }, + { + description: "with_network_config", + input: &Model{ + DisplayName: types.StringValue("test-gateway"), + PlanId: types.StringValue("p500"), + RoutingType: types.StringValue("ROUTE_BASED"), + AvailabilityZones: &AvailabilityZonesModel{ + Tunnel1: types.StringValue("eu01-1"), + Tunnel2: types.StringValue("eu01-2"), + }, + NetworkConfig: types.ObjectValueMust(networkConfigTypes, map[string]attr.Value{ + "predefined_network_prefix": types.StringValue("10.0.0.0/28"), + "routing_table_id": types.StringValue("routing-table-id"), + }), + }, + expected: &vpn.UpdateGatewayPayload{ + DisplayName: "test-gateway", + PlanId: "p500", + RoutingType: vpn.RoutingType("ROUTE_BASED"), + AvailabilityZones: vpn.UpdateGatewayPayloadAvailabilityZones{ + Tunnel1: "eu01-1", + Tunnel2: "eu01-2", + }, + NetworkConfig: &vpn.NetworkConfig{ + PredefinedNetworkPrefix: new("10.0.0.0/28"), + RoutingTableId: new("routing-table-id"), + }, + Labels: &map[string]string{}, + }, + isValid: true, + }, { description: "nil_model", input: nil, @@ -467,3 +504,53 @@ func TestToUpdatePayload(t *testing.T) { }) } } + +func TestUpdateGatewayRetriesOnConflict(t *testing.T) { + tests := []struct { + description string + conflicts int + isValid bool + wantAttempts int + }{ + {"succeeds immediately", 0, true, 1}, + {"one conflict then success", 1, true, 2}, + {"conflicts until attempts used up", updateGatewayAttempts, false, updateGatewayAttempts}, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + attempts := 0 + client := &vpn.DefaultAPIServiceMock{ + UpdateGatewayExecuteMock: new(func(_ vpn.ApiUpdateGatewayRequest) (*vpn.GatewayResponse, error) { + attempts++ + if attempts <= tt.conflicts { + return nil, &oapierror.GenericOpenAPIError{StatusCode: http.StatusConflict} + } + return &vpn.GatewayResponse{}, nil + }), + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + retryConfig := tfutils.RetryConfig{ + Attempts: updateGatewayAttempts, + Delay: updateGatewayRetryDelay, + RetryStatusCodes: []int{http.StatusConflict}, + } + + _, err := tfutils.RetryRequest(ctx, client.UpdateGateway(ctx, projectId, region, "gateway-id").UpdateGatewayPayload(vpn.UpdateGatewayPayload{}).Execute, retryConfig) + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if !tt.isValid && err == nil { + t.Fatal("Should have failed") + } + if attempts != tt.wantAttempts { + t.Fatalf("Expected %d attempts, got %d", tt.wantAttempts, attempts) + } + }) + }) + } +} diff --git a/stackit/internal/services/vpn/testdata/gateway-max.tf b/stackit/internal/services/vpn/testdata/gateway-max.tf index 6283c1140..947823a74 100644 --- a/stackit/internal/services/vpn/testdata/gateway-max.tf +++ b/stackit/internal/services/vpn/testdata/gateway-max.tf @@ -1,4 +1,9 @@ -variable "project_id" {} +variable "organization_id" {} +variable "parent_container_id" {} +variable "owner_email" {} +variable "network_area_name" {} +variable "project_name" {} +variable "routing_table_name" {} variable "region" {} variable "display_name" {} variable "plan_id" {} @@ -11,8 +16,50 @@ variable "label_key" {} variable "label_value" {} variable "network_config_prefix" {} +resource "stackit_network_area" "network_area" { + organization_id = var.organization_id + name = var.network_area_name + labels = { + "preview/routingtables" = "true" + } +} + +resource "stackit_resourcemanager_project" "project" { + parent_container_id = var.parent_container_id + name = var.project_name + labels = { + networkArea = stackit_network_area.network_area.network_area_id + } + owner_email = var.owner_email + + depends_on = [stackit_network_area_region.network_area_region] +} + +resource "stackit_network_area_region" "network_area_region" { + organization_id = var.organization_id + network_area_id = stackit_network_area.network_area.network_area_id + ipv4 = { + network_ranges = [ + { + prefix = "10.0.0.0/16" + }, + { + prefix = "10.2.2.0/24" + } + ] + transfer_network = "10.1.2.0/24" + } +} + +resource "stackit_routing_table" "routing_table" { + organization_id = stackit_network_area.network_area.organization_id + network_area_id = stackit_network_area.network_area.network_area_id + name = var.routing_table_name + depends_on = [stackit_network_area_region.network_area_region] +} + resource "stackit_vpn_gateway" "gateway" { - project_id = var.project_id + project_id = stackit_resourcemanager_project.project.project_id region = var.region display_name = var.display_name plan_id = var.plan_id @@ -24,7 +71,8 @@ resource "stackit_vpn_gateway" "gateway" { } network_config = { - predefined_network_prefix = [var.network_config_prefix] + predefined_network_prefix = var.network_config_prefix + routing_table_id = stackit_routing_table.routing_table.routing_table_id } bgp = { diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go index 0024477b6..3a0bcd533 100644 --- a/stackit/internal/services/vpn/vpn_acc_test.go +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -52,7 +52,12 @@ var gatewayMinVarsUpdated = func() config.Variables { }() var gatewayMaxVars = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), + "organization_id": config.StringVariable(testutil.OrganizationId), + "parent_container_id": config.StringVariable(testutil.TestProjectParentContainerID), + "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), + "network_area_name": config.StringVariable("vpn-na-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "project_name": config.StringVariable("vpn-proj-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "routing_table_name": config.StringVariable("vpn-rt-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), "region": config.StringVariable(testutil.Region), "display_name": config.StringVariable("vpn-gw-acc-test-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), "plan_id": config.StringVariable("p500"), @@ -61,7 +66,7 @@ var gatewayMaxVars = config.Variables{ "az_tunnel2": config.StringVariable("eu01-2"), "local_asn": config.IntegerVariable(65000), "override_advertised_routes": config.ListVariable(config.StringVariable("10.0.0.0/16"), config.StringVariable("192.168.0.0/24")), - "network_config_prefix": config.StringVariable("10.20.0.0/28"), + "network_config_prefix": config.StringVariable("10.0.0.0/28"), "label_key": config.StringVariable("env"), "label_value": config.StringVariable("test"), } @@ -275,9 +280,9 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { // Creation { ConfigVariables: gatewayMaxVars, - Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig), + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentRoutingTables).BuildProviderConfig(), gatewayMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttrPair("stackit_vpn_gateway.gateway", "project_id", "stackit_resourcemanager_project.project", "project_id"), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "region", testutil.ConvertConfigVariable(gatewayMaxVars["region"])), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMaxVars["display_name"])), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(gatewayMaxVars["plan_id"])), @@ -287,8 +292,8 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(gatewayMaxVars["local_asn"])), testutil.CheckListAttr("stackit_vpn_gateway.gateway", "bgp.override_advertised_routes", gatewayMaxVars["override_advertised_routes"]), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(gatewayMaxVars["label_key"]), testutil.ConvertConfigVariable(gatewayMaxVars["label_value"])), - resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix.0", testutil.ConvertConfigVariable(gatewayMaxVars["network_config_prefix"])), - resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "network_config.routing_table_id"), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix", testutil.ConvertConfigVariable(gatewayMaxVars["network_config_prefix"])), + resource.TestCheckResourceAttrPair("stackit_vpn_gateway.gateway", "network_config.routing_table_id", "stackit_routing_table.routing_table", "routing_table_id"), resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), ), }, @@ -304,10 +309,10 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { gateway_id = stackit_vpn_gateway.gateway.gateway_id } `, - testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig, + testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentRoutingTables).BuildProviderConfig(), gatewayMaxConfig, ), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "project_id", testutil.ConvertConfigVariable(gatewayMaxVars["project_id"])), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "project_id", "stackit_resourcemanager_project.project", "project_id"), resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMaxVars["display_name"])), resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(gatewayMaxVars["plan_id"])), resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "routing_type", testutil.ConvertConfigVariable(gatewayMaxVars["routing_type"])), @@ -317,7 +322,8 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { testutil.CheckListAttr("data.stackit_vpn_gateway.gateway", "bgp.override_advertised_routes", gatewayMaxVars["override_advertised_routes"]), resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(gatewayMaxVars["label_key"]), testutil.ConvertConfigVariable(gatewayMaxVars["label_value"])), - resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix.0", testutil.ConvertConfigVariable(gatewayMaxVars["network_config_prefix"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix", testutil.ConvertConfigVariable(gatewayMaxVars["network_config_prefix"])), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "network_config.routing_table_id", "stackit_routing_table.routing_table", "routing_table_id"), resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway.gateway", "gateway_id"), resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "region", "stackit_vpn_gateway.gateway", "region"), @@ -336,11 +342,11 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { gateway_id = stackit_vpn_gateway.gateway.gateway_id } `, - testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig, + testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentRoutingTables).BuildProviderConfig(), gatewayMaxConfig, ), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway_status.gateway", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), - resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway_status.gateway", "project_id", "stackit_resourcemanager_project.project", "project_id"), resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "region", testutil.Region), resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMaxVars["display_name"])), @@ -359,9 +365,9 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { // Update { ConfigVariables: gatewayMaxVarsUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig), + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentRoutingTables).BuildProviderConfig(), gatewayMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttrPair("stackit_vpn_gateway.gateway", "project_id", "stackit_resourcemanager_project.project", "project_id"), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "region", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["region"])), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["display_name"])), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["plan_id"])), @@ -371,16 +377,17 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["local_asn"])), testutil.CheckListAttr("stackit_vpn_gateway.gateway", "bgp.override_advertised_routes", gatewayMaxVarsUpdated["override_advertised_routes"]), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["label_key"]), testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["label_value"])), - resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix.0", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["network_config_prefix"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["network_config_prefix"])), + resource.TestCheckResourceAttrPair("stackit_vpn_gateway.gateway", "network_config.routing_table_id", "stackit_routing_table.routing_table", "routing_table_id"), resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), ), }, // Update step 2 - test removal of optional fields { ConfigVariables: gatewayMaxVarsUpdated2, - Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig), + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentRoutingTables).BuildProviderConfig(), gatewayMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttrPair("stackit_vpn_gateway.gateway", "project_id", "stackit_resourcemanager_project.project", "project_id"), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "region", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["region"])), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["display_name"])), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["plan_id"])), @@ -390,7 +397,8 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["local_asn"])), testutil.CheckListAttr("stackit_vpn_gateway.gateway", "bgp.override_advertised_routes", gatewayMaxVarsUpdated2["override_advertised_routes"]), resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels.#", "0"), - resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix.0", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["network_config_prefix"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["network_config_prefix"])), + resource.TestCheckResourceAttrPair("stackit_vpn_gateway.gateway", "network_config.routing_table_id", "stackit_routing_table.routing_table", "routing_table_id"), resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), ), }, @@ -407,7 +415,15 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute gateway_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, gatewayId), nil + projectId, ok := r.Primary.Attributes["project_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute project_id") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + region = testutil.Region + } + return fmt.Sprintf("%s,%s,%s", projectId, region, gatewayId), nil }, ImportState: true, ImportStateVerify: true, @@ -606,10 +622,10 @@ func TestAccVpnConnectionResourceMax(t *testing.T) { // 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), + Config: fmt.Sprintf("%s\n%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentRoutingTables).BuildProviderConfig(), gatewayMaxConfig, connectionMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( // Gateway - resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttrPair("stackit_vpn_gateway.gateway", "project_id", "stackit_resourcemanager_project.project", "project_id"), 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"])), @@ -621,7 +637,7 @@ func TestAccVpnConnectionResourceMax(t *testing.T) { 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.TestCheckResourceAttrPair("stackit_vpn_connection.connection", "project_id", "stackit_resourcemanager_project.project", "project_id"), 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"), @@ -703,10 +719,10 @@ func TestAccVpnConnectionResourceMax(t *testing.T) { connection_id = stackit_vpn_connection.connection.connection_id } `, - testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig, connectionMaxConfig, + testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentRoutingTables).BuildProviderConfig(), gatewayMaxConfig, connectionMaxConfig, ), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_vpn_connection.connection", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttrPair("data.stackit_vpn_connection.connection", "project_id", "stackit_resourcemanager_project.project", "project_id"), 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"), @@ -748,10 +764,10 @@ func TestAccVpnConnectionResourceMax(t *testing.T) { // 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), + Config: fmt.Sprintf("%s\n%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentRoutingTables).BuildProviderConfig(), gatewayMaxConfig, connectionMaxConfig), Check: resource.ComposeAggregateTestCheckFunc( // Gateway unchanged - resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttrPair("stackit_vpn_gateway.gateway", "project_id", "stackit_resourcemanager_project.project", "project_id"), 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"])), @@ -759,7 +775,7 @@ func TestAccVpnConnectionResourceMax(t *testing.T) { 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.TestCheckResourceAttrPair("stackit_vpn_connection.connection", "project_id", "stackit_resourcemanager_project.project", "project_id"), 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"), @@ -808,13 +824,13 @@ func TestAccVpnConnectionResourceMax(t *testing.T) { // observable signal that the rotation was applied correctly. { ConfigVariables: connectionMaxVarsPskRotated, - Config: fmt.Sprintf("%s\n%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig, connectionMaxConfig), + Config: fmt.Sprintf("%s\n%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentRoutingTables).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.TestCheckResourceAttrPair("stackit_vpn_connection.connection", "project_id", "stackit_resourcemanager_project.project", "project_id"), 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"), @@ -857,9 +873,17 @@ func TestAccVpnConnectionResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute gateway_id") } + projectId, ok := r.Primary.Attributes["project_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute project_id") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + region = testutil.Region + } return fmt.Sprintf("%s,%s,%s,%s", - testutil.ProjectId, - testutil.Region, + projectId, + region, gatewayId, connectionId, ), nil