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 8a93bbef9..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"
@@ -20,6 +21,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"
@@ -34,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{}
@@ -54,6 +61,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 +81,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 +99,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 +236,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,
@@ -431,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
@@ -525,6 +580,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 +629,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 +646,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 +707,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..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 (
@@ -182,6 +188,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) {
@@ -359,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,
@@ -385,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 486122d01..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" {}
@@ -9,9 +14,52 @@ variable "local_asn" {}
variable "override_advertised_routes" {}
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
@@ -22,6 +70,11 @@ resource "stackit_vpn_gateway" "gateway" {
tunnel2 = var.az_tunnel2
}
+ network_config = {
+ predefined_network_prefix = var.network_config_prefix
+ routing_table_id = stackit_routing_table.routing_table.routing_table_id
+ }
+
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..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,6 +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.0.0.0/28"),
"label_key": config.StringVariable("env"),
"label_value": config.StringVariable("test"),
}
@@ -274,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"])),
@@ -286,6 +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", 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"),
),
},
@@ -301,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"])),
@@ -314,6 +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", 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"),
@@ -332,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"])),
@@ -355,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"])),
@@ -367,15 +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", 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"])),
@@ -385,6 +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", 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"),
),
},
@@ -401,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,
@@ -600,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"])),
@@ -615,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"),
@@ -697,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"),
@@ -742,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"])),
@@ -753,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"),
@@ -802,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"),
@@ -851,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