diff --git a/docs/data-sources/vpn_gateway.md b/docs/data-sources/vpn_gateway.md
index ee514ae05..df9e4b757 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. Once the gateway is created, this attribute cannot be changed.
+- `routing_table_id` (String) Custom routing table ID for the VPN gateway.
diff --git a/docs/resources/vpn_gateway.md b/docs/resources/vpn_gateway.md
index bbd557e1b..6a88a5ea7 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. Once the gateway is created, this attribute cannot be changed.
+- `routing_table_id` (String) Custom routing table ID for the VPN gateway.
+
## Import
Import is supported using the following syntax:
diff --git a/stackit/internal/services/vpn/gateway/datasource.go b/stackit/internal/services/vpn/gateway/datasource.go
index eebfd3904..d2f75fabb 100644
--- a/stackit/internal/services/vpn/gateway/datasource.go
+++ b/stackit/internal/services/vpn/gateway/datasource.go
@@ -123,6 +123,20 @@ func (d *vpnGatewayDataSource) Schema(_ context.Context, _ datasource.SchemaRequ
},
},
},
+ "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,
+ },
+ },
+ },
"labels": schema.MapAttribute{
Description: schemaDescriptions["labels"],
Computed: true,
diff --git a/stackit/internal/services/vpn/gateway/resource.go b/stackit/internal/services/vpn/gateway/resource.go
index 8a93bbef9..b92c8c97f 100644
--- a/stackit/internal/services/vpn/gateway/resource.go
+++ b/stackit/internal/services/vpn/gateway/resource.go
@@ -16,10 +16,12 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"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-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 +56,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 +76,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 +94,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. Once the gateway is created, this attribute cannot be changed.",
+ "network_config_routing_table_id": "Custom routing table ID for the VPN gateway.",
+ "labels": "Map of custom labels (key-value string pairs).",
}
type gatewayResource struct {
@@ -215,6 +231,32 @@ func (r *gatewayResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
},
},
+ "network_config": schema.SingleNestedAttribute{
+ Description: schemaDescriptions["network_config"],
+ Optional: true,
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "predefined_network_prefix": schema.StringAttribute{
+ Description: schemaDescriptions["network_config_predefined_network_prefix"],
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ stringplanmodifier.RequiresReplace(),
+ },
+ Validators: []validator.String{
+ validate.CIDR(),
+ },
+ },
+ "routing_table_id": schema.StringAttribute{
+ Description: schemaDescriptions["network_config_routing_table_id"],
+ Optional: true,
+ },
+ },
+ PlanModifiers: []planmodifier.Object{
+ objectplanmodifier.UseStateForUnknown(),
+ },
+ },
"labels": schema.MapAttribute{
Description: schemaDescriptions["labels"],
Optional: true,
@@ -525,6 +567,20 @@ func toCreatePayload(ctx context.Context, model *Model) (*vpn.CreateGatewayPaylo
payload.Bgp = bgpConfig
}
+ if !tfutils.IsUndefined(model.NetworkConfig) {
+ var networkConfigModel NetworkConfigModel
+ diags := model.NetworkConfig.As(ctx, &networkConfigModel, basetypes.ObjectAsOptions{})
+ if diags.HasError() {
+ return nil, core.DiagsToError(diags)
+ }
+
+ networkConfig, err := toNetworkConfigPayload(&networkConfigModel)
+ if err != nil {
+ return nil, err
+ }
+ payload.NetworkConfig = networkConfig
+ }
+
labels, err := tfutils.LabelsToPayload(ctx, model.Labels)
if err != nil {
return nil, err
@@ -534,6 +590,23 @@ func toCreatePayload(ctx context.Context, model *Model) (*vpn.CreateGatewayPaylo
return payload, nil
}
+func toNetworkConfigPayload(model *NetworkConfigModel) (*vpn.NetworkConfig, error) {
+ networkConfig := &vpn.NetworkConfig{}
+ if !tfutils.IsUndefined(model.PredefinedNetworkPrefix) {
+ // The generated SDK types PredefinedNetworkPrefix as []string, but the API actually
+ // expects a plain string. Route it through AdditionalProperties to get the correct
+ // wire format until the upstream OpenAPI spec is fixed, see
+ // https://github.com/stackitcloud/stackit-api-specifications/issues/69.
+ networkConfig.AdditionalProperties = map[string]interface{}{
+ "predefinedNetworkPrefix": model.PredefinedNetworkPrefix.ValueString(),
+ }
+ }
+ if !tfutils.IsUndefined(model.RoutingTableId) {
+ networkConfig.RoutingTableId = model.RoutingTableId.ValueStringPointer()
+ }
+ return networkConfig, nil
+}
+
func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateGatewayPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
@@ -566,6 +639,20 @@ func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateGatewayPaylo
payload.Bgp = bgpConfig
}
+ if !tfutils.IsUndefined(model.NetworkConfig) {
+ var networkConfigModel NetworkConfigModel
+ diags := model.NetworkConfig.As(ctx, &networkConfigModel, basetypes.ObjectAsOptions{})
+ if diags.HasError() {
+ return nil, core.DiagsToError(diags)
+ }
+
+ networkConfig, err := toNetworkConfigPayload(&networkConfigModel)
+ if err != nil {
+ return nil, err
+ }
+ payload.NetworkConfig = networkConfig
+ }
+
labels, err := tfutils.LabelsToPayload(ctx, model.Labels)
if err != nil {
return nil, err
@@ -617,6 +704,32 @@ func mapFields(ctx context.Context, gateway *vpn.GatewayResponse, model *Model,
model.Bgp = bgpModel
}
+ if gateway.NetworkConfig != nil {
+ // The generated SDK types PredefinedNetworkPrefix as []string, but the API actually
+ // returns/expects a plain string. Take the first element until the upstream OpenAPI
+ // spec is fixed, see https://github.com/stackitcloud/stackit-api-specifications/issues/69.
+ predefinedNetworkPrefix := types.StringNull()
+ if len(gateway.NetworkConfig.PredefinedNetworkPrefix) > 0 {
+ predefinedNetworkPrefix = types.StringValue(gateway.NetworkConfig.PredefinedNetworkPrefix[0])
+ }
+
+ 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
+ } else {
+ model.NetworkConfig = types.ObjectNull(networkConfigTypes)
+ }
+
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..dd535b11b 100644
--- a/stackit/internal/services/vpn/gateway/resource_test.go
+++ b/stackit/internal/services/vpn/gateway/resource_test.go
@@ -159,6 +159,48 @@ func TestMapFields(t *testing.T) {
},
isValid: true,
},
+ {
+ 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: []string{"10.10.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.10.0.0/28"),
+ "routing_table_id": types.StringValue("routing-table-id"),
+ }),
+ Bgp: nil,
+ Labels: types.MapNull(types.StringType),
+ },
+ isValid: true,
+ },
{
description: "nil_response",
args: args{
@@ -268,6 +310,39 @@ func TestToCreatePayload(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.10.0.0/28"),
+ "routing_table_id": types.StringValue("routing-table-id"),
+ }),
+ },
+ expected: &vpn.CreateGatewayPayload{
+ DisplayName: "test-gateway",
+ PlanId: "p500",
+ RoutingType: vpn.RoutingType("ROUTE_BASED"),
+ AvailabilityZones: vpn.CreateGatewayPayloadAvailabilityZones{
+ Tunnel1: "eu01-1",
+ Tunnel2: "eu01-2",
+ },
+ NetworkConfig: &vpn.NetworkConfig{
+ AdditionalProperties: map[string]interface{}{
+ "predefinedNetworkPrefix": "10.10.0.0/28",
+ },
+ RoutingTableId: new("routing-table-id"),
+ },
+ Labels: &map[string]string{},
+ },
+ isValid: true,
+ },
{
description: "nil_model",
input: nil,
@@ -359,6 +434,36 @@ 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.StringNull(),
+ "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{
+ RoutingTableId: new("routing-table-id"),
+ },
+ Labels: &map[string]string{},
+ },
+ isValid: true,
+ },
{
description: "nil_model",
input: nil,
diff --git a/stackit/internal/services/vpn/testdata/gateway-max.tf b/stackit/internal/services/vpn/testdata/gateway-max.tf
index 486122d01..8fb43e5eb 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 "predefined_network_prefix" {}
resource "stackit_vpn_gateway" "gateway" {
project_id = var.project_id
@@ -27,6 +28,10 @@ resource "stackit_vpn_gateway" "gateway" {
override_advertised_routes = var.override_advertised_routes
}
+ network_config = {
+ predefined_network_prefix = var.predefined_network_prefix
+ }
+
labels = var.label_key == "" ? {} : {
(var.label_key) = var.label_value
}
diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go
index 18946237d..22aa77ab7 100644
--- a/stackit/internal/services/vpn/vpn_acc_test.go
+++ b/stackit/internal/services/vpn/vpn_acc_test.go
@@ -63,6 +63,7 @@ var gatewayMaxVars = config.Variables{
"override_advertised_routes": config.ListVariable(config.StringVariable("10.0.0.0/16"), config.StringVariable("192.168.0.0/24")),
"label_key": config.StringVariable("env"),
"label_value": config.StringVariable("test"),
+ "predefined_network_prefix": config.StringVariable("10.20.30.0/28"),
}
var gatewayMaxVarsUpdated = func() config.Variables {
@@ -285,6 +286,7 @@ func TestAccVpnGatewayResourceMax(t *testing.T) {
resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "availability_zones.tunnel2", testutil.ConvertConfigVariable(gatewayMaxVars["az_tunnel2"])),
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", "network_config.predefined_network_prefix", testutil.ConvertConfigVariable(gatewayMaxVars["predefined_network_prefix"])),
resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(gatewayMaxVars["label_key"]), testutil.ConvertConfigVariable(gatewayMaxVars["label_value"])),
resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"),
),
@@ -312,6 +314,7 @@ func TestAccVpnGatewayResourceMax(t *testing.T) {
resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "availability_zones.tunnel2", testutil.ConvertConfigVariable(gatewayMaxVars["az_tunnel2"])),
resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(gatewayMaxVars["local_asn"])),
testutil.CheckListAttr("data.stackit_vpn_gateway.gateway", "bgp.override_advertised_routes", gatewayMaxVars["override_advertised_routes"]),
+ resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "network_config.predefined_network_prefix", testutil.ConvertConfigVariable(gatewayMaxVars["predefined_network_prefix"])),
resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(gatewayMaxVars["label_key"]), testutil.ConvertConfigVariable(gatewayMaxVars["label_value"])),
resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway.gateway", "gateway_id"),
@@ -366,6 +369,7 @@ func TestAccVpnGatewayResourceMax(t *testing.T) {
resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "availability_zones.tunnel2", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["az_tunnel2"])),
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", "network_config.predefined_network_prefix", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["predefined_network_prefix"])),
resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels."+testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["label_key"]), testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["label_value"])),
resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"),
),
@@ -384,6 +388,7 @@ func TestAccVpnGatewayResourceMax(t *testing.T) {
resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "availability_zones.tunnel2", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["az_tunnel2"])),
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", "network_config.predefined_network_prefix", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated2["predefined_network_prefix"])),
resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "labels.#", "0"),
resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"),
),