Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 87f6ed8

Browse files
authored
feat: add default_org_member_roles to coderd_organization (#355)
<!-- Authored by Coder Agents on behalf of @Emyrk. --> Refs [PLAT-217](https://linear.app/codercom/issue/PLAT-217/rfc-for-gateway-accounts), depends on coder/coder#25994. Adds a `default_org_member_roles` set attribute to `coderd_organization` so callers can override the deployment-wide default member roles per organization. - New attribute is Optional + Computed; omitting it leaves the deployment defaults in place. - Create flow follows the `workspace_sharing` precedent: post-create `UpdateOrganization` PATCH when the user provides a value, since `CreateOrganization` doesn't accept the field. - Update flow plumbs the value through the existing `UpdateOrganization` call as a `*[]string` so an unset attribute does not overwrite server state. - Bumps `coder/coder` to a SHA on the gateway-accounts stack and reacts to coder/coder#24984, which migrated `UpdateTemplateMeta` fields from values to optional pointers. <details><summary>Agent context</summary> - `internal/provider/organization_resource.go`: new field on the resource model and schema, plus a `defaultOrgMemberRolesValueFromAPI` helper that maps a nil slice to an empty set so the attribute always has a known value. - `internal/provider/organization_resource_test.go`: new acceptance step under `enableExperimentalSteps` that sets the field to `["organization-member", "organization-template-admin"]` and asserts state. The happy-path test now enables the `minimum-implicit-member` experiment alongside `workspace-sharing` since the server gates non-default values behind it. - `internal/provider/template_resource.go` and `internal/provider/template_data_source_test.go`: react to coder/coder#24984's pointer migration on `UpdateTemplateMeta`. Mechanical conversions: `.ValueString()` becomes `.ValueStringPointer()` etc., and `bool` literals get wrapped with `ptr.Ref(...)`. - `docs/resources/organization.md`: regenerated via `make gen`. </details> --- <sub>Coder Agents on behalf of @Emyrk.</sub>
1 parent f865ac1 commit 87f6ed8

3 files changed

Lines changed: 103 additions & 5 deletions

File tree

docs/resources/organization.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ resource "coderd_organization" "blueberry" {
5454

5555
### Optional
5656

57+
- `default_org_member_roles` (List of String) Built-in organization role names that are unioned into every member's effective roles. Changes propagate to members on their next request. Setting any value other than the deployment defaults requires the `minimum-implicit-member` experiment to be enabled on the Coder Deployment.
5758
- `description` (String)
5859
- `display_name` (String) Display name of the organization. Defaults to name.
5960
- `group_sync` (Block, Optional, Deprecated) Group sync settings to sync groups from an IdP.

internal/provider/organization_resource.go

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ type OrganizationResourceModel struct {
4545
Icon types.String `tfsdk:"icon"`
4646
WorkspaceSharing types.String `tfsdk:"workspace_sharing"`
4747

48+
DefaultOrgMemberRoles types.List `tfsdk:"default_org_member_roles"`
49+
4850
OrgSyncIdpGroups types.Set `tfsdk:"org_sync_idp_groups"`
4951
GroupSync types.Object `tfsdk:"group_sync"`
5052
RoleSync types.Object `tfsdk:"role_sync"`
@@ -149,6 +151,15 @@ This resource is only compatible with Coder version [2.16.0](https://github.com/
149151
},
150152
},
151153

154+
"default_org_member_roles": schema.ListAttribute{
155+
ElementType: types.StringType,
156+
MarkdownDescription: "Built-in organization role names that are unioned into every member's effective roles. " +
157+
"Changes propagate to members on their next request. Setting any value other than the deployment defaults " +
158+
"requires the `minimum-implicit-member` experiment to be enabled on the Coder Deployment.",
159+
Optional: true,
160+
Computed: true,
161+
},
162+
152163
"org_sync_idp_groups": schema.SetAttribute{
153164
ElementType: types.StringType,
154165
Optional: true,
@@ -352,6 +363,13 @@ func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadReques
352363
data.Icon = types.StringValue(org.Icon)
353364
data.WorkspaceSharing = workspaceSharing
354365

366+
defaultOrgMemberRoles, diags := defaultOrgMemberRolesValueFromAPI(ctx, org.DefaultOrgMemberRoles)
367+
resp.Diagnostics.Append(diags...)
368+
if resp.Diagnostics.HasError() {
369+
return
370+
}
371+
data.DefaultOrgMemberRoles = defaultOrgMemberRoles
372+
355373
// Save updated data into Terraform state
356374
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
357375
}
@@ -456,6 +474,26 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe
456474
}
457475
}
458476

477+
// Apply default_org_member_roles if the user specified them.
478+
if !data.DefaultOrgMemberRoles.IsNull() && !data.DefaultOrgMemberRoles.IsUnknown() {
479+
tflog.Trace(ctx, "updating default org member roles", map[string]any{
480+
"orgID": orgID,
481+
})
482+
483+
var roles []string
484+
resp.Diagnostics.Append(data.DefaultOrgMemberRoles.ElementsAs(ctx, &roles, false)...)
485+
if resp.Diagnostics.HasError() {
486+
return
487+
}
488+
org, err = r.Client.UpdateOrganization(ctx, orgID.String(), codersdk.UpdateOrganizationRequest{
489+
DefaultOrgMemberRoles: &roles,
490+
})
491+
if err != nil {
492+
resp.Diagnostics.AddError("Default Org Member Roles Update error", err.Error())
493+
return
494+
}
495+
}
496+
459497
// This is computed, we need to write a known value to the state
460498
// in any case.
461499
workspaceSharing, err := fetchWorkspaceSharingValue(ctx, r.Client, orgID.String())
@@ -466,6 +504,13 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe
466504
}
467505
data.WorkspaceSharing = workspaceSharing
468506

507+
defaultOrgMemberRoles, diags := defaultOrgMemberRolesValueFromAPI(ctx, org.DefaultOrgMemberRoles)
508+
resp.Diagnostics.Append(diags...)
509+
if resp.Diagnostics.HasError() {
510+
return
511+
}
512+
data.DefaultOrgMemberRoles = defaultOrgMemberRoles
513+
469514
// Save data into Terraform state
470515
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
471516
}
@@ -488,11 +533,23 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe
488533
"new_description": data.Description.ValueString(),
489534
"new_icon": data.Icon.ValueString(),
490535
})
536+
537+
var defaultRolesPtr *[]string
538+
if !data.DefaultOrgMemberRoles.IsNull() && !data.DefaultOrgMemberRoles.IsUnknown() {
539+
var roles []string
540+
resp.Diagnostics.Append(data.DefaultOrgMemberRoles.ElementsAs(ctx, &roles, false)...)
541+
if resp.Diagnostics.HasError() {
542+
return
543+
}
544+
defaultRolesPtr = &roles
545+
}
546+
491547
org, err := r.Client.UpdateOrganization(ctx, orgID.String(), codersdk.UpdateOrganizationRequest{
492-
Name: data.Name.ValueString(),
493-
DisplayName: data.DisplayName.ValueString(),
494-
Description: data.Description.ValueStringPointer(),
495-
Icon: data.Icon.ValueStringPointer(),
548+
Name: data.Name.ValueString(),
549+
DisplayName: data.DisplayName.ValueString(),
550+
Description: data.Description.ValueStringPointer(),
551+
Icon: data.Icon.ValueStringPointer(),
552+
DefaultOrgMemberRoles: defaultRolesPtr,
496553
})
497554
if err != nil {
498555
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update organization %s, got error: %s", orgID, err))
@@ -579,6 +636,13 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe
579636
}
580637
data.WorkspaceSharing = workspaceSharing
581638

639+
defaultOrgMemberRoles, diags := defaultOrgMemberRolesValueFromAPI(ctx, org.DefaultOrgMemberRoles)
640+
resp.Diagnostics.Append(diags...)
641+
if resp.Diagnostics.HasError() {
642+
return
643+
}
644+
data.DefaultOrgMemberRoles = defaultOrgMemberRoles
645+
582646
// Save updated data into Terraform state
583647
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
584648
}
@@ -790,3 +854,13 @@ func isWorkspaceSharingExperimentOff(err error) bool {
790854
}
791855
return false
792856
}
857+
858+
// defaultOrgMemberRolesValueFromAPI converts the API's []string into a
859+
// types.List[string]. A nil slice from an older server is treated as an
860+
// empty list so the attribute always has a known value.
861+
func defaultOrgMemberRolesValueFromAPI(ctx context.Context, roles []string) (types.List, diag.Diagnostics) {
862+
if roles == nil {
863+
roles = []string{}
864+
}
865+
return types.ListValueFrom(ctx, types.StringType, roles)
866+
}

internal/provider/organization_resource_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func TestAccOrganizationResource(t *testing.T) {
2525
}
2626

2727
ctx := t.Context()
28-
client := integration.StartCoder(ctx, t, "organization_acc", integration.UseLicense, integration.CoderExperiments("workspace-sharing"))
28+
client := integration.StartCoder(ctx, t, "organization_acc", integration.UseLicense, integration.CoderExperiments("workspace-sharing,minimum-implicit-member"))
2929
_, err := client.User(ctx, codersdk.Me)
3030
require.NoError(t, err)
3131
runOrganizationResourceTest(t, client, true)
@@ -165,6 +165,9 @@ func runOrganizationResourceTest(t *testing.T, client *codersdk.Client, enableEx
165165
cfg7 := cfg6
166166
cfg7.WorkspaceSharing = new("everyone")
167167

168+
cfg8 := cfg7
169+
cfg8.DefaultOrgMemberRoles = ptr.Ref([]string{"organization-template-admin", "organization-workspace-access"})
170+
168171
steps = append(steps,
169172
// Disable workspace sharing for org
170173
resource.TestStep{
@@ -180,6 +183,16 @@ func runOrganizationResourceTest(t *testing.T, client *codersdk.Client, enableEx
180183
statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("workspace_sharing"), knownvalue.StringExact("everyone")),
181184
},
182185
},
186+
// Set default_org_member_roles to a non-default value
187+
resource.TestStep{
188+
Config: cfg8.String(t),
189+
ConfigStateChecks: []statecheck.StateCheck{
190+
statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("default_org_member_roles"), knownvalue.ListExact([]knownvalue.Check{
191+
knownvalue.StringExact("organization-template-admin"),
192+
knownvalue.StringExact("organization-workspace-access"),
193+
})),
194+
},
195+
},
183196
)
184197
}
185198
return steps
@@ -221,6 +234,8 @@ type testAccOrganizationResourceConfig struct {
221234
Icon *string
222235
WorkspaceSharing *string
223236

237+
DefaultOrgMemberRoles *[]string
238+
224239
OrgSyncIdpGroups []string
225240
GroupSync *codersdk.GroupSyncSettings
226241
RoleSync *codersdk.RoleSyncSettings
@@ -241,6 +256,14 @@ resource "coderd_organization" "test" {
241256
icon = {{orNull .Icon}}
242257
workspace_sharing = {{orNull .WorkspaceSharing}}
243258
259+
{{- if .DefaultOrgMemberRoles}}
260+
default_org_member_roles = [
261+
{{- range $role := .DefaultOrgMemberRoles }}
262+
"{{$role}}",
263+
{{- end}}
264+
]
265+
{{- end}}
266+
244267
{{- if .OrgSyncIdpGroups}}
245268
org_sync_idp_groups = [
246269
{{- range $name := .OrgSyncIdpGroups }}

0 commit comments

Comments
 (0)