From 38faf3bad7ec40e7568e605733f2a6fabd53ed0f Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Tue, 28 Apr 2026 10:56:01 +0200 Subject: [PATCH 01/13] feat(vpn): add vpn to provider and core --- stackit/internal/core/core.go | 1 + stackit/provider.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index f8fa8f9f0..48eb04a1c 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -71,6 +71,7 @@ type ProviderData struct { ServiceEnablementCustomEndpoint string SfsCustomEndpoint string ServiceAccountCustomEndpoint string + VpnCustomEndpoint string EnableBetaResources bool Experiments []string diff --git a/stackit/provider.go b/stackit/provider.go index 1dc8da94c..5655278c2 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -197,6 +197,7 @@ type providerModel struct { SkeCustomEndpoint types.String `tfsdk:"ske_custom_endpoint"` SqlServerFlexCustomEndpoint types.String `tfsdk:"sqlserverflex_custom_endpoint"` TokenCustomEndpoint types.String `tfsdk:"token_custom_endpoint"` + VpnCustomEndpoint types.String `tfsdk:"vpn_custom_endpoint"` OIDCTokenRequestURL types.String `tfsdk:"oidc_request_url"` OIDCTokenRequestToken types.String `tfsdk:"oidc_request_token"` @@ -253,6 +254,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "service_enablement_custom_endpoint": "Custom endpoint for the Service Enablement API", "sfs_custom_endpoint": "Custom endpoint for the Stackit Filestorage API", "token_custom_endpoint": "Custom endpoint for the token API, which is used to request access tokens when using the key flow", + "vpn_custom_endpoint": "Custom endpoint for the VPN service", "enable_beta_resources": "Enable beta resources. Default is false.", "experiments": fmt.Sprintf("Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: %v", strings.Join(features.AvailableExperiments, ", ")), } @@ -458,6 +460,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["sfs_custom_endpoint"], }, + "vpn_custom_endpoint": schema.StringAttribute{ + Optional: true, + Description: descriptions["vpn_custom_endpoint"], + }, "token_custom_endpoint": schema.StringAttribute{ Optional: true, Description: descriptions["token_custom_endpoint"], @@ -544,6 +550,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.SfsCustomEndpoint, func(v string) { providerData.SfsCustomEndpoint = v }) setStringField(providerConfig.SkeCustomEndpoint, func(v string) { providerData.SKECustomEndpoint = v }) setStringField(providerConfig.SqlServerFlexCustomEndpoint, func(v string) { providerData.SQLServerFlexCustomEndpoint = v }) + setStringField(providerConfig.VpnCustomEndpoint, func(v string) { providerData.VpnCustomEndpoint = v }) if !(providerConfig.Experiments.IsUnknown() || providerConfig.Experiments.IsNull()) { var experimentValues []string From c7bc13af41e2aeede094c8e3c724a1a5ad10d42a Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Tue, 28 Apr 2026 11:42:18 +0200 Subject: [PATCH 02/13] feat(vpn): add utils --- go.mod | 1 + go.sum | 2 + stackit/internal/services/vpn/utils/utils.go | 32 +++++++ .../internal/services/vpn/utils/utils_test.go | 95 +++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 stackit/internal/services/vpn/utils/utils.go create mode 100644 stackit/internal/services/vpn/utils/utils_test.go diff --git a/go.mod b/go.mod index 207f2cfd4..6f9287c21 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/sfs v0.9.0 github.com/stackitcloud/stackit-sdk-go/services/ske v1.14.0 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.10.0 + github.com/stackitcloud/stackit-sdk-go/services/vpn v0.5.1 github.com/teambition/rrule-go v1.8.2 golang.org/x/mod v0.35.0 ) diff --git a/go.sum b/go.sum index fba53fc30..537671778 100644 --- a/go.sum +++ b/go.sum @@ -732,6 +732,8 @@ github.com/stackitcloud/stackit-sdk-go/services/ske v1.14.0 h1:Zy3yxmHzW+ydu1nae github.com/stackitcloud/stackit-sdk-go/services/ske v1.14.0/go.mod h1:TbqmZhLMofmfl+HhVl6oHYcI3zvXTm1vRjN3A/fOkM4= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.10.0 h1:angvO3z0TGqZtdwTDsG/tgTw9hxB76A6leUsiUXQtME= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.10.0/go.mod h1:AiUoMAqQcOlMgDtkVJlqI7P/VGD5xjN3dYjERGnwN/M= +github.com/stackitcloud/stackit-sdk-go/services/vpn v0.5.1 h1:qmfgch4YCMMstSgzDysCLhvaAhHpTf+EPy0wUUmxN70= +github.com/stackitcloud/stackit-sdk-go/services/vpn v0.5.1/go.mod h1:zcCQlA79aWlv2Voc34EgerEFgPGxkO7CE8Ilm+nG9Yw= github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/stackit/internal/services/vpn/utils/utils.go b/stackit/internal/services/vpn/utils/utils.go new file mode 100644 index 000000000..766f42ab3 --- /dev/null +++ b/stackit/internal/services/vpn/utils/utils.go @@ -0,0 +1,32 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/stackit-sdk-go/core/config" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *vpn.APIClient { + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(providerData.RoundTripper), + utils.UserAgentConfigOption(providerData.Version), + } + if providerData.VpnCustomEndpoint != "" { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.VpnCustomEndpoint)) + } else { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) + } + apiClient, err := vpn.NewAPIClient(apiClientConfigOptions...) + if err != nil { + core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) + return nil + } + + return apiClient +} diff --git a/stackit/internal/services/vpn/utils/utils_test.go b/stackit/internal/services/vpn/utils/utils_test.go new file mode 100644 index 000000000..400081971 --- /dev/null +++ b/stackit/internal/services/vpn/utils/utils_test.go @@ -0,0 +1,95 @@ +package utils + +import ( + "context" + "os" + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/diag" + sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" + "github.com/stackitcloud/stackit-sdk-go/core/config" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +const ( + testVersion = "1.2.3" + testCustomEndpoint = "https://vpn-custom-endpoint.api.stackit.cloud" +) + +func TestConfigureClient(t *testing.T) { + /* mock authentication by setting service account token env variable */ + os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") + if err != nil { + t.Errorf("error setting env variable: %v", err) + } + + type args struct { + providerData *core.ProviderData + } + tests := []struct { + name string + args args + wantErr bool + expected *vpn.APIClient + }{ + { + name: "default endpoint", + args: args{ + providerData: &core.ProviderData{ + Version: testVersion, + }, + }, + expected: func() *vpn.APIClient { + apiClient, err := vpn.NewAPIClient( + config.WithRegion("eu01"), + utils.UserAgentConfigOption(testVersion), + ) + if err != nil { + t.Errorf("error configuring client: %v", err) + } + return apiClient + }(), + wantErr: false, + }, + { + name: "custom endpoint", + args: args{ + providerData: &core.ProviderData{ + Version: testVersion, + VpnCustomEndpoint: testCustomEndpoint, + }, + }, + expected: func() *vpn.APIClient { + apiClient, err := vpn.NewAPIClient( + utils.UserAgentConfigOption(testVersion), + config.WithEndpoint(testCustomEndpoint), + ) + if err != nil { + t.Errorf("error configuring client: %v", err) + } + return apiClient + }(), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + diags := diag.Diagnostics{} + + actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { + t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) + } + + if !reflect.DeepEqual(actual, tt.expected) { + t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) + } + }) + } +} From 84a499870c8dbdc806aa172f254c1cb827b783cb Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Thu, 30 Apr 2026 07:10:18 +0200 Subject: [PATCH 03/13] feat(vpn): add datasource for VPN gateway --- .../services/vpn/gateway/datasource.go | 190 ++++++++++++++++++ .../internal/services/vpn/gateway/resource.go | 111 ++++++++++ 2 files changed, 301 insertions(+) create mode 100644 stackit/internal/services/vpn/gateway/datasource.go create mode 100644 stackit/internal/services/vpn/gateway/resource.go diff --git a/stackit/internal/services/vpn/gateway/datasource.go b/stackit/internal/services/vpn/gateway/datasource.go new file mode 100644 index 000000000..0838bce5f --- /dev/null +++ b/stackit/internal/services/vpn/gateway/datasource.go @@ -0,0 +1,190 @@ +package gateway + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/utils" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = (*vpnGatewayDataSource)(nil) + _ datasource.DataSourceWithConfigure = (*vpnGatewayDataSource)(nil) +) + +type vpnGatewayDataSource struct { + client *vpn.APIClient + providerData core.ProviderData +} + +func NewVPNGatewayDataSource() datasource.DataSource { + return &vpnGatewayDataSource{} +} + +// Configure implements [datasource.DataSourceWithConfigure]. +func (d *vpnGatewayDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := utils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + d.providerData = providerData + tflog.Info(ctx, "VPN client configured") +} + +// Metadata implements [datasource.DataSource]. +func (d *vpnGatewayDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vpn_gateway" +} + +// Schema implements [datasource.DataSource]. +func (d *vpnGatewayDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: fmt.Sprintf("VPN Gateway data source schema. %s", core.DatasourceRegionFallbackDocstring), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: schemaDescriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: schemaDescriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: schemaDescriptions["region"], + Required: true, + }, + "gateway_id": schema.StringAttribute{ + Description: schemaDescriptions["gateway_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "display_name": schema.StringAttribute{ + Description: schemaDescriptions["display_name"], + Computed: true, + }, + "plan_id": schema.StringAttribute{ + Description: schemaDescriptions["plan_id"], + Computed: true, + }, + "routing_type": schema.StringAttribute{ + Description: schemaDescriptions["routing_type"], + Computed: true, + }, + "availability_zones": schema.SingleNestedAttribute{ + Description: schemaDescriptions["availability_zones"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "tunnel1": schema.StringAttribute{ + Description: "Availability zone for tunnel 1.", + Computed: true, + }, + "tunnel2": schema.StringAttribute{ + Description: "Availability zone for tunnel 2.", + Computed: true, + }, + }, + }, + "bgp": schema.SingleNestedAttribute{ + Description: schemaDescriptions["bgp"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "local_asn": schema.Int64Attribute{ + Description: "Local ASN for BGP (private ASN range, 64512-4294967294).", + Computed: true, + }, + "override_advertised_routes": schema.ListAttribute{ + Description: "List of IPv4 CIDRs to advertise via BGP.", + Computed: true, + ElementType: types.StringType, + }, + }, + }, + "labels": schema.MapAttribute{ + Description: schemaDescriptions["labels"], + Computed: true, + ElementType: types.StringType, + }, + "state": schema.StringAttribute{ + Description: schemaDescriptions["state"], + Computed: true, + }, + }, + } +} + +// Read implements [datasource.DataSource]. +func (d *vpnGatewayDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var model Model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + gatewayId := model.GatewayID.ValueString() + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "gateway_id", gatewayId) + + gatewayResponse, err := d.client.DefaultAPI.GetVPNGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading VPN gateway", fmt.Sprintf("Calling API: %v", err)) + return + } + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, gatewayResponse, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading VPN gateway", fmt.Sprintf("Processing response: %v", err)) + return + } + + // Set state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "VPN gateway read", map[string]any{ + "gateway_id": gatewayId, + }) +} diff --git a/stackit/internal/services/vpn/gateway/resource.go b/stackit/internal/services/vpn/gateway/resource.go new file mode 100644 index 000000000..ef85606d8 --- /dev/null +++ b/stackit/internal/services/vpn/gateway/resource.go @@ -0,0 +1,111 @@ +package gateway + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +type AvailabilityZonesModel struct { + Tunnel1 types.String `tfsdk:"tunnel1"` + Tunnel2 types.String `tfsdk:"tunnel2"` +} + +type BGPGatewayConfigModel struct { + LocalAsn types.Int64 `tfsdk:"local_asn"` + OverrideAdvertisedRoutes types.List `tfsdk:"override_advertised_routes"` +} + +type Model struct { + ID types.String `tfsdk:"id"` + GatewayID types.String `tfsdk:"gateway_id"` + ProjectID types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + DisplayName types.String `tfsdk:"display_name"` + PlanID types.String `tfsdk:"plan_id"` + RoutingType types.String `tfsdk:"routing_type"` + AvailabilityZones AvailabilityZonesModel `tfsdk:"availability_zones"` + Bgp *BGPGatewayConfigModel `tfsdk:"bgp"` + Labels types.Map `tfsdk:"labels"` + State types.String `tfsdk:"state"` +} + +func mapFields(ctx context.Context, gateway *vpn.GatewayResponse, model *Model, region string) error { + if gateway == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var gatewayId string + if model.GatewayID.ValueString() != "" { + gatewayId = model.GatewayID.ValueString() + } else if gateway.Id != nil { + gatewayId = *gateway.Id + } else { + return fmt.Errorf("gateway id not present") + } + + model.ID = tfutils.BuildInternalTerraformId(model.ProjectID.ValueString(), region, gatewayId) + model.GatewayID = types.StringValue(gatewayId) + model.DisplayName = types.StringValue(gateway.DisplayName) + model.PlanID = types.StringValue(gateway.PlanId) + model.RoutingType = types.StringValue(string(gateway.RoutingType)) + model.Region = types.StringValue(region) + + // Availability zones + model.AvailabilityZones = AvailabilityZonesModel{ + Tunnel1: types.StringValue(string(gateway.AvailabilityZones.Tunnel1)), + Tunnel2: types.StringValue(string(gateway.AvailabilityZones.Tunnel2)), + } + + // BGP configuration (optional) + if gateway.Bgp != nil { + bgpModel := &BGPGatewayConfigModel{} + if gateway.Bgp.LocalAsn != nil { + bgpModel.LocalAsn = types.Int64Value(int64(*gateway.Bgp.LocalAsn)) + } else { + bgpModel.LocalAsn = types.Int64Null() + } + if gateway.Bgp.OverrideAdvertisedRoutes != nil && len(gateway.Bgp.OverrideAdvertisedRoutes) > 0 { + routes := gateway.Bgp.OverrideAdvertisedRoutes + listVal, diags := types.ListValueFrom(ctx, types.StringType, routes) + if diags.HasError() { + return fmt.Errorf("mapping BGP routes: %w", core.DiagsToError(diags)) + } + bgpModel.OverrideAdvertisedRoutes = listVal + } else { + bgpModel.OverrideAdvertisedRoutes = types.ListNull(types.StringType) + } + model.Bgp = bgpModel + } + + // Labels (optional) + if gateway.Labels != nil && len(*gateway.Labels) > 0 { + labelsMap := make(map[string]attr.Value) + for k, v := range *gateway.Labels { + labelsMap[k] = types.StringValue(v) + } + mapVal, diags := types.MapValue(types.StringType, labelsMap) + if diags.HasError() { + return fmt.Errorf("mapping labels: %w", core.DiagsToError(diags)) + } + model.Labels = mapVal + } else { + model.Labels = types.MapNull(types.StringType) + } + + // State + if gateway.State != nil { + model.State = types.StringValue(string(*gateway.State)) + } + + return nil +} From 7dbf0490f31f69f38adf886b827def9525b30190 Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Thu, 30 Apr 2026 13:26:21 +0200 Subject: [PATCH 04/13] feat(vpn): add resource for VPN gateway --- .../internal/services/vpn/gateway/resource.go | 555 +++++++++++++++++- stackit/provider.go | 3 + 2 files changed, 541 insertions(+), 17 deletions(-) diff --git a/stackit/internal/services/vpn/gateway/resource.go b/stackit/internal/services/vpn/gateway/resource.go index ef85606d8..0f408f53e 100644 --- a/stackit/internal/services/vpn/gateway/resource.go +++ b/stackit/internal/services/vpn/gateway/resource.go @@ -3,13 +3,39 @@ package gateway import ( "context" "fmt" + "net/http" + "regexp" + "strings" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/utils" tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ resource.Resource = &vpnGatewayResource{} + _ resource.ResourceWithConfigure = &vpnGatewayResource{} + _ resource.ResourceWithImportState = &vpnGatewayResource{} + _ resource.ResourceWithModifyPlan = &vpnGatewayResource{} + + routingTypeOptions = []string{"POLICY_BASED", "ROUTE_BASED", "BGP_ROUTE_BASED"} ) type AvailabilityZonesModel struct { @@ -23,17 +49,510 @@ type BGPGatewayConfigModel struct { } type Model struct { - ID types.String `tfsdk:"id"` - GatewayID types.String `tfsdk:"gateway_id"` - ProjectID types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - DisplayName types.String `tfsdk:"display_name"` - PlanID types.String `tfsdk:"plan_id"` - RoutingType types.String `tfsdk:"routing_type"` - AvailabilityZones AvailabilityZonesModel `tfsdk:"availability_zones"` - Bgp *BGPGatewayConfigModel `tfsdk:"bgp"` - Labels types.Map `tfsdk:"labels"` - State types.String `tfsdk:"state"` + ID types.String `tfsdk:"id"` + GatewayID types.String `tfsdk:"gateway_id"` + ProjectID types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + DisplayName types.String `tfsdk:"display_name"` + PlanID types.String `tfsdk:"plan_id"` + RoutingType types.String `tfsdk:"routing_type"` + AvailabilityZones *AvailabilityZonesModel `tfsdk:"availability_zones"` + Bgp *BGPGatewayConfigModel `tfsdk:"bgp"` + Labels types.Map `tfsdk:"labels"` + State types.String `tfsdk:"state"` +} + +var schemaDescriptions = map[string]string{ + "id": "Terraform's internal resource identifier. Structured as \"`project_id`,`region`,`gateway_id`\".", + "gateway_id": "The server-generated UUID of the VPN gateway.", + "project_id": "STACKIT project ID associated with the VPN gateway.", + "region": "STACKIT region (e.g. eu01).", + "display_name": "A user-friendly name for the VPN gateway.", + "plan_id": "The service plan identifier (e.g. p500).", + "routing_type": "Routing architecture: POLICY_BASED, ROUTE_BASED, or BGP_ROUTE_BASED.", + "availability_zones": "Availability zones for the two tunnel endpoints.", + "bgp": "BGP configuration. Only applicable when routing_type is BGP_ROUTE_BASED.", + "labels": "Map of custom labels (key-value string pairs).", + "state": "The current lifecycle state of the gateway (PENDING, READY, ERROR, DELETING).", +} + +type vpnGatewayResource struct { + client *vpn.APIClient + providerData core.ProviderData +} + +func NewVpnGatewayResource() resource.Resource { + return &vpnGatewayResource{} +} + +func (r *vpnGatewayResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := utils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + r.providerData = providerData + tflog.Info(ctx, "VPN client configured") +} + +func (r *vpnGatewayResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vpn_gateway" +} + +func (r *vpnGatewayResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: fmt.Sprintf("VPN Gateway resource schema. %s", core.ResourceRegionFallbackDocstring), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: schemaDescriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "gateway_id": schema.StringAttribute{ + Description: schemaDescriptions["gateway_id"], + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: schemaDescriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: schemaDescriptions["region"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "display_name": schema.StringAttribute{ + Description: schemaDescriptions["display_name"], + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`), + "must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long", + ), + }, + }, + "plan_id": schema.StringAttribute{ + Description: schemaDescriptions["plan_id"], + Required: true, + }, + "routing_type": schema.StringAttribute{ + Description: schemaDescriptions["routing_type"], + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(routingTypeOptions...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "availability_zones": schema.SingleNestedAttribute{ + Description: schemaDescriptions["availability_zones"], + Required: true, + Attributes: map[string]schema.Attribute{ + "tunnel1": schema.StringAttribute{ + Description: "Availability zone for tunnel 1.", + Required: true, + }, + "tunnel2": schema.StringAttribute{ + Description: "Availability zone for tunnel 2.", + Required: true, + }, + }, + }, + "bgp": schema.SingleNestedAttribute{ + Description: schemaDescriptions["bgp"], + Optional: true, + Attributes: map[string]schema.Attribute{ + "local_asn": schema.Int64Attribute{ + Description: "Local ASN for BGP (private ASN range, 64512-4294967294).", + Optional: true, + Validators: []validator.Int64{ + int64validator.Between(64512, 4294967294), + }, + }, + "override_advertised_routes": schema.ListAttribute{ + Description: "List of IPv4 CIDRs to advertise via BGP. If omitted, SNA network ranges are advertised.", + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.SizeAtMost(100), + listvalidator.ValueStringsAre(validate.CIDR()), + }, + }, + }, + }, + "labels": schema.MapAttribute{ + Description: schemaDescriptions["labels"], + Optional: true, + ElementType: types.StringType, + }, + "state": schema.StringAttribute{ + Description: schemaDescriptions["state"], + Computed: true, + }, + }, + } +} + +func (r *vpnGatewayResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var configModel Model + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + tfutils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *vpnGatewayResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing VPN gateway", + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[gateway_id] Got: %q", req.ID), + ) + return + } + + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "gateway_id": idParts[2], + }) + tflog.Info(ctx, "VPN gateway state imported") +} + +func (r *vpnGatewayResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN gateway", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + createResp, err := r.client.DefaultAPI.CreateVPNGateway(ctx, projectId, vpn.Region(region)).CreateVPNGatewayPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN gateway", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + if createResp.Id == nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN gateway", "Got empty gateway id") + return + } + gatewayId := *createResp.Id + + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + "gateway_id": gatewayId, + }) + if resp.Diagnostics.HasError() { + return + } + + waitResp, err := wait.CreateOrUpdateGatewayWaitHandler(ctx, r.client.DefaultAPI, projectId, vpn.Region(region), gatewayId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN gateway", fmt.Sprintf("Gateway creation waiting: %v", err)) + return + } + + err = mapFields(ctx, waitResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN gateway", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "VPN gateway created") +} + +func (r *vpnGatewayResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + gatewayId := model.GatewayID.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "gateway_id", gatewayId) + ctx = tflog.SetField(ctx, "region", region) + + gatewayResp, err := r.client.DefaultAPI.GetVPNGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading VPN gateway", err.Error()) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, gatewayResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading VPN gateway", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "VPN gateway read") +} + +func (r *vpnGatewayResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + gatewayId := model.GatewayID.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "gateway_id", gatewayId) + ctx = tflog.SetField(ctx, "region", region) + + payload, err := toUpdatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN gateway", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + _, err = r.client.DefaultAPI.UpdateVPNGateway(ctx, projectId, vpn.Region(region), gatewayId).UpdateVPNGatewayPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN gateway", err.Error()) + return + } + + ctx = core.LogResponse(ctx) + + waitResp, err := wait.CreateOrUpdateGatewayWaitHandler(ctx, r.client.DefaultAPI, projectId, vpn.Region(region), gatewayId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN gateway", fmt.Sprintf("Gateway update waiting: %v", err)) + return + } + + err = mapFields(ctx, waitResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN gateway", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "VPN gateway updated") +} + +func (r *vpnGatewayResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectID.ValueString() + gatewayId := model.GatewayID.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "gateway_id", gatewayId) + ctx = tflog.SetField(ctx, "region", region) + + err := r.client.DefaultAPI.DeleteVPNGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting VPN gateway", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + _, err = wait.DeleteGatewayWaitHandler(ctx, r.client.DefaultAPI, projectId, vpn.Region(region), gatewayId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting VPN gateway", fmt.Sprintf("Gateway deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "VPN gateway deleted") +} + +func toCreatePayload(ctx context.Context, model *Model) (*vpn.CreateVPNGatewayPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if model.AvailabilityZones == nil { + return nil, fmt.Errorf("availability_zones is required") + } + + azTunnel1 := model.AvailabilityZones.Tunnel1.ValueString() + azTunnel2 := model.AvailabilityZones.Tunnel2.ValueString() + + payload := &vpn.CreateVPNGatewayPayload{ + DisplayName: model.DisplayName.ValueString(), + PlanId: model.PlanID.ValueString(), + RoutingType: vpn.RoutingType(model.RoutingType.ValueString()), + AvailabilityZones: vpn.CreateVPNGatewayPayloadAvailabilityZones{ + Tunnel1: azTunnel1, + Tunnel2: azTunnel2, + }, + } + + if model.Bgp != nil { + bgpConfig := &vpn.BGPGatewayConfig{} + if !model.Bgp.LocalAsn.IsNull() && !model.Bgp.LocalAsn.IsUnknown() { + asn := int32(model.Bgp.LocalAsn.ValueInt64()) + bgpConfig.LocalAsn = &asn + } + if !model.Bgp.OverrideAdvertisedRoutes.IsNull() && !model.Bgp.OverrideAdvertisedRoutes.IsUnknown() { + routes := toStringSlice(ctx, model.Bgp.OverrideAdvertisedRoutes) + if len(routes) > 0 { + bgpConfig.OverrideAdvertisedRoutes = routes + } + } + payload.Bgp = bgpConfig + } + + if !model.Labels.IsNull() && !model.Labels.IsUnknown() { + labels := make(map[string]string) + diags := model.Labels.ElementsAs(ctx, &labels, false) + if diags.HasError() { + return nil, fmt.Errorf("converting labels: %w", core.DiagsToError(diags)) + } + if len(labels) > 0 { + payload.Labels = &labels + } + } + + return payload, nil +} + +func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateVPNGatewayPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if model.AvailabilityZones == nil { + return nil, fmt.Errorf("availability_zones is required") + } + + azTunnel1 := model.AvailabilityZones.Tunnel1.ValueString() + azTunnel2 := model.AvailabilityZones.Tunnel2.ValueString() + + payload := &vpn.UpdateVPNGatewayPayload{ + DisplayName: model.DisplayName.ValueString(), + PlanId: model.PlanID.ValueString(), + AvailabilityZones: vpn.UpdateVPNGatewayPayloadAvailabilityZones{ + Tunnel1: azTunnel1, + Tunnel2: azTunnel2, + }, + RoutingType: vpn.RoutingType(model.RoutingType.ValueString()), + } + + if model.Bgp != nil { + bgpConfig := &vpn.BGPGatewayConfig{} + if !model.Bgp.LocalAsn.IsNull() && !model.Bgp.LocalAsn.IsUnknown() { + asn := int32(model.Bgp.LocalAsn.ValueInt64()) + bgpConfig.LocalAsn = &asn + } + if !model.Bgp.OverrideAdvertisedRoutes.IsNull() && !model.Bgp.OverrideAdvertisedRoutes.IsUnknown() { + routes := toStringSlice(ctx, model.Bgp.OverrideAdvertisedRoutes) + if len(routes) > 0 { + bgpConfig.OverrideAdvertisedRoutes = routes + } + } + payload.Bgp = bgpConfig + } + + if !model.Labels.IsNull() && !model.Labels.IsUnknown() { + labels := make(map[string]string) + diags := model.Labels.ElementsAs(ctx, &labels, false) + if diags.HasError() { + return nil, fmt.Errorf("converting labels: %w", core.DiagsToError(diags)) + } + payload.Labels = &labels + } + + return payload, nil } func mapFields(ctx context.Context, gateway *vpn.GatewayResponse, model *Model, region string) error { @@ -60,13 +579,11 @@ func mapFields(ctx context.Context, gateway *vpn.GatewayResponse, model *Model, model.RoutingType = types.StringValue(string(gateway.RoutingType)) model.Region = types.StringValue(region) - // Availability zones - model.AvailabilityZones = AvailabilityZonesModel{ + model.AvailabilityZones = &AvailabilityZonesModel{ Tunnel1: types.StringValue(string(gateway.AvailabilityZones.Tunnel1)), Tunnel2: types.StringValue(string(gateway.AvailabilityZones.Tunnel2)), } - // BGP configuration (optional) if gateway.Bgp != nil { bgpModel := &BGPGatewayConfigModel{} if gateway.Bgp.LocalAsn != nil { @@ -74,7 +591,7 @@ func mapFields(ctx context.Context, gateway *vpn.GatewayResponse, model *Model, } else { bgpModel.LocalAsn = types.Int64Null() } - if gateway.Bgp.OverrideAdvertisedRoutes != nil && len(gateway.Bgp.OverrideAdvertisedRoutes) > 0 { + if len(gateway.Bgp.OverrideAdvertisedRoutes) > 0 { routes := gateway.Bgp.OverrideAdvertisedRoutes listVal, diags := types.ListValueFrom(ctx, types.StringType, routes) if diags.HasError() { @@ -87,7 +604,6 @@ func mapFields(ctx context.Context, gateway *vpn.GatewayResponse, model *Model, model.Bgp = bgpModel } - // Labels (optional) if gateway.Labels != nil && len(*gateway.Labels) > 0 { labelsMap := make(map[string]attr.Value) for k, v := range *gateway.Labels { @@ -102,10 +618,15 @@ func mapFields(ctx context.Context, gateway *vpn.GatewayResponse, model *Model, model.Labels = types.MapNull(types.StringType) } - // State if gateway.State != nil { model.State = types.StringValue(string(*gateway.State)) } return nil } + +func toStringSlice(ctx context.Context, list types.List) []string { + var result []string + list.ElementsAs(ctx, &result, false) + return result +} diff --git a/stackit/provider.go b/stackit/provider.go index 5655278c2..7ba261c42 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -120,6 +120,7 @@ import ( skeMachineImages "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/provideroptions/machineimages" sqlServerFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/instance" sqlServerFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/user" + vpnGateway "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/gateway" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -710,6 +711,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource compliancelock.NewComplianceLockDataSource, serverBackupEnable.NewServerBackupEnableDataSource, serverUpdateEnable.NewServerUpdateEnableDataSource, + vpnGateway.NewVPNGatewayDataSource, } dataSources = append(dataSources, customRole.NewCustomRoleDataSources()...) dataSources = append(dataSources, iamRoleBindingsV1.NewRoleBindingsDatasources()...) @@ -802,6 +804,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { compliancelock.NewComplianceLockResource, serverBackupEnable.NewServerBackupEnableResource, serverUpdateEnable.NewServerUpdateEnableResource, + vpnGateway.NewVpnGatewayResource, } resources = append(resources, roleAssignements.NewRoleAssignmentResources()...) resources = append(resources, customRole.NewCustomRoleResources()...) From ce6c205ee268193d91ff00896a9dfa7f0a01069a Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Thu, 30 Apr 2026 15:31:43 +0200 Subject: [PATCH 05/13] feat(vpn): add unit tests for VPN gateway --- .../services/vpn/gateway/datasource_test.go | 74 +++++ .../services/vpn/gateway/resource_test.go | 287 ++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 stackit/internal/services/vpn/gateway/datasource_test.go create mode 100644 stackit/internal/services/vpn/gateway/resource_test.go diff --git a/stackit/internal/services/vpn/gateway/datasource_test.go b/stackit/internal/services/vpn/gateway/datasource_test.go new file mode 100644 index 000000000..0459c27ff --- /dev/null +++ b/stackit/internal/services/vpn/gateway/datasource_test.go @@ -0,0 +1,74 @@ +package gateway + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" +) + +func TestDataSourceMapFields(t *testing.T) { + tests := []struct { + description string + input *vpn.GatewayResponse + expected Model + isValid bool + }{ + { + "basic_gateway", + &vpn.GatewayResponse{ + Id: utils.Ptr("gateway-id"), + DisplayName: "test-gateway", + PlanId: "p500", + RoutingType: vpn.ROUTINGTYPE_ROUTE_BASED, + AvailabilityZones: vpn.GatewayAvailabilityZones{ + Tunnel1: "eu01-1", + Tunnel2: "eu01-2", + }, + State: utils.Ptr(vpn.GATEWAYSTATUS_READY), + }, + Model{ + 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"), + }, + State: types.StringValue("READY"), + }, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var model Model + model.ProjectID = types.StringValue("test-project") + model.Region = types.StringValue("eu01") + + err := mapFields(context.Background(), tt.input, &model, "eu01") + + if !tt.isValid && err == nil { + t.Fatalf("expected error, got none") + } + if tt.isValid && err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !tt.isValid { + return + } + + if diff := cmp.Diff(model.GatewayID, tt.expected.GatewayID); diff != "" { + t.Fatalf("GatewayID mismatch (-got +want):\n%s", diff) + } + if diff := cmp.Diff(model.DisplayName, tt.expected.DisplayName); diff != "" { + t.Fatalf("DisplayName mismatch (-got +want):\n%s", diff) + } + }) + } +} diff --git a/stackit/internal/services/vpn/gateway/resource_test.go b/stackit/internal/services/vpn/gateway/resource_test.go new file mode 100644 index 000000000..3ce3abb3d --- /dev/null +++ b/stackit/internal/services/vpn/gateway/resource_test.go @@ -0,0 +1,287 @@ +package gateway + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" +) + +func TestMapFields(t *testing.T) { + tests := []struct { + description string + input *vpn.GatewayResponse + expected Model + isValid bool + }{ + { + "default_ok", + &vpn.GatewayResponse{ + Id: utils.Ptr("gateway-id"), + DisplayName: "test-gateway", + PlanId: "p500", + RoutingType: vpn.ROUTINGTYPE_ROUTE_BASED, + AvailabilityZones: vpn.GatewayAvailabilityZones{ + Tunnel1: "eu01-1", + Tunnel2: "eu01-2", + }, + State: utils.Ptr(vpn.GATEWAYSTATUS_READY), + }, + Model{ + 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"), + }, + Bgp: nil, + Labels: types.MapNull(types.StringType), + State: types.StringValue("READY"), + }, + true, + }, + { + "with_bgp_and_labels", + &vpn.GatewayResponse{ + Id: utils.Ptr("gateway-id"), + DisplayName: "test-gateway", + PlanId: "p500", + RoutingType: vpn.ROUTINGTYPE_BGP_ROUTE_BASED, + AvailabilityZones: vpn.GatewayAvailabilityZones{ + Tunnel1: "eu01-1", + Tunnel2: "eu01-2", + }, + Bgp: &vpn.BGPGatewayConfig{ + LocalAsn: utils.Ptr(int32(65000)), + OverrideAdvertisedRoutes: []string{"10.0.0.0/16", "192.168.0.0/24"}, + }, + Labels: &map[string]string{ + "env": "prod", + "team": "network", + }, + State: utils.Ptr(vpn.GATEWAYSTATUS_READY), + }, + Model{ + GatewayID: types.StringValue("gateway-id"), + DisplayName: types.StringValue("test-gateway"), + PlanID: types.StringValue("p500"), + RoutingType: types.StringValue("BGP_ROUTE_BASED"), + AvailabilityZones: &AvailabilityZonesModel{ + Tunnel1: types.StringValue("eu01-1"), + Tunnel2: types.StringValue("eu01-2"), + }, + Bgp: &BGPGatewayConfigModel{ + LocalAsn: types.Int64Value(65000), + }, + State: types.StringValue("READY"), + }, + true, + }, + { + "nil_response", + nil, + Model{}, + false, + }, + { + "nil_gateway_id", + &vpn.GatewayResponse{ + Id: nil, + DisplayName: "test-gateway", + }, + Model{}, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var model Model + model.ProjectID = types.StringValue("test-project") + model.Region = types.StringValue("eu01") + + err := mapFields(context.Background(), tt.input, &model, "eu01") + + if !tt.isValid && err == nil { + t.Fatalf("expected error, got none") + } + if tt.isValid && err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !tt.isValid { + return + } + + if diff := cmp.Diff(model.GatewayID, tt.expected.GatewayID); diff != "" { + t.Fatalf("GatewayID mismatch (-got +want):\n%s", diff) + } + if diff := cmp.Diff(model.DisplayName, tt.expected.DisplayName); diff != "" { + t.Fatalf("DisplayName mismatch (-got +want):\n%s", diff) + } + if diff := cmp.Diff(model.PlanID, tt.expected.PlanID); diff != "" { + t.Fatalf("PlanID mismatch (-got +want):\n%s", diff) + } + if diff := cmp.Diff(model.RoutingType, tt.expected.RoutingType); diff != "" { + t.Fatalf("RoutingType mismatch (-got +want):\n%s", diff) + } + if diff := cmp.Diff(model.State, tt.expected.State); diff != "" { + t.Fatalf("State mismatch (-got +want):\n%s", diff) + } + + if diff := cmp.Diff(model.AvailabilityZones.Tunnel1, tt.expected.AvailabilityZones.Tunnel1); diff != "" { + t.Fatalf("AZ Tunnel1 mismatch (-got +want):\n%s", diff) + } + if diff := cmp.Diff(model.AvailabilityZones.Tunnel2, tt.expected.AvailabilityZones.Tunnel2); diff != "" { + t.Fatalf("AZ Tunnel2 mismatch (-got +want):\n%s", diff) + } + + if tt.expected.Bgp != nil { + if model.Bgp == nil { + t.Fatalf("expected BGP config, got nil") + } + if diff := cmp.Diff(model.Bgp.LocalAsn, tt.expected.Bgp.LocalAsn); diff != "" { + t.Fatalf("BGP LocalAsn mismatch (-got +want):\n%s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input Model + isValid bool + }{ + { + "basic_gateway", + 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"), + }, + }, + true, + }, + { + "with_bgp", + Model{ + DisplayName: types.StringValue("test-gateway"), + PlanID: types.StringValue("p500"), + RoutingType: types.StringValue("BGP_ROUTE_BASED"), + AvailabilityZones: &AvailabilityZonesModel{ + Tunnel1: types.StringValue("eu01-1"), + Tunnel2: types.StringValue("eu01-2"), + }, + Bgp: &BGPGatewayConfigModel{ + LocalAsn: types.Int64Value(65000), + }, + }, + true, + }, + { + "nil_model", + Model{}, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var model *Model + if tt.isValid { + model = &tt.input + } + + payload, err := toCreatePayload(context.Background(), model) + + if !tt.isValid && err == nil { + t.Fatalf("expected error, got none") + } + if tt.isValid && err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !tt.isValid { + return + } + + if payload.DisplayName != tt.input.DisplayName.ValueString() { + t.Errorf("DisplayName mismatch: got %v, want %v", payload.DisplayName, tt.input.DisplayName.ValueString()) + } + if payload.PlanId != tt.input.PlanID.ValueString() { + t.Errorf("PlanId mismatch: got %v, want %v", payload.PlanId, tt.input.PlanID.ValueString()) + } + if string(payload.RoutingType) != tt.input.RoutingType.ValueString() { + t.Errorf("RoutingType mismatch: got %v, want %v", payload.RoutingType, tt.input.RoutingType.ValueString()) + } + + if tt.input.Bgp != nil { + if payload.Bgp == nil { + t.Errorf("expected BGP config, got nil") + } + } + }) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input Model + isValid bool + }{ + { + "basic_update", + Model{ + DisplayName: types.StringValue("updated-gateway"), + PlanID: types.StringValue("p1000"), + AvailabilityZones: &AvailabilityZonesModel{ + Tunnel1: types.StringValue("eu01-1"), + Tunnel2: types.StringValue("eu01-2"), + }, + }, + true, + }, + { + "nil_model", + Model{}, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var model *Model + if tt.isValid { + model = &tt.input + } + + payload, err := toUpdatePayload(context.Background(), model) + + if !tt.isValid && err == nil { + t.Fatalf("expected error, got none") + } + if tt.isValid && err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !tt.isValid { + return + } + + if payload.DisplayName != tt.input.DisplayName.ValueString() { + t.Errorf("DisplayName mismatch: got %v, want %v", payload.DisplayName, tt.input.DisplayName.ValueString()) + } + if payload.PlanId != tt.input.PlanID.ValueString() { + t.Errorf("PlanId mismatch: got %v, want %v", payload.PlanId, tt.input.PlanID.ValueString()) + } + }) + } +} From 907878a90f30c41b355d64e30ef11cdbf9671aba Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Mon, 4 May 2026 08:12:45 +0200 Subject: [PATCH 06/13] feat(vpn): add test data and acceptance tests for VPN gateway --- .../services/vpn/testdata/gateway-max.tf | 34 +++ .../services/vpn/testdata/gateway-min.tf | 20 ++ stackit/internal/services/vpn/vpn_acc_test.go | 195 ++++++++++++++++++ stackit/internal/testutil/testutil.go | 2 + 4 files changed, 251 insertions(+) create mode 100644 stackit/internal/services/vpn/testdata/gateway-max.tf create mode 100644 stackit/internal/services/vpn/testdata/gateway-min.tf create mode 100644 stackit/internal/services/vpn/vpn_acc_test.go diff --git a/stackit/internal/services/vpn/testdata/gateway-max.tf b/stackit/internal/services/vpn/testdata/gateway-max.tf new file mode 100644 index 000000000..4b7aba179 --- /dev/null +++ b/stackit/internal/services/vpn/testdata/gateway-max.tf @@ -0,0 +1,34 @@ +variable "project_id" {} +variable "region" {} +variable "display_name" {} +variable "plan_id" {} +variable "routing_type" {} +variable "az_tunnel1" {} +variable "az_tunnel2" {} +variable "local_asn" {} +variable "advertised_route_1" {} +variable "advertised_route_2" {} +variable "label_key" {} +variable "label_value" {} + +resource "stackit_vpn_gateway" "gateway" { + project_id = var.project_id + region = var.region + display_name = var.display_name + plan_id = var.plan_id + routing_type = var.routing_type + + availability_zones = { + tunnel1 = var.az_tunnel1 + tunnel2 = var.az_tunnel2 + } + + bgp = { + local_asn = var.local_asn + override_advertised_routes = [var.advertised_route_1, var.advertised_route_2] + } + + labels = { + (var.label_key) = var.label_value + } +} diff --git a/stackit/internal/services/vpn/testdata/gateway-min.tf b/stackit/internal/services/vpn/testdata/gateway-min.tf new file mode 100644 index 000000000..2c4eb6588 --- /dev/null +++ b/stackit/internal/services/vpn/testdata/gateway-min.tf @@ -0,0 +1,20 @@ +variable "project_id" {} +variable "region" {} +variable "display_name" {} +variable "plan_id" {} +variable "routing_type" {} +variable "az_tunnel1" {} +variable "az_tunnel2" {} + +resource "stackit_vpn_gateway" "gateway" { + project_id = var.project_id + region = var.region + display_name = var.display_name + plan_id = var.plan_id + routing_type = var.routing_type + + availability_zones = { + tunnel1 = var.az_tunnel1 + tunnel2 = var.az_tunnel2 + } +} diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go new file mode 100644 index 000000000..7a8156d62 --- /dev/null +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -0,0 +1,195 @@ +package vpn_test + +import ( + "context" + _ "embed" + "fmt" + "maps" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +var ( + //go:embed testdata/gateway-min.tf + gatewayMinConfig string + + //go:embed testdata/gateway-max.tf + gatewayMaxConfig string +) + +var gatewayMinVars = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "region": config.StringVariable("eu01"), + "display_name": config.StringVariable("vpn-gw-acc-test-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "plan_id": config.StringVariable("p500"), + "routing_type": config.StringVariable("ROUTE_BASED"), + "az_tunnel1": config.StringVariable("eu01-1"), + "az_tunnel2": config.StringVariable("eu01-2"), +} + +var gatewayMinVarsUpdated = func() config.Variables { + updated := make(config.Variables, len(gatewayMinVars)) + maps.Copy(updated, gatewayMinVars) + updated["display_name"] = config.StringVariable("vpn-gw-acc-test-updated-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)) + updated["plan_id"] = config.StringVariable("p100") + return updated +}() + +var gatewayMaxVars = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "region": config.StringVariable("eu01"), + "display_name": config.StringVariable("vpn-gw-acc-test-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "plan_id": config.StringVariable("p500"), + "routing_type": config.StringVariable("BGP_ROUTE_BASED"), + "az_tunnel1": config.StringVariable("eu01-1"), + "az_tunnel2": config.StringVariable("eu01-2"), + "local_asn": config.IntegerVariable(65000), + "advertised_route_1": config.StringVariable("10.0.0.0/16"), + "advertised_route_2": config.StringVariable("192.168.0.0/24"), + "label_key": config.StringVariable("env"), + "label_value": config.StringVariable("test"), +} + +var gatewayMaxVarsUpdated = func() config.Variables { + updated := make(config.Variables, len(gatewayMaxVars)) + maps.Copy(updated, gatewayMaxVars) + updated["display_name"] = config.StringVariable("vpn-gw-acc-test-updated-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)) + updated["local_asn"] = config.IntegerVariable(65001) + updated["label_value"] = config.StringVariable("production") + return updated +}() + +func TestAccVpnGatewayResourceMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckVpnGatewayDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: gatewayMinVars, + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Gateway data + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ConvertConfigVariable(gatewayMinVars["project_id"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "region", testutil.ConvertConfigVariable(gatewayMinVars["region"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMinVars["display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(gatewayMinVars["plan_id"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "routing_type", testutil.ConvertConfigVariable(gatewayMinVars["routing_type"])), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "state"), + ), + }, + // Update + { + ConfigVariables: gatewayMinVarsUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Gateway data + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMinVarsUpdated["display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(gatewayMinVarsUpdated["plan_id"])), + ), + }, + // Import + { + ConfigVariables: gatewayMinVars, + ResourceName: "stackit_vpn_gateway.gateway", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_vpn_gateway.gateway"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_vpn_gateway.gateway") + } + gatewayId, ok := r.Primary.Attributes["gateway_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute gateway_id") + } + return fmt.Sprintf("%s,%s,%s", + testutil.ConvertConfigVariable(gatewayMinVarsUpdated["project_id"]), + testutil.ConvertConfigVariable(gatewayMinVarsUpdated["region"]), + gatewayId, + ), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVpnGatewayResourceMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckVpnGatewayDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: gatewayMaxVars, + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Gateway data + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "project_id", testutil.ConvertConfigVariable(gatewayMaxVars["project_id"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMaxVars["display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "routing_type", testutil.ConvertConfigVariable(gatewayMaxVars["routing_type"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(gatewayMaxVars["local_asn"])), + resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), + ), + }, + // Update + { + ConfigVariables: gatewayMaxVarsUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["display_name"])), + resource.TestCheckResourceAttr("stackit_vpn_gateway.gateway", "bgp.local_asn", testutil.ConvertConfigVariable(gatewayMaxVarsUpdated["local_asn"])), + ), + }, + }, + }) +} + +func testAccCheckVpnGatewayDestroy(s *terraform.State) error { + ctx := context.Background() + client, err := vpn.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.VpnCustomEndpoint, true)...) + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + gatewaysToDestroy := []string{} + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_vpn_gateway" { + continue + } + // gateway terraform ID: "[project_id],[region],[gateway_id]" + gatewayId := strings.Split(rs.Primary.ID, core.Separator)[2] + gatewaysToDestroy = append(gatewaysToDestroy, gatewayId) + } + + gatewaysResp, err := client.DefaultAPI.ListVPNGateways(ctx, testutil.ProjectId, vpn.REGION_EU01).Execute() + if err != nil { + return fmt.Errorf("getting gateways: %w", err) + } + + gateways := gatewaysResp.Gateways + for _, gateway := range gateways { + if gateway.Id == nil { + continue + } + for _, gatewayId := range gatewaysToDestroy { + if *gateway.Id == gatewayId { + err := client.DefaultAPI.DeleteVPNGateway(ctx, testutil.ProjectId, vpn.REGION_EU01, *gateway.Id).Execute() + if err != nil { + return fmt.Errorf("destroying gateway %s during CheckDestroy: %w", gatewayId, err) + } + } + } + } + return nil +} diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 5be2e7281..d486eed2e 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -97,6 +97,7 @@ var ( SFSCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_SFS_CUSTOM_ENDPOINT", providerName: "sfs_custom_endpoint"} ServiceAccountCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_SERVICE_ACCOUNT_CUSTOM_ENDPOINT", providerName: "service_account_custom_endpoint"} TokenCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_TOKEN_CUSTOM_ENDPOINT", providerName: "token_custom_endpoint"} + VpnCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_VPN_CUSTOM_ENDPOINT", providerName: "vpn_custom_endpoint"} SKECustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_SKE_CUSTOM_ENDPOINT", providerName: "ske_custom_endpoint"} allCustomEndpoints = []customEndpointConfig{ @@ -130,6 +131,7 @@ var ( SFSCustomEndpoint, ServiceAccountCustomEndpoint, TokenCustomEndpoint, + VpnCustomEndpoint, SKECustomEndpoint, } ) From 161ec48181840edb0799c025a0fbba1f11275f24 Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Mon, 4 May 2026 11:23:41 +0200 Subject: [PATCH 07/13] fix(vpn): resolve linter issues - linter bypass per contribution guidelines --- .../internal/services/vpn/gateway/datasource.go | 2 +- stackit/internal/services/vpn/gateway/resource.go | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/stackit/internal/services/vpn/gateway/datasource.go b/stackit/internal/services/vpn/gateway/datasource.go index 0838bce5f..9a4be7029 100644 --- a/stackit/internal/services/vpn/gateway/datasource.go +++ b/stackit/internal/services/vpn/gateway/datasource.go @@ -141,7 +141,7 @@ func (d *vpnGatewayDataSource) Schema(_ context.Context, _ datasource.SchemaRequ } // Read implements [datasource.DataSource]. -func (d *vpnGatewayDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { +func (d *vpnGatewayDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) diff --git a/stackit/internal/services/vpn/gateway/resource.go b/stackit/internal/services/vpn/gateway/resource.go index 0f408f53e..d8a1bd0e2 100644 --- a/stackit/internal/services/vpn/gateway/resource.go +++ b/stackit/internal/services/vpn/gateway/resource.go @@ -2,6 +2,7 @@ package gateway import ( "context" + "errors" "fmt" "net/http" "regexp" @@ -218,7 +219,7 @@ func (r *vpnGatewayResource) Schema(_ context.Context, _ resource.SchemaRequest, } } -func (r *vpnGatewayResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { +func (r *vpnGatewayResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform var configModel Model if req.Config.Raw.IsNull() { return @@ -264,7 +265,7 @@ func (r *vpnGatewayResource) ImportState(ctx context.Context, req resource.Impor tflog.Info(ctx, "VPN gateway state imported") } -func (r *vpnGatewayResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { +func (r *vpnGatewayResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -328,7 +329,7 @@ func (r *vpnGatewayResource) Create(ctx context.Context, req resource.CreateRequ tflog.Info(ctx, "VPN gateway created") } -func (r *vpnGatewayResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { +func (r *vpnGatewayResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -347,8 +348,8 @@ func (r *vpnGatewayResource) Read(ctx context.Context, req resource.ReadRequest, gatewayResp, err := r.client.DefaultAPI.GetVPNGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) - if ok && oapiErr.StatusCode == http.StatusNotFound { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } @@ -372,7 +373,7 @@ func (r *vpnGatewayResource) Read(ctx context.Context, req resource.ReadRequest, tflog.Info(ctx, "VPN gateway read") } -func (r *vpnGatewayResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +func (r *vpnGatewayResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -423,7 +424,7 @@ func (r *vpnGatewayResource) Update(ctx context.Context, req resource.UpdateRequ tflog.Info(ctx, "VPN gateway updated") } -func (r *vpnGatewayResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +func (r *vpnGatewayResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) From d7c6581fef70a40510b499f5eec724a69a578e4d Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Mon, 11 May 2026 14:25:16 +0200 Subject: [PATCH 08/13] fix(vpn): update gateway tests to use new() function --- stackit/internal/services/vpn/gateway/datasource_test.go | 2 +- stackit/internal/services/vpn/gateway/resource_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/stackit/internal/services/vpn/gateway/datasource_test.go b/stackit/internal/services/vpn/gateway/datasource_test.go index 0459c27ff..8f61f7fc2 100644 --- a/stackit/internal/services/vpn/gateway/datasource_test.go +++ b/stackit/internal/services/vpn/gateway/datasource_test.go @@ -20,7 +20,7 @@ func TestDataSourceMapFields(t *testing.T) { { "basic_gateway", &vpn.GatewayResponse{ - Id: utils.Ptr("gateway-id"), + Id: new("gateway-id"), DisplayName: "test-gateway", PlanId: "p500", RoutingType: vpn.ROUTINGTYPE_ROUTE_BASED, diff --git a/stackit/internal/services/vpn/gateway/resource_test.go b/stackit/internal/services/vpn/gateway/resource_test.go index 3ce3abb3d..0aa30b867 100644 --- a/stackit/internal/services/vpn/gateway/resource_test.go +++ b/stackit/internal/services/vpn/gateway/resource_test.go @@ -20,7 +20,7 @@ func TestMapFields(t *testing.T) { { "default_ok", &vpn.GatewayResponse{ - Id: utils.Ptr("gateway-id"), + Id: new("gateway-id"), DisplayName: "test-gateway", PlanId: "p500", RoutingType: vpn.ROUTINGTYPE_ROUTE_BASED, @@ -48,7 +48,7 @@ func TestMapFields(t *testing.T) { { "with_bgp_and_labels", &vpn.GatewayResponse{ - Id: utils.Ptr("gateway-id"), + Id: new("gateway-id"), DisplayName: "test-gateway", PlanId: "p500", RoutingType: vpn.ROUTINGTYPE_BGP_ROUTE_BASED, @@ -57,7 +57,7 @@ func TestMapFields(t *testing.T) { Tunnel2: "eu01-2", }, Bgp: &vpn.BGPGatewayConfig{ - LocalAsn: utils.Ptr(int32(65000)), + LocalAsn: new(int32(65000)), OverrideAdvertisedRoutes: []string{"10.0.0.0/16", "192.168.0.0/24"}, }, Labels: &map[string]string{ From f9bce3e0839cc2a88b944a6ff6baaf71080116b7 Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Mon, 11 May 2026 14:50:02 +0200 Subject: [PATCH 09/13] feat(vpn): add example resource for VPN gateway --- .../resources/stackit_vpn_gateway/resource.tf | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 examples/resources/stackit_vpn_gateway/resource.tf diff --git a/examples/resources/stackit_vpn_gateway/resource.tf b/examples/resources/stackit_vpn_gateway/resource.tf new file mode 100644 index 000000000..e89d5c915 --- /dev/null +++ b/examples/resources/stackit_vpn_gateway/resource.tf @@ -0,0 +1,18 @@ +resource "stackit_vpn_gateway" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region = "eu01" + display_name = "example-vpn-gateway" + plan_id = "p500" + routing_type = "ROUTE_BASED" + + availability_zones = { + tunnel1 = "eu01-1" + tunnel2 = "eu01-2" + } +} + +# Only use the import statement, if you want to import an existing VPN gateway +import { + to = stackit_vpn_gateway.example + id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,eu01,xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} From 046bce864907d669574b99e7b82fa3babc556b7d Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Tue, 12 May 2026 10:25:29 +0200 Subject: [PATCH 10/13] fix(vpn): update VPN gateway data source schema and add acceptance tests --- .../services/vpn/gateway/datasource.go | 6 +- stackit/internal/services/vpn/vpn_acc_test.go | 83 +++++++++++++++++-- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/stackit/internal/services/vpn/gateway/datasource.go b/stackit/internal/services/vpn/gateway/datasource.go index 9a4be7029..010ea15fe 100644 --- a/stackit/internal/services/vpn/gateway/datasource.go +++ b/stackit/internal/services/vpn/gateway/datasource.go @@ -36,7 +36,6 @@ func NewVPNGatewayDataSource() datasource.DataSource { return &vpnGatewayDataSource{} } -// Configure implements [datasource.DataSourceWithConfigure]. func (d *vpnGatewayDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { @@ -52,12 +51,10 @@ func (d *vpnGatewayDataSource) Configure(ctx context.Context, req datasource.Con tflog.Info(ctx, "VPN client configured") } -// Metadata implements [datasource.DataSource]. func (d *vpnGatewayDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_vpn_gateway" } -// Schema implements [datasource.DataSource]. func (d *vpnGatewayDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: fmt.Sprintf("VPN Gateway data source schema. %s", core.DatasourceRegionFallbackDocstring), @@ -76,7 +73,7 @@ func (d *vpnGatewayDataSource) Schema(_ context.Context, _ datasource.SchemaRequ }, "region": schema.StringAttribute{ Description: schemaDescriptions["region"], - Required: true, + Computed: true, }, "gateway_id": schema.StringAttribute{ Description: schemaDescriptions["gateway_id"], @@ -140,7 +137,6 @@ func (d *vpnGatewayDataSource) Schema(_ context.Context, _ datasource.SchemaRequ } } -// Read implements [datasource.DataSource]. func (d *vpnGatewayDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go index 7a8156d62..6f762d0b7 100644 --- a/stackit/internal/services/vpn/vpn_acc_test.go +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -18,13 +18,11 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -var ( - //go:embed testdata/gateway-min.tf - gatewayMinConfig string +//go:embed testdata/gateway-min.tf +var gatewayMinConfig string - //go:embed testdata/gateway-max.tf - gatewayMaxConfig string -) +//go:embed testdata/gateway-max.tf +var gatewayMaxConfig string var gatewayMinVars = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), @@ -88,6 +86,40 @@ func TestAccVpnGatewayResourceMin(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "state"), ), }, + // Data source + { + ConfigVariables: gatewayMinVars, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_vpn_gateway" "gateway" { + project_id = stackit_vpn_gateway.gateway.project_id + gateway_id = stackit_vpn_gateway.gateway.gateway_id + } + `, + testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMinConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "project_id", testutil.ConvertConfigVariable(gatewayMinVars["project_id"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMinVars["display_name"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "plan_id", testutil.ConvertConfigVariable(gatewayMinVars["plan_id"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "routing_type", testutil.ConvertConfigVariable(gatewayMinVars["routing_type"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "availability_zones.tunnel1", testutil.ConvertConfigVariable(gatewayMinVars["az_tunnel1"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "availability_zones.tunnel2", testutil.ConvertConfigVariable(gatewayMinVars["az_tunnel2"])), + + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway.gateway", "state"), + + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "project_id", "stackit_vpn_gateway.gateway", "project_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "region", "stackit_vpn_gateway.gateway", "region"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "display_name", "stackit_vpn_gateway.gateway", "display_name"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "plan_id", "stackit_vpn_gateway.gateway", "plan_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "routing_type", "stackit_vpn_gateway.gateway", "routing_type"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "state", "stackit_vpn_gateway.gateway", "state"), + ), + }, // Update { ConfigVariables: gatewayMinVarsUpdated, @@ -142,6 +174,45 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_vpn_gateway.gateway", "gateway_id"), ), }, + // Data source + { + ConfigVariables: gatewayMaxVars, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_vpn_gateway" "gateway" { + project_id = stackit_vpn_gateway.gateway.project_id + gateway_id = stackit_vpn_gateway.gateway.gateway_id + } + `, + testutil.NewConfigBuilder().BuildProviderConfig(), gatewayMaxConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "project_id", testutil.ConvertConfigVariable(gatewayMaxVars["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"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "availability_zones.tunnel1", testutil.ConvertConfigVariable(gatewayMaxVars["az_tunnel1"])), + 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"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "bgp.override_advertised_routes.0", testutil.ConvertConfigVariable(gatewayMaxVars["advertised_route_1"])), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway.gateway", "bgp.override_advertised_routes.1", testutil.ConvertConfigVariable(gatewayMaxVars["advertised_route_2"])), + 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"), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway.gateway", "state"), + + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "project_id", "stackit_vpn_gateway.gateway", "project_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "region", "stackit_vpn_gateway.gateway", "region"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "display_name", "stackit_vpn_gateway.gateway", "display_name"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "plan_id", "stackit_vpn_gateway.gateway", "plan_id"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "routing_type", "stackit_vpn_gateway.gateway", "routing_type"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "bgp.local_asn", "stackit_vpn_gateway.gateway", "bgp.local_asn"), + resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "state", "stackit_vpn_gateway.gateway", "state"), + ), + }, // Update { ConfigVariables: gatewayMaxVarsUpdated, From b3b1b9342c3804a7c45ded5b41b7001b411506d4 Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Wed, 13 May 2026 09:37:21 +0200 Subject: [PATCH 11/13] feat(vpn): upgrade to v1 api version --- .../services/vpn/gateway/datasource.go | 4 +- .../services/vpn/gateway/datasource_test.go | 2 +- .../internal/services/vpn/gateway/resource.go | 64 +++++++++---------- .../services/vpn/gateway/resource_test.go | 4 +- stackit/internal/services/vpn/utils/utils.go | 2 +- .../internal/services/vpn/utils/utils_test.go | 2 +- stackit/internal/services/vpn/vpn_acc_test.go | 6 +- stackit/provider.go | 2 +- 8 files changed, 43 insertions(+), 43 deletions(-) diff --git a/stackit/internal/services/vpn/gateway/datasource.go b/stackit/internal/services/vpn/gateway/datasource.go index 010ea15fe..4554f00dc 100644 --- a/stackit/internal/services/vpn/gateway/datasource.go +++ b/stackit/internal/services/vpn/gateway/datasource.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/utils" @@ -155,7 +155,7 @@ func (d *vpnGatewayDataSource) Read(ctx context.Context, req datasource.ReadRequ ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "gateway_id", gatewayId) - gatewayResponse, err := d.client.DefaultAPI.GetVPNGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() + gatewayResponse, err := d.client.DefaultAPI.GetGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError ok := errors.As(err, &oapiErr) diff --git a/stackit/internal/services/vpn/gateway/datasource_test.go b/stackit/internal/services/vpn/gateway/datasource_test.go index 8f61f7fc2..d34a52e57 100644 --- a/stackit/internal/services/vpn/gateway/datasource_test.go +++ b/stackit/internal/services/vpn/gateway/datasource_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" ) func TestDataSourceMapFields(t *testing.T) { diff --git a/stackit/internal/services/vpn/gateway/resource.go b/stackit/internal/services/vpn/gateway/resource.go index d8a1bd0e2..b39b4a2e7 100644 --- a/stackit/internal/services/vpn/gateway/resource.go +++ b/stackit/internal/services/vpn/gateway/resource.go @@ -20,8 +20,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" - "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api/wait" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -31,10 +31,10 @@ import ( ) var ( - _ resource.Resource = &vpnGatewayResource{} - _ resource.ResourceWithConfigure = &vpnGatewayResource{} - _ resource.ResourceWithImportState = &vpnGatewayResource{} - _ resource.ResourceWithModifyPlan = &vpnGatewayResource{} + _ resource.Resource = &gatewayResource{} + _ resource.ResourceWithConfigure = &gatewayResource{} + _ resource.ResourceWithImportState = &gatewayResource{} + _ resource.ResourceWithModifyPlan = &gatewayResource{} routingTypeOptions = []string{"POLICY_BASED", "ROUTE_BASED", "BGP_ROUTE_BASED"} ) @@ -77,16 +77,16 @@ var schemaDescriptions = map[string]string{ "state": "The current lifecycle state of the gateway (PENDING, READY, ERROR, DELETING).", } -type vpnGatewayResource struct { +type gatewayResource struct { client *vpn.APIClient providerData core.ProviderData } -func NewVpnGatewayResource() resource.Resource { - return &vpnGatewayResource{} +func NewGatewayResource() resource.Resource { + return &gatewayResource{} } -func (r *vpnGatewayResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *gatewayResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return @@ -101,11 +101,11 @@ func (r *vpnGatewayResource) Configure(ctx context.Context, req resource.Configu tflog.Info(ctx, "VPN client configured") } -func (r *vpnGatewayResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *gatewayResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_vpn_gateway" } -func (r *vpnGatewayResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *gatewayResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: fmt.Sprintf("VPN Gateway resource schema. %s", core.ResourceRegionFallbackDocstring), Attributes: map[string]schema.Attribute{ @@ -219,7 +219,7 @@ func (r *vpnGatewayResource) Schema(_ context.Context, _ resource.SchemaRequest, } } -func (r *vpnGatewayResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *gatewayResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform var configModel Model if req.Config.Raw.IsNull() { return @@ -246,7 +246,7 @@ func (r *vpnGatewayResource) ModifyPlan(ctx context.Context, req resource.Modify } } -func (r *vpnGatewayResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *gatewayResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -265,7 +265,7 @@ func (r *vpnGatewayResource) ImportState(ctx context.Context, req resource.Impor tflog.Info(ctx, "VPN gateway state imported") } -func (r *vpnGatewayResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *gatewayResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -286,7 +286,7 @@ func (r *vpnGatewayResource) Create(ctx context.Context, req resource.CreateRequ return } - createResp, err := r.client.DefaultAPI.CreateVPNGateway(ctx, projectId, vpn.Region(region)).CreateVPNGatewayPayload(*payload).Execute() + createResp, err := r.client.DefaultAPI.CreateGateway(ctx, projectId, vpn.Region(region)).CreateGatewayPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN gateway", fmt.Sprintf("Calling API: %v", err)) return @@ -309,7 +309,7 @@ func (r *vpnGatewayResource) Create(ctx context.Context, req resource.CreateRequ return } - waitResp, err := wait.CreateOrUpdateGatewayWaitHandler(ctx, r.client.DefaultAPI, projectId, vpn.Region(region), gatewayId).WaitWithContext(ctx) + waitResp, err := wait.CreateGatewayWaitHandler(ctx, r.client.DefaultAPI, projectId, vpn.Region(region), gatewayId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating VPN gateway", fmt.Sprintf("Gateway creation waiting: %v", err)) return @@ -329,7 +329,7 @@ func (r *vpnGatewayResource) Create(ctx context.Context, req resource.CreateRequ tflog.Info(ctx, "VPN gateway created") } -func (r *vpnGatewayResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *gatewayResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -346,7 +346,7 @@ func (r *vpnGatewayResource) Read(ctx context.Context, req resource.ReadRequest, ctx = tflog.SetField(ctx, "gateway_id", gatewayId) ctx = tflog.SetField(ctx, "region", region) - gatewayResp, err := r.client.DefaultAPI.GetVPNGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() + gatewayResp, err := r.client.DefaultAPI.GetGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { @@ -373,7 +373,7 @@ func (r *vpnGatewayResource) Read(ctx context.Context, req resource.ReadRequest, tflog.Info(ctx, "VPN gateway read") } -func (r *vpnGatewayResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *gatewayResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -396,7 +396,7 @@ func (r *vpnGatewayResource) Update(ctx context.Context, req resource.UpdateRequ return } - _, err = r.client.DefaultAPI.UpdateVPNGateway(ctx, projectId, vpn.Region(region), gatewayId).UpdateVPNGatewayPayload(*payload).Execute() + _, err = r.client.DefaultAPI.UpdateGateway(ctx, projectId, vpn.Region(region), gatewayId).UpdateGatewayPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN gateway", err.Error()) return @@ -404,7 +404,7 @@ func (r *vpnGatewayResource) Update(ctx context.Context, req resource.UpdateRequ ctx = core.LogResponse(ctx) - waitResp, err := wait.CreateOrUpdateGatewayWaitHandler(ctx, r.client.DefaultAPI, projectId, vpn.Region(region), gatewayId).WaitWithContext(ctx) + waitResp, err := wait.UpdateGatewayWaitHandler(ctx, r.client.DefaultAPI, projectId, vpn.Region(region), gatewayId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating VPN gateway", fmt.Sprintf("Gateway update waiting: %v", err)) return @@ -424,7 +424,7 @@ func (r *vpnGatewayResource) Update(ctx context.Context, req resource.UpdateRequ tflog.Info(ctx, "VPN gateway updated") } -func (r *vpnGatewayResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *gatewayResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -441,7 +441,7 @@ func (r *vpnGatewayResource) Delete(ctx context.Context, req resource.DeleteRequ ctx = tflog.SetField(ctx, "gateway_id", gatewayId) ctx = tflog.SetField(ctx, "region", region) - err := r.client.DefaultAPI.DeleteVPNGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() + err := r.client.DefaultAPI.DeleteGateway(ctx, projectId, vpn.Region(region), gatewayId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting VPN gateway", fmt.Sprintf("Calling API: %v", err)) return @@ -458,7 +458,7 @@ func (r *vpnGatewayResource) Delete(ctx context.Context, req resource.DeleteRequ tflog.Info(ctx, "VPN gateway deleted") } -func toCreatePayload(ctx context.Context, model *Model) (*vpn.CreateVPNGatewayPayload, error) { +func toCreatePayload(ctx context.Context, model *Model) (*vpn.CreateGatewayPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } @@ -469,11 +469,11 @@ func toCreatePayload(ctx context.Context, model *Model) (*vpn.CreateVPNGatewayPa azTunnel1 := model.AvailabilityZones.Tunnel1.ValueString() azTunnel2 := model.AvailabilityZones.Tunnel2.ValueString() - payload := &vpn.CreateVPNGatewayPayload{ + payload := &vpn.CreateGatewayPayload{ DisplayName: model.DisplayName.ValueString(), PlanId: model.PlanID.ValueString(), RoutingType: vpn.RoutingType(model.RoutingType.ValueString()), - AvailabilityZones: vpn.CreateVPNGatewayPayloadAvailabilityZones{ + AvailabilityZones: vpn.CreateGatewayPayloadAvailabilityZones{ Tunnel1: azTunnel1, Tunnel2: azTunnel2, }, @@ -482,7 +482,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*vpn.CreateVPNGatewayPa if model.Bgp != nil { bgpConfig := &vpn.BGPGatewayConfig{} if !model.Bgp.LocalAsn.IsNull() && !model.Bgp.LocalAsn.IsUnknown() { - asn := int32(model.Bgp.LocalAsn.ValueInt64()) + asn := model.Bgp.LocalAsn.ValueInt64() bgpConfig.LocalAsn = &asn } if !model.Bgp.OverrideAdvertisedRoutes.IsNull() && !model.Bgp.OverrideAdvertisedRoutes.IsUnknown() { @@ -508,7 +508,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*vpn.CreateVPNGatewayPa return payload, nil } -func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateVPNGatewayPayload, error) { +func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateGatewayPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } @@ -519,10 +519,10 @@ func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateVPNGatewayPa azTunnel1 := model.AvailabilityZones.Tunnel1.ValueString() azTunnel2 := model.AvailabilityZones.Tunnel2.ValueString() - payload := &vpn.UpdateVPNGatewayPayload{ + payload := &vpn.UpdateGatewayPayload{ DisplayName: model.DisplayName.ValueString(), PlanId: model.PlanID.ValueString(), - AvailabilityZones: vpn.UpdateVPNGatewayPayloadAvailabilityZones{ + AvailabilityZones: vpn.UpdateGatewayPayloadAvailabilityZones{ Tunnel1: azTunnel1, Tunnel2: azTunnel2, }, @@ -532,7 +532,7 @@ func toUpdatePayload(ctx context.Context, model *Model) (*vpn.UpdateVPNGatewayPa if model.Bgp != nil { bgpConfig := &vpn.BGPGatewayConfig{} if !model.Bgp.LocalAsn.IsNull() && !model.Bgp.LocalAsn.IsUnknown() { - asn := int32(model.Bgp.LocalAsn.ValueInt64()) + asn := model.Bgp.LocalAsn.ValueInt64() bgpConfig.LocalAsn = &asn } if !model.Bgp.OverrideAdvertisedRoutes.IsNull() && !model.Bgp.OverrideAdvertisedRoutes.IsUnknown() { diff --git a/stackit/internal/services/vpn/gateway/resource_test.go b/stackit/internal/services/vpn/gateway/resource_test.go index 0aa30b867..d2998eb2c 100644 --- a/stackit/internal/services/vpn/gateway/resource_test.go +++ b/stackit/internal/services/vpn/gateway/resource_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" ) func TestMapFields(t *testing.T) { @@ -57,7 +57,7 @@ func TestMapFields(t *testing.T) { Tunnel2: "eu01-2", }, Bgp: &vpn.BGPGatewayConfig{ - LocalAsn: new(int32(65000)), + LocalAsn: new(int64(65000)), OverrideAdvertisedRoutes: []string{"10.0.0.0/16", "192.168.0.0/24"}, }, Labels: &map[string]string{ diff --git a/stackit/internal/services/vpn/utils/utils.go b/stackit/internal/services/vpn/utils/utils.go index 766f42ab3..e8e6577ef 100644 --- a/stackit/internal/services/vpn/utils/utils.go +++ b/stackit/internal/services/vpn/utils/utils.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" diff --git a/stackit/internal/services/vpn/utils/utils_test.go b/stackit/internal/services/vpn/utils/utils_test.go index 400081971..fde745c68 100644 --- a/stackit/internal/services/vpn/utils/utils_test.go +++ b/stackit/internal/services/vpn/utils/utils_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" "github.com/stackitcloud/stackit-sdk-go/core/config" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go index 6f762d0b7..fbae0b010 100644 --- a/stackit/internal/services/vpn/vpn_acc_test.go +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" @@ -243,7 +243,7 @@ func testAccCheckVpnGatewayDestroy(s *terraform.State) error { gatewaysToDestroy = append(gatewaysToDestroy, gatewayId) } - gatewaysResp, err := client.DefaultAPI.ListVPNGateways(ctx, testutil.ProjectId, vpn.REGION_EU01).Execute() + gatewaysResp, err := client.DefaultAPI.ListGateways(ctx, testutil.ProjectId, vpn.REGION_EU01).Execute() if err != nil { return fmt.Errorf("getting gateways: %w", err) } @@ -255,7 +255,7 @@ func testAccCheckVpnGatewayDestroy(s *terraform.State) error { } for _, gatewayId := range gatewaysToDestroy { if *gateway.Id == gatewayId { - err := client.DefaultAPI.DeleteVPNGateway(ctx, testutil.ProjectId, vpn.REGION_EU01, *gateway.Id).Execute() + err := client.DefaultAPI.DeleteGateway(ctx, testutil.ProjectId, vpn.REGION_EU01, *gateway.Id).Execute() if err != nil { return fmt.Errorf("destroying gateway %s during CheckDestroy: %w", gatewayId, err) } diff --git a/stackit/provider.go b/stackit/provider.go index 7ba261c42..c864757ff 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -804,7 +804,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { compliancelock.NewComplianceLockResource, serverBackupEnable.NewServerBackupEnableResource, serverUpdateEnable.NewServerUpdateEnableResource, - vpnGateway.NewVpnGatewayResource, + vpnGateway.NewGatewayResource, } resources = append(resources, roleAssignements.NewRoleAssignmentResources()...) resources = append(resources, customRole.NewCustomRoleResources()...) From 9dddb67365ba2df5b82a71ca9548ea2ab0c9e892 Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Wed, 13 May 2026 13:59:41 +0200 Subject: [PATCH 12/13] feat(vpn): update vpn service dependency to v0.8.0 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 506d9b7bb..5671b8c41 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/sfs v0.9.0 github.com/stackitcloud/stackit-sdk-go/services/ske v1.14.0 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.10.0 - github.com/stackitcloud/stackit-sdk-go/services/vpn v0.5.1 + github.com/stackitcloud/stackit-sdk-go/services/vpn v0.8.0 github.com/teambition/rrule-go v1.8.2 golang.org/x/mod v0.35.0 ) diff --git a/go.sum b/go.sum index d6b2921ab..972d94068 100644 --- a/go.sum +++ b/go.sum @@ -734,8 +734,8 @@ github.com/stackitcloud/stackit-sdk-go/services/ske v1.14.0 h1:Zy3yxmHzW+ydu1nae github.com/stackitcloud/stackit-sdk-go/services/ske v1.14.0/go.mod h1:TbqmZhLMofmfl+HhVl6oHYcI3zvXTm1vRjN3A/fOkM4= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.10.0 h1:angvO3z0TGqZtdwTDsG/tgTw9hxB76A6leUsiUXQtME= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.10.0/go.mod h1:AiUoMAqQcOlMgDtkVJlqI7P/VGD5xjN3dYjERGnwN/M= -github.com/stackitcloud/stackit-sdk-go/services/vpn v0.5.1 h1:qmfgch4YCMMstSgzDysCLhvaAhHpTf+EPy0wUUmxN70= -github.com/stackitcloud/stackit-sdk-go/services/vpn v0.5.1/go.mod h1:zcCQlA79aWlv2Voc34EgerEFgPGxkO7CE8Ilm+nG9Yw= +github.com/stackitcloud/stackit-sdk-go/services/vpn v0.8.0 h1:YCFkFcWLf6MaKGGuin5YztQGb5CadE/23ciCkvxJh1U= +github.com/stackitcloud/stackit-sdk-go/services/vpn v0.8.0/go.mod h1:toIjQk1dhxdUFVyCWJJja0w/0nFpDid8MWX0ukQfvfo= github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From a1d144a48ea0492c4f86892e49d52e4e285b446e Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Wed, 13 May 2026 13:59:46 +0200 Subject: [PATCH 13/13] feat(docs): add VPN Gateway documentation --- docs/data-sources/vpn_gateway.md | 50 +++++++++++++++++++++ docs/index.md | 1 + docs/resources/vpn_gateway.md | 74 ++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 docs/data-sources/vpn_gateway.md create mode 100644 docs/resources/vpn_gateway.md diff --git a/docs/data-sources/vpn_gateway.md b/docs/data-sources/vpn_gateway.md new file mode 100644 index 000000000..8da1e9115 --- /dev/null +++ b/docs/data-sources/vpn_gateway.md @@ -0,0 +1,50 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_vpn_gateway Data Source - stackit" +subcategory: "" +description: |- + VPN Gateway data source schema. Uses the default_region specified in the provider configuration as a fallback in case no region is defined on datasource level. +--- + +# stackit_vpn_gateway (Data Source) + +VPN Gateway data source schema. Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on datasource level. + + + + +## Schema + +### Required + +- `gateway_id` (String) The server-generated UUID of the VPN gateway. +- `project_id` (String) STACKIT project ID associated with the VPN gateway. + +### Read-Only + +- `availability_zones` (Attributes) Availability zones for the two tunnel endpoints. (see [below for nested schema](#nestedatt--availability_zones)) +- `bgp` (Attributes) BGP configuration. Only applicable when routing_type is BGP_ROUTE_BASED. (see [below for nested schema](#nestedatt--bgp)) +- `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). +- `plan_id` (String) The service plan identifier (e.g. p500). +- `region` (String) STACKIT region (e.g. eu01). +- `routing_type` (String) Routing architecture: POLICY_BASED, ROUTE_BASED, or BGP_ROUTE_BASED. +- `state` (String) The current lifecycle state of the gateway (PENDING, READY, ERROR, DELETING). + + +### Nested Schema for `availability_zones` + +Read-Only: + +- `tunnel1` (String) Availability zone for tunnel 1. +- `tunnel2` (String) Availability zone for tunnel 2. + + + +### Nested Schema for `bgp` + +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. diff --git a/docs/index.md b/docs/index.md index 300fcbd63..a8f566726 100644 --- a/docs/index.md +++ b/docs/index.md @@ -212,3 +212,4 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `sqlserverflex_custom_endpoint` (String) Custom endpoint for the SQL Server Flex service - `token_custom_endpoint` (String) Custom endpoint for the token API, which is used to request access tokens when using the key flow - `use_oidc` (Boolean) Enables OIDC for Authentication. This can also be sourced from the `STACKIT_USE_OIDC` Environment Variable. Defaults to `false`. +- `vpn_custom_endpoint` (String) Custom endpoint for the VPN service diff --git a/docs/resources/vpn_gateway.md b/docs/resources/vpn_gateway.md new file mode 100644 index 000000000..21b1e68e2 --- /dev/null +++ b/docs/resources/vpn_gateway.md @@ -0,0 +1,74 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_vpn_gateway Resource - stackit" +subcategory: "" +description: |- + VPN Gateway resource schema. Uses the default_region specified in the provider configuration as a fallback in case no region is defined on resource level. +--- + +# stackit_vpn_gateway (Resource) + +VPN Gateway resource schema. Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on resource level. + +## Example Usage + +```terraform +resource "stackit_vpn_gateway" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region = "eu01" + display_name = "example-vpn-gateway" + plan_id = "p500" + routing_type = "ROUTE_BASED" + + availability_zones = { + tunnel1 = "eu01-1" + tunnel2 = "eu01-2" + } +} + +# Only use the import statement, if you want to import an existing VPN gateway +import { + to = stackit_vpn_gateway.example + id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,eu01,xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `availability_zones` (Attributes) Availability zones for the two tunnel endpoints. (see [below for nested schema](#nestedatt--availability_zones)) +- `display_name` (String) A user-friendly name for the VPN gateway. +- `plan_id` (String) The service plan identifier (e.g. p500). +- `project_id` (String) STACKIT project ID associated with the VPN gateway. +- `routing_type` (String) Routing architecture: POLICY_BASED, ROUTE_BASED, or BGP_ROUTE_BASED. + +### Optional + +- `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). +- `region` (String) STACKIT region (e.g. eu01). + +### Read-Only + +- `gateway_id` (String) The server-generated UUID of the VPN gateway. +- `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`gateway_id`". +- `state` (String) The current lifecycle state of the gateway (PENDING, READY, ERROR, DELETING). + + +### Nested Schema for `availability_zones` + +Required: + +- `tunnel1` (String) Availability zone for tunnel 1. +- `tunnel2` (String) Availability zone for tunnel 2. + + + +### Nested Schema for `bgp` + +Optional: + +- `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.