From 75a558a2cfcc04d139f0a5b10df9dd9f1749974d Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Wed, 10 Jun 2026 17:36:39 +0200 Subject: [PATCH 1/4] 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 f62e157e2..ad4882731 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -127,6 +127,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" ) @@ -746,6 +747,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 471732d3591ee7ad4bf46bb4da7357d40bcd9a91 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Wed, 10 Jun 2026 17:41:40 +0200 Subject: [PATCH 2/4] 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 775b0ae9eb921bd8137b55a7aea5142151a18a68 Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Thu, 11 Jun 2026 14:56:01 +0200 Subject: [PATCH 3/4] 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 aef23cb646d6da89d8c6829ba8dc96c2ad4b500f Mon Sep 17 00:00:00 2001 From: Manuel Vaas Date: Thu, 11 Jun 2026 17:29:01 +0200 Subject: [PATCH 4/4] 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 8a54ad3b5..aaa9f4401 100644 --- a/stackit/internal/services/vpn/vpn_acc_test.go +++ b/stackit/internal/services/vpn/vpn_acc_test.go @@ -129,6 +129,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, @@ -231,6 +261,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,