diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 3fa570e..79d248d 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -248,14 +248,15 @@ func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp client := r.data.Client + // Lookup by ID to handle imports user, err := client.User(ctx, data.ID.ValueString()) if err != nil { if isNotFound(err) { - resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("User with ID %q not found. Marking as deleted.", data.ID.ValueString())) + resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("User with ID %q not found. Marking resource as deleted.", data.ID.ValueString())) resp.State.RemoveResource(ctx) return } - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user, got error: %s", err)) + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user by ID, got error: %s", err)) return } if len(user.OrganizationIDs) < 1 { @@ -274,6 +275,30 @@ func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp data.LoginType = types.StringValue(string(user.LoginType)) data.Suspended = types.BoolValue(user.Status == codersdk.UserStatusSuspended) + // The user-by-ID API returns deleted users if the authorized user has + // permission. It does not indicate whether the user is deleted or not. + // The user-by-username API will never return deleted users. + // So, we do another lookup by username. + userByName, err := client.User(ctx, data.Username.ValueString()) + if err != nil { + if isNotFound(err) { + resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf( + "User with username %q not found. Marking resource as deleted.", + data.Username.ValueString())) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user by username, got error: %s", err)) + return + } + if userByName.ID != data.ID.ValueUUID() { + resp.Diagnostics.AddWarning("Client Error", fmt.Sprintf( + "The username %q has been reassigned to a new user not managed by this Terraform resource. Marking resource as deleted.", + user.Username)) + resp.State.RemoveResource(ctx) + return + } + // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/provider/user_resource_test.go b/internal/provider/user_resource_test.go index d717bfa..8c78acc 100644 --- a/internal/provider/user_resource_test.go +++ b/internal/provider/user_resource_test.go @@ -9,6 +9,7 @@ import ( "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/terraform-provider-coderd/integration" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stretchr/testify/require" ) @@ -100,6 +101,19 @@ func TestAccUserResource(t *testing.T) { resource.TestCheckResourceAttr("coderd_user.test", "login_type", "github"), ), }, + // Verify config drift via deletion is handled + { + Config: cfg4.String(t), + Check: func(*terraform.State) error { + user, err := client.User(ctx, "exampleNew") + if err != nil { + return err + } + return client.DeleteUser(ctx, user.ID) + }, + // The Plan should be to create the entire resource + ExpectNonEmptyPlan: true, + }, }, }) } diff --git a/internal/provider/util.go b/internal/provider/util.go index e409738..3f35a25 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "github.com/coder/coder/v2/codersdk" "github.com/google/uuid" @@ -110,5 +111,15 @@ func memberDiff(currentMembers []uuid.UUID, plannedMembers []UUID) (add, remove func isNotFound(err error) bool { var sdkErr *codersdk.Error - return errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound + if !errors.As(err, &sdkErr) { + return false + } + if sdkErr.StatusCode() == http.StatusNotFound { + return true + } + // `httpmw/ExtractUserContext` returns a 400 w/ this message if the user is not found + if sdkErr.StatusCode() == http.StatusBadRequest && strings.Contains(sdkErr.Message, "must be an existing uuid or username") { + return true + } + return false }
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: