Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix: support unmanaged roles on user resource#250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ethanndickson merged 5 commits intomainfromethan/user-unmanaged-roles
Aug 19, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletiondocs/resources/user.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,7 +56,7 @@ resource "coderd_user" "admin" {
- `login_type` (String) Type of login for the user. Valid types are `none`, `password`, `github`, and `oidc`.
- `name` (String) Display name of the user. Defaults to username.
- `password` (String, Sensitive) Password for the user. Required when `login_type` is `password`. Passwords are saved into the state as plain text and should only be used for testing purposes.
- `roles` (Set of String) Roles assigned to the user. Valid roles are `owner`, `template-admin`, `user-admin`, and `auditor`.
- `roles` (Set of String) 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 is an OIDC user and role sync is configured
- `suspended` (Boolean) Whether the user is suspended.

### Read-Only
Expand Down
2 changes: 1 addition & 1 deletionintegration/integration.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ func StartCoder(ctx context.Context, t *testing.T, name string, useLicense bool)
t.Logf("not ready yet: %s", err.Error())
}
return err == nil
},20*time.Second, time.Second, "coder failed to become ready in time")
},30*time.Second, time.Second, "coder failed to become ready in time")
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
Email: testEmail,
Username: testUsername,
Expand Down
81 changes: 45 additions & 36 deletionsinternal/provider/user_resource.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
Expand DownExpand Up@@ -88,16 +87,14 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r
Required: true,
},
"roles": schema.SetAttribute{
MarkdownDescription: "Roles assigned to the user. Valid roles are `owner`, `template-admin`, `user-admin`, and `auditor`.",
Computed: true,
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 is an OIDC user and role sync is configured",
Optional: true,
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.ValueStringsAre(
stringvalidator.OneOf("owner", "template-admin", "user-admin", "auditor"),
),
},
Default: setdefault.StaticValue(types.SetValueMust(types.StringType, []attr.Value{})),
},
"login_type": schema.StringAttribute{
MarkdownDescription: "Type of login for the user. Valid types are `none`, `password`, `github`, and `oidc`.",
Expand DownExpand Up@@ -209,21 +206,26 @@ func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, r
tflog.Info(ctx, "successfully updated user profile")
data.Name = types.StringValue(user.Name)

var roles []string
resp.Diagnostics.Append(
data.Roles.ElementsAs(ctx, &roles, false)...,
)
tflog.Info(ctx, "updating user roles", map[string]any{
"new_roles": roles,
})
user, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
Roles: roles,
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update newly created user roles, got error: %s", err))
return
if !data.Roles.IsNull() {
var roles []string
resp.Diagnostics.Append(
data.Roles.ElementsAs(ctx, &roles, false)...,
)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "updating user roles", map[string]any{
"new_roles": roles,
})
user, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
Roles: roles,
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update newly created user roles, got error: %s", err))
return
}
tflog.Info(ctx, "successfully updated user roles")
}
tflog.Info(ctx, "successfully updated user roles")

if data.Suspended.ValueBool() {
_, err = client.UpdateUserStatus(ctx, data.ID.ValueString(), codersdk.UserStatus("suspended"))
Expand DownExpand Up@@ -267,11 +269,13 @@ func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp
data.Email = types.StringValue(user.Email)
data.Name = types.StringValue(user.Name)
data.Username = types.StringValue(user.Username)
roles := make([]attr.Value, 0, len(user.Roles))
for _, role := range user.Roles {
roles = append(roles, types.StringValue(role.Name))
if !data.Roles.IsNull() {
roles := make([]attr.Value, 0, len(user.Roles))
for _, role := range user.Roles {
roles = append(roles, types.StringValue(role.Name))
}
data.Roles = types.SetValueMust(types.StringType, roles)
}
data.Roles = types.SetValueMust(types.StringType, roles)
data.LoginType = types.StringValue(string(user.LoginType))
data.Suspended = types.BoolValue(user.Status == codersdk.UserStatusSuspended)

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

var roles []string
resp.Diagnostics.Append(
data.Roles.ElementsAs(ctx, &roles, false)...,
)
tflog.Info(ctx, "updating user roles", map[string]any{
"new_roles": roles,
})
_, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
Roles: roles,
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update user roles, got error: %s", err))
return
if !data.Roles.IsNull() {
var roles []string
resp.Diagnostics.Append(
data.Roles.ElementsAs(ctx, &roles, false)...,
)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "updating user roles", map[string]any{
"new_roles": roles,
})
_, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
Roles: roles,
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update user roles, got error: %s", err))
return
}
tflog.Info(ctx, "successfully updated user roles")
}
tflog.Info(ctx, "successfully updated user roles")

if data.LoginType.ValueString() == string(codersdk.LoginTypePassword) && !data.Password.IsNull() {
tflog.Info(ctx, "updating password")
Expand Down
41 changes: 39 additions & 2 deletionsinternal/provider/user_resource_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,9 @@ func TestAccUserResource(t *testing.T) {
cfg4.LoginType = ptr.Ref("github")
cfg4.Password = nil

cfg5 := cfg4
cfg5.Roles = nil

resource.Test(t, resource.TestCase{
IsUnitTest: true,
PreCheck: func() { testAccPreCheck(t) },
Expand All@@ -68,7 +71,7 @@ func TestAccUserResource(t *testing.T) {
ImportState: true,
ImportStateVerify: true,
// We can't pull the password from the API.
ImportStateVerifyIgnore: []string{"password"},
ImportStateVerifyIgnore: []string{"password", "roles"},
},
// ImportState by username
{
Expand All@@ -77,7 +80,7 @@ func TestAccUserResource(t *testing.T) {
ImportStateVerify: true,
ImportStateId: "example",
// We can't pull the password from the API.
ImportStateVerifyIgnore: []string{"password"},
ImportStateVerifyIgnore: []string{"password", "roles"},
Copy link
MemberAuthor

@ethanndicksonethanndicksonAug 18, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Because we're usingnull to signify thatroles shouldn't be managed by Terraform, we have the same problem we have with thegroup resource: we don't know whether or not thenull value was set in the config, or is null because it's an import. Frustratingly, the provider framework doesn't provide a way to differentiate the two.

},
// Update and Read testing
{
Expand DownExpand Up@@ -114,8 +117,42 @@ func TestAccUserResource(t *testing.T) {
// The Plan should be to create the entire resource
ExpectNonEmptyPlan: true,
},
// Unmanaged roles
{
Config: cfg5.String(t),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckNoResourceAttr("coderd_user.test", "roles"),
),
},
},
})

t.Run("CreateUnmanagedRolesOk", func(t *testing.T) {
cfg := testAccUserResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
Username: ptr.Ref("unmanaged"),
Name: ptr.Ref("Unmanaged User"),
Email: ptr.Ref("unmanaged@coder.com"),
Roles: nil, // Start with unmanaged roles
LoginType: ptr.Ref("password"),
Password: ptr.Ref("SomeSecurePassword!"),
}

resource.Test(t, resource.TestCase{
IsUnitTest: true,
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: cfg.String(t),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckNoResourceAttr("coderd_user.test", "roles"),
),
},
},
})
})
}

type testAccUserResourceConfig struct {
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp