Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3327cab

Browse files
committedFeb 21, 2025·
Merge branch 'main' into lilac/parallel
2 parents 88ae71d + ed8270c commit 3327cab

File tree

6 files changed

+207
-23
lines changed

6 files changed

+207
-23
lines changed
 

‎docs/resources/organization.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,35 @@ An organization on the Coder deployment.
1515
~> **Warning**
1616
This resource is only compatible with Coder version [2.16.0](https://github.com/coder/coder/releases/tag/v2.16.0) and later.
1717

18+
## Example Usage
1819

20+
```terraform
21+
resource "coderd_organization" "blueberry" {
22+
name = "blueberry"
23+
display_name = "Blueberry"
24+
description = "The organization for blueberries"
25+
icon = "/emojis/1fad0.png"
26+
27+
org_sync_idp_groups = [
28+
"wibble",
29+
"wobble",
30+
]
31+
32+
group_sync {
33+
field = "coder_groups"
34+
mapping = {
35+
toast = [coderd_group.bread.id]
36+
}
37+
}
38+
39+
role_sync {
40+
field = "coder_roles"
41+
mapping = {
42+
manager = ["organization-user-admin"]
43+
}
44+
}
45+
}
46+
```
1947

2048
<!-- schema generated by tfplugindocs -->
2149
## Schema
@@ -30,6 +58,7 @@ This resource is only compatible with Coder version [2.16.0](https://github.com/
3058
- `display_name` (String) Display name of the organization. Defaults to name.
3159
- `group_sync` (Block, Optional) Group sync settings to sync groups from an IdP. (see [below for nested schema](#nestedblock--group_sync))
3260
- `icon` (String)
61+
- `org_sync_idp_groups` (Set of String) Claims from the IdP provider that will give users access to this organization.
3362
- `role_sync` (Block, Optional) Role sync settings to sync organization roles from an IdP. (see [below for nested schema](#nestedblock--role_sync))
3463

3564
### Read-Only
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
resource "coderd_organization" "blueberry" {
2+
name = "blueberry"
3+
display_name = "Blueberry"
4+
description = "The organization for blueberries"
5+
icon = "/emojis/1fad0.png"
6+
7+
org_sync_idp_groups = [
8+
"wibble",
9+
"wobble",
10+
]
11+
12+
group_sync {
13+
field = "coder_groups"
14+
mapping = {
15+
toast = [coderd_group.bread.id]
16+
}
17+
}
18+
19+
role_sync {
20+
field = "coder_roles"
21+
mapping = {
22+
manager = ["organization-user-admin"]
23+
}
24+
}
25+
}

‎internal/provider/organization_resource.go

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"regexp"
77

8+
"github.com/coder/coder/v2/coderd/util/slice"
89
"github.com/coder/coder/v2/codersdk"
910
"github.com/coder/terraform-provider-coderd/internal/codersdkvalidator"
1011
"github.com/google/uuid"
@@ -40,8 +41,9 @@ type OrganizationResourceModel struct {
4041
Description types.String `tfsdk:"description"`
4142
Icon types.String `tfsdk:"icon"`
4243

43-
GroupSync types.Object `tfsdk:"group_sync"`
44-
RoleSync types.Object `tfsdk:"role_sync"`
44+
OrgSyncIdpGroups types.Set `tfsdk:"org_sync_idp_groups"`
45+
GroupSync types.Object `tfsdk:"group_sync"`
46+
RoleSync types.Object `tfsdk:"role_sync"`
4547
}
4648

4749
type GroupSyncModel struct {
@@ -134,6 +136,12 @@ This resource is only compatible with Coder version [2.16.0](https://github.com/
134136
Computed: true,
135137
Default: stringdefault.StaticString(""),
136138
},
139+
140+
"org_sync_idp_groups": schema.SetAttribute{
141+
ElementType: types.StringType,
142+
Optional: true,
143+
MarkdownDescription: "Claims from the IdP provider that will give users access to this organization.",
144+
},
137145
},
138146

139147
Blocks: map[string]schema.Block{
@@ -361,21 +369,38 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe
361369
// default it.
362370
data.DisplayName = types.StringValue(org.DisplayName)
363371

364-
// Now apply group and role sync settings, if specified
365372
orgID := data.ID.ValueUUID()
366-
tflog.Trace(ctx, "updating group sync", map[string]any{
367-
"orgID": orgID,
368-
})
373+
374+
// Apply org sync patches, if specified
375+
if !data.OrgSyncIdpGroups.IsNull() {
376+
tflog.Trace(ctx, "updating org sync", map[string]any{
377+
"orgID": orgID,
378+
})
379+
380+
var claims []string
381+
resp.Diagnostics.Append(data.OrgSyncIdpGroups.ElementsAs(ctx, &claims, false)...)
382+
if resp.Diagnostics.HasError() {
383+
return
384+
}
385+
386+
resp.Diagnostics.Append(r.patchOrgSyncMapping(ctx, orgID, []string{}, claims)...)
387+
}
388+
389+
// Apply group and role sync settings, if specified
369390
if !data.GroupSync.IsNull() {
391+
tflog.Trace(ctx, "updating group sync", map[string]any{
392+
"orgID": orgID,
393+
})
394+
370395
resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data.GroupSync)...)
371396
if resp.Diagnostics.HasError() {
372397
return
373398
}
374399
}
375-
tflog.Trace(ctx, "updating role sync", map[string]any{
376-
"orgID": orgID,
377-
})
378400
if !data.RoleSync.IsNull() {
401+
tflog.Trace(ctx, "updating role sync", map[string]any{
402+
"orgID": orgID,
403+
})
379404
resp.Diagnostics.Append(r.patchRoleSync(ctx, orgID, data.RoleSync)...)
380405
if resp.Diagnostics.HasError() {
381406
return
@@ -423,19 +448,42 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe
423448
"icon": org.Icon,
424449
})
425450

426-
tflog.Trace(ctx, "updating group sync", map[string]any{
427-
"orgID": orgID,
428-
})
451+
// Apply org sync patches, if specified
452+
if !data.OrgSyncIdpGroups.IsNull() {
453+
tflog.Trace(ctx, "updating org sync mappings", map[string]any{
454+
"orgID": orgID,
455+
})
456+
457+
var state OrganizationResourceModel
458+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
459+
var currentClaims []string
460+
resp.Diagnostics.Append(state.OrgSyncIdpGroups.ElementsAs(ctx, &currentClaims, false)...)
461+
462+
var plannedClaims []string
463+
resp.Diagnostics.Append(data.OrgSyncIdpGroups.ElementsAs(ctx, &plannedClaims, false)...)
464+
if resp.Diagnostics.HasError() {
465+
return
466+
}
467+
468+
resp.Diagnostics.Append(r.patchOrgSyncMapping(ctx, orgID, currentClaims, plannedClaims)...)
469+
if resp.Diagnostics.HasError() {
470+
return
471+
}
472+
}
473+
429474
if !data.GroupSync.IsNull() {
475+
tflog.Trace(ctx, "updating group sync", map[string]any{
476+
"orgID": orgID,
477+
})
430478
resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data.GroupSync)...)
431479
if resp.Diagnostics.HasError() {
432480
return
433481
}
434482
}
435-
tflog.Trace(ctx, "updating role sync", map[string]any{
436-
"orgID": orgID,
437-
})
438483
if !data.RoleSync.IsNull() {
484+
tflog.Trace(ctx, "updating role sync", map[string]any{
485+
"orgID": orgID,
486+
})
439487
resp.Diagnostics.Append(r.patchRoleSync(ctx, orgID, data.RoleSync)...)
440488
if resp.Diagnostics.HasError() {
441489
return
@@ -456,6 +504,21 @@ func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRe
456504

457505
orgID := data.ID.ValueUUID()
458506

507+
// Remove org sync mappings, if we were managing them
508+
if !data.OrgSyncIdpGroups.IsNull() {
509+
tflog.Trace(ctx, "deleting org sync mappings", map[string]any{
510+
"orgID": orgID,
511+
})
512+
513+
var claims []string
514+
resp.Diagnostics.Append(data.OrgSyncIdpGroups.ElementsAs(ctx, &claims, false)...)
515+
if resp.Diagnostics.HasError() {
516+
return
517+
}
518+
519+
resp.Diagnostics.Append(r.patchOrgSyncMapping(ctx, orgID, claims, []string{})...)
520+
}
521+
459522
tflog.Trace(ctx, "deleting organization", map[string]any{
460523
"id": orgID,
461524
"name": data.Name.ValueString(),
@@ -554,3 +617,37 @@ func (r *OrganizationResource) patchRoleSync(
554617

555618
return diags
556619
}
620+
621+
func (r *OrganizationResource) patchOrgSyncMapping(
622+
ctx context.Context,
623+
orgID uuid.UUID,
624+
currentClaims, plannedClaims []string,
625+
) diag.Diagnostics {
626+
var diags diag.Diagnostics
627+
628+
add, remove := slice.SymmetricDifference(currentClaims, plannedClaims)
629+
var addMappings []codersdk.IDPSyncMapping[uuid.UUID]
630+
for _, claim := range add {
631+
addMappings = append(addMappings, codersdk.IDPSyncMapping[uuid.UUID]{
632+
Given: claim,
633+
Gets: orgID,
634+
})
635+
}
636+
var removeMappings []codersdk.IDPSyncMapping[uuid.UUID]
637+
for _, claim := range remove {
638+
removeMappings = append(removeMappings, codersdk.IDPSyncMapping[uuid.UUID]{
639+
Given: claim,
640+
Gets: orgID,
641+
})
642+
}
643+
644+
_, err := r.Client.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{
645+
Add: addMappings,
646+
Remove: removeMappings,
647+
})
648+
if err != nil {
649+
diags.AddError("Org Sync Update error", err.Error())
650+
}
651+
652+
return diags
653+
}

‎internal/provider/organization_resource_test.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,19 @@ func TestAccOrganizationResource(t *testing.T) {
4343
cfg2.DisplayName = ptr.Ref("Example Organization New")
4444

4545
cfg3 := cfg2
46-
cfg3.GroupSync = ptr.Ref(codersdk.GroupSyncSettings{
46+
cfg3.OrgSyncIdpGroups = []string{"wibble", "wobble"}
47+
48+
cfg4 := cfg3
49+
cfg4.OrgSyncIdpGroups = []string{"wibbley", "wobbley"}
50+
51+
cfg5 := cfg4
52+
cfg5.GroupSync = ptr.Ref(codersdk.GroupSyncSettings{
4753
Field: "wibble",
4854
Mapping: map[string][]uuid.UUID{
4955
"wibble": {uuid.MustParse("6e57187f-6543-46ab-a62c-a10065dd4314")},
5056
},
5157
})
52-
cfg3.RoleSync = ptr.Ref(codersdk.RoleSyncSettings{
58+
cfg5.RoleSync = ptr.Ref(codersdk.RoleSyncSettings{
5359
Field: "wobble",
5460
Mapping: map[string][]string{
5561
"wobble": {"wobbly"},
@@ -87,9 +93,25 @@ func TestAccOrganizationResource(t *testing.T) {
8793
statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("display_name"), knownvalue.StringExact("Example Organization New")),
8894
},
8995
},
90-
// Add group and role sync
96+
// Add org sync
9197
{
9298
Config: cfg3.String(t),
99+
ConfigStateChecks: []statecheck.StateCheck{
100+
statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(0), knownvalue.StringExact("wibble")),
101+
statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(1), knownvalue.StringExact("wobble")),
102+
},
103+
},
104+
// Patch org sync
105+
{
106+
Config: cfg4.String(t),
107+
ConfigStateChecks: []statecheck.StateCheck{
108+
statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(0), knownvalue.StringExact("wibbley")),
109+
statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(1), knownvalue.StringExact("wobbley")),
110+
},
111+
},
112+
// Add group and role sync
113+
{
114+
Config: cfg5.String(t),
93115
ConfigStateChecks: []statecheck.StateCheck{
94116
statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("group_sync").AtMapKey("field"), knownvalue.StringExact("wibble")),
95117
statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("group_sync").AtMapKey("mapping").AtMapKey("wibble").AtSliceIndex(0), knownvalue.StringExact("6e57187f-6543-46ab-a62c-a10065dd4314")),
@@ -111,8 +133,9 @@ type testAccOrganizationResourceConfig struct {
111133
Description *string
112134
Icon *string
113135

114-
GroupSync *codersdk.GroupSyncSettings
115-
RoleSync *codersdk.RoleSyncSettings
136+
OrgSyncIdpGroups []string
137+
GroupSync *codersdk.GroupSyncSettings
138+
RoleSync *codersdk.RoleSyncSettings
116139
}
117140

118141
func (c testAccOrganizationResourceConfig) String(t *testing.T) string {
@@ -129,6 +152,14 @@ resource "coderd_organization" "test" {
129152
description = {{orNull .Description}}
130153
icon = {{orNull .Icon}}
131154
155+
{{- if .OrgSyncIdpGroups}}
156+
org_sync_idp_groups = [
157+
{{- range $name := .OrgSyncIdpGroups }}
158+
"{{$name}}",
159+
{{- end}}
160+
]
161+
{{- end}}
162+
132163
{{- if .GroupSync}}
133164
group_sync {
134165
field = "{{.GroupSync.Field}}"

‎internal/provider/organization_sync_settings_resource.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func (r *OrganizationSyncSettingsResource) Delete(ctx context.Context, req resou
244244
tflog.Trace(ctx, "deleting organization sync", map[string]any{})
245245
_, err := r.Client.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{
246246
// This disables organization sync without causing state conflicts for
247-
// organization resources that might still specify `sync_mapping`.
247+
// organization resources that might still specify `org_sync_idp_groups`.
248248
Field: "",
249249
})
250250
if err != nil {

‎internal/provider/util.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@ func computeDirectoryHash(directory string) (string, error) {
8383
return hex.EncodeToString(hash.Sum(nil)), nil
8484
}
8585

86-
// memberDiff returns the members to add and remove from the group, given the current members and the planned members.
87-
// plannedMembers is deliberately our custom type, as Terraform cannot automatically produce `[]uuid.UUID` from a set.
86+
// memberDiff returns the members to add and remove from the group, given the
87+
// current members and the planned members. plannedMembers is deliberately our
88+
// custom type, as Terraform cannot automatically produce `[]uuid.UUID` from a
89+
// set.
8890
func memberDiff(currentMembers []uuid.UUID, plannedMembers []UUID) (add, remove []string) {
8991
curSet := make(map[uuid.UUID]struct{}, len(currentMembers))
9092
planSet := make(map[uuid.UUID]struct{}, len(plannedMembers))

0 commit comments

Comments
 (0)
Please sign in to comment.