Skip to content

Commit e123f70

Browse files
committed
fix: support unmanaged roles on user resource
1 parent e5680f3 commit e123f70

File tree

2 files changed

+82
-35
lines changed

2 files changed

+82
-35
lines changed

internal/provider/user_resource.go

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1515
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
1616
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
17-
"github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault"
1817
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
1918
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
2019
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
@@ -88,7 +87,7 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r
8887
Required: true,
8988
},
9089
"roles": schema.SetAttribute{
91-
MarkdownDescription: "Roles assigned to the user. Valid roles are `owner`, `template-admin`, `user-admin`, and `auditor`.",
90+
MarkdownDescription: "Roles assigned to the user. Valid roles are `owner`, `template-admin`, `user-admin`, and `auditor`. If `null`, roles will not be managed by Terraform. This attribute must be null if the user was created via OIDC and uses role sync.",
9291
Computed: true,
9392
Optional: true,
9493
ElementType: types.StringType,
@@ -97,7 +96,6 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r
9796
stringvalidator.OneOf("owner", "template-admin", "user-admin", "auditor"),
9897
),
9998
},
100-
Default: setdefault.StaticValue(types.SetValueMust(types.StringType, []attr.Value{})),
10199
},
102100
"login_type": schema.StringAttribute{
103101
MarkdownDescription: "Type of login for the user. Valid types are `none`, `password`, `github`, and `oidc`.",
@@ -209,21 +207,26 @@ func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, r
209207
tflog.Info(ctx, "successfully updated user profile")
210208
data.Name = types.StringValue(user.Name)
211209

212-
var roles []string
213-
resp.Diagnostics.Append(
214-
data.Roles.ElementsAs(ctx, &roles, false)...,
215-
)
216-
tflog.Info(ctx, "updating user roles", map[string]any{
217-
"new_roles": roles,
218-
})
219-
user, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
220-
Roles: roles,
221-
})
222-
if err != nil {
223-
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update newly created user roles, got error: %s", err))
224-
return
210+
if !data.Roles.IsNull() {
211+
var roles []string
212+
resp.Diagnostics.Append(
213+
data.Roles.ElementsAs(ctx, &roles, false)...,
214+
)
215+
if resp.Diagnostics.HasError() {
216+
return
217+
}
218+
tflog.Info(ctx, "updating user roles", map[string]any{
219+
"new_roles": roles,
220+
})
221+
user, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
222+
Roles: roles,
223+
})
224+
if err != nil {
225+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update newly created user roles, got error: %s", err))
226+
return
227+
}
228+
tflog.Info(ctx, "successfully updated user roles")
225229
}
226-
tflog.Info(ctx, "successfully updated user roles")
227230

228231
if data.Suspended.ValueBool() {
229232
_, err = client.UpdateUserStatus(ctx, data.ID.ValueString(), codersdk.UserStatus("suspended"))
@@ -267,11 +270,13 @@ func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp
267270
data.Email = types.StringValue(user.Email)
268271
data.Name = types.StringValue(user.Name)
269272
data.Username = types.StringValue(user.Username)
270-
roles := make([]attr.Value, 0, len(user.Roles))
271-
for _, role := range user.Roles {
272-
roles = append(roles, types.StringValue(role.Name))
273+
if !data.Roles.IsNull() {
274+
roles := make([]attr.Value, 0, len(user.Roles))
275+
for _, role := range user.Roles {
276+
roles = append(roles, types.StringValue(role.Name))
277+
}
278+
data.Roles = types.SetValueMust(types.StringType, roles)
273279
}
274-
data.Roles = types.SetValueMust(types.StringType, roles)
275280
data.LoginType = types.StringValue(string(user.LoginType))
276281
data.Suspended = types.BoolValue(user.Status == codersdk.UserStatusSuspended)
277282

@@ -344,21 +349,26 @@ func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, r
344349
data.Name = name
345350
tflog.Info(ctx, "successfully updated user profile")
346351

347-
var roles []string
348-
resp.Diagnostics.Append(
349-
data.Roles.ElementsAs(ctx, &roles, false)...,
350-
)
351-
tflog.Info(ctx, "updating user roles", map[string]any{
352-
"new_roles": roles,
353-
})
354-
_, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
355-
Roles: roles,
356-
})
357-
if err != nil {
358-
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update user roles, got error: %s", err))
359-
return
352+
if !data.Roles.IsNull() {
353+
var roles []string
354+
resp.Diagnostics.Append(
355+
data.Roles.ElementsAs(ctx, &roles, false)...,
356+
)
357+
if resp.Diagnostics.HasError() {
358+
return
359+
}
360+
tflog.Info(ctx, "updating user roles", map[string]any{
361+
"new_roles": roles,
362+
})
363+
_, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
364+
Roles: roles,
365+
})
366+
if err != nil {
367+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update user roles, got error: %s", err))
368+
return
369+
}
370+
tflog.Info(ctx, "successfully updated user roles")
360371
}
361-
tflog.Info(ctx, "successfully updated user roles")
362372

363373
if data.LoginType.ValueString() == string(codersdk.LoginTypePassword) && !data.Password.IsNull() {
364374
tflog.Info(ctx, "updating password")

internal/provider/user_resource_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ func TestAccUserResource(t *testing.T) {
4242
cfg4.LoginType = ptr.Ref("github")
4343
cfg4.Password = nil
4444

45+
cfg5 := cfg4
46+
cfg5.Roles = nil
47+
4548
resource.Test(t, resource.TestCase{
4649
IsUnitTest: true,
4750
PreCheck: func() { testAccPreCheck(t) },
@@ -114,8 +117,42 @@ func TestAccUserResource(t *testing.T) {
114117
// The Plan should be to create the entire resource
115118
ExpectNonEmptyPlan: true,
116119
},
120+
// Unmanaged roles
121+
{
122+
Config: cfg5.String(t),
123+
Check: resource.ComposeAggregateTestCheckFunc(
124+
resource.TestCheckNoResourceAttr("coderd_user.test", "roles"),
125+
),
126+
},
117127
},
118128
})
129+
130+
t.Run("CreateUnmanagedRolesOk", func(t *testing.T) {
131+
cfg := testAccUserResourceConfig{
132+
URL: client.URL.String(),
133+
Token: client.SessionToken(),
134+
Username: ptr.Ref("unmanaged"),
135+
Name: ptr.Ref("Unmanaged User"),
136+
Email: ptr.Ref("[email protected]"),
137+
Roles: nil, // Start with unmanaged roles
138+
LoginType: ptr.Ref("password"),
139+
Password: ptr.Ref("SomeSecurePassword!"),
140+
}
141+
142+
resource.Test(t, resource.TestCase{
143+
IsUnitTest: true,
144+
PreCheck: func() { testAccPreCheck(t) },
145+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
146+
Steps: []resource.TestStep{
147+
{
148+
Config: cfg.String(t),
149+
Check: resource.ComposeAggregateTestCheckFunc(
150+
resource.TestCheckNoResourceAttr("coderd_user.test", "roles"),
151+
),
152+
},
153+
},
154+
})
155+
})
119156
}
120157

121158
type testAccUserResourceConfig struct {

0 commit comments

Comments
 (0)