From 5d92c9b460c1c1078a000eb29efd68cad5cc85f3 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 16:24:06 +0100 Subject: [PATCH] feat: refactor user data source and resource schemas to utilize shared models --- .../database/datasource.go.bak | 220 ------- .../database/resource.go.bak | 539 ------------------ .../postgresflexalpha/user/datasource.go | 180 ++---- .../postgresflexalpha/user/datasource_test.go | 146 ----- .../postgresflexalpha/user/resource.go | 216 ++----- .../postgresflexalpha/user/resource_test.go | 448 --------------- 6 files changed, 86 insertions(+), 1663 deletions(-) delete mode 100644 stackit/internal/services/postgresflexalpha/database/datasource.go.bak delete mode 100644 stackit/internal/services/postgresflexalpha/database/resource.go.bak delete mode 100644 stackit/internal/services/postgresflexalpha/user/datasource_test.go delete mode 100644 stackit/internal/services/postgresflexalpha/user/resource_test.go diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go.bak b/stackit/internal/services/postgresflexalpha/database/datasource.go.bak deleted file mode 100644 index 36fc5333..00000000 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go.bak +++ /dev/null @@ -1,220 +0,0 @@ -package postgresflexalpha - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" - postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &databaseDataSource{} -) - -// NewDatabaseDataSource is a helper function to simplify the provider implementation. -func NewDatabaseDataSource() datasource.DataSource { - return &databaseDataSource{} -} - -// databaseDataSource is the data source implementation. -type databaseDataSource struct { - client *postgresflexalpha.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *databaseDataSource) Metadata( - _ context.Context, - req datasource.MetadataRequest, - resp *datasource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_postgresflexalpha_database" -} - -// Configure adds the provider configured client to the data source. -func (r *databaseDataSource) Configure( - ctx context.Context, - req datasource.ConfigureRequest, - resp *datasource.ConfigureResponse, -) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex database client configured") -} - -// Schema defines the schema for the data source. -func (r *databaseDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex database resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`database_id`\".", - "database_id": "Database ID.", - "instance_id": "ID of the Postgres Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Database name.", - "owner": "Username of the database owner.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "database_id": schema.Int64Attribute{ - Description: descriptions["database_id"], - Optional: true, - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "owner": schema.StringAttribute{ - Description: descriptions["owner"], - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *databaseDataSource) Read( - ctx context.Context, - req datasource.ReadRequest, - resp *datasource.ReadResponse, -) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // validation for exactly one of database_id or name - isIdSet := !model.DatabaseId.IsNull() && !model.DatabaseId.IsUnknown() - isNameSet := !model.Name.IsNull() && !model.Name.IsUnknown() - - if (isIdSet && isNameSet) || (!isIdSet && !isNameSet) { - core.LogAndAddError( - ctx, &resp.Diagnostics, - "Invalid configuration", "Exactly one of 'database_id' or 'name' must be specified.", - ) - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - databaseId := model.DatabaseId.ValueInt64() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "database_id", databaseId) - ctx = tflog.SetField(ctx, "region", region) - - var databaseResp *postgresflexalpha.ListDatabase - var err error - - if isIdSet { - databaseId := model.DatabaseId.ValueInt64() - ctx = tflog.SetField(ctx, "database_id", databaseId) - databaseResp, err = getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) - } else { - databaseName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "name", databaseName) - databaseResp, err = getDatabaseByName(ctx, r.client, projectId, region, instanceId, databaseName) - } - - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading database", - fmt.Sprintf( - "Database with ID %q or instance with ID %q does not exist in project %q.", - databaseId, - instanceId, - projectId, - ), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema and populate Computed attribute values - err = mapFields(databaseResp, &model, region) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error reading database", - fmt.Sprintf("Processing API payload: %v", err), - ) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database read") -} diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go.bak b/stackit/internal/services/postgresflexalpha/database/resource.go.bak deleted file mode 100644 index 06bf2021..00000000 --- a/stackit/internal/services/postgresflexalpha/database/resource.go.bak +++ /dev/null @@ -1,539 +0,0 @@ -package postgresflexalpha - -import ( - "context" - "errors" - "fmt" - "math" - "net/http" - "regexp" - "strconv" - "strings" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" - postgresflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/datasources_gen" - postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &databaseResource{} - _ resource.ResourceWithConfigure = &databaseResource{} - _ resource.ResourceWithImportState = &databaseResource{} - _ resource.ResourceWithModifyPlan = &databaseResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - DatabaseId types.Int64 `tfsdk:"database_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - Owner types.String `tfsdk:"owner"` - Region types.String `tfsdk:"region"` -} - -// NewDatabaseResource is a helper function to simplify the provider implementation. -func NewDatabaseResource() resource.Resource { - return &databaseResource{} -} - -// databaseResource is the resource implementation. -type databaseResource struct { - client *postgresflexalpha.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *databaseResource) ModifyPlan( - ctx context.Context, - req resource.ModifyPlanRequest, - resp *resource.ModifyPlanResponse, -) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *databaseResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_postgresflexalpha_database" -} - -// Configure adds the provider configured client to the resource. -func (r *databaseResource) Configure( - ctx context.Context, - req resource.ConfigureRequest, - resp *resource.ConfigureResponse, -) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex database client configured") -} - -// Schema defines the schema for the resource. -func (r *databaseResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex database resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`database_id`\".", - "database_id": "Database ID.", - "instance_id": "ID of the Postgres Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Database name.", - "owner": "Username of the database owner.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "database_id": schema.Int64Attribute{ - Description: descriptions["database_id"], - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - Validators: []validator.Int64{}, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{}, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile("^[a-z]([a-z0-9]*)?$"), - "must start with a letter, must have lower case letters or numbers", - ), - }, - }, - "owner": schema.StringAttribute{ - Description: descriptions["owner"], - Required: true, - PlanModifiers: []planmodifier.String{}, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *databaseResource) Create( - ctx context.Context, - req resource.CreateRequest, - resp *resource.CreateResponse, -) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error creating database", - fmt.Sprintf("Creating API payload: %v", err), - ) - return - } - // Create new database - databaseResp, err := r.client.CreateDatabaseRequest( - ctx, - projectId, - region, - instanceId, - ).CreateDatabaseRequestPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if databaseResp == nil || databaseResp.Id == nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error creating database", - "API didn't return database Id. A database might have been created", - ) - return - } - databaseId := *databaseResp.Id - ctx = tflog.SetField(ctx, "database_id", databaseId) - - database, err := getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error creating database", - fmt.Sprintf("Getting database details after creation: %v", err), - ) - return - } - - // Map response body to schema - err = mapFields(database, &model, region) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error creating database", - fmt.Sprintf("Processing API payload: %v", err), - ) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *databaseResource) Read( - ctx context.Context, - req resource.ReadRequest, - resp *resource.ReadResponse, -) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - databaseId := model.DatabaseId.ValueInt64() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "database_id", databaseId) - ctx = tflog.SetField(ctx, "region", region) - - databaseResp, err := getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if (ok && oapiErr.StatusCode == http.StatusNotFound) || errors.Is(err, errDatabaseNotFound) { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading database", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(databaseResp, &model, region) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error reading database", - fmt.Sprintf("Processing API payload: %v", err), - ) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *databaseResource) Update( - ctx context.Context, - req resource.UpdateRequest, - resp *resource.UpdateResponse, -) { - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - databaseId64 := model.DatabaseId.ValueInt64() - if databaseId64 > math.MaxInt32 { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error in type conversion", "int value too large (databaseId)") - return - } - databaseId := int32(databaseId64) - - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "database_id", databaseId) - ctx = tflog.SetField(ctx, "region", region) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - modified := false - var payload postgresflexalpha.UpdateDatabasePartiallyRequestPayload - if stateModel.Name != model.Name { - payload.Name = model.Name.ValueStringPointer() - modified = true - } - - if stateModel.Owner != model.Owner { - payload.Owner = model.Owner.ValueStringPointer() - modified = true - } - - if !modified { - tflog.Info(ctx, "no modification detected") - return - } - - // Update existing database - res, err := r.client.UpdateDatabasePartiallyRequest( - ctx, - projectId, - region, - instanceId, - databaseId, - ).UpdateDatabasePartiallyRequestPayload(payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "error updating database", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFieldsUpdatePartially(res, &model, region) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error updating database", - fmt.Sprintf("Processing API payload: %v", err), - ) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *databaseResource) Delete( - ctx context.Context, - req resource.DeleteRequest, - resp *resource.DeleteResponse, -) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - databaseId64 := model.DatabaseId.ValueInt64() - - if databaseId64 > math.MaxInt32 { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error in type conversion", "int value too large (databaseId)") - return - } - databaseId := int32(databaseId64) - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "database_id", databaseId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing record set - err := r.client.DeleteDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseId) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Postgres Flex database deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id -func (r *databaseResource) ImportState( - ctx context.Context, - req resource.ImportStateRequest, - resp *resource.ImportStateResponse, -) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError( - ctx, &resp.Diagnostics, - "Error importing database", - fmt.Sprintf( - "Expected import identifier with format [project_id],[region],[instance_id],[database_id], got %q", - req.ID, - ), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_id"), idParts[3])...) - core.LogAndAddWarning( - ctx, - &resp.Diagnostics, - "Postgresflex database imported with empty password", - "The database password is not imported as it is only available upon creation of a new database. The password field will be empty.", - ) - tflog.Info(ctx, "Postgres Flex database state imported") -} - -func mapFieldsUpdatePartially( - res *postgresflexalpha.UpdateDatabasePartiallyResponse, - model *postgresflexalpha2.DatabaseModel, - region string, -) error { - if res == nil { - return fmt.Errorf("response is nil") - } - return mapFields(res.Database, model, region) -} - -func toCreatePayload(model *Model) (*postgresflexalpha.CreateDatabaseRequestPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &postgresflexalpha.CreateDatabaseRequestPayload{ - Name: model.Name.ValueStringPointer(), - Owner: model.Owner.ValueStringPointer(), - }, nil -} - -var errDatabaseNotFound = errors.New("database not found") diff --git a/stackit/internal/services/postgresflexalpha/user/datasource.go b/stackit/internal/services/postgresflexalpha/user/datasource.go index 7b3a1428..782c5d73 100644 --- a/stackit/internal/services/postgresflexalpha/user/datasource.go +++ b/stackit/internal/services/postgresflexalpha/user/datasource.go @@ -5,22 +5,19 @@ import ( "fmt" "math" "net/http" - "strconv" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" + postgresflexalpha "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/datasources_gen" postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" ) // Ensure the implementation satisfies the expected interfaces. @@ -28,18 +25,10 @@ var ( _ datasource.DataSource = &userDataSource{} ) +// DataSourceModel maps the data source schema data. type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.Int64 `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Region types.String `tfsdk:"region"` - Status types.String `tfsdk:"status"` - ConnectionString types.String `tfsdk:"connection_string"` + postgresflexalpha.UserModel + TerraformID types.String `tfsdk:"id"` } // NewUserDataSource is a helper function to simplify the provider implementation. @@ -83,84 +72,16 @@ func (r *userDataSource) Configure( } // Schema defines the schema for the data source. -func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex user data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. ID. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the PostgresFlex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "username": "The name of the user.", - "roles": "The roles assigned to the user.", - "host": "The host address for the user to connect to the instance.", - "port": "The port number for the user to connect to the instance.", - "region": "The resource region. If not defined, the provider region is used.", - "status": "The current status of the user.", - "connection_string": "The connection string for the user to the instance.", +func (r *userDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + s := postgresflexalpha.UserDataSourceSchema(ctx) + s.Attributes["id"] = schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`," + + "`user_id`\\\".\",", + Optional: true, + Computed: true, } - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Computed: true, - }, - "roles": schema.SetAttribute{ - Description: descriptions["roles"], - ElementType: types.StringType, - Computed: true, - }, - "host": schema.StringAttribute{ - Description: descriptions["host"], - Computed: true, - }, - "port": schema.Int64Attribute{ - Description: descriptions["port"], - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found automatically, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - "status": schema.StringAttribute{ - Description: descriptions["status"], - Computed: true, - }, - "connection_string": schema.StringAttribute{ - Description: descriptions["connection_string"], - Computed: true, - }, - }, - } + resp.Schema = s } // Read refreshes the Terraform state with the latest data. @@ -190,26 +111,12 @@ func (r *userDataSource) Read( region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "user_id", userId) recordSetResp, err := r.client.GetUserRequest(ctx, projectId, region, instanceId, userId).Execute() if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading user", - fmt.Sprintf( - "User with ID %q or instance with ID %q does not exist in project %q.", - userId, - instanceId, - projectId, - ), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) + handleReadError(ctx, &diags, err, projectId, instanceId, userId) resp.State.RemoveResource(ctx) return } @@ -237,26 +144,39 @@ func (r *userDataSource) Read( tflog.Info(ctx, "Postgres Flex user read") } -func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSourceModel, region string) error { - if userResp == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp - - var userId int64 - if model.UserId.ValueInt64() != 0 { - userId = model.UserId.ValueInt64() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10), +// handleReadError centralizes API error handling for the Read operation. +func handleReadError( + ctx context.Context, + diags *diag.Diagnostics, + err error, + projectId, instanceId string, + userId int32, +) { + utils.LogError( + ctx, + diags, + err, + "Reading user", + fmt.Sprintf( + "User with ID %q or instance with ID %q does not exist in project %q.", + userId, + instanceId, + projectId, + ), + map[int]string{ + http.StatusBadRequest: fmt.Sprintf( + "Invalid user request parameters for project %q and instance %q.", + projectId, + instanceId, + ), + http.StatusNotFound: fmt.Sprintf( + "User, instance %q, or project %q or user %q not found.", + instanceId, + projectId, + userId, + ), + http.StatusForbidden: fmt.Sprintf("Forbidden access to project %q.", projectId), + }, ) model.UserId = types.Int64Value(userId) model.Username = types.StringPointerValue(user.Name) diff --git a/stackit/internal/services/postgresflexalpha/user/datasource_test.go b/stackit/internal/services/postgresflexalpha/user/datasource_test.go deleted file mode 100644 index 679bef85..00000000 --- a/stackit/internal/services/postgresflexalpha/user/datasource_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package postgresflexalpha - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" -) - -func TestMapDataSourceFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflexalpha.GetUserResponse - region string - expected DataSourceModel - isValid bool - }{ - { - "default_values", - &postgresflexalpha.GetUserResponse{}, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &postgresflexalpha.GetUserResponse{ - Roles: &[]postgresflexalpha.UserRole{ - "role_1", - "role_2", - "", - }, - Name: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust( - types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }, - ), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Roles: &[]postgresflexalpha.UserRole{}, - Name: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - Status: utils.Ptr("status"), - ConnectionString: utils.Ptr("connection_string"), - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - Status: types.StringValue("status"), - ConnectionString: types.StringValue("connection_string"), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - DataSourceModel{}, - false, - }, - { - "nil_response_2", - &postgresflexalpha.GetUserResponse{}, - testRegion, - DataSourceModel{}, - false, - }, - { - "no_resource_id", - &postgresflexalpha.GetUserResponse{}, - testRegion, - DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run( - tt.description, func(t *testing.T) { - state := &DataSourceModel{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - UserId: tt.expected.UserId, - } - err := mapDataSourceFields(tt.input, state, tt.region) - 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(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }, - ) - } -} diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index 3aa5f519..0c19988f 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -2,36 +2,27 @@ package postgresflexalpha import ( "context" + _ "embed" "errors" "fmt" "math" "net/http" - "strconv" "strings" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" + postgresflexalpha "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/resources_gen" postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" - postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" - - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/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" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" ) // Ensure the implementation satisfies the expected interfaces. @@ -43,18 +34,8 @@ var ( ) type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.Int64 `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Password types.String `tfsdk:"password"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Region types.String `tfsdk:"region"` - Status types.String `tfsdk:"status"` - ConnectionString types.String `tfsdk:"connection_string"` + postgresflexalpha.UserModel + TerraformID types.String `tfsdk:"id"` } // NewUserResource is a helper function to simplify the provider implementation. @@ -123,117 +104,30 @@ func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequ tflog.Info(ctx, "Postgres Flex user client configured") } +//go:embed planModifiers.yaml +var modifiersFileByte []byte + // Schema defines the schema for the resource. -func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - rolesOptions := []string{"login", "createdb", "createrole"} - - descriptions := map[string]string{ - "main": "Postgres Flex user resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the PostgresFlex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "username": "The name of the user.", - "roles": "Database access levels for the user. " + utils.FormatPossibleValues(rolesOptions...), - "region": "The resource region. If not defined, the provider region is used.", - "status": "The current status of the user.", - "password": "The password for the user. This is only set upon creation.", - "host": "The host of the Postgres Flex instance.", - "port": "The port of the Postgres Flex instance.", - "connection_string": "The connection string for the user to the instance.", +func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + s := postgresflexalpha.UserResourceSchema(ctx) + s.Attributes["id"] = schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`database_id`\\\".\",", + Optional: true, + Computed: true, } - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "user_id": schema.Int64Attribute{ - Description: descriptions["user_id"], - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - Validators: []validator.Int64{}, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Required: true, - PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - }, - }, - "roles": schema.SetAttribute{ - Description: descriptions["roles"], - ElementType: types.StringType, - Required: true, - Validators: []validator.Set{ - setvalidator.ValueStringsAre( - stringvalidator.OneOf(rolesOptions...), - ), - }, - }, - "password": schema.StringAttribute{ - Description: descriptions["password"], - Computed: true, - Sensitive: true, - }, - "host": schema.StringAttribute{ - Description: descriptions["host"], - Computed: true, - }, - "port": schema.Int64Attribute{ - Description: descriptions["port"], - Computed: true, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "status": schema.StringAttribute{ - Description: descriptions["status"], - Computed: true, - }, - "connection_string": schema.StringAttribute{ - Description: descriptions["connection_string"], - Computed: true, - }, - }, + fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) + if err != nil { + resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) + return } + + err = postgresflexUtils.AddPlanModifiersToResourceSchema(fields, &s) + if err != nil { + resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) + return + } + resp.Schema = s } // Create creates the resource and sets the initial Terraform state. @@ -277,8 +171,6 @@ func (r *userResource) Create( return } - ctx = core.LogResponse(ctx) - if userResp.Id == nil || *userResp.Id == 0 { core.LogAndAddError( ctx, @@ -288,11 +180,14 @@ func (r *userResource) Create( ) return } + model.Id = types.Int64PointerValue(userResp.Id) model.UserId = types.Int64PointerValue(userResp.Id) model.Password = types.StringPointerValue(userResp.Password) ctx = tflog.SetField(ctx, "user_id", *userResp.Id) + ctx = core.LogResponse(ctx) + exists, err := r.getUserResource(ctx, &model) if err != nil { @@ -585,7 +480,7 @@ func (r *userResource) getUserResource(ctx context.Context, model *Model) (bool, return false, fmt.Errorf("error fetching user resource: %w", err) } - if err := mapFields(userResp, model, arg.region); err != nil { + if err := mapResourceFields(userResp, model, arg.region); err != nil { return false, fmt.Errorf("error mapping user resource: %w", err) } @@ -615,13 +510,13 @@ func (r *userResource) setTFLogFields(ctx context.Context, model *Model) context ctx = tflog.SetField(ctx, "project_id", usrCtx.projectId) ctx = tflog.SetField(ctx, "instance_id", usrCtx.instanceId) - ctx = tflog.SetField(ctx, "user_id", usrCtx.userId) ctx = tflog.SetField(ctx, "region", usrCtx.region) + ctx = tflog.SetField(ctx, "user_id", usrCtx.userId) return ctx } -func (r *userResource) expandRoles(ctx context.Context, rolesSet types.Set, diags *diag.Diagnostics) []string { +func (r *userResource) expandRoles(ctx context.Context, rolesSet types.List, diags *diag.Diagnostics) []string { if rolesSet.IsNull() || rolesSet.IsUnknown() { return nil } @@ -629,42 +524,3 @@ func (r *userResource) expandRoles(ctx context.Context, rolesSet types.Set, diag diags.Append(rolesSet.ElementsAs(ctx, &roles, false)...) return roles } - -func toCreatePayload(model *Model, roles *[]string) (*postgresflex.CreateUserRequestPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if roles == nil { - return nil, fmt.Errorf("nil roles") - } - - return &postgresflex.CreateUserRequestPayload{ - Roles: toPayloadRoles(roles), - Name: conversion.StringValueToPointer(model.Username), - }, nil -} - -func toPayloadRoles(roles *[]string) *[]postgresflex.UserRole { - var userRoles = make([]postgresflex.UserRole, 0, len(*roles)) - for _, role := range *roles { - userRoles = append(userRoles, postgresflex.UserRole(role)) - } - return &userRoles -} - -func toUpdatePayload(model *Model, roles *[]string) ( - *postgresflex.UpdateUserRequestPayload, - error, -) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if roles == nil { - return nil, fmt.Errorf("nil roles") - } - - return &postgresflex.UpdateUserRequestPayload{ - Name: conversion.StringValueToPointer(model.Username), - Roles: toPayloadRoles(roles), - }, nil -} diff --git a/stackit/internal/services/postgresflexalpha/user/resource_test.go b/stackit/internal/services/postgresflexalpha/user/resource_test.go deleted file mode 100644 index e4a13482..00000000 --- a/stackit/internal/services/postgresflexalpha/user/resource_test.go +++ /dev/null @@ -1,448 +0,0 @@ -package postgresflexalpha - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" -) - -func TestMapFieldsCreate(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflexalpha.GetUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Password: types.StringNull(), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "simple_values", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Name: utils.Ptr("username"), - ConnectionString: utils.Ptr("connection_string"), - Status: utils.Ptr("status"), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetNull(types.StringType), - Password: types.StringNull(), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - Status: types.StringValue("status"), - ConnectionString: types.StringValue("connection_string"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Name: nil, - ConnectionString: nil, - Status: nil, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Password: types.StringNull(), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &postgresflexalpha.GetUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &postgresflexalpha.GetUserResponse{}, - testRegion, - Model{}, - false, - }, - } - 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.region) - 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(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }, - ) - } -} - -func TestMapFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflexalpha.GetUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &postgresflexalpha.GetUserResponse{}, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(int64(1)), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "simple_values", - &postgresflexalpha.GetUserResponse{ - Roles: &[]postgresflexalpha.UserRole{ - "role_1", - "role_2", - "", - }, - Name: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust( - types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }, - ), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Name: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &postgresflexalpha.GetUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &postgresflexalpha.GetUserResponse{}, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run( - tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - UserId: tt.expected.UserId, - } - err := mapFields(tt.input, state, tt.region) - 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(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }, - ) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles *[]string - expected *postgresflexalpha.CreateUserRequestPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &[]string{}, - &postgresflexalpha.CreateUserRequestPayload{ - Name: nil, - Roles: &[]postgresflexalpha.UserRole{}, - }, - true, - }, - { - "simple_values", - &Model{ - Username: types.StringValue("username"), - }, - &[]string{ - "role_1", - "role_2", - }, - &postgresflexalpha.CreateUserRequestPayload{ - Name: utils.Ptr("username"), - Roles: &[]postgresflexalpha.UserRole{ - "role_1", - "role_2", - }, - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Username: types.StringNull(), - }, - &[]string{ - "", - }, - &postgresflexalpha.CreateUserRequestPayload{ - Roles: &[]postgresflexalpha.UserRole{ - "", - }, - Name: nil, - }, - true, - }, - { - "nil_model", - nil, - &[]string{}, - nil, - false, - }, - { - "nil_roles", - &Model{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run( - tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputRoles) - 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(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }, - ) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles *[]string - expected *postgresflexalpha.UpdateUserRequestPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &[]string{}, - &postgresflexalpha.UpdateUserRequestPayload{ - Roles: &[]postgresflexalpha.UserRole{}, - }, - true, - }, - { - "default_values", - &Model{ - Username: types.StringValue("username"), - }, - &[]string{ - "role_1", - "role_2", - }, - &postgresflexalpha.UpdateUserRequestPayload{ - Name: utils.Ptr("username"), - Roles: &[]postgresflexalpha.UserRole{ - "role_1", - "role_2", - }, - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Username: types.StringNull(), - }, - &[]string{ - "", - }, - &postgresflexalpha.UpdateUserRequestPayload{ - Roles: &[]postgresflexalpha.UserRole{ - "", - }, - }, - true, - }, - { - "nil_model", - nil, - &[]string{}, - nil, - false, - }, - { - "nil_roles", - &Model{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run( - tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input, tt.inputRoles) - 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(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }, - ) - } -}