Skip to content

Commit 4704fc6

Browse files
committed
fix(postgresflex): User role should be updatable
Signed-off-by: Alexander Dahmen <alexander.dahmen@inovex.de>
1 parent 23e9a25 commit 4704fc6

2 files changed

Lines changed: 162 additions & 7 deletions

File tree

stackit/internal/services/postgresflex/user/resource.go

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"github.com/hashicorp/terraform-plugin-framework/resource"
2121
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
2222
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
23-
"github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier"
2423
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
2524
"github.com/hashicorp/terraform-plugin-framework/types"
2625
"github.com/stackitcloud/stackit-sdk-go/core/config"
@@ -167,9 +166,6 @@ func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp
167166
Description: descriptions["roles"],
168167
ElementType: types.StringType,
169168
Required: true,
170-
PlanModifiers: []planmodifier.Set{
171-
setplanmodifier.RequiresReplace(),
172-
},
173169
Validators: []validator.Set{
174170
setvalidator.ValueStringsAre(
175171
stringvalidator.OneOf("login", "createdb"),
@@ -293,9 +289,72 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp
293289
}
294290

295291
// Update updates the resource and sets the updated Terraform state on success.
296-
func (r *userResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
297-
// Update shouldn't be called
298-
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "User can't be updated")
292+
func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
293+
// Retrieve values from plan
294+
var model Model
295+
diags := req.Plan.Get(ctx, &model)
296+
resp.Diagnostics.Append(diags...)
297+
if resp.Diagnostics.HasError() {
298+
return
299+
}
300+
projectId := model.ProjectId.ValueString()
301+
instanceId := model.InstanceId.ValueString()
302+
userId := model.UserId.ValueString()
303+
ctx = tflog.SetField(ctx, "project_id", projectId)
304+
ctx = tflog.SetField(ctx, "instance_id", instanceId)
305+
ctx = tflog.SetField(ctx, "user_id", userId)
306+
307+
// Retrieve values from state
308+
var stateModel Model
309+
diags = req.State.Get(ctx, &stateModel)
310+
resp.Diagnostics.Append(diags...)
311+
if resp.Diagnostics.HasError() {
312+
return
313+
}
314+
315+
var roles []string
316+
if !(model.Roles.IsNull() || model.Roles.IsUnknown()) {
317+
diags = model.Roles.ElementsAs(ctx, &roles, false)
318+
resp.Diagnostics.Append(diags...)
319+
if resp.Diagnostics.HasError() {
320+
return
321+
}
322+
}
323+
324+
// Generate API request body from model
325+
payload, err := toUpdatePayload(&model, roles)
326+
if err != nil {
327+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Updating API payload: %v", err))
328+
return
329+
}
330+
331+
// Update existing instance
332+
err = r.client.UpdateUser(ctx, projectId, instanceId, userId).UpdateUserPayload(*payload).Execute()
333+
if err != nil {
334+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", err.Error())
335+
return
336+
}
337+
338+
userResp, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute()
339+
if err != nil {
340+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API: %v", err))
341+
return
342+
}
343+
344+
// Map response body to schema
345+
err = mapFields(userResp, &stateModel)
346+
if err != nil {
347+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Processing API payload: %v", err))
348+
return
349+
}
350+
351+
// Set state to fully populated data
352+
diags = resp.State.Set(ctx, stateModel)
353+
resp.Diagnostics.Append(diags...)
354+
if resp.Diagnostics.HasError() {
355+
return
356+
}
357+
tflog.Info(ctx, "Postgres Flex user updated")
299358
}
300359

301360
// Delete deletes the resource and removes the Terraform state on success.
@@ -457,3 +516,16 @@ func toCreatePayload(model *Model, roles []string) (*postgresflex.CreateUserPayl
457516
Username: conversion.StringValueToPointer(model.Username),
458517
}, nil
459518
}
519+
520+
func toUpdatePayload(model *Model, roles []string) (*postgresflex.UpdateUserPayload, error) {
521+
if model == nil {
522+
return nil, fmt.Errorf("nil model")
523+
}
524+
if roles == nil {
525+
return nil, fmt.Errorf("nil roles")
526+
}
527+
528+
return &postgresflex.UpdateUserPayload{
529+
Roles: &roles,
530+
}, nil
531+
}

stackit/internal/services/postgresflex/user/resource_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,86 @@ func TestToCreatePayload(t *testing.T) {
362362
})
363363
}
364364
}
365+
366+
func TestToUpdatePayload(t *testing.T) {
367+
tests := []struct {
368+
description string
369+
input *Model
370+
inputRoles []string
371+
expected *postgresflex.UpdateUserPayload
372+
isValid bool
373+
}{
374+
{
375+
"default_values",
376+
&Model{},
377+
[]string{},
378+
&postgresflex.UpdateUserPayload{
379+
Roles: &[]string{},
380+
},
381+
true,
382+
},
383+
{
384+
"default_values",
385+
&Model{
386+
Username: types.StringValue("username"),
387+
},
388+
[]string{
389+
"role_1",
390+
"role_2",
391+
},
392+
&postgresflex.UpdateUserPayload{
393+
Roles: &[]string{
394+
"role_1",
395+
"role_2",
396+
},
397+
},
398+
true,
399+
},
400+
{
401+
"null_fields_and_int_conversions",
402+
&Model{
403+
Username: types.StringNull(),
404+
},
405+
[]string{
406+
"",
407+
},
408+
&postgresflex.UpdateUserPayload{
409+
Roles: &[]string{
410+
"",
411+
},
412+
},
413+
true,
414+
},
415+
{
416+
"nil_model",
417+
nil,
418+
[]string{},
419+
nil,
420+
false,
421+
},
422+
{
423+
"nil_roles",
424+
&Model{},
425+
nil,
426+
nil,
427+
false,
428+
},
429+
}
430+
for _, tt := range tests {
431+
t.Run(tt.description, func(t *testing.T) {
432+
output, err := toUpdatePayload(tt.input, tt.inputRoles)
433+
if !tt.isValid && err == nil {
434+
t.Fatalf("Should have failed")
435+
}
436+
if tt.isValid && err != nil {
437+
t.Fatalf("Should not have failed: %v", err)
438+
}
439+
if tt.isValid {
440+
diff := cmp.Diff(output, tt.expected)
441+
if diff != "" {
442+
t.Fatalf("Data does not match: %s", diff)
443+
}
444+
}
445+
})
446+
}
447+
}

0 commit comments

Comments
 (0)