diff --git a/docs/resources/postgresflex_user.md b/docs/resources/postgresflex_user.md index 6d5a8890b..1789bbd16 100644 --- a/docs/resources/postgresflex_user.md +++ b/docs/resources/postgresflex_user.md @@ -72,4 +72,13 @@ import { to = stackit_postgresflex_user.import-example id = "${var.project_id},${var.region},${var.postgres_instance_id},${var.user_id}" } + +# Only use the import statement, if you want to import an existing postgresflex user +# usually the imported user will not have a password after importing +# to be able to reference the imported users password, add reset as fifth parameter +# (this will reset the password of the imported user) +import { + to = stackit_postgresflex_user.import-example + id = "${var.project_id},${var.region},${var.postgres_instance_id},${var.user_id},reset" +} ``` diff --git a/examples/resources/stackit_postgresflex_user/import-by-string-id.tf b/examples/resources/stackit_postgresflex_user/import-by-string-id.tf index d9e62c961..ee2dd8ee6 100644 --- a/examples/resources/stackit_postgresflex_user/import-by-string-id.tf +++ b/examples/resources/stackit_postgresflex_user/import-by-string-id.tf @@ -3,3 +3,12 @@ import { to = stackit_postgresflex_user.import-example id = "${var.project_id},${var.region},${var.postgres_instance_id},${var.user_id}" } + +# Only use the import statement, if you want to import an existing postgresflex user +# usually the imported user will not have a password after importing +# to be able to reference the imported users password, add reset as fifth parameter +# (this will reset the password of the imported user) +import { + to = stackit_postgresflex_user.import-example + id = "${var.project_id},${var.region},${var.postgres_instance_id},${var.user_id},reset" +} diff --git a/stackit/internal/services/postgresflex/postgresflex_acc_test.go b/stackit/internal/services/postgresflex/postgresflex_acc_test.go index a5922ec4b..5d9ff0daa 100644 --- a/stackit/internal/services/postgresflex/postgresflex_acc_test.go +++ b/stackit/internal/services/postgresflex/postgresflex_acc_test.go @@ -140,11 +140,12 @@ var testConfigDatabaseVarsMinUpdated = func() config.Variables { // User - MIN var testConfigUserVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "acl": config.StringVariable("192.168.0.0/24"), - "backup_schedule": config.StringVariable("0 16 * * *"), - "flavor_id": config.StringVariable("4.8-replica"), + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), + "acl": config.StringVariable("192.168.0.0/24"), + "backup_schedule": config.StringVariable("0 16 * * *"), + // "flavor_id": config.StringVariable("4.8-replica"), + "flavor_id": config.StringVariable("2.4"), "storage_class": config.StringVariable("premium-perf2-stackit"), "storage_size": config.IntegerVariable(5), "instance_version": config.StringVariable("16"), @@ -722,6 +723,7 @@ func TestAccPostgresFlexDatabaseMin(t *testing.T) { } func TestAccPostgresFlexUserMin(t *testing.T) { + var initialPassword string resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testCheckDestroy, @@ -832,6 +834,52 @@ func TestAccPostgresFlexUserMin(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{"password", "uri"}, }, + // Import with password reset + { + ConfigVariables: testConfigUserVarsMin, + ResourceName: "stackit_postgresflex_user.user", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_postgresflex_user.user"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_postgresflex_user.user") + } + + projectId, ok := r.Primary.Attributes["project_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute project_id") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + instanceId, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instance_id") + } + userId, ok := r.Primary.Attributes["user_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute user_id") + } + initialPassword = r.Primary.Attributes["password"] + return fmt.Sprintf("%s,%s,%s,%s,reset", projectId, region, instanceId, userId), nil + }, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password", "uri"}, + ImportStateCheck: func(s []*terraform.InstanceState) error { + if len(s) != 1 { + return fmt.Errorf("expected 1 state, got %d", len(s)) + } + newPassword, ok := s[0].Attributes["password"] + if !ok || newPassword == "" { + return fmt.Errorf("expected password to be set in imported state") + } + if initialPassword != "" && newPassword == initialPassword { + return fmt.Errorf("expected password to have changed after reset, but got same password") + } + return nil + }, + }, // Update { ConfigVariables: testConfigUserVarsMinUpdated, diff --git a/stackit/internal/services/postgresflex/user/resource.go b/stackit/internal/services/postgresflex/user/resource.go index 7db66929b..e206f570c 100644 --- a/stackit/internal/services/postgresflex/user/resource.go +++ b/stackit/internal/services/postgresflex/user/resource.go @@ -554,7 +554,7 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r _, err = wait.DeleteUserWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId, userId).WaitWithContext(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Instance deletion waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("User deletion waiting: %v", err)) return } tflog.Info(ctx, "Postgres Flex user deleted") @@ -564,6 +564,24 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r // The expected format of the resource import identifier is: project_id,zone_id,record_set_id func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) == 4 { + r.defaultImportState(ctx, req, resp, idParts) + return + } + + if len(idParts) == 5 { + r.withPasswordResetImportState(ctx, req, resp, idParts) + return + } + + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing user", + fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[user_id] - got %q", req.ID), + ) +} + +func (r *userResource) defaultImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse, idParts []string) { if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing user", @@ -577,12 +595,67 @@ func (r *userResource) ImportState(ctx context.Context, req resource.ImportState resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), idParts[3])...) core.LogAndAddWarning(ctx, &resp.Diagnostics, - "Postgresflex user imported with empty password and empty uri", + "Postgresflex user imported with empty password and uri", "The user password and uri are not imported as they are only available upon creation of a new user. The password and uri fields will be empty.", ) tflog.Info(ctx, "Postgresflex user state imported") } +func (r *userResource) withPasswordResetImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse, idParts []string) { + if len(idParts) != 5 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" || idParts[4] != "reset" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing user", + fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[user_id],reset, got %q", req.ID), + ) + return + } + + userId, err := strconv.ParseInt(idParts[3], 10, 64) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing user", + fmt.Sprintf("Could not convert value %q to int64", idParts[3]), + ) + return + } + + resPwResp, err := r.client.DefaultAPI.ResetUserPassword(ctx, idParts[0], idParts[1], idParts[2], userId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing user", + fmt.Sprintf("Could not convert value %q to int64", idParts[3]), + ) + return + } + + if resPwResp == nil { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing user", + "Api response is nil", + ) + return + } + + pw, ok := resPwResp.GetPasswordOk() + if !ok { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing user", + fmt.Sprintf("Returned password is not ok, got %q", *pw), + ) + } + + 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("user_id"), idParts[3])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("password"), *pw)...) + core.LogAndAddWarning(ctx, &resp.Diagnostics, + "Postgresflex user imported with empty uri", + "The user uri is not imported as it is only available upon creation of a new user. The uri field will be empty.", + ) + tflog.Info(ctx, "Postgresflex user state imported") +} + func mapFieldsCreate(userResp *postgresflex.CreateUserResponse, getUserResp *postgresflex.GetUserResponse, instanceResp *postgresflex.GetInstanceResponse, model *Model, region string) error { if userResp == nil { return fmt.Errorf("create response is nil")