diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 9bc8018..907e203 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -41,13 +41,14 @@ type UserResource struct { type UserResourceModel struct { ID types.String `tfsdk:"id"` - Username types.String `tfsdk:"username"` - Name types.String `tfsdk:"name"` - Email types.String `tfsdk:"email"` - Roles types.Set `tfsdk:"roles"` // owner, template-admin, user-admin, auditor (member is implicit) - LoginType types.String `tfsdk:"login_type"` // none, password, github, oidc - Password types.String `tfsdk:"password"` // only when login_type is password - Suspended types.Bool `tfsdk:"suspended"` + Username types.String `tfsdk:"username"` + Name types.String `tfsdk:"name"` + Email types.String `tfsdk:"email"` + Roles types.Set `tfsdk:"roles"` // owner, template-admin, user-admin, auditor (member is implicit) + LoginType types.String `tfsdk:"login_type"` // none, password, github, oidc + Password types.String `tfsdk:"password"` // only when login_type is password + Suspended types.Bool `tfsdk:"suspended"` + CascadeDelete types.Bool `tfsdk:"cascade_delete"` } func (r *UserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -79,8 +80,11 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r // Defaulted in Create }, "email": schema.StringAttribute{ - MarkdownDescription: "Email address of the user.", + MarkdownDescription: "Email address of the user. Modifying this field will trigger a resource replacement.", Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "roles": schema.SetAttribute{ MarkdownDescription: "Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'.", @@ -118,6 +122,13 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r Optional: true, Default: booldefault.StaticBool(false), }, + "cascade_delete": schema.BoolAttribute{ + Computed: true, + MarkdownDescription: "Whether to delete owned workspaces when this resource is deleted or replaced.", + Required: false, + Optional: true, + Default: booldefault.StaticBool(false), + }, }, } } @@ -363,6 +374,29 @@ func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, r resp.Diagnostics.AddError("Data Error", fmt.Sprintf("Unable to parse user ID, got error: %s", err)) return } + + if data.CascadeDelete.ValueBool() { + tflog.Trace(ctx, "deleting user workspaces") + workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ + Owner: data.Username.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get user workspaces, got error: %s", err)) + return + } + for _, workspace := range workspaces.Workspaces { + _, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionDelete, + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete user workspace, got error: %s", err)) + return + } + } + //TODO: Wait for builds to finish + tflog.Trace(ctx, "successfully deleted user workspaces") + } + tflog.Trace(ctx, "deleting user") err = client.DeleteUser(ctx, id) if err != nil { diff --git a/internal/provider/user_resource_test.go b/internal/provider/user_resource_test.go index f955310..40a91ae 100644 --- a/internal/provider/user_resource_test.go +++ b/internal/provider/user_resource_test.go @@ -10,6 +10,7 @@ import ( "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" ) @@ -35,6 +36,10 @@ func TestAccUserResource(t *testing.T) { cfg2.Username = PtrTo("exampleNew") cfg2.Name = PtrTo("Example User New") + cfg3 := cfg1 + cfg3.Email = PtrTo("example2@coder.com") + + var userId string resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, @@ -68,6 +73,15 @@ func TestAccUserResource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("coderd_user.test", "username", "exampleNew"), resource.TestCheckResourceAttr("coderd_user.test", "name", "Example User New"), + testAccIdChanged("coderd_user.test", &userId), + ), + }, + // Replace testing + { + Config: cfg3.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("coderd_user.test", "email", "example2@coder.com"), + testAccIdChanged("coderd_user.test", &userId), ), }, // Delete testing automatically occurs in TestCase @@ -151,3 +165,24 @@ resource "coderd_user" "test" { return buf.String() } + +// Check if the id has changed since the last time this check function was run. +func testAccIdChanged(resourceName string, id *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Resource %s not found", resourceName) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + if *id == "" { + *id = rs.Primary.ID + return nil + } + if rs.Primary.ID == *id { + return fmt.Errorf("ID did not change from %s", rs.Primary.ID) + } + return nil + } +}