diff --git a/stackit/internal/services/mariadb/credential/datasource.go b/stackit/internal/services/mariadb/credential/datasource.go index 03b7dd9ad..f8244cbf3 100644 --- a/stackit/internal/services/mariadb/credential/datasource.go +++ b/stackit/internal/services/mariadb/credential/datasource.go @@ -26,6 +26,20 @@ var ( _ datasource.DataSource = &credentialDataSource{} ) +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + CredentialId types.String `tfsdk:"credential_id"` + InstanceId types.String `tfsdk:"instance_id"` + ProjectId types.String `tfsdk:"project_id"` + Host types.String `tfsdk:"host"` + Hosts types.List `tfsdk:"hosts"` + Name types.String `tfsdk:"name"` + Password types.String `tfsdk:"password"` + Port types.Int32 `tfsdk:"port"` + Uri types.String `tfsdk:"uri"` + Username types.String `tfsdk:"username"` +} + // NewCredentialDataSource is a helper function to simplify the provider implementation. func NewCredentialDataSource() datasource.DataSource { return &credentialDataSource{} @@ -127,7 +141,7 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ // Read refreshes the Terraform state with the latest data. func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model + var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -162,7 +176,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) + err = mapDataSourceFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -176,3 +190,60 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ } tflog.Info(ctx, "mariadb credential read") } + +func mapDataSourceFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse, model *DataSourceModel) error { + if credentialsResp == nil { + return fmt.Errorf("response input is nil") + } + if credentialsResp.Raw == nil { + return fmt.Errorf("response credentials raw is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + credentials := credentialsResp.Raw.Credentials + + var credentialId string + if model.CredentialId.ValueString() != "" { + credentialId = model.CredentialId.ValueString() + } else if credentialsResp.Id != "" { + credentialId = credentialsResp.Id + } else { + return fmt.Errorf("credentials id not present") + } + + model.Id = utils.BuildInternalTerraformId( + model.ProjectId.ValueString(), + model.InstanceId.ValueString(), + 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.Hosts != nil { + 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 = hostsTF + } + model.Host = types.StringValue(credentials.Host) + model.Name = types.StringPointerValue(credentials.Name) + model.Password = types.StringValue(credentials.Password) + model.Port = types.Int32PointerValue(credentials.Port) + model.Uri = types.StringPointerValue(credentials.Uri) + model.Username = types.StringValue(credentials.Username) + + return nil +} diff --git a/stackit/internal/services/mariadb/credential/datasource_test.go b/stackit/internal/services/mariadb/credential/datasource_test.go new file mode 100644 index 000000000..bbf487742 --- /dev/null +++ b/stackit/internal/services/mariadb/credential/datasource_test.go @@ -0,0 +1,220 @@ +package mariadb + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + state DataSourceModel + input *mariadb.CredentialsResponse + expected DataSourceModel + isValid bool + }{ + { + "default_values", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &mariadb.CredentialsResponse{ + Id: "cid", + Raw: &mariadb.RawCredentials{}, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue(""), + Hosts: types.ListNull(types.StringType), + Name: types.StringNull(), + Password: types.StringValue(""), + Port: types.Int32Null(), + Uri: types.StringNull(), + Username: types.StringValue(""), + }, + true, + }, + { + "simple_values", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &mariadb.CredentialsResponse{ + Id: "cid", + Raw: &mariadb.RawCredentials{ + Credentials: mariadb.Credentials{ + Host: "host", + Hosts: []string{ + "host_1", + "", + }, + Name: new("name"), + Password: "password", + Port: new(int32(1234)), + Uri: new("uri"), + Username: "username", + }, + }, + }, + DataSourceModel{ + 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_1"), + types.StringValue(""), + }), + Name: types.StringValue("name"), + Password: types.StringValue("password"), + Port: types.Int32Value(1234), + Uri: types.StringValue("uri"), + Username: types.StringValue("username"), + }, + true, + }, + { + "hosts_unordered", + DataSourceModel{ + 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: "cid", + Raw: &mariadb.RawCredentials{ + Credentials: mariadb.Credentials{ + Host: "host", + Hosts: []string{ + "", + "host_1", + "host_2", + }, + Name: new("name"), + Password: "password", + Port: new(int32(1234)), + Uri: new("uri"), + Username: "username", + }, + }, + }, + DataSourceModel{ + 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.Int32Value(1234), + Uri: types.StringValue("uri"), + Username: types.StringValue("username"), + }, + true, + }, + { + "null_fields_and_int_conversions", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &mariadb.CredentialsResponse{ + Id: "cid", + Raw: &mariadb.RawCredentials{ + Credentials: mariadb.Credentials{ + Host: "", + Hosts: []string{}, + Name: nil, + Password: "", + Port: new(int32(2123456789)), + Uri: nil, + Username: "", + }, + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue(""), + Hosts: types.ListValueMust(types.StringType, []attr.Value{}), + Name: types.StringNull(), + Password: types.StringValue(""), + Port: types.Int32Value(2123456789), + Uri: types.StringNull(), + Username: types.StringValue(""), + }, + true, + }, + { + "nil_response", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + nil, + DataSourceModel{}, + false, + }, + { + "no_resource_id", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &mariadb.CredentialsResponse{}, + DataSourceModel{}, + false, + }, + { + "nil_raw_credential", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &mariadb.CredentialsResponse{ + Id: "cid", + }, + DataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/opensearch/credential/datasource.go b/stackit/internal/services/opensearch/credential/datasource.go index 6d88ff040..22ca53a9f 100644 --- a/stackit/internal/services/opensearch/credential/datasource.go +++ b/stackit/internal/services/opensearch/credential/datasource.go @@ -26,6 +26,20 @@ var ( _ datasource.DataSource = &credentialDataSource{} ) +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + CredentialId types.String `tfsdk:"credential_id"` + InstanceId types.String `tfsdk:"instance_id"` + ProjectId types.String `tfsdk:"project_id"` + Host types.String `tfsdk:"host"` + Hosts types.List `tfsdk:"hosts"` + Password types.String `tfsdk:"password"` + Port types.Int32 `tfsdk:"port"` + Scheme types.String `tfsdk:"scheme"` + Uri types.String `tfsdk:"uri"` + Username types.String `tfsdk:"username"` +} + // NewCredentialDataSource is a helper function to simplify the provider implementation. func NewCredentialDataSource() datasource.DataSource { return &credentialDataSource{} @@ -127,7 +141,7 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ // Read refreshes the Terraform state with the latest data. func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model + var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -162,7 +176,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) + err = mapDataSourceFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -176,3 +190,53 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ } tflog.Info(ctx, "OpenSearch credential read") } + +func mapDataSourceFields(ctx context.Context, credentialsResp *opensearch.CredentialsResponse, model *DataSourceModel) error { + if credentialsResp == nil { + return fmt.Errorf("response input is nil") + } + if credentialsResp.Raw == nil { + return fmt.Errorf("response credentials raw is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + credentials := credentialsResp.Raw.Credentials + + var credentialId string + if model.CredentialId.ValueString() != "" { + credentialId = model.CredentialId.ValueString() + } else { + credentialId = credentialsResp.Id + } + + model.Id = utils.BuildInternalTerraformId( + model.ProjectId.ValueString(), model.InstanceId.ValueString(), credentialId, + ) + + modelHosts, err := utils.ListValueToStringSlice(model.Hosts) + if err != nil { + return err + } + + model.CredentialId = types.StringValue(credentialId) + model.Hosts = types.ListNull(types.StringType) + if credentials.Hosts != nil { + 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 = hostsTF + } + model.Host = types.StringValue(credentials.Host) + model.Password = types.StringValue(credentials.Password) + model.Port = types.Int32PointerValue(credentials.Port) + model.Scheme = types.StringPointerValue(credentials.Scheme) + model.Uri = types.StringPointerValue(credentials.Uri) + model.Username = types.StringValue(credentials.Username) + return nil +} diff --git a/stackit/internal/services/opensearch/credential/datasource_test.go b/stackit/internal/services/opensearch/credential/datasource_test.go new file mode 100644 index 000000000..a95775058 --- /dev/null +++ b/stackit/internal/services/opensearch/credential/datasource_test.go @@ -0,0 +1,220 @@ +package opensearch + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v1api" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + state DataSourceModel + input *opensearch.CredentialsResponse + expected DataSourceModel + isValid bool + }{ + { + "default_values", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &opensearch.CredentialsResponse{ + Id: "cid", + Raw: &opensearch.RawCredentials{}, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue(""), + Hosts: types.ListNull(types.StringType), + Password: types.StringValue(""), + Port: types.Int32Null(), + Scheme: types.StringNull(), + Uri: types.StringNull(), + Username: types.StringValue(""), + }, + true, + }, + { + "simple_values", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &opensearch.CredentialsResponse{ + Id: "cid", + Raw: &opensearch.RawCredentials{ + Credentials: opensearch.Credentials{ + Host: "host", + Hosts: []string{ + "host_1", + "", + }, + Password: "password", + Port: new(int32(1234)), + Scheme: new("scheme"), + Uri: new("uri"), + Username: "username", + }, + }, + }, + DataSourceModel{ + 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_1"), + types.StringValue(""), + }), + Password: types.StringValue("password"), + Port: types.Int32Value(1234), + Scheme: types.StringValue("scheme"), + Uri: types.StringValue("uri"), + Username: types.StringValue("username"), + }, + true, + }, + { + "hosts_unordered", + DataSourceModel{ + 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: "cid", + Raw: &opensearch.RawCredentials{ + Credentials: opensearch.Credentials{ + Host: "host", + Hosts: []string{ + "", + "host_1", + "host_2", + }, + Password: "password", + Port: new(int32(1234)), + Scheme: new("scheme"), + Uri: new("uri"), + Username: "username", + }, + }, + }, + DataSourceModel{ + 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.Int32Value(1234), + Scheme: types.StringValue("scheme"), + Uri: types.StringValue("uri"), + Username: types.StringValue("username"), + }, + true, + }, + { + "null_fields_and_int_conversions", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &opensearch.CredentialsResponse{ + Id: "cid", + Raw: &opensearch.RawCredentials{ + Credentials: opensearch.Credentials{ + Host: "", + Hosts: []string{}, + Password: "", + Port: new(int32(2123456789)), + Scheme: nil, + Uri: nil, + Username: "", + }, + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue(""), + Hosts: types.ListValueMust(types.StringType, []attr.Value{}), + Password: types.StringValue(""), + Port: types.Int32Value(2123456789), + Scheme: types.StringNull(), + Uri: types.StringNull(), + Username: types.StringValue(""), + }, + true, + }, + { + "nil_response", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + nil, + DataSourceModel{}, + false, + }, + { + "no_resource_id", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &opensearch.CredentialsResponse{}, + DataSourceModel{}, + false, + }, + { + "nil_raw_credential", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &opensearch.CredentialsResponse{ + Id: "cid", + }, + DataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/rabbitmq/credential/datasource.go b/stackit/internal/services/rabbitmq/credential/datasource.go index d1e91b1c2..0b0c63d0c 100644 --- a/stackit/internal/services/rabbitmq/credential/datasource.go +++ b/stackit/internal/services/rabbitmq/credential/datasource.go @@ -26,6 +26,23 @@ var ( _ datasource.DataSource = &credentialDataSource{} ) +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + CredentialId types.String `tfsdk:"credential_id"` + InstanceId types.String `tfsdk:"instance_id"` + ProjectId types.String `tfsdk:"project_id"` + Host types.String `tfsdk:"host"` + Hosts types.List `tfsdk:"hosts"` + HttpAPIURI types.String `tfsdk:"http_api_uri"` + HttpAPIURIs types.List `tfsdk:"http_api_uris"` + Management types.String `tfsdk:"management"` + Password types.String `tfsdk:"password"` + Port types.Int32 `tfsdk:"port"` + Uri types.String `tfsdk:"uri"` + Uris types.List `tfsdk:"uris"` + Username types.String `tfsdk:"username"` +} + // NewCredentialDataSource is a helper function to simplify the provider implementation. func NewCredentialDataSource() datasource.DataSource { return &credentialDataSource{} @@ -138,7 +155,7 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ // Read refreshes the Terraform state with the latest data. func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model + var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -173,7 +190,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) + err = mapDataSourceFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -187,3 +204,93 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ } tflog.Info(ctx, "RabbitMQ credential read") } + +func mapDataSourceFields(ctx context.Context, credentialsResp *rabbitmq.CredentialsResponse, model *DataSourceModel) error { + if credentialsResp == nil { + return fmt.Errorf("response input is nil") + } + if credentialsResp.Raw == nil { + return fmt.Errorf("response credentials raw is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + credentials := credentialsResp.Raw.Credentials + + var credentialId string + if model.CredentialId.ValueString() != "" { + credentialId = model.CredentialId.ValueString() + } else if credentialsResp.Id != "" { + credentialId = credentialsResp.Id + } else { + return fmt.Errorf("credentials id not present") + } + + model.Id = utils.BuildInternalTerraformId( + model.ProjectId.ValueString(), model.InstanceId.ValueString(), credentialId, + ) + 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.Hosts != nil { + 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 = hostsTF + } + model.Host = types.StringValue(credentials.Host) + if credentials.HttpApiUris != nil { + 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 = 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.StringValue(credentials.Password) + model.Port = types.Int32PointerValue(credentials.Port) + model.Uri = types.StringPointerValue(credentials.Uri) + model.Username = types.StringValue(credentials.Username) + + return nil +} diff --git a/stackit/internal/services/rabbitmq/credential/datasource_test.go b/stackit/internal/services/rabbitmq/credential/datasource_test.go new file mode 100644 index 000000000..367617408 --- /dev/null +++ b/stackit/internal/services/rabbitmq/credential/datasource_test.go @@ -0,0 +1,279 @@ +package rabbitmq + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v1api" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + state DataSourceModel + input *rabbitmq.CredentialsResponse + expected DataSourceModel + isValid bool + }{ + { + "default_values", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &rabbitmq.CredentialsResponse{ + Id: "cid", + Raw: &rabbitmq.RawCredentials{}, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue(""), + Hosts: types.ListNull(types.StringType), + HttpAPIURI: types.StringNull(), + HttpAPIURIs: types.ListNull(types.StringType), + Management: types.StringNull(), + Password: types.StringValue(""), + Port: types.Int32Null(), + Uri: types.StringNull(), + Uris: types.ListNull(types.StringType), + Username: types.StringValue(""), + }, + true, + }, + { + "simple_values", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &rabbitmq.CredentialsResponse{ + Id: "cid", + Raw: &rabbitmq.RawCredentials{ + Credentials: rabbitmq.Credentials{ + Host: "host", + Hosts: []string{ + "host_1", + "", + }, + HttpApiUri: new("http"), + HttpApiUris: []string{ + "http_api_uri_1", + "", + }, + Management: new("management"), + Password: "password", + Port: new(int32(1234)), + Uri: new("uri"), + Uris: []string{ + "uri_1", + "", + }, + Username: "username", + }, + }, + }, + DataSourceModel{ + 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_1"), + types.StringValue(""), + }), + HttpAPIURI: types.StringValue("http"), + HttpAPIURIs: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("http_api_uri_1"), + types.StringValue(""), + }), + Management: types.StringValue("management"), + Password: types.StringValue("password"), + Port: types.Int32Value(1234), + Uri: types.StringValue("uri"), + Uris: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("uri_1"), + types.StringValue(""), + }), + Username: types.StringValue("username"), + }, + true, + }, + { + "hosts_uris_unordered", + DataSourceModel{ + 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: "cid", + Raw: &rabbitmq.RawCredentials{ + Credentials: rabbitmq.Credentials{ + Host: "host", + Hosts: []string{ + "", + "host_1", + "host_2", + }, + HttpApiUri: new("http"), + HttpApiUris: []string{ + "", + "http_api_uri_1", + "http_api_uri_2", + }, + Management: new("management"), + Password: "password", + Port: new(int32(1234)), + Uri: new("uri"), + Uris: []string{ + "", + "uri_1", + "uri_2", + }, + Username: "username", + }, + }, + }, + DataSourceModel{ + 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.Int32Value(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", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &rabbitmq.CredentialsResponse{ + Id: "cid", + Raw: &rabbitmq.RawCredentials{ + Credentials: rabbitmq.Credentials{ + Host: "", + Hosts: []string{}, + HttpApiUri: nil, + HttpApiUris: []string{}, + Management: nil, + Password: "", + Port: new(int32(2123456789)), + Uri: nil, + Uris: []string{}, + Username: "", + }, + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue(""), + Hosts: types.ListValueMust(types.StringType, []attr.Value{}), + HttpAPIURI: types.StringNull(), + HttpAPIURIs: types.ListValueMust(types.StringType, []attr.Value{}), + Management: types.StringNull(), + Password: types.StringValue(""), + Port: types.Int32Value(2123456789), + Uri: types.StringNull(), + Uris: types.ListValueMust(types.StringType, []attr.Value{}), + Username: types.StringValue(""), + }, + true, + }, + { + "nil_response", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + nil, + DataSourceModel{}, + false, + }, + { + "no_resource_id", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &rabbitmq.CredentialsResponse{}, + DataSourceModel{}, + false, + }, + { + "nil_raw_credential", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &rabbitmq.CredentialsResponse{ + Id: "cid", + }, + DataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/redis/credential/datasource.go b/stackit/internal/services/redis/credential/datasource.go index ed7a8aaaf..45f8fd054 100644 --- a/stackit/internal/services/redis/credential/datasource.go +++ b/stackit/internal/services/redis/credential/datasource.go @@ -26,6 +26,20 @@ var ( _ datasource.DataSource = &credentialDataSource{} ) +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + CredentialId types.String `tfsdk:"credential_id"` + InstanceId types.String `tfsdk:"instance_id"` + ProjectId types.String `tfsdk:"project_id"` + Host types.String `tfsdk:"host"` + Hosts types.List `tfsdk:"hosts"` + LoadBalancedHost types.String `tfsdk:"load_balanced_host"` + Password types.String `tfsdk:"password"` + Port types.Int32 `tfsdk:"port"` + Uri types.String `tfsdk:"uri"` + Username types.String `tfsdk:"username"` +} + // NewCredentialDataSource is a helper function to simplify the provider implementation. func NewCredentialDataSource() datasource.DataSource { return &credentialDataSource{} @@ -129,7 +143,7 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ // Read refreshes the Terraform state with the latest data. func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model + var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -164,7 +178,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) + err = mapDataSourceFields(ctx, recordSetResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -178,3 +192,56 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ } tflog.Info(ctx, "Redis credential read") } + +func mapDataSourceFields(ctx context.Context, credentialsResp *redis.CredentialsResponse, model *DataSourceModel) error { + if credentialsResp == nil { + return fmt.Errorf("response input is nil") + } + if credentialsResp.Raw == nil { + return fmt.Errorf("response credentials raw is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + credentials := credentialsResp.Raw.Credentials + + var credentialId string + if model.CredentialId.ValueString() != "" { + credentialId = model.CredentialId.ValueString() + } else if credentialsResp.Id != "" { + credentialId = credentialsResp.Id + } else { + return fmt.Errorf("credentials id not present") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), 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.Hosts != nil { + 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 = hostsTF + } + model.Host = types.StringValue(credentials.Host) + model.LoadBalancedHost = types.StringPointerValue(credentials.LoadBalancedHost) + model.Password = types.StringValue(credentials.Password) + model.Port = types.Int32PointerValue(credentials.Port) + model.Uri = types.StringPointerValue(credentials.Uri) + model.Username = types.StringValue(credentials.Username) + + return nil +} diff --git a/stackit/internal/services/redis/credential/datasource_test.go b/stackit/internal/services/redis/credential/datasource_test.go new file mode 100644 index 000000000..057435812 --- /dev/null +++ b/stackit/internal/services/redis/credential/datasource_test.go @@ -0,0 +1,220 @@ +package redis + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v1api" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + state DataSourceModel + input *redis.CredentialsResponse + expected DataSourceModel + isValid bool + }{ + { + "default_values", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &redis.CredentialsResponse{ + Id: "cid", + Raw: &redis.RawCredentials{}, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue(""), + Hosts: types.ListNull(types.StringType), + LoadBalancedHost: types.StringNull(), + Password: types.StringValue(""), + Port: types.Int32Null(), + Uri: types.StringNull(), + Username: types.StringValue(""), + }, + true, + }, + { + "simple_values", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &redis.CredentialsResponse{ + Id: "cid", + Raw: &redis.RawCredentials{ + Credentials: redis.Credentials{ + Host: "host", + Hosts: []string{ + "host_1", + "", + }, + LoadBalancedHost: new("load_balanced_host"), + Password: "password", + Port: new(int32(1234)), + Uri: new("uri"), + Username: "username", + }, + }, + }, + DataSourceModel{ + 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_1"), + types.StringValue(""), + }), + LoadBalancedHost: types.StringValue("load_balanced_host"), + Password: types.StringValue("password"), + Port: types.Int32Value(1234), + Uri: types.StringValue("uri"), + Username: types.StringValue("username"), + }, + true, + }, + { + "hosts_unordered", + DataSourceModel{ + 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: "cid", + Raw: &redis.RawCredentials{ + Credentials: redis.Credentials{ + Host: "host", + Hosts: []string{ + "", + "host_1", + "host_2", + }, + LoadBalancedHost: new("load_balanced_host"), + Password: "password", + Port: new(int32(1234)), + Uri: new("uri"), + Username: "username", + }, + }, + }, + DataSourceModel{ + 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.Int32Value(1234), + Uri: types.StringValue("uri"), + Username: types.StringValue("username"), + }, + true, + }, + { + "null_fields_and_int_conversions", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &redis.CredentialsResponse{ + Id: "cid", + Raw: &redis.RawCredentials{ + Credentials: redis.Credentials{ + Host: "", + Hosts: []string{}, + LoadBalancedHost: nil, + Password: "", + Port: new(int32(2123456789)), + Uri: nil, + Username: "", + }, + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,cid"), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Host: types.StringValue(""), + Hosts: types.ListValueMust(types.StringType, []attr.Value{}), + LoadBalancedHost: types.StringNull(), + Password: types.StringValue(""), + Port: types.Int32Value(2123456789), + Uri: types.StringNull(), + Username: types.StringValue(""), + }, + true, + }, + { + "nil_response", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + nil, + DataSourceModel{}, + false, + }, + { + "no_resource_id", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &redis.CredentialsResponse{}, + DataSourceModel{}, + false, + }, + { + "nil_raw_credential", + DataSourceModel{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + }, + &redis.CredentialsResponse{ + Id: "cid", + }, + DataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +}