diff --git a/docs/resources/organization.md b/docs/resources/organization.md index 6dbc5d1..88246dd 100644 --- a/docs/resources/organization.md +++ b/docs/resources/organization.md @@ -15,7 +15,35 @@ An organization on the Coder deployment. ~> **Warning** This resource is only compatible with Coder version [2.16.0](https://github.com/coder/coder/releases/tag/v2.16.0) and later. +## Example Usage +```terraform +resource "coderd_organization" "blueberry" { + name = "blueberry" + display_name = "Blueberry" + description = "The organization for blueberries" + icon = "/emojis/1fad0.png" + + org_sync_idp_groups = [ + "wibble", + "wobble", + ] + + group_sync { + field = "coder_groups" + mapping = { + toast = [coderd_group.bread.id] + } + } + + role_sync { + field = "coder_roles" + mapping = { + manager = ["organization-user-admin"] + } + } +} +``` ## Schema @@ -30,6 +58,7 @@ This resource is only compatible with Coder version [2.16.0](https://github.com/ - `display_name` (String) Display name of the organization. Defaults to name. - `group_sync` (Block, Optional) Group sync settings to sync groups from an IdP. (see [below for nested schema](#nestedblock--group_sync)) - `icon` (String) +- `org_sync_idp_groups` (Set of String) Claims from the IdP provider that will give users access to this organization. - `role_sync` (Block, Optional) Role sync settings to sync organization roles from an IdP. (see [below for nested schema](#nestedblock--role_sync)) ### Read-Only diff --git a/examples/resources/coderd_organization/resource.tf b/examples/resources/coderd_organization/resource.tf new file mode 100644 index 0000000..cb26a86 --- /dev/null +++ b/examples/resources/coderd_organization/resource.tf @@ -0,0 +1,25 @@ +resource "coderd_organization" "blueberry" { + name = "blueberry" + display_name = "Blueberry" + description = "The organization for blueberries" + icon = "/emojis/1fad0.png" + + org_sync_idp_groups = [ + "wibble", + "wobble", + ] + + group_sync { + field = "coder_groups" + mapping = { + toast = [coderd_group.bread.id] + } + } + + role_sync { + field = "coder_roles" + mapping = { + manager = ["organization-user-admin"] + } + } +} diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index 2c39f0c..a83ff25 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -5,6 +5,7 @@ import ( "fmt" "regexp" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator" "github.com/google/uuid" @@ -40,8 +41,9 @@ type OrganizationResourceModel struct { Description types.String `tfsdk:"description"` Icon types.String `tfsdk:"icon"` - GroupSync types.Object `tfsdk:"group_sync"` - RoleSync types.Object `tfsdk:"role_sync"` + OrgSyncIdpGroups types.Set `tfsdk:"org_sync_idp_groups"` + GroupSync types.Object `tfsdk:"group_sync"` + RoleSync types.Object `tfsdk:"role_sync"` } type GroupSyncModel struct { @@ -134,6 +136,12 @@ This resource is only compatible with Coder version [2.16.0](https://github.com/ Computed: true, Default: stringdefault.StaticString(""), }, + + "org_sync_idp_groups": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Claims from the IdP provider that will give users access to this organization.", + }, }, Blocks: map[string]schema.Block{ @@ -361,21 +369,38 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe // default it. data.DisplayName = types.StringValue(org.DisplayName) - // Now apply group and role sync settings, if specified orgID := data.ID.ValueUUID() - tflog.Trace(ctx, "updating group sync", map[string]any{ - "orgID": orgID, - }) + + // Apply org sync patches, if specified + if !data.OrgSyncIdpGroups.IsNull() { + tflog.Trace(ctx, "updating org sync", map[string]any{ + "orgID": orgID, + }) + + var claims []string + resp.Diagnostics.Append(data.OrgSyncIdpGroups.ElementsAs(ctx, &claims, false)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(r.patchOrgSyncMapping(ctx, orgID, []string{}, claims)...) + } + + // Apply group and role sync settings, if specified if !data.GroupSync.IsNull() { + tflog.Trace(ctx, "updating group sync", map[string]any{ + "orgID": orgID, + }) + resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data.GroupSync)...) if resp.Diagnostics.HasError() { return } } - tflog.Trace(ctx, "updating role sync", map[string]any{ - "orgID": orgID, - }) if !data.RoleSync.IsNull() { + tflog.Trace(ctx, "updating role sync", map[string]any{ + "orgID": orgID, + }) resp.Diagnostics.Append(r.patchRoleSync(ctx, orgID, data.RoleSync)...) if resp.Diagnostics.HasError() { return @@ -423,19 +448,42 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe "icon": org.Icon, }) - tflog.Trace(ctx, "updating group sync", map[string]any{ - "orgID": orgID, - }) + // Apply org sync patches, if specified + if !data.OrgSyncIdpGroups.IsNull() { + tflog.Trace(ctx, "updating org sync mappings", map[string]any{ + "orgID": orgID, + }) + + var state OrganizationResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + var currentClaims []string + resp.Diagnostics.Append(state.OrgSyncIdpGroups.ElementsAs(ctx, ¤tClaims, false)...) + + var plannedClaims []string + resp.Diagnostics.Append(data.OrgSyncIdpGroups.ElementsAs(ctx, &plannedClaims, false)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(r.patchOrgSyncMapping(ctx, orgID, currentClaims, plannedClaims)...) + if resp.Diagnostics.HasError() { + return + } + } + if !data.GroupSync.IsNull() { + tflog.Trace(ctx, "updating group sync", map[string]any{ + "orgID": orgID, + }) resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data.GroupSync)...) if resp.Diagnostics.HasError() { return } } - tflog.Trace(ctx, "updating role sync", map[string]any{ - "orgID": orgID, - }) if !data.RoleSync.IsNull() { + tflog.Trace(ctx, "updating role sync", map[string]any{ + "orgID": orgID, + }) resp.Diagnostics.Append(r.patchRoleSync(ctx, orgID, data.RoleSync)...) if resp.Diagnostics.HasError() { return @@ -456,6 +504,21 @@ func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRe orgID := data.ID.ValueUUID() + // Remove org sync mappings, if we were managing them + if !data.OrgSyncIdpGroups.IsNull() { + tflog.Trace(ctx, "deleting org sync mappings", map[string]any{ + "orgID": orgID, + }) + + var claims []string + resp.Diagnostics.Append(data.OrgSyncIdpGroups.ElementsAs(ctx, &claims, false)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(r.patchOrgSyncMapping(ctx, orgID, claims, []string{})...) + } + tflog.Trace(ctx, "deleting organization", map[string]any{ "id": orgID, "name": data.Name.ValueString(), @@ -554,3 +617,37 @@ func (r *OrganizationResource) patchRoleSync( return diags } + +func (r *OrganizationResource) patchOrgSyncMapping( + ctx context.Context, + orgID uuid.UUID, + currentClaims, plannedClaims []string, +) diag.Diagnostics { + var diags diag.Diagnostics + + add, remove := slice.SymmetricDifference(currentClaims, plannedClaims) + var addMappings []codersdk.IDPSyncMapping[uuid.UUID] + for _, claim := range add { + addMappings = append(addMappings, codersdk.IDPSyncMapping[uuid.UUID]{ + Given: claim, + Gets: orgID, + }) + } + var removeMappings []codersdk.IDPSyncMapping[uuid.UUID] + for _, claim := range remove { + removeMappings = append(removeMappings, codersdk.IDPSyncMapping[uuid.UUID]{ + Given: claim, + Gets: orgID, + }) + } + + _, err := r.Client.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{ + Add: addMappings, + Remove: removeMappings, + }) + if err != nil { + diags.AddError("Org Sync Update error", err.Error()) + } + + return diags +} diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index 0a755c4..40a7465 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -42,13 +42,19 @@ func TestAccOrganizationResource(t *testing.T) { cfg2.DisplayName = ptr.Ref("Example Organization New") cfg3 := cfg2 - cfg3.GroupSync = ptr.Ref(codersdk.GroupSyncSettings{ + cfg3.OrgSyncIdpGroups = []string{"wibble", "wobble"} + + cfg4 := cfg3 + cfg4.OrgSyncIdpGroups = []string{"wibbley", "wobbley"} + + cfg5 := cfg4 + cfg5.GroupSync = ptr.Ref(codersdk.GroupSyncSettings{ Field: "wibble", Mapping: map[string][]uuid.UUID{ "wibble": {uuid.MustParse("6e57187f-6543-46ab-a62c-a10065dd4314")}, }, }) - cfg3.RoleSync = ptr.Ref(codersdk.RoleSyncSettings{ + cfg5.RoleSync = ptr.Ref(codersdk.RoleSyncSettings{ Field: "wobble", Mapping: map[string][]string{ "wobble": {"wobbly"}, @@ -86,9 +92,25 @@ func TestAccOrganizationResource(t *testing.T) { statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("display_name"), knownvalue.StringExact("Example Organization New")), }, }, - // Add group and role sync + // Add org sync { Config: cfg3.String(t), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(0), knownvalue.StringExact("wibble")), + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(1), knownvalue.StringExact("wobble")), + }, + }, + // Patch org sync + { + Config: cfg4.String(t), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(0), knownvalue.StringExact("wibbley")), + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(1), knownvalue.StringExact("wobbley")), + }, + }, + // Add group and role sync + { + Config: cfg5.String(t), ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("group_sync").AtMapKey("field"), knownvalue.StringExact("wibble")), statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("group_sync").AtMapKey("mapping").AtMapKey("wibble").AtSliceIndex(0), knownvalue.StringExact("6e57187f-6543-46ab-a62c-a10065dd4314")), @@ -110,8 +132,9 @@ type testAccOrganizationResourceConfig struct { Description *string Icon *string - GroupSync *codersdk.GroupSyncSettings - RoleSync *codersdk.RoleSyncSettings + OrgSyncIdpGroups []string + GroupSync *codersdk.GroupSyncSettings + RoleSync *codersdk.RoleSyncSettings } func (c testAccOrganizationResourceConfig) String(t *testing.T) string { @@ -128,6 +151,14 @@ resource "coderd_organization" "test" { description = {{orNull .Description}} icon = {{orNull .Icon}} + {{- if .OrgSyncIdpGroups}} + org_sync_idp_groups = [ + {{- range $name := .OrgSyncIdpGroups }} + "{{$name}}", + {{- end}} + ] + {{- end}} + {{- if .GroupSync}} group_sync { field = "{{.GroupSync.Field}}" diff --git a/internal/provider/organization_sync_settings_resource.go b/internal/provider/organization_sync_settings_resource.go index 0283c40..d492d3d 100644 --- a/internal/provider/organization_sync_settings_resource.go +++ b/internal/provider/organization_sync_settings_resource.go @@ -244,7 +244,7 @@ func (r *OrganizationSyncSettingsResource) Delete(ctx context.Context, req resou tflog.Trace(ctx, "deleting organization sync", map[string]any{}) _, err := r.Client.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{ // This disables organization sync without causing state conflicts for - // organization resources that might still specify `sync_mapping`. + // organization resources that might still specify `org_sync_idp_groups`. Field: "", }) if err != nil { diff --git a/internal/provider/util.go b/internal/provider/util.go index 169286f..e409738 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -83,8 +83,10 @@ func computeDirectoryHash(directory string) (string, error) { return hex.EncodeToString(hash.Sum(nil)), nil } -// memberDiff returns the members to add and remove from the group, given the current members and the planned members. -// plannedMembers is deliberately our custom type, as Terraform cannot automatically produce `[]uuid.UUID` from a set. +// memberDiff returns the members to add and remove from the group, given the +// current members and the planned members. plannedMembers is deliberately our +// custom type, as Terraform cannot automatically produce `[]uuid.UUID` from a +// set. func memberDiff(currentMembers []uuid.UUID, plannedMembers []UUID) (add, remove []string) { curSet := make(map[uuid.UUID]struct{}, len(currentMembers)) planSet := make(map[uuid.UUID]struct{}, len(plannedMembers)) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy