From f339a8c0e8a5922fd36e37db716f99ef439dc52b Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Wed, 10 Jun 2026 17:36:39 +0200 Subject: [PATCH 1/8] feature(vpn): onboard vpn gateway status endpoint relates to STACKITTPR-665 --- docs/data-sources/vpn_gateway_status.md | 40 +++ .../services/vpn/gateway_status/datasource.go | 301 ++++++++++++++++++ .../vpn/gateway_status/datasource_test.go | 164 ++++++++++ stackit/provider.go | 2 + 4 files changed, 507 insertions(+) create mode 100644 docs/data-sources/vpn_gateway_status.md create mode 100644 stackit/internal/services/vpn/gateway_status/datasource.go create mode 100644 stackit/internal/services/vpn/gateway_status/datasource_test.go diff --git a/docs/data-sources/vpn_gateway_status.md b/docs/data-sources/vpn_gateway_status.md new file mode 100644 index 000000000..a02803981 --- /dev/null +++ b/docs/data-sources/vpn_gateway_status.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_vpn_gateway_status Data Source - stackit" +subcategory: "" +description: |- + VPN Gateway Status 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_status (Data Source) + +VPN Gateway Status 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 + +- `display_name` (String) A user-friendly name for the VPN gateway. +- `error_message` (String) A descriptive message provided when the gateway is in an error state. +- `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`gateway_id`". +- `region` (String) STACKIT region name the resource is located in. If not defined, the provider region is used. +- `state` (String) The current life cycle state of the gateway. Possible values are: `PENDING`, `READY`, `ERROR`, `DELETING`. +- `tunnels` (Attributes List) List of the VPN tunnels in the gateway. (see [below for nested schema](#nestedatt--tunnels)) + + +### Nested Schema for `tunnels` + +Read-Only: + +- `instance_state` (String) The current life cycle state of the tunnel. Possible values are: `PENDING`, `READY`, `ERROR`, `DELETING`. +- `internal_next_hop_ip` (String) The IPv4 address of the endpoint in the SNA. +- `name` (String) The name of the VPN tunnel. Possible values are: `tunnel1`, `tunnel2`. +- `public_ip` (String) The public IPv4 address of this endpoint. diff --git a/stackit/internal/services/vpn/gateway_status/datasource.go b/stackit/internal/services/vpn/gateway_status/datasource.go new file mode 100644 index 000000000..af376a62c --- /dev/null +++ b/stackit/internal/services/vpn/gateway_status/datasource.go @@ -0,0 +1,301 @@ +package gateway_status + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + 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" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = &vpnGatewayStatusDataSource{} + _ datasource.DataSourceWithConfigure = &vpnGatewayStatusDataSource{} + + gatewayStates = sdkUtils.EnumSliceToStringSlice(vpn.AllowedGatewayStatusEnumValues) + tunnelNames = sdkUtils.EnumSliceToStringSlice(vpn.AllowedVPNTunnelsNameEnumValues) +) + +type vpnGatewayStatusDataSource struct { + client *vpn.APIClient + providerData core.ProviderData +} + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + GatewayId types.String `tfsdk:"gateway_id"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + DisplayName types.String `tfsdk:"display_name"` + GatewayStatus types.String `tfsdk:"state"` + ErrorMessage types.String `tfsdk:"error_message"` + Tunnels types.List `tfsdk:"tunnels"` +} + +type Tunnel struct { + InstanceState types.String `tfsdk:"instance_state"` + InternalNextHopIP types.String `tfsdk:"internal_next_hop_ip"` + Name types.String `tfsdk:"name"` + PublicIP types.String `tfsdk:"public_ip"` +} + +var tunnelsType = map[string]attr.Type{ + "instance_state": basetypes.StringType{}, + "internal_next_hop_ip": basetypes.StringType{}, + "name": basetypes.StringType{}, + "public_ip": basetypes.StringType{}, +} + +func NewVPNGatewayStatusDataSource() datasource.DataSource { + return &vpnGatewayStatusDataSource{} +} + +func (d *vpnGatewayStatusDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + d.client = utils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "VPN client configured") +} + +func (d *vpnGatewayStatusDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vpn_gateway_status" +} + +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 name the resource is located in. If not defined, the provider region is used.", + "display_name": "A user-friendly name for the VPN gateway.", + "error_message": "A descriptive message provided when the gateway is in an error state.", + "state": fmt.Sprintf("The current life cycle state of the gateway. %s", tfutils.FormatPossibleValues(gatewayStates...)), + "tunnels": "List of the VPN tunnels in the gateway.", + "tunnel_instance_state": fmt.Sprintf("The current life cycle state of the tunnel. %s", tfutils.FormatPossibleValues(gatewayStates...)), + "tunnel_internal_next_hop_ip": "The IPv4 address of the endpoint in the SNA.", + "tunnel_name": fmt.Sprintf("The name of the VPN tunnel. %s", tfutils.FormatPossibleValues(tunnelNames...)), + "tunnel_public_ip": "The public IPv4 address of this endpoint.", +} + +func (d *vpnGatewayStatusDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: fmt.Sprintf("VPN Gateway Status 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"], + Computed: 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, + }, + "error_message": schema.StringAttribute{ + Description: schemaDescriptions["error_message"], + Computed: true, + }, + "state": schema.StringAttribute{ + Description: schemaDescriptions["state"], + Computed: true, + }, + "tunnels": schema.ListNestedAttribute{ + Description: schemaDescriptions["tunnels"], + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "instance_state": schema.StringAttribute{ + Description: schemaDescriptions["tunnel_instance_state"], + Computed: true, + }, + "internal_next_hop_ip": schema.StringAttribute{ + Description: schemaDescriptions["tunnel_internal_next_hop_ip"], + Computed: true, + }, + "name": schema.StringAttribute{ + Description: schemaDescriptions["tunnel_name"], + Computed: true, + }, + "public_ip": schema.StringAttribute{ + Description: schemaDescriptions["tunnel_public_ip"], + Computed: true, + }, + }, + }, + }, + }, + } +} + +func (d *vpnGatewayStatusDataSource) 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...) + 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.GetGatewayStatus(ctx, projectId, 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 + } + + 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, + }) +} + +func mapFields(ctx context.Context, gatewayStatus *vpn.GatewayStatusResponse, model *Model, region string) error { + if gatewayStatus == 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 gatewayStatus.Id != nil { + gatewayId = *gatewayStatus.Id + } else { + return fmt.Errorf("gateway id not present") + } + + model.Id = tfutils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, gatewayId) + model.GatewayId = types.StringValue(gatewayId) + model.Region = types.StringValue(region) + + if gatewayStatus.DisplayName != nil { + model.DisplayName = types.StringValue(*gatewayStatus.DisplayName) + } + + if gatewayStatus.GatewayStatus != nil { + model.GatewayStatus = types.StringValue(string(*gatewayStatus.GatewayStatus)) + } + + if gatewayStatus.ErrorMessage != nil { + model.ErrorMessage = types.StringValue(*gatewayStatus.ErrorMessage) + } + + if err := mapTunnels(ctx, gatewayStatus, model); err != nil { + return fmt.Errorf("map tunnels: %w", err) + } + + return nil +} + +func mapTunnels(ctx context.Context, gatewayStatus *vpn.GatewayStatusResponse, model *Model) error { + if gatewayStatus == nil { + return fmt.Errorf("gatewayStatus is nil") + } + if model == nil { + return fmt.Errorf("model is nil") + } + + tunnels := []attr.Value{} + + for _, tunnelItem := range gatewayStatus.Tunnels { + tunnel := Tunnel{} + + if tunnelItem.InstanceState != nil { + tunnel.InstanceState = types.StringValue(string(*tunnelItem.InstanceState)) + } + if tunnelItem.InternalNextHopIP != nil { + tunnel.InternalNextHopIP = types.StringValue(string(*tunnelItem.InternalNextHopIP)) + } + if tunnelItem.Name != nil { + tunnel.Name = types.StringValue(string(*tunnelItem.Name)) + } + if tunnelItem.PublicIP != nil { + tunnel.PublicIP = types.StringValue(string(*tunnelItem.PublicIP)) + } + + tunnelValue, diags := types.ObjectValueFrom(ctx, tunnelsType, tunnel) + if diags.HasError() { + return fmt.Errorf("mapping tunnel: %w", core.DiagsToError(diags)) + } + + tunnels = append(tunnels, tunnelValue) + } + + var diags diag.Diagnostics + model.Tunnels, diags = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: tunnelsType}, tunnels) + if diags.HasError() { + return fmt.Errorf("mapping tunnels: %w", core.DiagsToError(diags)) + } + + return nil +} diff --git a/stackit/internal/services/vpn/gateway_status/datasource_test.go b/stackit/internal/services/vpn/gateway_status/datasource_test.go new file mode 100644 index 000000000..44441609e --- /dev/null +++ b/stackit/internal/services/vpn/gateway_status/datasource_test.go @@ -0,0 +1,164 @@ +package gateway_status + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" +) + +var ( + testProjectId = uuid.NewString() + testGatewayId = uuid.NewString() + testRegion = "eu01" + testDisplayName = "Gateway" + testId = testProjectId + "," + testRegion + "," + testGatewayId + testTunnel1InternalNextHopIP = "123.45.67.89" + testTunnel1PublicIP = "98.76.54.32" + testTunnel2InternalNextHopIP = "123.45.67.89" + testTunnel2PublicIP = "98.76.54.32" + testErrorMessage = "foo bar" +) + +func fixtureInput(mods ...func(m *vpn.GatewayStatusResponse)) *vpn.GatewayStatusResponse { + resp := &vpn.GatewayStatusResponse{ + Id: &testGatewayId, + Connections: []vpn.ConnectionStatusResponse{}, + DisplayName: &testDisplayName, + GatewayStatus: vpn.GATEWAYSTATUS_READY.Ptr(), + ErrorMessage: &testErrorMessage, + Tunnels: []vpn.VPNTunnels{ + { + InstanceState: vpn.GATEWAYSTATUS_READY.Ptr(), + InternalNextHopIP: &testTunnel1InternalNextHopIP, + Name: vpn.VPNTUNNELSNAME_TUNNEL1.Ptr(), + PublicIP: &testTunnel1PublicIP, + }, + { + InstanceState: vpn.GATEWAYSTATUS_READY.Ptr(), + InternalNextHopIP: &testTunnel2InternalNextHopIP, + Name: vpn.VPNTUNNELSNAME_TUNNEL2.Ptr(), + PublicIP: &testTunnel2PublicIP, + }, + }, + } + for _, mod := range mods { + mod(resp) + } + return resp +} + +func fixtureModel(mods ...func(m *Model)) *Model { + resp := &Model{ + ProjectId: types.StringValue(testProjectId), + Region: types.StringValue(testRegion), + Id: types.StringValue(testId), + GatewayId: types.StringValue(testGatewayId), + DisplayName: types.StringValue(testDisplayName), + GatewayStatus: types.StringValue(string(vpn.GATEWAYSTATUS_READY)), + ErrorMessage: types.StringValue(testErrorMessage), + Tunnels: types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{ + types.ObjectValueMust(tunnelsType, map[string]attr.Value{ + "instance_state": types.StringValue(string(vpn.GATEWAYSTATUS_READY)), + "internal_next_hop_ip": types.StringValue(testTunnel1InternalNextHopIP), + "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL1)), + "public_ip": types.StringValue(testTunnel1PublicIP), + }), + types.ObjectValueMust(tunnelsType, map[string]attr.Value{ + "instance_state": types.StringValue(string(vpn.GATEWAYSTATUS_READY)), + "internal_next_hop_ip": types.StringValue(testTunnel2InternalNextHopIP), + "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL2)), + "public_ip": types.StringValue(testTunnel2PublicIP), + }), + }), + } + for _, mod := range mods { + mod(resp) + } + return resp +} + +func TestMapDatasourceFields(t *testing.T) { + tests := []struct { + name string + region string + state *Model + input *vpn.GatewayStatusResponse + expected *Model + isValid bool + }{ + { + "default_values", + "eu01", + &Model{ + ProjectId: types.StringValue(testProjectId), + }, + fixtureInput(), + fixtureModel(), + true, + }, + { + "no_input", + "eu01", + &Model{ + ProjectId: types.StringValue(testProjectId), + GatewayId: types.StringValue(testGatewayId), + }, + nil, + nil, + false, + }, + { + "no_model", + "eu01", + nil, + &vpn.GatewayStatusResponse{}, + nil, + false, + }, + { + "no_gateway_id", + "eu01", + &Model{ + ProjectId: types.StringValue(testProjectId), + }, + &vpn.GatewayStatusResponse{}, + nil, + false, + }, + { + "empty_input", + "eu01", + &Model{ + ProjectId: types.StringValue(testProjectId), + GatewayId: types.StringValue(testGatewayId), + }, + &vpn.GatewayStatusResponse{}, + &Model{ + Id: types.StringValue(testId), + ProjectId: types.StringValue(testProjectId), + GatewayId: types.StringValue(testGatewayId), + Region: types.StringValue(testRegion), + Tunnels: types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{}), + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if err := mapFields(ctx, tt.input, tt.state, tt.region); (err == nil) != tt.isValid { + t.Errorf("unexpected error: %s", err) + } + if tt.isValid { + if diff := cmp.Diff(tt.state, tt.expected); diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/provider.go b/stackit/provider.go index 0470af21a..7c653f474 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -129,6 +129,7 @@ import ( telemetryRouterDestination "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/destination" telemetryRouterInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetryrouter/instance" vpnGateway "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/gateway" + vpnGatewayStatus "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/vpn/gateway_status" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -757,6 +758,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource telemetryRouterDestination.NewTelemetryRouterDestinationDataSource, telemetryLink.NewTelemetryLinkDataSource, vpnGateway.NewVPNGatewayDataSource, + vpnGatewayStatus.NewVPNGatewayStatusDataSource, } dataSources = append(dataSources, customRole.NewCustomRoleDataSources()...) dataSources = append(dataSources, iamRoleBindingsV1.NewRoleBindingsDatasources()...) From 8877ffb1a46b03fab5a919c084eae382044b93b2 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Wed, 10 Jun 2026 17:41:40 +0200 Subject: [PATCH 2/8] Add example --- docs/data-sources/vpn_gateway_status.md | 9 ++++++++- .../stackit_vpn_gateway_status/data-source.tf | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 examples/data-sources/stackit_vpn_gateway_status/data-source.tf diff --git a/docs/data-sources/vpn_gateway_status.md b/docs/data-sources/vpn_gateway_status.md index a02803981..01777be3f 100644 --- a/docs/data-sources/vpn_gateway_status.md +++ b/docs/data-sources/vpn_gateway_status.md @@ -10,7 +10,14 @@ description: |- VPN Gateway Status data source schema. Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on datasource level. - +## Example Usage + +```terraform +data "stackit_vpn_gateway_status" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + gateway_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` ## Schema diff --git a/examples/data-sources/stackit_vpn_gateway_status/data-source.tf b/examples/data-sources/stackit_vpn_gateway_status/data-source.tf new file mode 100644 index 000000000..cd4559e44 --- /dev/null +++ b/examples/data-sources/stackit_vpn_gateway_status/data-source.tf @@ -0,0 +1,4 @@ +data "stackit_vpn_gateway_status" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + gateway_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} From a0aecf06594cce858e9a128052030f54957f3cd5 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Thu, 11 Jun 2026 14:56:01 +0200 Subject: [PATCH 3/8] remove status and error message --- .../services/vpn/gateway_status/datasource.go | 45 +++---------------- .../vpn/gateway_status/datasource_test.go | 25 ++++------- 2 files changed, 15 insertions(+), 55 deletions(-) diff --git a/stackit/internal/services/vpn/gateway_status/datasource.go b/stackit/internal/services/vpn/gateway_status/datasource.go index af376a62c..be5765c8d 100644 --- a/stackit/internal/services/vpn/gateway_status/datasource.go +++ b/stackit/internal/services/vpn/gateway_status/datasource.go @@ -31,8 +31,7 @@ var ( _ datasource.DataSource = &vpnGatewayStatusDataSource{} _ datasource.DataSourceWithConfigure = &vpnGatewayStatusDataSource{} - gatewayStates = sdkUtils.EnumSliceToStringSlice(vpn.AllowedGatewayStatusEnumValues) - tunnelNames = sdkUtils.EnumSliceToStringSlice(vpn.AllowedVPNTunnelsNameEnumValues) + tunnelNames = sdkUtils.EnumSliceToStringSlice(vpn.AllowedVPNTunnelsNameEnumValues) ) type vpnGatewayStatusDataSource struct { @@ -41,25 +40,21 @@ type vpnGatewayStatusDataSource struct { } type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - GatewayId types.String `tfsdk:"gateway_id"` - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - DisplayName types.String `tfsdk:"display_name"` - GatewayStatus types.String `tfsdk:"state"` - ErrorMessage types.String `tfsdk:"error_message"` - Tunnels types.List `tfsdk:"tunnels"` + Id types.String `tfsdk:"id"` // needed by TF + GatewayId types.String `tfsdk:"gateway_id"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + DisplayName types.String `tfsdk:"display_name"` + Tunnels types.List `tfsdk:"tunnels"` } type Tunnel struct { - InstanceState types.String `tfsdk:"instance_state"` InternalNextHopIP types.String `tfsdk:"internal_next_hop_ip"` Name types.String `tfsdk:"name"` PublicIP types.String `tfsdk:"public_ip"` } var tunnelsType = map[string]attr.Type{ - "instance_state": basetypes.StringType{}, "internal_next_hop_ip": basetypes.StringType{}, "name": basetypes.StringType{}, "public_ip": basetypes.StringType{}, @@ -93,10 +88,7 @@ var schemaDescriptions = map[string]string{ "project_id": "STACKIT project ID associated with the VPN gateway.", "region": "STACKIT region name the resource is located in. If not defined, the provider region is used.", "display_name": "A user-friendly name for the VPN gateway.", - "error_message": "A descriptive message provided when the gateway is in an error state.", - "state": fmt.Sprintf("The current life cycle state of the gateway. %s", tfutils.FormatPossibleValues(gatewayStates...)), "tunnels": "List of the VPN tunnels in the gateway.", - "tunnel_instance_state": fmt.Sprintf("The current life cycle state of the tunnel. %s", tfutils.FormatPossibleValues(gatewayStates...)), "tunnel_internal_next_hop_ip": "The IPv4 address of the endpoint in the SNA.", "tunnel_name": fmt.Sprintf("The name of the VPN tunnel. %s", tfutils.FormatPossibleValues(tunnelNames...)), "tunnel_public_ip": "The public IPv4 address of this endpoint.", @@ -134,23 +126,11 @@ func (d *vpnGatewayStatusDataSource) Schema(_ context.Context, _ datasource.Sche Description: schemaDescriptions["display_name"], Computed: true, }, - "error_message": schema.StringAttribute{ - Description: schemaDescriptions["error_message"], - Computed: true, - }, - "state": schema.StringAttribute{ - Description: schemaDescriptions["state"], - Computed: true, - }, "tunnels": schema.ListNestedAttribute{ Description: schemaDescriptions["tunnels"], Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "instance_state": schema.StringAttribute{ - Description: schemaDescriptions["tunnel_instance_state"], - Computed: true, - }, "internal_next_hop_ip": schema.StringAttribute{ Description: schemaDescriptions["tunnel_internal_next_hop_ip"], Computed: true, @@ -242,14 +222,6 @@ func mapFields(ctx context.Context, gatewayStatus *vpn.GatewayStatusResponse, mo model.DisplayName = types.StringValue(*gatewayStatus.DisplayName) } - if gatewayStatus.GatewayStatus != nil { - model.GatewayStatus = types.StringValue(string(*gatewayStatus.GatewayStatus)) - } - - if gatewayStatus.ErrorMessage != nil { - model.ErrorMessage = types.StringValue(*gatewayStatus.ErrorMessage) - } - if err := mapTunnels(ctx, gatewayStatus, model); err != nil { return fmt.Errorf("map tunnels: %w", err) } @@ -270,9 +242,6 @@ func mapTunnels(ctx context.Context, gatewayStatus *vpn.GatewayStatusResponse, m for _, tunnelItem := range gatewayStatus.Tunnels { tunnel := Tunnel{} - if tunnelItem.InstanceState != nil { - tunnel.InstanceState = types.StringValue(string(*tunnelItem.InstanceState)) - } if tunnelItem.InternalNextHopIP != nil { tunnel.InternalNextHopIP = types.StringValue(string(*tunnelItem.InternalNextHopIP)) } diff --git a/stackit/internal/services/vpn/gateway_status/datasource_test.go b/stackit/internal/services/vpn/gateway_status/datasource_test.go index 44441609e..32a25352e 100644 --- a/stackit/internal/services/vpn/gateway_status/datasource_test.go +++ b/stackit/internal/services/vpn/gateway_status/datasource_test.go @@ -21,25 +21,20 @@ var ( testTunnel1PublicIP = "98.76.54.32" testTunnel2InternalNextHopIP = "123.45.67.89" testTunnel2PublicIP = "98.76.54.32" - testErrorMessage = "foo bar" ) func fixtureInput(mods ...func(m *vpn.GatewayStatusResponse)) *vpn.GatewayStatusResponse { resp := &vpn.GatewayStatusResponse{ - Id: &testGatewayId, - Connections: []vpn.ConnectionStatusResponse{}, - DisplayName: &testDisplayName, - GatewayStatus: vpn.GATEWAYSTATUS_READY.Ptr(), - ErrorMessage: &testErrorMessage, + Id: &testGatewayId, + Connections: []vpn.ConnectionStatusResponse{}, + DisplayName: &testDisplayName, Tunnels: []vpn.VPNTunnels{ { - InstanceState: vpn.GATEWAYSTATUS_READY.Ptr(), InternalNextHopIP: &testTunnel1InternalNextHopIP, Name: vpn.VPNTUNNELSNAME_TUNNEL1.Ptr(), PublicIP: &testTunnel1PublicIP, }, { - InstanceState: vpn.GATEWAYSTATUS_READY.Ptr(), InternalNextHopIP: &testTunnel2InternalNextHopIP, Name: vpn.VPNTUNNELSNAME_TUNNEL2.Ptr(), PublicIP: &testTunnel2PublicIP, @@ -54,22 +49,18 @@ func fixtureInput(mods ...func(m *vpn.GatewayStatusResponse)) *vpn.GatewayStatus func fixtureModel(mods ...func(m *Model)) *Model { resp := &Model{ - ProjectId: types.StringValue(testProjectId), - Region: types.StringValue(testRegion), - Id: types.StringValue(testId), - GatewayId: types.StringValue(testGatewayId), - DisplayName: types.StringValue(testDisplayName), - GatewayStatus: types.StringValue(string(vpn.GATEWAYSTATUS_READY)), - ErrorMessage: types.StringValue(testErrorMessage), + ProjectId: types.StringValue(testProjectId), + Region: types.StringValue(testRegion), + Id: types.StringValue(testId), + GatewayId: types.StringValue(testGatewayId), + DisplayName: types.StringValue(testDisplayName), Tunnels: types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{ types.ObjectValueMust(tunnelsType, map[string]attr.Value{ - "instance_state": types.StringValue(string(vpn.GATEWAYSTATUS_READY)), "internal_next_hop_ip": types.StringValue(testTunnel1InternalNextHopIP), "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL1)), "public_ip": types.StringValue(testTunnel1PublicIP), }), types.ObjectValueMust(tunnelsType, map[string]attr.Value{ - "instance_state": types.StringValue(string(vpn.GATEWAYSTATUS_READY)), "internal_next_hop_ip": types.StringValue(testTunnel2InternalNextHopIP), "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL2)), "public_ip": types.StringValue(testTunnel2PublicIP), From 1d863f18def5ebb6a0d0f82de68c3534a5b32526 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Thu, 11 Jun 2026 17:29:01 +0200 Subject: [PATCH 4/8] add acc tests --- stackit/internal/services/vpn/vpn_acc_test.go | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go index 535e2061e..5c097cf90 100644 --- a/stackit/internal/services/vpn/vpn_acc_test.go +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -127,6 +127,36 @@ func TestAccVpnGatewayResourceMin(t *testing.T) { resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), ), }, + // Status data source + { + ConfigVariables: gatewayMinVars, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_vpn_gateway_status" "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.TestCheckResourceAttrPair("data.stackit_vpn_gateway_status.gateway", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "region", testutil.Region), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMinVars["display_name"])), + + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "tunnels.#", "2"), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "tunnels.0.name", string(vpn.VPNTUNNELSNAME_TUNNEL1)), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.0.internal_next_hop_ip"), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.0.public_ip"), + + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "tunnels.1.name", string(vpn.VPNTUNNELSNAME_TUNNEL2)), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.1.internal_next_hop_ip"), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.1.public_ip"), + ), + }, // Update { ConfigVariables: gatewayMinVarsUpdated, @@ -226,6 +256,36 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { resource.TestCheckResourceAttrPair("data.stackit_vpn_gateway.gateway", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), ), }, + // Status data source + { + ConfigVariables: gatewayMaxVars, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_vpn_gateway_status" "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.TestCheckResourceAttrPair("data.stackit_vpn_gateway_status.gateway", "gateway_id", "stackit_vpn_gateway.gateway", "gateway_id"), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "region", testutil.Region), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "display_name", testutil.ConvertConfigVariable(gatewayMaxVars["display_name"])), + + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "tunnels.#", "2"), + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "tunnels.0.name", string(vpn.VPNTUNNELSNAME_TUNNEL1)), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.0.internal_next_hop_ip"), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.0.public_ip"), + + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "tunnels.1.name", string(vpn.VPNTUNNELSNAME_TUNNEL2)), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.1.internal_next_hop_ip"), + resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.1.public_ip"), + ), + }, // Update { ConfigVariables: gatewayMaxVarsUpdated, From 0bd47ee76fafffb9c93ab83b1d7e6a6430fe0254 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Tue, 16 Jun 2026 17:41:57 +0200 Subject: [PATCH 5/8] resolved comments --- docs/data-sources/vpn_gateway_status.md | 3 - .../services/vpn/gateway_status/datasource.go | 30 ++-- .../vpn/gateway_status/datasource_test.go | 143 +++++++++++++----- 3 files changed, 114 insertions(+), 62 deletions(-) diff --git a/docs/data-sources/vpn_gateway_status.md b/docs/data-sources/vpn_gateway_status.md index 01777be3f..821c90cd0 100644 --- a/docs/data-sources/vpn_gateway_status.md +++ b/docs/data-sources/vpn_gateway_status.md @@ -30,10 +30,8 @@ data "stackit_vpn_gateway_status" "example" { ### Read-Only - `display_name` (String) A user-friendly name for the VPN gateway. -- `error_message` (String) A descriptive message provided when the gateway is in an error state. - `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`gateway_id`". - `region` (String) STACKIT region name the resource is located in. If not defined, the provider region is used. -- `state` (String) The current life cycle state of the gateway. Possible values are: `PENDING`, `READY`, `ERROR`, `DELETING`. - `tunnels` (Attributes List) List of the VPN tunnels in the gateway. (see [below for nested schema](#nestedatt--tunnels)) @@ -41,7 +39,6 @@ data "stackit_vpn_gateway_status" "example" { Read-Only: -- `instance_state` (String) The current life cycle state of the tunnel. Possible values are: `PENDING`, `READY`, `ERROR`, `DELETING`. - `internal_next_hop_ip` (String) The IPv4 address of the endpoint in the SNA. - `name` (String) The name of the VPN tunnel. Possible values are: `tunnel1`, `tunnel2`. - `public_ip` (String) The public IPv4 address of this endpoint. diff --git a/stackit/internal/services/vpn/gateway_status/datasource.go b/stackit/internal/services/vpn/gateway_status/datasource.go index be5765c8d..0da332137 100644 --- a/stackit/internal/services/vpn/gateway_status/datasource.go +++ b/stackit/internal/services/vpn/gateway_status/datasource.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" @@ -30,8 +29,6 @@ import ( var ( _ datasource.DataSource = &vpnGatewayStatusDataSource{} _ datasource.DataSourceWithConfigure = &vpnGatewayStatusDataSource{} - - tunnelNames = sdkUtils.EnumSliceToStringSlice(vpn.AllowedVPNTunnelsNameEnumValues) ) type vpnGatewayStatusDataSource struct { @@ -90,7 +87,7 @@ var schemaDescriptions = map[string]string{ "display_name": "A user-friendly name for the VPN gateway.", "tunnels": "List of the VPN tunnels in the gateway.", "tunnel_internal_next_hop_ip": "The IPv4 address of the endpoint in the SNA.", - "tunnel_name": fmt.Sprintf("The name of the VPN tunnel. %s", tfutils.FormatPossibleValues(tunnelNames...)), + "tunnel_name": fmt.Sprintf("The name of the VPN tunnel. %s", tfutils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(vpn.AllowedVPNTunnelsNameEnumValues)...)), "tunnel_public_ip": "The public IPv4 address of this endpoint.", } @@ -222,24 +219,20 @@ func mapFields(ctx context.Context, gatewayStatus *vpn.GatewayStatusResponse, mo model.DisplayName = types.StringValue(*gatewayStatus.DisplayName) } - if err := mapTunnels(ctx, gatewayStatus, model); err != nil { + tfTunnels, err := mapTunnels(ctx, gatewayStatus.Tunnels) + if err != nil { return fmt.Errorf("map tunnels: %w", err) + } else if tfTunnels != nil { + model.Tunnels = *tfTunnels } return nil } -func mapTunnels(ctx context.Context, gatewayStatus *vpn.GatewayStatusResponse, model *Model) error { - if gatewayStatus == nil { - return fmt.Errorf("gatewayStatus is nil") - } - if model == nil { - return fmt.Errorf("model is nil") - } - +func mapTunnels(ctx context.Context, vpnTunnels []vpn.VPNTunnels) (*basetypes.ListValue, error) { tunnels := []attr.Value{} - for _, tunnelItem := range gatewayStatus.Tunnels { + for _, tunnelItem := range vpnTunnels { tunnel := Tunnel{} if tunnelItem.InternalNextHopIP != nil { @@ -254,17 +247,16 @@ func mapTunnels(ctx context.Context, gatewayStatus *vpn.GatewayStatusResponse, m tunnelValue, diags := types.ObjectValueFrom(ctx, tunnelsType, tunnel) if diags.HasError() { - return fmt.Errorf("mapping tunnel: %w", core.DiagsToError(diags)) + return nil, fmt.Errorf("mapping tunnel: %w", core.DiagsToError(diags)) } tunnels = append(tunnels, tunnelValue) } - var diags diag.Diagnostics - model.Tunnels, diags = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: tunnelsType}, tunnels) + tfTunnels, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: tunnelsType}, tunnels) if diags.HasError() { - return fmt.Errorf("mapping tunnels: %w", core.DiagsToError(diags)) + return nil, fmt.Errorf("mapping tunnels: %w", core.DiagsToError(diags)) } - return nil + return &tfTunnels, nil } diff --git a/stackit/internal/services/vpn/gateway_status/datasource_test.go b/stackit/internal/services/vpn/gateway_status/datasource_test.go index 32a25352e..e4759f6f9 100644 --- a/stackit/internal/services/vpn/gateway_status/datasource_test.go +++ b/stackit/internal/services/vpn/gateway_status/datasource_test.go @@ -2,42 +2,47 @@ package gateway_status import ( "context" + "reflect" "testing" "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" ) -var ( - testProjectId = uuid.NewString() - testGatewayId = uuid.NewString() +const ( testRegion = "eu01" testDisplayName = "Gateway" - testId = testProjectId + "," + testRegion + "," + testGatewayId testTunnel1InternalNextHopIP = "123.45.67.89" testTunnel1PublicIP = "98.76.54.32" testTunnel2InternalNextHopIP = "123.45.67.89" testTunnel2PublicIP = "98.76.54.32" ) +var ( + testProjectId = uuid.NewString() + testGatewayId = uuid.NewString() + testId = testProjectId + "," + testRegion + "," + testGatewayId +) + func fixtureInput(mods ...func(m *vpn.GatewayStatusResponse)) *vpn.GatewayStatusResponse { resp := &vpn.GatewayStatusResponse{ - Id: &testGatewayId, + Id: new(testGatewayId), Connections: []vpn.ConnectionStatusResponse{}, - DisplayName: &testDisplayName, + DisplayName: new(testDisplayName), Tunnels: []vpn.VPNTunnels{ { - InternalNextHopIP: &testTunnel1InternalNextHopIP, + InternalNextHopIP: new(testTunnel1InternalNextHopIP), Name: vpn.VPNTUNNELSNAME_TUNNEL1.Ptr(), - PublicIP: &testTunnel1PublicIP, + PublicIP: new(testTunnel1PublicIP), }, { - InternalNextHopIP: &testTunnel2InternalNextHopIP, + InternalNextHopIP: new(testTunnel2InternalNextHopIP), Name: vpn.VPNTUNNELSNAME_TUNNEL2.Ptr(), - PublicIP: &testTunnel2PublicIP, + PublicIP: new(testTunnel2PublicIP), }, }, } @@ -83,60 +88,60 @@ func TestMapDatasourceFields(t *testing.T) { isValid bool }{ { - "default_values", - "eu01", - &Model{ + name: "default_values", + region: "eu01", + state: &Model{ ProjectId: types.StringValue(testProjectId), }, - fixtureInput(), - fixtureModel(), - true, + input: fixtureInput(), + expected: fixtureModel(), + isValid: true, }, { - "no_input", - "eu01", - &Model{ + name: "no_input", + region: "eu01", + state: &Model{ ProjectId: types.StringValue(testProjectId), GatewayId: types.StringValue(testGatewayId), }, - nil, - nil, - false, + input: nil, + expected: nil, + isValid: false, }, { - "no_model", - "eu01", - nil, - &vpn.GatewayStatusResponse{}, - nil, - false, + name: "no_model", + region: "eu01", + state: nil, + input: &vpn.GatewayStatusResponse{}, + expected: nil, + isValid: false, }, { - "no_gateway_id", - "eu01", - &Model{ + name: "no_gateway_id", + region: "eu01", + state: &Model{ ProjectId: types.StringValue(testProjectId), }, - &vpn.GatewayStatusResponse{}, - nil, - false, + input: &vpn.GatewayStatusResponse{}, + expected: nil, + isValid: false, }, { - "empty_input", - "eu01", - &Model{ + name: "empty_input", + region: "eu01", + state: &Model{ ProjectId: types.StringValue(testProjectId), GatewayId: types.StringValue(testGatewayId), }, - &vpn.GatewayStatusResponse{}, - &Model{ + input: &vpn.GatewayStatusResponse{}, + expected: &Model{ Id: types.StringValue(testId), ProjectId: types.StringValue(testProjectId), GatewayId: types.StringValue(testGatewayId), Region: types.StringValue(testRegion), Tunnels: types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{}), }, - true, + isValid: true, }, } for _, tt := range tests { @@ -153,3 +158,61 @@ func TestMapDatasourceFields(t *testing.T) { }) } } + +func TestMapTunnels(t *testing.T) { + tests := []struct { + name string + input []vpn.VPNTunnels + expected *basetypes.ListValue + isValid bool + }{ + { + name: "default_values", + input: []vpn.VPNTunnels{ + { + InternalNextHopIP: new(testTunnel1InternalNextHopIP), + Name: vpn.VPNTUNNELSNAME_TUNNEL1.Ptr(), + PublicIP: new(testTunnel1PublicIP), + }, + { + InternalNextHopIP: new(testTunnel2InternalNextHopIP), + Name: vpn.VPNTUNNELSNAME_TUNNEL2.Ptr(), + PublicIP: new(testTunnel2PublicIP), + }, + }, + expected: new(types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{ + types.ObjectValueMust(tunnelsType, map[string]attr.Value{ + "internal_next_hop_ip": types.StringValue(testTunnel1InternalNextHopIP), + "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL1)), + "public_ip": types.StringValue(testTunnel1PublicIP), + }), + types.ObjectValueMust(tunnelsType, map[string]attr.Value{ + "internal_next_hop_ip": types.StringValue(testTunnel2InternalNextHopIP), + "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL2)), + "public_ip": types.StringValue(testTunnel2PublicIP), + }), + })), + isValid: true, + }, + { + name: "empty", + input: []vpn.VPNTunnels{}, + expected: new(types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{})), + isValid: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + tfTunnels, err := mapTunnels(ctx, tt.input) + if (err == nil) != tt.isValid { + t.Errorf("unexpected error: %s", err) + } + if tt.isValid { + if !reflect.DeepEqual(tfTunnels, tt.expected) { + t.Errorf("ParseProviderData() got = %v, want %v", tfTunnels, tt.expected) + } + } + }) + } +} From 561d8c5a493868fa83ed6002752b9711a1b6f6c4 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Fri, 19 Jun 2026 14:37:59 +0200 Subject: [PATCH 6/8] add connections --- .../services/vpn/gateway_status/datasource.go | 78 ++++++++++++++++++- .../vpn/gateway_status/datasource_test.go | 60 +++++++++----- 2 files changed, 117 insertions(+), 21 deletions(-) diff --git a/stackit/internal/services/vpn/gateway_status/datasource.go b/stackit/internal/services/vpn/gateway_status/datasource.go index 0da332137..6ad427836 100644 --- a/stackit/internal/services/vpn/gateway_status/datasource.go +++ b/stackit/internal/services/vpn/gateway_status/datasource.go @@ -41,17 +41,30 @@ type Model struct { GatewayId types.String `tfsdk:"gateway_id"` ProjectId types.String `tfsdk:"project_id"` Region types.String `tfsdk:"region"` + Connections types.List `tfsdk:"connections"` DisplayName types.String `tfsdk:"display_name"` Tunnels types.List `tfsdk:"tunnels"` } +type Connection struct { + DisplayName types.String `tfsdk:"display_name"` + Enabled types.Bool `tfsdk:"enabled"` + Id types.String `tfsdk:"id"` +} + type Tunnel struct { InternalNextHopIP types.String `tfsdk:"internal_next_hop_ip"` Name types.String `tfsdk:"name"` PublicIP types.String `tfsdk:"public_ip"` } -var tunnelsType = map[string]attr.Type{ +var connectionType = map[string]attr.Type{ + "display_name": basetypes.StringType{}, + "enabled": basetypes.BoolType{}, + "id": basetypes.StringType{}, +} + +var tunnelType = map[string]attr.Type{ "internal_next_hop_ip": basetypes.StringType{}, "name": basetypes.StringType{}, "public_ip": basetypes.StringType{}, @@ -119,6 +132,26 @@ func (d *vpnGatewayStatusDataSource) Schema(_ context.Context, _ datasource.Sche validate.NoSeparator(), }, }, + "connections": schema.ListNestedAttribute{ + Description: schemaDescriptions["connections"], + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "display_name": schema.StringAttribute{ + Description: schemaDescriptions["connection_display_name"], + Computed: true, + }, + "enabled": schema.BoolAttribute{ + Description: schemaDescriptions["connection_enabled"], + Computed: true, + }, + "id": schema.StringAttribute{ + Description: schemaDescriptions["connection_id"], + Computed: true, + }, + }, + }, + }, "display_name": schema.StringAttribute{ Description: schemaDescriptions["display_name"], Computed: true, @@ -219,6 +252,13 @@ func mapFields(ctx context.Context, gatewayStatus *vpn.GatewayStatusResponse, mo model.DisplayName = types.StringValue(*gatewayStatus.DisplayName) } + tfConnections, err := mapConnections(ctx, gatewayStatus.Connections) + if err != nil { + return fmt.Errorf("map tunnels: %w", err) + } else if tfConnections != nil { + model.Connections = *tfConnections + } + tfTunnels, err := mapTunnels(ctx, gatewayStatus.Tunnels) if err != nil { return fmt.Errorf("map tunnels: %w", err) @@ -229,6 +269,38 @@ func mapFields(ctx context.Context, gatewayStatus *vpn.GatewayStatusResponse, mo return nil } +func mapConnections(ctx context.Context, vpnConnections []vpn.ConnectionStatusResponse) (*basetypes.ListValue, error) { + connections := []attr.Value{} + + for _, connectionItem := range vpnConnections { + connection := Connection{} + + if connectionItem.DisplayName != nil { + connection.DisplayName = types.StringValue(string(*connectionItem.DisplayName)) + } + if connectionItem.Enabled != nil { + connection.Enabled = types.BoolValue(*connectionItem.Enabled) + } + if connectionItem.Id != nil { + connection.Id = types.StringValue(string(*connectionItem.Id)) + } + + connectionValue, diags := types.ObjectValueFrom(ctx, connectionType, connection) + if diags.HasError() { + return nil, fmt.Errorf("mapping connection: %w", core.DiagsToError(diags)) + } + + connections = append(connections, connectionValue) + } + + tfConnections, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: connectionType}, connections) + if diags.HasError() { + return nil, fmt.Errorf("mapping connections: %w", core.DiagsToError(diags)) + } + + return &tfConnections, nil +} + func mapTunnels(ctx context.Context, vpnTunnels []vpn.VPNTunnels) (*basetypes.ListValue, error) { tunnels := []attr.Value{} @@ -245,7 +317,7 @@ func mapTunnels(ctx context.Context, vpnTunnels []vpn.VPNTunnels) (*basetypes.Li tunnel.PublicIP = types.StringValue(string(*tunnelItem.PublicIP)) } - tunnelValue, diags := types.ObjectValueFrom(ctx, tunnelsType, tunnel) + tunnelValue, diags := types.ObjectValueFrom(ctx, tunnelType, tunnel) if diags.HasError() { return nil, fmt.Errorf("mapping tunnel: %w", core.DiagsToError(diags)) } @@ -253,7 +325,7 @@ func mapTunnels(ctx context.Context, vpnTunnels []vpn.VPNTunnels) (*basetypes.Li tunnels = append(tunnels, tunnelValue) } - tfTunnels, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: tunnelsType}, tunnels) + tfTunnels, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: tunnelType}, tunnels) if diags.HasError() { return nil, fmt.Errorf("mapping tunnels: %w", core.DiagsToError(diags)) } diff --git a/stackit/internal/services/vpn/gateway_status/datasource_test.go b/stackit/internal/services/vpn/gateway_status/datasource_test.go index e4759f6f9..85b5acd3b 100644 --- a/stackit/internal/services/vpn/gateway_status/datasource_test.go +++ b/stackit/internal/services/vpn/gateway_status/datasource_test.go @@ -30,8 +30,19 @@ var ( func fixtureInput(mods ...func(m *vpn.GatewayStatusResponse)) *vpn.GatewayStatusResponse { resp := &vpn.GatewayStatusResponse{ - Id: new(testGatewayId), - Connections: []vpn.ConnectionStatusResponse{}, + Id: new(testGatewayId), + Connections: []vpn.ConnectionStatusResponse{ + vpn.ConnectionStatusResponse{ + DisplayName: new("Conn1"), + Enabled: new(true), + Id: new("foo"), + }, + vpn.ConnectionStatusResponse{ + DisplayName: new("Conn2"), + Enabled: new(false), + Id: new("bar"), + }, + }, DisplayName: new(testDisplayName), Tunnels: []vpn.VPNTunnels{ { @@ -54,18 +65,30 @@ func fixtureInput(mods ...func(m *vpn.GatewayStatusResponse)) *vpn.GatewayStatus func fixtureModel(mods ...func(m *Model)) *Model { resp := &Model{ - ProjectId: types.StringValue(testProjectId), - Region: types.StringValue(testRegion), - Id: types.StringValue(testId), - GatewayId: types.StringValue(testGatewayId), + ProjectId: types.StringValue(testProjectId), + Region: types.StringValue(testRegion), + Id: types.StringValue(testId), + GatewayId: types.StringValue(testGatewayId), + Connections: types.ListValueMust(types.ObjectType{AttrTypes: connectionType}, []attr.Value{ + types.ObjectValueMust(connectionType, map[string]attr.Value{ + "display_name": types.StringValue("Conn1"), + "enabled": types.BoolValue(true), + "id": types.StringValue("foo"), + }), + types.ObjectValueMust(connectionType, map[string]attr.Value{ + "display_name": types.StringValue("Conn2"), + "enabled": types.BoolValue(false), + "id": types.StringValue("bar"), + }), + }), DisplayName: types.StringValue(testDisplayName), - Tunnels: types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{ - types.ObjectValueMust(tunnelsType, map[string]attr.Value{ + Tunnels: types.ListValueMust(types.ObjectType{AttrTypes: tunnelType}, []attr.Value{ + types.ObjectValueMust(tunnelType, map[string]attr.Value{ "internal_next_hop_ip": types.StringValue(testTunnel1InternalNextHopIP), "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL1)), "public_ip": types.StringValue(testTunnel1PublicIP), }), - types.ObjectValueMust(tunnelsType, map[string]attr.Value{ + types.ObjectValueMust(tunnelType, map[string]attr.Value{ "internal_next_hop_ip": types.StringValue(testTunnel2InternalNextHopIP), "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL2)), "public_ip": types.StringValue(testTunnel2PublicIP), @@ -135,11 +158,12 @@ func TestMapDatasourceFields(t *testing.T) { }, input: &vpn.GatewayStatusResponse{}, expected: &Model{ - Id: types.StringValue(testId), - ProjectId: types.StringValue(testProjectId), - GatewayId: types.StringValue(testGatewayId), - Region: types.StringValue(testRegion), - Tunnels: types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{}), + Id: types.StringValue(testId), + ProjectId: types.StringValue(testProjectId), + GatewayId: types.StringValue(testGatewayId), + Region: types.StringValue(testRegion), + Connections: types.ListValueMust(types.ObjectType{AttrTypes: connectionType}, []attr.Value{}), + Tunnels: types.ListValueMust(types.ObjectType{AttrTypes: tunnelType}, []attr.Value{}), }, isValid: true, }, @@ -180,13 +204,13 @@ func TestMapTunnels(t *testing.T) { PublicIP: new(testTunnel2PublicIP), }, }, - expected: new(types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{ - types.ObjectValueMust(tunnelsType, map[string]attr.Value{ + expected: new(types.ListValueMust(types.ObjectType{AttrTypes: tunnelType}, []attr.Value{ + types.ObjectValueMust(tunnelType, map[string]attr.Value{ "internal_next_hop_ip": types.StringValue(testTunnel1InternalNextHopIP), "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL1)), "public_ip": types.StringValue(testTunnel1PublicIP), }), - types.ObjectValueMust(tunnelsType, map[string]attr.Value{ + types.ObjectValueMust(tunnelType, map[string]attr.Value{ "internal_next_hop_ip": types.StringValue(testTunnel2InternalNextHopIP), "name": types.StringValue(string(vpn.VPNTUNNELSNAME_TUNNEL2)), "public_ip": types.StringValue(testTunnel2PublicIP), @@ -197,7 +221,7 @@ func TestMapTunnels(t *testing.T) { { name: "empty", input: []vpn.VPNTunnels{}, - expected: new(types.ListValueMust(types.ObjectType{AttrTypes: tunnelsType}, []attr.Value{})), + expected: new(types.ListValueMust(types.ObjectType{AttrTypes: tunnelType}, []attr.Value{})), isValid: true, }, } From 77ae989d8b7f3869a4dcfb9392a8747c2aafe68a Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Fri, 19 Jun 2026 14:41:07 +0200 Subject: [PATCH 7/8] add documentation --- docs/data-sources/vpn_gateway_status.md | 11 +++++++++++ .../services/vpn/gateway_status/datasource.go | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/docs/data-sources/vpn_gateway_status.md b/docs/data-sources/vpn_gateway_status.md index 821c90cd0..fdb2add5e 100644 --- a/docs/data-sources/vpn_gateway_status.md +++ b/docs/data-sources/vpn_gateway_status.md @@ -29,11 +29,22 @@ data "stackit_vpn_gateway_status" "example" { ### Read-Only +- `connections` (Attributes List) List of connections in the VPN gateway. (see [below for nested schema](#nestedatt--connections)) - `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`". - `region` (String) STACKIT region name the resource is located in. If not defined, the provider region is used. - `tunnels` (Attributes List) List of the VPN tunnels in the gateway. (see [below for nested schema](#nestedatt--tunnels)) + +### Nested Schema for `connections` + +Read-Only: + +- `display_name` (String) Display name of the VPN connection. +- `enabled` (Boolean) Wether the VPN connection is enabled or not. +- `id` (String) ID of the VPN connection. + + ### Nested Schema for `tunnels` diff --git a/stackit/internal/services/vpn/gateway_status/datasource.go b/stackit/internal/services/vpn/gateway_status/datasource.go index 6ad427836..fd38c373b 100644 --- a/stackit/internal/services/vpn/gateway_status/datasource.go +++ b/stackit/internal/services/vpn/gateway_status/datasource.go @@ -97,6 +97,10 @@ var schemaDescriptions = map[string]string{ "gateway_id": "The server-generated UUID of the VPN gateway.", "project_id": "STACKIT project ID associated with the VPN gateway.", "region": "STACKIT region name the resource is located in. If not defined, the provider region is used.", + "connections": "List of connections in the VPN gateway.", + "connection_display_name": "Display name of the VPN connection.", + "connection_enabled": "Wether the VPN connection is enabled or not.", + "connection_id": "ID of the VPN connection.", "display_name": "A user-friendly name for the VPN gateway.", "tunnels": "List of the VPN tunnels in the gateway.", "tunnel_internal_next_hop_ip": "The IPv4 address of the endpoint in the SNA.", From cacd0da106a0dc637cb8b365a4edb2dfd98060af Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Fri, 19 Jun 2026 15:37:11 +0200 Subject: [PATCH 8/8] resolve comments --- docs/data-sources/vpn_gateway_status.md | 2 +- .../services/vpn/gateway_status/datasource.go | 10 +++++----- .../vpn/gateway_status/datasource_test.go | 16 ++++++++-------- stackit/internal/services/vpn/vpn_acc_test.go | 4 ++++ 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/data-sources/vpn_gateway_status.md b/docs/data-sources/vpn_gateway_status.md index fdb2add5e..fa50f30c7 100644 --- a/docs/data-sources/vpn_gateway_status.md +++ b/docs/data-sources/vpn_gateway_status.md @@ -40,9 +40,9 @@ data "stackit_vpn_gateway_status" "example" { Read-Only: +- `connection_id` (String) ID of the VPN connection. - `display_name` (String) Display name of the VPN connection. - `enabled` (Boolean) Wether the VPN connection is enabled or not. -- `id` (String) ID of the VPN connection. diff --git a/stackit/internal/services/vpn/gateway_status/datasource.go b/stackit/internal/services/vpn/gateway_status/datasource.go index fd38c373b..f56134633 100644 --- a/stackit/internal/services/vpn/gateway_status/datasource.go +++ b/stackit/internal/services/vpn/gateway_status/datasource.go @@ -49,7 +49,7 @@ type Model struct { type Connection struct { DisplayName types.String `tfsdk:"display_name"` Enabled types.Bool `tfsdk:"enabled"` - Id types.String `tfsdk:"id"` + Id types.String `tfsdk:"connection_id"` } type Tunnel struct { @@ -59,9 +59,9 @@ type Tunnel struct { } var connectionType = map[string]attr.Type{ - "display_name": basetypes.StringType{}, - "enabled": basetypes.BoolType{}, - "id": basetypes.StringType{}, + "display_name": basetypes.StringType{}, + "enabled": basetypes.BoolType{}, + "connection_id": basetypes.StringType{}, } var tunnelType = map[string]attr.Type{ @@ -149,7 +149,7 @@ func (d *vpnGatewayStatusDataSource) Schema(_ context.Context, _ datasource.Sche Description: schemaDescriptions["connection_enabled"], Computed: true, }, - "id": schema.StringAttribute{ + "connection_id": schema.StringAttribute{ Description: schemaDescriptions["connection_id"], Computed: true, }, diff --git a/stackit/internal/services/vpn/gateway_status/datasource_test.go b/stackit/internal/services/vpn/gateway_status/datasource_test.go index 85b5acd3b..c0a35a1cd 100644 --- a/stackit/internal/services/vpn/gateway_status/datasource_test.go +++ b/stackit/internal/services/vpn/gateway_status/datasource_test.go @@ -32,12 +32,12 @@ func fixtureInput(mods ...func(m *vpn.GatewayStatusResponse)) *vpn.GatewayStatus resp := &vpn.GatewayStatusResponse{ Id: new(testGatewayId), Connections: []vpn.ConnectionStatusResponse{ - vpn.ConnectionStatusResponse{ + { DisplayName: new("Conn1"), Enabled: new(true), Id: new("foo"), }, - vpn.ConnectionStatusResponse{ + { DisplayName: new("Conn2"), Enabled: new(false), Id: new("bar"), @@ -71,14 +71,14 @@ func fixtureModel(mods ...func(m *Model)) *Model { GatewayId: types.StringValue(testGatewayId), Connections: types.ListValueMust(types.ObjectType{AttrTypes: connectionType}, []attr.Value{ types.ObjectValueMust(connectionType, map[string]attr.Value{ - "display_name": types.StringValue("Conn1"), - "enabled": types.BoolValue(true), - "id": types.StringValue("foo"), + "display_name": types.StringValue("Conn1"), + "enabled": types.BoolValue(true), + "connection_id": types.StringValue("foo"), }), types.ObjectValueMust(connectionType, map[string]attr.Value{ - "display_name": types.StringValue("Conn2"), - "enabled": types.BoolValue(false), - "id": types.StringValue("bar"), + "display_name": types.StringValue("Conn2"), + "enabled": types.BoolValue(false), + "connection_id": types.StringValue("bar"), }), }), DisplayName: types.StringValue(testDisplayName), diff --git a/stackit/internal/services/vpn/vpn_acc_test.go b/stackit/internal/services/vpn/vpn_acc_test.go index 5c097cf90..387a5ed1f 100644 --- a/stackit/internal/services/vpn/vpn_acc_test.go +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -155,6 +155,8 @@ func TestAccVpnGatewayResourceMin(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "tunnels.1.name", string(vpn.VPNTUNNELSNAME_TUNNEL2)), resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.1.internal_next_hop_ip"), resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.1.public_ip"), + + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "connections.#", "0"), ), }, // Update @@ -284,6 +286,8 @@ func TestAccVpnGatewayResourceMax(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "tunnels.1.name", string(vpn.VPNTUNNELSNAME_TUNNEL2)), resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.1.internal_next_hop_ip"), resource.TestCheckResourceAttrSet("data.stackit_vpn_gateway_status.gateway", "tunnels.1.public_ip"), + + resource.TestCheckResourceAttr("data.stackit_vpn_gateway_status.gateway", "connections.#", "0"), ), }, // Update