diff --git a/stackit/internal/services/postgresflex/user/resource.go b/stackit/internal/services/postgresflex/user/resource.go index 75ba451e..1c14cd8b 100644 --- a/stackit/internal/services/postgresflex/user/resource.go +++ b/stackit/internal/services/postgresflex/user/resource.go @@ -20,7 +20,6 @@ import ( "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/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/config" @@ -202,9 +201,6 @@ func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp Description: descriptions["roles"], ElementType: types.StringType, Required: true, - PlanModifiers: []planmodifier.Set{ - setplanmodifier.RequiresReplace(), - }, Validators: []validator.Set{ setvalidator.ValueStringsAre( stringvalidator.OneOf("login", "createdb"), @@ -344,9 +340,74 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp } // Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "User can't be updated") +func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + region := model.Region.ValueString() + 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) + + // Retrieve values from state + var stateModel Model + diags = req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var roles []string + if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { + diags = model.Roles.ElementsAs(ctx, &roles, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } + + // Generate API request body from model + payload, err := toUpdatePayload(&model, roles) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Updating API payload: %v", err)) + return + } + + // Update existing instance + err = r.client.UpdateUser(ctx, projectId, region, instanceId, userId).UpdateUserPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", err.Error()) + return + } + + userResp, err := r.client.GetUser(ctx, projectId, region, instanceId, userId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(userResp, &stateModel, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Postgres Flex user updated") } // Delete deletes the resource and removes the Terraform state on success. @@ -515,3 +576,16 @@ func toCreatePayload(model *Model, roles []string) (*postgresflex.CreateUserPayl Username: conversion.StringValueToPointer(model.Username), }, nil } + +func toUpdatePayload(model *Model, roles []string) (*postgresflex.UpdateUserPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if roles == nil { + return nil, fmt.Errorf("nil roles") + } + + return &postgresflex.UpdateUserPayload{ + Roles: &roles, + }, nil +} diff --git a/stackit/internal/services/postgresflex/user/resource_test.go b/stackit/internal/services/postgresflex/user/resource_test.go index 9bd59841..b5c13716 100644 --- a/stackit/internal/services/postgresflex/user/resource_test.go +++ b/stackit/internal/services/postgresflex/user/resource_test.go @@ -385,3 +385,86 @@ func TestToCreatePayload(t *testing.T) { }) } } + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + inputRoles []string + expected *postgresflex.UpdateUserPayload + isValid bool + }{ + { + "default_values", + &Model{}, + []string{}, + &postgresflex.UpdateUserPayload{ + Roles: &[]string{}, + }, + true, + }, + { + "default_values", + &Model{ + Username: types.StringValue("username"), + }, + []string{ + "role_1", + "role_2", + }, + &postgresflex.UpdateUserPayload{ + Roles: &[]string{ + "role_1", + "role_2", + }, + }, + true, + }, + { + "null_fields_and_int_conversions", + &Model{ + Username: types.StringNull(), + }, + []string{ + "", + }, + &postgresflex.UpdateUserPayload{ + Roles: &[]string{ + "", + }, + }, + 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) + } + } + }) + } +}