From 9bd1da7ceeae80056c737f569145092aaa434f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 16 Apr 2024 11:20:19 +0100 Subject: [PATCH] Bugfix inconsistent applies of ListAttributes (#328) * sort the list of ACLs for MongoDBFlex * Fix and test other cases of ListAttribute ordering * fix linting * revert sorting changes, introduce new reconcilestrlist function * merge main * Fix rabbitmq * fix segmentation fault * Improve testing * pass context to mapfields, minor name fixes --- .../services/dns/recordset/datasource.go | 2 +- .../services/dns/recordset/resource.go | 28 ++-- .../services/dns/recordset/resource_test.go | 49 ++++++- .../internal/services/dns/zone/datasource.go | 2 +- .../internal/services/dns/zone/resource.go | 31 +++-- .../services/dns/zone/resource_test.go | 98 +++++++++++++- .../services/mariadb/credential/datasource.go | 2 +- .../services/mariadb/credential/resource.go | 29 +++-- .../mariadb/credential/resource_test.go | 82 +++++++++++- .../mongodbflex/instance/datasource.go | 2 +- .../services/mongodbflex/instance/resource.go | 21 +-- .../mongodbflex/instance/resource_test.go | 102 ++++++++++++++- .../opensearch/credential/datasource.go | 2 +- .../opensearch/credential/resource.go | 26 ++-- .../opensearch/credential/resource_test.go | 82 +++++++++++- .../postgresflex/instance/datasource.go | 2 +- .../postgresflex/instance/resource.go | 21 +-- .../postgresflex/instance/resource_test.go | 93 ++++++++++++- .../rabbitmq/credential/datasource.go | 2 +- .../services/rabbitmq/credential/resource.go | 72 +++++++---- .../rabbitmq/credential/resource_test.go | 114 +++++++++++++++- .../services/redis/credential/datasource.go | 2 +- .../services/redis/credential/resource.go | 27 ++-- .../redis/credential/resource_test.go | 82 +++++++++++- stackit/internal/utils/utils.go | 62 +++++++++ stackit/internal/utils/utils_test.go | 122 ++++++++++++++++++ 26 files changed, 1011 insertions(+), 146 deletions(-) create mode 100644 stackit/internal/utils/utils.go create mode 100644 stackit/internal/utils/utils_test.go diff --git a/stackit/internal/services/dns/recordset/datasource.go b/stackit/internal/services/dns/recordset/datasource.go index 9d7057b7..1356dd41 100644 --- a/stackit/internal/services/dns/recordset/datasource.go +++ b/stackit/internal/services/dns/recordset/datasource.go @@ -164,7 +164,7 @@ func (d *recordSetDataSource) Read(ctx context.Context, req datasource.ReadReque return } - err = mapFields(zoneResp, &model) + err = mapFields(ctx, zoneResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/dns/recordset/resource.go b/stackit/internal/services/dns/recordset/resource.go index 30149f6b..a9d82fa6 100644 --- a/stackit/internal/services/dns/recordset/resource.go +++ b/stackit/internal/services/dns/recordset/resource.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -23,6 +22,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -249,7 +249,7 @@ func (r *recordSetResource) Create(ctx context.Context, req resource.CreateReque } // Map response body to schema - err = mapFields(waitResp, &model) + err = mapFields(ctx, waitResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Processing API payload: %v", err)) return @@ -285,7 +285,7 @@ func (r *recordSetResource) Read(ctx context.Context, req resource.ReadRequest, } // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Processing API payload: %v", err)) return @@ -335,7 +335,7 @@ func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateReque return } - err = mapFields(waitResp, &model) + err = mapFields(ctx, waitResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Processing API payload: %v", err)) return @@ -396,7 +396,7 @@ func (r *recordSetResource) ImportState(ctx context.Context, req resource.Import tflog.Info(ctx, "DNS record set state imported") } -func mapFields(recordSetResp *dns.RecordSetResponse, model *Model) error { +func mapFields(ctx context.Context, recordSetResp *dns.RecordSetResponse, model *Model) error { if recordSetResp == nil || recordSetResp.Rrset == nil { return fmt.Errorf("response input is nil") } @@ -417,15 +417,25 @@ func mapFields(recordSetResp *dns.RecordSetResponse, model *Model) error { if recordSet.Records == nil { model.Records = types.ListNull(types.StringType) } else { - records := []attr.Value{} + respRecords := []string{} + for _, record := range *recordSet.Records { - records = append(records, types.StringPointerValue(record.Content)) + respRecords = append(respRecords, *record.Content) } - recordsList, diags := types.ListValue(types.StringType, records) + + modelRecords, err := utils.ListValuetoStringSlice(model.Records) + if err != nil { + return err + } + + reconciledRecords := utils.ReconcileStringSlices(modelRecords, respRecords) + + recordsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledRecords) if diags.HasError() { return fmt.Errorf("failed to map records: %w", core.DiagsToError(diags)) } - model.Records = recordsList + + model.Records = recordsTF } idParts := []string{ model.ProjectId.ValueString(), diff --git a/stackit/internal/services/dns/recordset/resource_test.go b/stackit/internal/services/dns/recordset/resource_test.go index 8064ddfa..c3d33755 100644 --- a/stackit/internal/services/dns/recordset/resource_test.go +++ b/stackit/internal/services/dns/recordset/resource_test.go @@ -1,6 +1,7 @@ package dns import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -88,6 +89,52 @@ func TestMapFields(t *testing.T) { }, true, }, + { + "unordered_records", + Model{ + ProjectId: types.StringValue("pid"), + ZoneId: types.StringValue("zid"), + Records: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("record_2"), + types.StringValue("record_1"), + }), + }, + &dns.RecordSetResponse{ + Rrset: &dns.RecordSet{ + Id: utils.Ptr("rid"), + Active: utils.Ptr(true), + Comment: utils.Ptr("comment"), + Error: utils.Ptr("error"), + Name: utils.Ptr("name"), + Records: &[]dns.Record{ + {Content: utils.Ptr("record_1")}, + {Content: utils.Ptr("record_2")}, + }, + State: utils.Ptr("state"), + Ttl: utils.Ptr(int64(1)), + Type: utils.Ptr("type"), + }, + }, + Model{ + Id: types.StringValue("pid,zid,rid"), + RecordSetId: types.StringValue("rid"), + ZoneId: types.StringValue("zid"), + ProjectId: types.StringValue("pid"), + Active: types.BoolValue(true), + Comment: types.StringValue("comment"), + Error: types.StringValue("error"), + Name: types.StringValue("name"), + FQDN: types.StringValue("name"), + Records: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("record_2"), + types.StringValue("record_1"), + }), + State: types.StringValue("state"), + TTL: types.Int64Value(1), + Type: types.StringValue("type"), + }, + true, + }, { "null_fields_and_int_conversions", Model{ @@ -148,7 +195,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(tt.input, &tt.state) + err := mapFields(context.Background(), tt.input, &tt.state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/dns/zone/datasource.go b/stackit/internal/services/dns/zone/datasource.go index e836689a..045a7598 100644 --- a/stackit/internal/services/dns/zone/datasource.go +++ b/stackit/internal/services/dns/zone/datasource.go @@ -194,7 +194,7 @@ func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, r return } - err = mapFields(zoneResp, &model) + err = mapFields(ctx, zoneResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/dns/zone/resource.go b/stackit/internal/services/dns/zone/resource.go index c824527b..cd994af8 100644 --- a/stackit/internal/services/dns/zone/resource.go +++ b/stackit/internal/services/dns/zone/resource.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -26,6 +25,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -329,7 +329,7 @@ func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, r } // Map response body to schema - err = mapFields(waitResp, &model) + err = mapFields(ctx, waitResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Processing API payload: %v", err)) return @@ -363,7 +363,7 @@ func (r *zoneResource) Read(ctx context.Context, req resource.ReadRequest, resp } // Map response body to schema - err = mapFields(zoneResp, &model) + err = mapFields(ctx, zoneResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Processing API payload: %v", err)) return @@ -409,7 +409,7 @@ func (r *zoneResource) Update(ctx context.Context, req resource.UpdateRequest, r return } - err = mapFields(waitResp, &model) + err = mapFields(ctx, waitResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Processing API payload: %v", err)) return @@ -475,7 +475,7 @@ func (r *zoneResource) ImportState(ctx context.Context, req resource.ImportState tflog.Info(ctx, "DNS zone state imported") } -func mapFields(zoneResp *dns.ZoneResponse, model *Model) error { +func mapFields(ctx context.Context, zoneResp *dns.ZoneResponse, model *Model) error { if zoneResp == nil || zoneResp.Zone == nil { return fmt.Errorf("response input is nil") } @@ -512,15 +512,20 @@ func mapFields(zoneResp *dns.ZoneResponse, model *Model) error { if z.Primaries == nil { model.Primaries = types.ListNull(types.StringType) } else { - respZonePrimaries := []attr.Value{} - for _, primary := range *z.Primaries { - respZonePrimaries = append(respZonePrimaries, types.StringValue(primary)) - respZonePrimariesList, diags := types.ListValue(types.StringType, respZonePrimaries) - if diags.HasError() { - return fmt.Errorf("creating primaries list: %w", core.DiagsToError(diags)) - } - model.Primaries = respZonePrimariesList + respPrimaries := *z.Primaries + modelPrimaries, err := utils.ListValuetoStringSlice(model.Primaries) + if err != nil { + return err } + + reconciledPrimaries := utils.ReconcileStringSlices(modelPrimaries, respPrimaries) + + primariesTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledPrimaries) + if diags.HasError() { + return fmt.Errorf("failed to map zone primaries: %w", core.DiagsToError(diags)) + } + + model.Primaries = primariesTF } model.ZoneId = types.StringValue(zoneId) model.Description = types.StringPointerValue(z.Description) diff --git a/stackit/internal/services/dns/zone/resource_test.go b/stackit/internal/services/dns/zone/resource_test.go index f6096993..5a2d56da 100644 --- a/stackit/internal/services/dns/zone/resource_test.go +++ b/stackit/internal/services/dns/zone/resource_test.go @@ -1,6 +1,7 @@ package dns import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -13,12 +14,17 @@ import ( func TestMapFields(t *testing.T) { tests := []struct { description string + state Model input *dns.ZoneResponse expected Model isValid bool }{ { "default_ok", + Model{ + ProjectId: types.StringValue("pid"), + ZoneId: types.StringValue("zid"), + }, &dns.ZoneResponse{ Zone: &dns.Zone{ Id: utils.Ptr("zid"), @@ -47,6 +53,10 @@ func TestMapFields(t *testing.T) { }, { "values_ok", + Model{ + ProjectId: types.StringValue("pid"), + ZoneId: types.StringValue("zid"), + }, &dns.ZoneResponse{ Zone: &dns.Zone{ Id: utils.Ptr("zid"), @@ -104,8 +114,84 @@ func TestMapFields(t *testing.T) { }, true, }, + { + "primaries_unordered", + Model{ + ProjectId: types.StringValue("pid"), + ZoneId: types.StringValue("zid"), + Primaries: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("primary2"), + types.StringValue("primary1"), + }), + }, + &dns.ZoneResponse{ + Zone: &dns.Zone{ + Id: utils.Ptr("zid"), + Name: utils.Ptr("name"), + DnsName: utils.Ptr("dnsname"), + Acl: utils.Ptr("acl"), + Active: utils.Ptr(false), + CreationStarted: utils.Ptr("bar"), + CreationFinished: utils.Ptr("foo"), + DefaultTTL: utils.Ptr(int64(1)), + ExpireTime: utils.Ptr(int64(2)), + RefreshTime: utils.Ptr(int64(3)), + RetryTime: utils.Ptr(int64(4)), + SerialNumber: utils.Ptr(int64(5)), + NegativeCache: utils.Ptr(int64(6)), + State: utils.Ptr("state"), + Type: utils.Ptr("type"), + Primaries: &[]string{ + "primary1", + "primary2", + }, + PrimaryNameServer: utils.Ptr("pns"), + UpdateStarted: utils.Ptr("ufoo"), + UpdateFinished: utils.Ptr("ubar"), + Visibility: utils.Ptr("visibility"), + Error: utils.Ptr("error"), + ContactEmail: utils.Ptr("a@b.cd"), + Description: utils.Ptr("description"), + IsReverseZone: utils.Ptr(false), + RecordCount: utils.Ptr(int64(3)), + }, + }, + Model{ + Id: types.StringValue("pid,zid"), + ProjectId: types.StringValue("pid"), + ZoneId: types.StringValue("zid"), + Name: types.StringValue("name"), + DnsName: types.StringValue("dnsname"), + Acl: types.StringValue("acl"), + Active: types.BoolValue(false), + DefaultTTL: types.Int64Value(1), + ExpireTime: types.Int64Value(2), + RefreshTime: types.Int64Value(3), + RetryTime: types.Int64Value(4), + SerialNumber: types.Int64Value(5), + NegativeCache: types.Int64Value(6), + Type: types.StringValue("type"), + State: types.StringValue("state"), + PrimaryNameServer: types.StringValue("pns"), + Primaries: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("primary2"), + types.StringValue("primary1"), + }), + Visibility: types.StringValue("visibility"), + ContactEmail: types.StringValue("a@b.cd"), + Description: types.StringValue("description"), + IsReverseZone: types.BoolValue(false), + RecordCount: types.Int64Value(3), + }, + true, + }, { "nullable_fields_and_int_conversions_ok", + Model{ + Id: types.StringValue("pid,zid"), + ProjectId: types.StringValue("pid"), + ZoneId: types.StringValue("zid"), + }, &dns.ZoneResponse{ Zone: &dns.Zone{ Id: utils.Ptr("zid"), @@ -162,12 +248,17 @@ func TestMapFields(t *testing.T) { }, { "response_nil_fail", + Model{}, nil, Model{}, false, }, { "no_resource_id", + Model{ + ProjectId: types.StringValue("pid"), + ZoneId: types.StringValue("zid"), + }, &dns.ZoneResponse{}, Model{}, false, @@ -175,10 +266,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - } - err := mapFields(tt.input, state) + err := mapFields(context.Background(), tt.input, &tt.state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -186,7 +274,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(state, &tt.expected) + diff := cmp.Diff(tt.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/mariadb/credential/datasource.go b/stackit/internal/services/mariadb/credential/datasource.go index b00b0af2..32e09f7b 100644 --- a/stackit/internal/services/mariadb/credential/datasource.go +++ b/stackit/internal/services/mariadb/credential/datasource.go @@ -162,7 +162,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ } // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/mariadb/credential/resource.go b/stackit/internal/services/mariadb/credential/resource.go index 2d1055c7..1cf7d338 100644 --- a/stackit/internal/services/mariadb/credential/resource.go +++ b/stackit/internal/services/mariadb/credential/resource.go @@ -8,9 +8,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" "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" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -209,7 +209,7 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ } // Map response body to schema - err = mapFields(waitResp, &model) + err = mapFields(ctx, waitResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -244,7 +244,7 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, } // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -312,7 +312,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor tflog.Info(ctx, "MariaDB credential state imported") } -func mapFields(credentialsResp *mariadb.CredentialsResponse, model *Model) error { +func mapFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse, model *Model) error { if credentialsResp == nil { return fmt.Errorf("response input is nil") } @@ -341,19 +341,26 @@ func mapFields(credentialsResp *mariadb.CredentialsResponse, model *Model) error model.Id = types.StringValue( strings.Join(idParts, core.Separator), ) - model.CredentialId = types.StringValue(credentialId) + + modelHosts, err := utils.ListValuetoStringSlice(model.Hosts) + if err != nil { + return err + } + model.Hosts = types.ListNull(types.StringType) + model.CredentialId = types.StringValue(credentialId) if credentials != nil { if credentials.Hosts != nil { - var hosts []attr.Value - for _, host := range *credentials.Hosts { - hosts = append(hosts, types.StringValue(host)) - } - hostsList, diags := types.ListValue(types.StringType, hosts) + respHosts := *credentials.Hosts + + reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts) + + hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts) if diags.HasError() { return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) } - model.Hosts = hostsList + + model.Hosts = hostsTF } model.Host = types.StringPointerValue(credentials.Host) model.Name = types.StringPointerValue(credentials.Name) diff --git a/stackit/internal/services/mariadb/credential/resource_test.go b/stackit/internal/services/mariadb/credential/resource_test.go index 4c313d62..911b57f3 100644 --- a/stackit/internal/services/mariadb/credential/resource_test.go +++ b/stackit/internal/services/mariadb/credential/resource_test.go @@ -1,6 +1,7 @@ package mariadb import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -13,12 +14,17 @@ import ( func TestMapFields(t *testing.T) { tests := []struct { description string + state Model input *mariadb.CredentialsResponse expected Model isValid bool }{ { "default_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &mariadb.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &mariadb.RawCredentials{}, @@ -40,6 +46,10 @@ func TestMapFields(t *testing.T) { }, { "simple_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &mariadb.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &mariadb.RawCredentials{ @@ -75,8 +85,60 @@ func TestMapFields(t *testing.T) { }, true, }, + { + "hosts_unordered", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Hosts: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("host_2"), + types.StringValue(""), + types.StringValue("host_1"), + }), + }, + &mariadb.CredentialsResponse{ + Id: utils.Ptr("cid"), + Raw: &mariadb.RawCredentials{ + Credentials: &mariadb.Credentials{ + Host: utils.Ptr("host"), + Hosts: &[]string{ + "", + "host_1", + "host_2", + }, + Name: utils.Ptr("name"), + Password: utils.Ptr("password"), + Port: utils.Ptr(int64(1234)), + Uri: utils.Ptr("uri"), + Username: utils.Ptr("username"), + }, + }, + }, + Model{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue("host"), + Hosts: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("host_2"), + types.StringValue(""), + types.StringValue("host_1"), + }), + Name: types.StringValue("name"), + Password: types.StringValue("password"), + Port: types.Int64Value(1234), + Uri: types.StringValue("uri"), + Username: types.StringValue("username"), + }, + true, + }, { "null_fields_and_int_conversions", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &mariadb.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &mariadb.RawCredentials{ @@ -108,18 +170,30 @@ func TestMapFields(t *testing.T) { }, { "nil_response", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, nil, Model{}, false, }, { "no_resource_id", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &mariadb.CredentialsResponse{}, Model{}, false, }, { "nil_raw_credential", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &mariadb.CredentialsResponse{ Id: utils.Ptr("cid"), }, @@ -129,11 +203,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, model) + err := mapFields(context.Background(), tt.input, &tt.state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -141,7 +211,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(model, &tt.expected) + diff := cmp.Diff(tt.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/mongodbflex/instance/datasource.go b/stackit/internal/services/mongodbflex/instance/datasource.go index 65bcff8e..fdc848be 100644 --- a/stackit/internal/services/mongodbflex/instance/datasource.go +++ b/stackit/internal/services/mongodbflex/instance/datasource.go @@ -213,7 +213,7 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques } } - err = mapFields(instanceResp, &model, flavor, storage, options) + err = mapFields(ctx, instanceResp, &model, flavor, storage, options) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/mongodbflex/instance/resource.go b/stackit/internal/services/mongodbflex/instance/resource.go index 6eacbe43..63e5aa78 100644 --- a/stackit/internal/services/mongodbflex/instance/resource.go +++ b/stackit/internal/services/mongodbflex/instance/resource.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -328,7 +329,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } // Map response body to schema - err = mapFields(waitResp, &model, flavor, storage, options) + err = mapFields(ctx, waitResp, &model, flavor, storage, options) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return @@ -388,7 +389,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r } // Map response body to schema - err = mapFields(instanceResp, &model, flavor, storage, options) + err = mapFields(ctx, instanceResp, &model, flavor, storage, options) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return @@ -474,7 +475,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } // Map response body to schema - err = mapFields(waitResp, &model, flavor, storage, options) + err = mapFields(ctx, waitResp, &model, flavor, storage, options) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return @@ -533,7 +534,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS tflog.Info(ctx, "MongoDB Flex instance state imported") } -func mapFields(resp *mongodbflex.GetInstanceResponse, model *Model, flavor *flavorModel, storage *storageModel, options *optionsModel) error { +func mapFields(ctx context.Context, resp *mongodbflex.GetInstanceResponse, model *Model, flavor *flavorModel, storage *storageModel, options *optionsModel) error { if resp == nil { return fmt.Errorf("response input is nil") } @@ -559,11 +560,15 @@ func mapFields(resp *mongodbflex.GetInstanceResponse, model *Model, flavor *flav if instance.Acl == nil || instance.Acl.Items == nil { aclList = types.ListNull(types.StringType) } else { - acl := []attr.Value{} - for _, ip := range *instance.Acl.Items { - acl = append(acl, types.StringValue(ip)) + respACL := *instance.Acl.Items + modelACL, err := utils.ListValuetoStringSlice(model.ACL) + if err != nil { + return err } - aclList, diags = types.ListValue(types.StringType, acl) + + reconciledACL := utils.ReconcileStringSlices(modelACL, respACL) + + aclList, diags = types.ListValueFrom(ctx, types.StringType, reconciledACL) if diags.HasError() { return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) } diff --git a/stackit/internal/services/mongodbflex/instance/resource_test.go b/stackit/internal/services/mongodbflex/instance/resource_test.go index 47683e24..23c03e28 100644 --- a/stackit/internal/services/mongodbflex/instance/resource_test.go +++ b/stackit/internal/services/mongodbflex/instance/resource_test.go @@ -28,6 +28,7 @@ func (c *mongoDBFlexClientMocked) ListFlavorsExecute(_ context.Context, _ string func TestMapFields(t *testing.T) { tests := []struct { description string + state Model input *mongodbflex.GetInstanceResponse flavor *flavorModel storage *storageModel @@ -37,6 +38,10 @@ func TestMapFields(t *testing.T) { }{ { "default_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &mongodbflex.GetInstanceResponse{ Item: &mongodbflex.Instance{}, }, @@ -70,6 +75,10 @@ func TestMapFields(t *testing.T) { }, { "simple_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &mongodbflex.GetInstanceResponse{ Item: &mongodbflex.Instance{ Acl: &mongodbflex.ACL{ @@ -134,6 +143,10 @@ func TestMapFields(t *testing.T) { }, { "simple_values_no_flavor_and_storage", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &mongodbflex.GetInstanceResponse{ Item: &mongodbflex.Instance{ Acl: &mongodbflex.ACL{ @@ -196,8 +209,85 @@ func TestMapFields(t *testing.T) { }, true, }, + { + "acls_unordered", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + ACL: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ip2"), + types.StringValue(""), + types.StringValue("ip1"), + }), + }, + &mongodbflex.GetInstanceResponse{ + Item: &mongodbflex.Instance{ + Acl: &mongodbflex.ACL{ + Items: &[]string{ + "", + "ip1", + "ip2", + }, + }, + BackupSchedule: utils.Ptr("schedule"), + Flavor: nil, + Id: utils.Ptr("iid"), + Name: utils.Ptr("name"), + Replicas: utils.Ptr(int64(56)), + Status: utils.Ptr("status"), + Storage: nil, + Options: &map[string]string{ + "type": "type", + }, + Version: utils.Ptr("version"), + }, + }, + &flavorModel{ + CPU: types.Int64Value(12), + RAM: types.Int64Value(34), + }, + &storageModel{ + Class: types.StringValue("class"), + Size: types.Int64Value(78), + }, + &optionsModel{ + Type: types.StringValue("type"), + }, + Model{ + Id: types.StringValue("pid,iid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("name"), + ACL: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ip2"), + types.StringValue(""), + types.StringValue("ip1"), + }), + BackupSchedule: types.StringValue("schedule"), + Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ + "id": types.StringNull(), + "description": types.StringNull(), + "cpu": types.Int64Value(12), + "ram": types.Int64Value(34), + }), + Replicas: types.Int64Value(56), + Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ + "class": types.StringValue("class"), + "size": types.Int64Value(78), + }), + Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ + "type": types.StringValue("type"), + }), + Version: types.StringValue("version"), + }, + true, + }, { "nil_response", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, nil, &flavorModel{}, &storageModel{}, @@ -207,6 +297,10 @@ func TestMapFields(t *testing.T) { }, { "no_resource_id", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &mongodbflex.GetInstanceResponse{}, &flavorModel{}, &storageModel{}, @@ -217,11 +311,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, state, tt.flavor, tt.storage, tt.options) + err := mapFields(context.Background(), tt.input, &tt.state, tt.flavor, tt.storage, tt.options) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -229,7 +319,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(state, &tt.expected) + diff := cmp.Diff(tt.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/opensearch/credential/datasource.go b/stackit/internal/services/opensearch/credential/datasource.go index 25d07ffc..f05dccd8 100644 --- a/stackit/internal/services/opensearch/credential/datasource.go +++ b/stackit/internal/services/opensearch/credential/datasource.go @@ -162,7 +162,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ } // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/opensearch/credential/resource.go b/stackit/internal/services/opensearch/credential/resource.go index f8927eae..fc55a7ac 100644 --- a/stackit/internal/services/opensearch/credential/resource.go +++ b/stackit/internal/services/opensearch/credential/resource.go @@ -8,9 +8,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" "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" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -209,7 +209,7 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ } // Map response body to schema - err = mapFields(waitResp, &model) + err = mapFields(ctx, waitResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -244,7 +244,7 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, } // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -312,7 +312,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor tflog.Info(ctx, "OpenSearch credential state imported") } -func mapFields(credentialsResp *opensearch.CredentialsResponse, model *Model) error { +func mapFields(ctx context.Context, credentialsResp *opensearch.CredentialsResponse, model *Model) error { if credentialsResp == nil { return fmt.Errorf("response input is nil") } @@ -341,19 +341,25 @@ func mapFields(credentialsResp *opensearch.CredentialsResponse, model *Model) er model.Id = types.StringValue( strings.Join(idParts, core.Separator), ) + + modelHosts, err := utils.ListValuetoStringSlice(model.Hosts) + if err != nil { + return err + } + model.CredentialId = types.StringValue(credentialId) model.Hosts = types.ListNull(types.StringType) if credentials != nil { if credentials.Hosts != nil { - var hosts []attr.Value - for _, host := range *credentials.Hosts { - hosts = append(hosts, types.StringValue(host)) - } - hostsList, diags := types.ListValue(types.StringType, hosts) + respHosts := *credentials.Hosts + reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts) + + hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts) if diags.HasError() { return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) } - model.Hosts = hostsList + + model.Hosts = hostsTF } model.Host = types.StringPointerValue(credentials.Host) model.Password = types.StringPointerValue(credentials.Password) diff --git a/stackit/internal/services/opensearch/credential/resource_test.go b/stackit/internal/services/opensearch/credential/resource_test.go index 1f4dc311..baab0252 100644 --- a/stackit/internal/services/opensearch/credential/resource_test.go +++ b/stackit/internal/services/opensearch/credential/resource_test.go @@ -1,6 +1,7 @@ package opensearch import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -13,12 +14,17 @@ import ( func TestMapFields(t *testing.T) { tests := []struct { description string + state Model input *opensearch.CredentialsResponse expected Model isValid bool }{ { "default_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &opensearch.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &opensearch.RawCredentials{}, @@ -40,6 +46,10 @@ func TestMapFields(t *testing.T) { }, { "simple_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &opensearch.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &opensearch.RawCredentials{ @@ -75,8 +85,60 @@ func TestMapFields(t *testing.T) { }, true, }, + { + "hosts_unordered", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Hosts: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("host_2"), + types.StringValue(""), + types.StringValue("host_1"), + }), + }, + &opensearch.CredentialsResponse{ + Id: utils.Ptr("cid"), + Raw: &opensearch.RawCredentials{ + Credentials: &opensearch.Credentials{ + Host: utils.Ptr("host"), + Hosts: &[]string{ + "", + "host_1", + "host_2", + }, + Password: utils.Ptr("password"), + Port: utils.Ptr(int64(1234)), + Scheme: utils.Ptr("scheme"), + Uri: utils.Ptr("uri"), + Username: utils.Ptr("username"), + }, + }, + }, + Model{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue("host"), + Hosts: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("host_2"), + types.StringValue(""), + types.StringValue("host_1"), + }), + Password: types.StringValue("password"), + Port: types.Int64Value(1234), + Scheme: types.StringValue("scheme"), + Uri: types.StringValue("uri"), + Username: types.StringValue("username"), + }, + true, + }, { "null_fields_and_int_conversions", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &opensearch.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &opensearch.RawCredentials{ @@ -108,18 +170,30 @@ func TestMapFields(t *testing.T) { }, { "nil_response", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, nil, Model{}, false, }, { "no_resource_id", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &opensearch.CredentialsResponse{}, Model{}, false, }, { "nil_raw_credential", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &opensearch.CredentialsResponse{ Id: utils.Ptr("cid"), }, @@ -129,11 +203,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, model) + err := mapFields(context.Background(), tt.input, &tt.state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -141,7 +211,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(model, &tt.expected) + diff := cmp.Diff(tt.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/postgresflex/instance/datasource.go b/stackit/internal/services/postgresflex/instance/datasource.go index 7ab1176a..c40e9038 100644 --- a/stackit/internal/services/postgresflex/instance/datasource.go +++ b/stackit/internal/services/postgresflex/instance/datasource.go @@ -193,7 +193,7 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques } } - err = mapFields(instanceResp, &model, flavor, storage) + err = mapFields(ctx, instanceResp, &model, flavor, storage) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/postgresflex/instance/resource.go b/stackit/internal/services/postgresflex/instance/resource.go index 3e11ef8c..bc87f8d2 100644 --- a/stackit/internal/services/postgresflex/instance/resource.go +++ b/stackit/internal/services/postgresflex/instance/resource.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "github.com/hashicorp/terraform-plugin-framework/path" @@ -297,7 +298,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } // Map response body to schema - err = mapFields(waitResp, &model, flavor, storage) + err = mapFields(ctx, waitResp, &model, flavor, storage) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return @@ -348,7 +349,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r } // Map response body to schema - err = mapFields(instanceResp, &model, flavor, storage) + err = mapFields(ctx, instanceResp, &model, flavor, storage) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return @@ -425,7 +426,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } // Map response body to schema - err = mapFields(waitResp, &model, flavor, storage) + err = mapFields(ctx, waitResp, &model, flavor, storage) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return @@ -484,7 +485,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS tflog.Info(ctx, "Postgresql instance state imported") } -func mapFields(resp *postgresflex.InstanceResponse, model *Model, flavor *flavorModel, storage *storageModel) error { +func mapFields(ctx context.Context, resp *postgresflex.InstanceResponse, model *Model, flavor *flavorModel, storage *storageModel) error { if resp == nil { return fmt.Errorf("response input is nil") } @@ -510,11 +511,15 @@ func mapFields(resp *postgresflex.InstanceResponse, model *Model, flavor *flavor if instance.Acl == nil || instance.Acl.Items == nil { aclList = types.ListNull(types.StringType) } else { - acl := []attr.Value{} - for _, ip := range *instance.Acl.Items { - acl = append(acl, types.StringValue(ip)) + respACL := *instance.Acl.Items + modelACL, err := utils.ListValuetoStringSlice(model.ACL) + if err != nil { + return err } - aclList, diags = types.ListValue(types.StringType, acl) + + reconciledACL := utils.ReconcileStringSlices(modelACL, respACL) + + aclList, diags = types.ListValueFrom(ctx, types.StringType, reconciledACL) if diags.HasError() { return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) } diff --git a/stackit/internal/services/postgresflex/instance/resource_test.go b/stackit/internal/services/postgresflex/instance/resource_test.go index c0d00713..7bd7b128 100644 --- a/stackit/internal/services/postgresflex/instance/resource_test.go +++ b/stackit/internal/services/postgresflex/instance/resource_test.go @@ -28,6 +28,7 @@ func (c *postgresFlexClientMocked) ListFlavorsExecute(_ context.Context, _ strin func TestMapFields(t *testing.T) { tests := []struct { description string + state Model input *postgresflex.InstanceResponse flavor *flavorModel storage *storageModel @@ -36,6 +37,10 @@ func TestMapFields(t *testing.T) { }{ { "default_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &postgresflex.InstanceResponse{ Item: &postgresflex.Instance{}, }, @@ -65,6 +70,10 @@ func TestMapFields(t *testing.T) { }, { "simple_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &postgresflex.InstanceResponse{ Item: &postgresflex.Instance{ Acl: &postgresflex.ACL{ @@ -122,6 +131,10 @@ func TestMapFields(t *testing.T) { }, { "simple_values_no_flavor_and_storage", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &postgresflex.InstanceResponse{ Item: &postgresflex.Instance{ Acl: &postgresflex.ACL{ @@ -175,8 +188,76 @@ func TestMapFields(t *testing.T) { }, true, }, + { + "acl_unordered", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + ACL: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ip2"), + types.StringValue(""), + types.StringValue("ip1"), + }), + }, + &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Acl: &postgresflex.ACL{ + Items: &[]string{ + "", + "ip1", + "ip2", + }, + }, + BackupSchedule: utils.Ptr("schedule"), + Flavor: nil, + Id: utils.Ptr("iid"), + Name: utils.Ptr("name"), + Replicas: utils.Ptr(int64(56)), + Status: utils.Ptr("status"), + Storage: nil, + Version: utils.Ptr("version"), + }, + }, + &flavorModel{ + CPU: types.Int64Value(12), + RAM: types.Int64Value(34), + }, + &storageModel{ + Class: types.StringValue("class"), + Size: types.Int64Value(78), + }, + Model{ + Id: types.StringValue("pid,iid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("name"), + ACL: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ip2"), + types.StringValue(""), + types.StringValue("ip1"), + }), + BackupSchedule: types.StringValue("schedule"), + Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ + "id": types.StringNull(), + "description": types.StringNull(), + "cpu": types.Int64Value(12), + "ram": types.Int64Value(34), + }), + Replicas: types.Int64Value(56), + Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ + "class": types.StringValue("class"), + "size": types.Int64Value(78), + }), + Version: types.StringValue("version"), + }, + true, + }, { "nil_response", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, nil, &flavorModel{}, &storageModel{}, @@ -185,6 +266,10 @@ func TestMapFields(t *testing.T) { }, { "no_resource_id", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &postgresflex.InstanceResponse{}, &flavorModel{}, &storageModel{}, @@ -194,11 +279,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, state, tt.flavor, tt.storage) + err := mapFields(context.Background(), tt.input, &tt.state, tt.flavor, tt.storage) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -206,7 +287,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(state, &tt.expected) + diff := cmp.Diff(tt.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/rabbitmq/credential/datasource.go b/stackit/internal/services/rabbitmq/credential/datasource.go index b985cab2..8d32180f 100644 --- a/stackit/internal/services/rabbitmq/credential/datasource.go +++ b/stackit/internal/services/rabbitmq/credential/datasource.go @@ -173,7 +173,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ } // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/rabbitmq/credential/resource.go b/stackit/internal/services/rabbitmq/credential/resource.go index b5e0f5f0..b66b82d3 100644 --- a/stackit/internal/services/rabbitmq/credential/resource.go +++ b/stackit/internal/services/rabbitmq/credential/resource.go @@ -8,9 +8,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" "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" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -223,7 +223,7 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ } // Map response body to schema - err = mapFields(waitResp, &model) + err = mapFields(ctx, waitResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -258,7 +258,7 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, } // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -326,7 +326,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor tflog.Info(ctx, "RabbitMQ credential state imported") } -func mapFields(credentialsResp *rabbitmq.CredentialsResponse, model *Model) error { +func mapFields(ctx context.Context, credentialsResp *rabbitmq.CredentialsResponse, model *Model) error { if credentialsResp == nil { return fmt.Errorf("response input is nil") } @@ -356,48 +356,66 @@ func mapFields(credentialsResp *rabbitmq.CredentialsResponse, model *Model) erro strings.Join(idParts, core.Separator), ) model.CredentialId = types.StringValue(credentialId) + + modelHosts, err := utils.ListValuetoStringSlice(model.Hosts) + if err != nil { + return err + } + modelHttpApiUris, err := utils.ListValuetoStringSlice(model.HttpAPIURIs) + if err != nil { + return err + } + modelUris, err := utils.ListValuetoStringSlice(model.Uris) + if err != nil { + return err + } + model.Hosts = types.ListNull(types.StringType) model.Uris = types.ListNull(types.StringType) model.HttpAPIURIs = types.ListNull(types.StringType) if credentials != nil { if credentials.Hosts != nil { - var hosts []attr.Value - for _, host := range *credentials.Hosts { - hosts = append(hosts, types.StringValue(host)) - } - hostsList, diags := types.ListValue(types.StringType, hosts) + respHosts := *credentials.Hosts + reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts) + + hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts) if diags.HasError() { return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) } - model.Hosts = hostsList + + model.Hosts = hostsTF } model.Host = types.StringPointerValue(credentials.Host) if credentials.HttpApiUris != nil { - var httpApiUris []attr.Value - for _, httpApiUri := range *credentials.HttpApiUris { - httpApiUris = append(httpApiUris, types.StringValue(httpApiUri)) - } - httpApiUrisList, diags := types.ListValue(types.StringType, httpApiUris) + respHttpApiUris := *credentials.HttpApiUris + + reconciledHttpApiUris := utils.ReconcileStringSlices(modelHttpApiUris, respHttpApiUris) + + httpApiUrisTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHttpApiUris) if diags.HasError() { return fmt.Errorf("failed to map httpApiUris: %w", core.DiagsToError(diags)) } - model.HttpAPIURIs = httpApiUrisList + + model.HttpAPIURIs = httpApiUrisTF } + + if credentials.Uris != nil { + respUris := *credentials.Uris + + reconciledUris := utils.ReconcileStringSlices(modelUris, respUris) + + urisTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledUris) + if diags.HasError() { + return fmt.Errorf("failed to map uris: %w", core.DiagsToError(diags)) + } + + model.Uris = urisTF + } + model.HttpAPIURI = types.StringPointerValue(credentials.HttpApiUri) model.Management = types.StringPointerValue(credentials.Management) model.Password = types.StringPointerValue(credentials.Password) model.Port = types.Int64PointerValue(credentials.Port) - if credentials.Uris != nil { - var uris []attr.Value - for _, uri := range *credentials.Uris { - uris = append(uris, types.StringValue(uri)) - } - urisList, diags := types.ListValue(types.StringType, uris) - if diags.HasError() { - return fmt.Errorf("failed to map uris: %w", core.DiagsToError(diags)) - } - model.Uris = urisList - } model.Uri = types.StringPointerValue(credentials.Uri) model.Username = types.StringPointerValue(credentials.Username) } diff --git a/stackit/internal/services/rabbitmq/credential/resource_test.go b/stackit/internal/services/rabbitmq/credential/resource_test.go index 3f54263a..5492d2fe 100644 --- a/stackit/internal/services/rabbitmq/credential/resource_test.go +++ b/stackit/internal/services/rabbitmq/credential/resource_test.go @@ -1,6 +1,7 @@ package rabbitmq import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -13,12 +14,17 @@ import ( func TestMapFields(t *testing.T) { tests := []struct { description string + state Model input *rabbitmq.CredentialsResponse expected Model isValid bool }{ { "default_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &rabbitmq.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &rabbitmq.RawCredentials{}, @@ -43,6 +49,10 @@ func TestMapFields(t *testing.T) { }, { "simple_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &rabbitmq.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &rabbitmq.RawCredentials{ @@ -96,8 +106,92 @@ func TestMapFields(t *testing.T) { }, true, }, + { + "hosts_uris_unordered", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Hosts: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("host_2"), + types.StringValue(""), + types.StringValue("host_1"), + }), + Uris: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("uri_2"), + types.StringValue(""), + types.StringValue("uri_1"), + }), + HttpAPIURIs: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("http_api_uri_2"), + types.StringValue(""), + types.StringValue("http_api_uri_1"), + }), + }, + &rabbitmq.CredentialsResponse{ + Id: utils.Ptr("cid"), + Raw: &rabbitmq.RawCredentials{ + Credentials: &rabbitmq.Credentials{ + Host: utils.Ptr("host"), + Hosts: &[]string{ + "", + "host_1", + "host_2", + }, + HttpApiUri: utils.Ptr("http"), + HttpApiUris: &[]string{ + "", + "http_api_uri_1", + "http_api_uri_2", + }, + Management: utils.Ptr("management"), + Password: utils.Ptr("password"), + Port: utils.Ptr(int64(1234)), + Uri: utils.Ptr("uri"), + Uris: &[]string{ + "", + "uri_1", + "uri_2", + }, + Username: utils.Ptr("username"), + }, + }, + }, + Model{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue("host"), + Hosts: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("host_2"), + types.StringValue(""), + types.StringValue("host_1"), + }), + HttpAPIURI: types.StringValue("http"), + HttpAPIURIs: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("http_api_uri_2"), + types.StringValue(""), + types.StringValue("http_api_uri_1"), + }), + Management: types.StringValue("management"), + Password: types.StringValue("password"), + Port: types.Int64Value(1234), + Uri: types.StringValue("uri"), + Uris: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("uri_2"), + types.StringValue(""), + types.StringValue("uri_1"), + }), + Username: types.StringValue("username"), + }, + true, + }, { "null_fields_and_int_conversions", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &rabbitmq.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &rabbitmq.RawCredentials{ @@ -135,18 +229,30 @@ func TestMapFields(t *testing.T) { }, { "nil_response", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, nil, Model{}, false, }, { "no_resource_id", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &rabbitmq.CredentialsResponse{}, Model{}, false, }, { "nil_raw_credential", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &rabbitmq.CredentialsResponse{ Id: utils.Ptr("cid"), }, @@ -156,11 +262,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, model) + err := mapFields(context.Background(), tt.input, &tt.state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -168,7 +270,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(model, &tt.expected) + diff := cmp.Diff(tt.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/services/redis/credential/datasource.go b/stackit/internal/services/redis/credential/datasource.go index f26bccfb..4d1d5616 100644 --- a/stackit/internal/services/redis/credential/datasource.go +++ b/stackit/internal/services/redis/credential/datasource.go @@ -164,7 +164,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ } // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/redis/credential/resource.go b/stackit/internal/services/redis/credential/resource.go index d1318f07..135a748f 100644 --- a/stackit/internal/services/redis/credential/resource.go +++ b/stackit/internal/services/redis/credential/resource.go @@ -8,9 +8,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" "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" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -211,7 +211,7 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ } // Map response body to schema - err = mapFields(waitResp, &model) + err = mapFields(ctx, waitResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -246,7 +246,7 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, } // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -314,7 +314,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor tflog.Info(ctx, "Redis credential state imported") } -func mapFields(credentialsResp *redis.CredentialsResponse, model *Model) error { +func mapFields(ctx context.Context, credentialsResp *redis.CredentialsResponse, model *Model) error { if credentialsResp == nil { return fmt.Errorf("response input is nil") } @@ -343,19 +343,26 @@ func mapFields(credentialsResp *redis.CredentialsResponse, model *Model) error { model.Id = types.StringValue( strings.Join(idParts, core.Separator), ) + + modelHosts, err := utils.ListValuetoStringSlice(model.Hosts) + if err != nil { + return err + } + model.CredentialId = types.StringValue(credentialId) model.Hosts = types.ListNull(types.StringType) if credentials != nil { if credentials.Hosts != nil { - var hosts []attr.Value - for _, host := range *credentials.Hosts { - hosts = append(hosts, types.StringValue(host)) - } - hostsList, diags := types.ListValue(types.StringType, hosts) + respHosts := *credentials.Hosts + + reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts) + + hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts) if diags.HasError() { return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) } - model.Hosts = hostsList + + model.Hosts = hostsTF } model.Host = types.StringPointerValue(credentials.Host) model.LoadBalancedHost = types.StringPointerValue(credentials.LoadBalancedHost) diff --git a/stackit/internal/services/redis/credential/resource_test.go b/stackit/internal/services/redis/credential/resource_test.go index 3a4da006..d4d1c641 100644 --- a/stackit/internal/services/redis/credential/resource_test.go +++ b/stackit/internal/services/redis/credential/resource_test.go @@ -1,6 +1,7 @@ package redis import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -13,12 +14,17 @@ import ( func TestMapFields(t *testing.T) { tests := []struct { description string + state Model input *redis.CredentialsResponse expected Model isValid bool }{ { "default_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &redis.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &redis.RawCredentials{}, @@ -40,6 +46,10 @@ func TestMapFields(t *testing.T) { }, { "simple_values", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &redis.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &redis.RawCredentials{ @@ -75,8 +85,60 @@ func TestMapFields(t *testing.T) { }, true, }, + { + "hosts_unordered", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Hosts: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("host_2"), + types.StringValue(""), + types.StringValue("host_1"), + }), + }, + &redis.CredentialsResponse{ + Id: utils.Ptr("cid"), + Raw: &redis.RawCredentials{ + Credentials: &redis.Credentials{ + Host: utils.Ptr("host"), + Hosts: &[]string{ + "", + "host_1", + "host_2", + }, + LoadBalancedHost: utils.Ptr("load_balanced_host"), + Password: utils.Ptr("password"), + Port: utils.Ptr(int64(1234)), + Uri: utils.Ptr("uri"), + Username: utils.Ptr("username"), + }, + }, + }, + Model{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue("host"), + Hosts: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("host_2"), + types.StringValue(""), + types.StringValue("host_1"), + }), + LoadBalancedHost: types.StringValue("load_balanced_host"), + Password: types.StringValue("password"), + Port: types.Int64Value(1234), + Uri: types.StringValue("uri"), + Username: types.StringValue("username"), + }, + true, + }, { "null_fields_and_int_conversions", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &redis.CredentialsResponse{ Id: utils.Ptr("cid"), Raw: &redis.RawCredentials{ @@ -108,18 +170,30 @@ func TestMapFields(t *testing.T) { }, { "nil_response", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, nil, Model{}, false, }, { "no_resource_id", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &redis.CredentialsResponse{}, Model{}, false, }, { "nil_raw_credential", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, &redis.CredentialsResponse{ Id: utils.Ptr("cid"), }, @@ -129,11 +203,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, model) + err := mapFields(context.Background(), tt.input, &tt.state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -141,7 +211,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("Should not have failed: %v", err) } if tt.isValid { - diff := cmp.Diff(model, &tt.expected) + diff := cmp.Diff(tt.state, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/stackit/internal/utils/utils.go b/stackit/internal/utils/utils.go new file mode 100644 index 00000000..bf200258 --- /dev/null +++ b/stackit/internal/utils/utils.go @@ -0,0 +1,62 @@ +package utils + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// ReconcileStringSlices reconciles two string lists by removing elements from the +// first list that are not in the second list and appending elements from the +// second list that are not in the first list. +// This preserves the order of the elements in the first list that are also in +// the second list, which is useful when using ListAttributes in Terraform. +// The source of truth for the order is the first list and the source of truth for the content is the second list. +func ReconcileStringSlices(list1, list2 []string) []string { + // Create a copy of list1 to avoid modifying the original list + list1Copy := append([]string{}, list1...) + + // Create a map to quickly check if an element is in list2 + inList2 := make(map[string]bool) + for _, elem := range list2 { + inList2[elem] = true + } + + // Remove elements from list1Copy that are not in list2 + i := 0 + for _, elem := range list1Copy { + if inList2[elem] { + list1Copy[i] = elem + i++ + } + } + list1Copy = list1Copy[:i] + + // Append elements to list1Copy that are in list2 but not in list1Copy + inList1 := make(map[string]bool) + for _, elem := range list1Copy { + inList1[elem] = true + } + for _, elem := range list2 { + if !inList1[elem] { + list1Copy = append(list1Copy, elem) + } + } + + return list1Copy +} + +func ListValuetoStringSlice(list basetypes.ListValue) ([]string, error) { + result := []string{} + for _, el := range list.Elements() { + elStr, ok := el.(types.String) + if !ok { + return result, fmt.Errorf("expected record to be of type %T, got %T", types.String{}, elStr) + } + result = append(result, elStr.ValueString()) + } + + return result, nil +} diff --git a/stackit/internal/utils/utils_test.go b/stackit/internal/utils/utils_test.go new file mode 100644 index 00000000..206647b3 --- /dev/null +++ b/stackit/internal/utils/utils_test.go @@ -0,0 +1,122 @@ +package utils + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestReconcileStrLists(t *testing.T) { + tests := []struct { + description string + list1 []string + list2 []string + expected []string + }{ + { + "empty lists", + []string{}, + []string{}, + []string{}, + }, + { + "list1 empty", + []string{}, + []string{"a", "b", "c"}, + []string{"a", "b", "c"}, + }, + { + "list2 empty", + []string{"a", "b", "c"}, + []string{}, + []string{}, + }, + { + "no common elements", + []string{"a", "b", "c"}, + []string{"d", "e", "f"}, + []string{"d", "e", "f"}, + }, + { + "common elements", + []string{"d", "a", "c"}, + []string{"b", "c", "d", "e"}, + []string{"d", "c", "b", "e"}, + }, + { + "common elements with empty string", + []string{"d", "", "c"}, + []string{"", "c", "d"}, + []string{"d", "", "c"}, + }, + { + "common elements with duplicates", + []string{"a", "b", "c", "c"}, + []string{"b", "c", "d", "e"}, + []string{"b", "c", "c", "d", "e"}, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output := ReconcileStringSlices(tt.list1, tt.list2) + diff := cmp.Diff(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestListValuetoStrSlice(t *testing.T) { + tests := []struct { + description string + input basetypes.ListValue + expected []string + isValid bool + }{ + { + description: "empty list", + input: types.ListValueMust(types.StringType, []attr.Value{}), + expected: []string{}, + isValid: true, + }, + { + description: "values ok", + input: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("a"), + types.StringValue("b"), + types.StringValue("c"), + }), + expected: []string{"a", "b", "c"}, + isValid: true, + }, + { + description: "different type", + input: types.ListValueMust(types.Int64Type, []attr.Value{ + types.Int64Value(12), + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := ListValuetoStringSlice(tt.input) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("Should not have failed: %v", err) + } + if !tt.isValid { + t.Fatalf("Should have failed") + } + diff := cmp.Diff(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +}