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
This commit is contained in:
Diogo Ferrão 2024-04-16 11:20:19 +01:00 committed by GitHub
parent 18d3f4d1fb
commit 9bd1da7cee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1011 additions and 146 deletions

View file

@ -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

View file

@ -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(),

View file

@ -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")
}

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}
})
}
}