From 7b693acc2dfe38cc0c3b68ff242759c3a99ff856 Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Fri, 22 Nov 2024 09:25:17 +0100 Subject: [PATCH] Add missing fields to datasource (#596) * add missing fields to datasource * split resource and datasource models --- .../services/iaas/network/datasource.go | 168 ++++++++- .../services/iaas/network/datasource_test.go | 347 ++++++++++++++++++ 2 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 stackit/internal/services/iaas/network/datasource_test.go diff --git a/stackit/internal/services/iaas/network/datasource.go b/stackit/internal/services/iaas/network/datasource.go index 3c26d3dc..6b2fb2f9 100644 --- a/stackit/internal/services/iaas/network/datasource.go +++ b/stackit/internal/services/iaas/network/datasource.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "net/http" + "strings" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "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-log/tflog" @@ -15,6 +17,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -23,6 +26,28 @@ var ( _ datasource.DataSource = &networkDataSource{} ) +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + NetworkId types.String `tfsdk:"network_id"` + Name types.String `tfsdk:"name"` + Nameservers types.List `tfsdk:"nameservers"` + IPv4Gateway types.String `tfsdk:"ipv4_gateway"` + IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` + IPv4Prefix types.String `tfsdk:"ipv4_prefix"` + IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` + Prefixes types.List `tfsdk:"prefixes"` + IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` + IPv6Gateway types.String `tfsdk:"ipv6_gateway"` + IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` + IPv6Prefix types.String `tfsdk:"ipv6_prefix"` + IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` + IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` + PublicIP types.String `tfsdk:"public_ip"` + Labels types.Map `tfsdk:"labels"` + Routed types.Bool `tfsdk:"routed"` +} + // NewNetworkDataSource is a helper function to simplify the provider implementation. func NewNetworkDataSource() datasource.DataSource { return &networkDataSource{} @@ -181,7 +206,7 @@ func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest // Read refreshes the Terraform state with the latest data. func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model + var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -203,7 +228,7 @@ func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest return } - err = mapFields(ctx, networkResp, &model) + err = mapDataSourceFields(ctx, networkResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) return @@ -215,3 +240,142 @@ func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest } tflog.Info(ctx, "Network read") } + +func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model *DataSourceModel) error { + if networkResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var networkId string + if model.NetworkId.ValueString() != "" { + networkId = model.NetworkId.ValueString() + } else if networkResp.NetworkId != nil { + networkId = *networkResp.NetworkId + } else { + return fmt.Errorf("network id not present") + } + + idParts := []string{ + model.ProjectId.ValueString(), + networkId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + + labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{}) + if diags.HasError() { + return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags)) + } + if networkResp.Labels != nil && len(*networkResp.Labels) != 0 { + var diags diag.Diagnostics + labels, diags = types.MapValueFrom(ctx, types.StringType, *networkResp.Labels) + if diags.HasError() { + return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags)) + } + } else if model.Labels.IsNull() { + labels = types.MapNull(types.StringType) + } + + // IPv4 + + if networkResp.Nameservers == nil { + model.Nameservers = types.ListNull(types.StringType) + model.IPv4Nameservers = types.ListNull(types.StringType) + } else { + respNameservers := *networkResp.Nameservers + modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) + modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) + if err != nil { + return fmt.Errorf("get current network nameservers from model: %w", err) + } + if errIpv4 != nil { + return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) + } + + reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) + reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers) + + nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) + ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) + if diags.HasError() { + return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) + } + if ipv4Diags.HasError() { + return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) + } + + model.Nameservers = nameserversTF + model.IPv4Nameservers = ipv4NameserversTF + } + + if networkResp.Prefixes == nil { + model.Prefixes = types.ListNull(types.StringType) + model.IPv4Prefixes = types.ListNull(types.StringType) + } else { + respPrefixes := *networkResp.Prefixes + prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) + if diags.HasError() { + return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) + } + + model.Prefixes = prefixesTF + model.IPv4Prefixes = prefixesTF + } + + if networkResp.Gateway != nil { + model.IPv4Gateway = types.StringPointerValue(networkResp.GetGateway()) + } else { + model.IPv4Gateway = types.StringNull() + } + + // IPv6 + + if networkResp.NameserversV6 == nil { + model.IPv6Nameservers = types.ListNull(types.StringType) + } else { + respIPv6Nameservers := *networkResp.NameserversV6 + modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) + if errIpv6 != nil { + return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) + } + + reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers) + + ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers) + if ipv6Diags.HasError() { + return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags)) + } + + model.IPv6Nameservers = ipv6NameserversTF + } + + if networkResp.PrefixesV6 == nil { + model.IPv6Prefixes = types.ListNull(types.StringType) + } else { + respPrefixesV6 := *networkResp.PrefixesV6 + prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) + if diags.HasError() { + return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) + } + + model.IPv6Prefixes = prefixesV6TF + } + + if networkResp.Gatewayv6 != nil { + model.IPv6Gateway = types.StringPointerValue(networkResp.GetGatewayv6()) + } else { + model.IPv6Gateway = types.StringNull() + } + + model.NetworkId = types.StringValue(networkId) + model.Name = types.StringPointerValue(networkResp.Name) + model.PublicIP = types.StringPointerValue(networkResp.PublicIp) + model.Labels = labels + model.Routed = types.BoolPointerValue(networkResp.Routed) + + return nil +} diff --git a/stackit/internal/services/iaas/network/datasource_test.go b/stackit/internal/services/iaas/network/datasource_test.go new file mode 100644 index 00000000..3d1912da --- /dev/null +++ b/stackit/internal/services/iaas/network/datasource_test.go @@ -0,0 +1,347 @@ +package network + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + state DataSourceModel + input *iaas.Network + expected DataSourceModel + isValid bool + }{ + { + "id_ok", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + Gateway: iaas.NewNullableString(nil), + }, + DataSourceModel{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + Nameservers: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + IPv4Gateway: types.StringNull(), + IPv4Prefix: types.StringNull(), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Gateway: types.StringNull(), + IPv6Prefix: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + PublicIP: types.StringNull(), + Labels: types.MapNull(types.StringType), + Routed: types.BoolNull(), + }, + true, + }, + { + "values_ok", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + Name: utils.Ptr("name"), + Nameservers: &[]string{ + "ns1", + "ns2", + }, + Prefixes: &[]string{ + "prefix1", + "prefix2", + }, + NameserversV6: &[]string{ + "ns1", + "ns2", + }, + PrefixesV6: &[]string{ + "prefix1", + "prefix2", + }, + PublicIp: utils.Ptr("publicIp"), + Labels: &map[string]interface{}{ + "key": "value", + }, + Routed: utils.Ptr(true), + Gateway: iaas.NewNullableString(utils.Ptr("gateway")), + Gatewayv6: iaas.NewNullableString(utils.Ptr("gateway")), + }, + DataSourceModel{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringValue("name"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4PrefixLength: types.Int64Null(), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix1"), + types.StringValue("prefix2"), + }), + IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix1"), + types.StringValue("prefix2"), + }), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv6PrefixLength: types.Int64Null(), + IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix1"), + types.StringValue("prefix2"), + }), + PublicIP: types.StringValue("publicIp"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + Routed: types.BoolValue(true), + IPv4Gateway: types.StringValue("gateway"), + IPv6Gateway: types.StringValue("gateway"), + }, + true, + }, + { + "ipv4_nameservers_changed_outside_tf", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + Nameservers: &[]string{ + "ns2", + "ns3", + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + Labels: types.MapNull(types.StringType), + }, + true, + }, + { + "ipv6_nameservers_changed_outside_tf", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns1"), + types.StringValue("ns2"), + }), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + NameserversV6: &[]string{ + "ns2", + "ns3", + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + Nameservers: types.ListNull(types.StringType), + IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ns2"), + types.StringValue("ns3"), + }), + Labels: types.MapNull(types.StringType), + }, + true, + }, + { + "ipv4_prefixes_changed_outside_tf", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix1"), + types.StringValue("prefix2"), + }), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + Prefixes: &[]string{ + "prefix2", + "prefix3", + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Prefixes: types.ListNull(types.StringType), + Labels: types.MapNull(types.StringType), + Nameservers: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix2"), + types.StringValue("prefix3"), + }), + IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix2"), + types.StringValue("prefix3"), + }), + }, + true, + }, + { + "ipv6_prefixes_changed_outside_tf", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix1"), + types.StringValue("prefix2"), + }), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + PrefixesV6: &[]string{ + "prefix2", + "prefix3", + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + Labels: types.MapNull(types.StringType), + Nameservers: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("prefix2"), + types.StringValue("prefix3"), + }), + }, + true, + }, + { + "ipv4_ipv6_gateway_nil", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + }, + &iaas.Network{ + NetworkId: utils.Ptr("nid"), + }, + DataSourceModel{ + Id: types.StringValue("pid,nid"), + ProjectId: types.StringValue("pid"), + NetworkId: types.StringValue("nid"), + Name: types.StringNull(), + Nameservers: types.ListNull(types.StringType), + IPv4Nameservers: types.ListNull(types.StringType), + IPv4PrefixLength: types.Int64Null(), + IPv4Gateway: types.StringNull(), + Prefixes: types.ListNull(types.StringType), + IPv4Prefixes: types.ListNull(types.StringType), + IPv6Nameservers: types.ListNull(types.StringType), + IPv6PrefixLength: types.Int64Null(), + IPv6Gateway: types.StringNull(), + IPv6Prefixes: types.ListNull(types.StringType), + PublicIP: types.StringNull(), + Labels: types.MapNull(types.StringType), + Routed: types.BoolNull(), + }, + true, + }, + { + "response_nil_fail", + DataSourceModel{}, + nil, + DataSourceModel{}, + false, + }, + { + "no_resource_id", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + }, + &iaas.Network{}, + DataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +}