diff --git a/docs/resources/organization.md b/docs/resources/organization.md index 53969bf..2848cbe 100644 --- a/docs/resources/organization.md +++ b/docs/resources/organization.md @@ -56,7 +56,9 @@ resource "coderd_organization" "blueberry" { - `description` (String) - `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)) +- `group_sync` (Block, Optional, Deprecated) Group sync settings to sync groups from an IdP. + +~> **Deprecated** This block is deprecated. Use the `coderd_organization_group_sync` resource instead. (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)) diff --git a/docs/resources/organization_group_sync.md b/docs/resources/organization_group_sync.md new file mode 100644 index 0000000..c372633 --- /dev/null +++ b/docs/resources/organization_group_sync.md @@ -0,0 +1,68 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coderd_organization_group_sync Resource - terraform-provider-coderd" +subcategory: "" +description: |- + Group sync settings for an organization on the Coder deployment. + Multiple instances of this resource for a single organization will conflict. + ~> Warning + This resource is only compatible with Coder version 2.16.0 https://github.com/coder/coder/releases/tag/v2.16.0 and later. +--- + +# coderd_organization_group_sync (Resource) + +Group sync settings for an organization on the Coder deployment. +Multiple instances of this resource for a single organization will conflict. + +~> **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_group_sync" "test" { + organization_id = coderd_organization.test.id + field = "groups" + regex_filter = "test_.*|admin_.*" + auto_create_missing = false + + mapping = { + "test_developers" = [coderd_group.test.id] + "admin_users" = [coderd_group.admins.id] + "mixed_group" = [coderd_group.test.id, coderd_group.admins.id] + } +} +``` + + +## Schema + +### Required + +- `field` (String) The claim field that specifies what groups a user should be in. +- `mapping` (Map of List of String) A map from OIDC group name to Coder group ID. +- `organization_id` (String) The ID of the organization to configure group sync for. + +### Optional + +- `auto_create_missing` (Boolean) Controls whether groups will be created if they are missing. Defaults to false. +- `regex_filter` (String) A regular expression that will be used to filter the groups returned by the OIDC provider. Any group not matched will be ignored. + +## Import + +Import is supported using the following syntax: + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + +```shell +# The ID supplied must be an organization UUID +$ terraform import coderd_organization_group_sync.main_group_sync +``` +Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used: + +```terraform +import { + to = coderd_organization_group_sync.main_group_sync + id = "" +} +``` diff --git a/examples/resources/coderd_organization_group_sync/import.sh b/examples/resources/coderd_organization_group_sync/import.sh new file mode 100644 index 0000000..e6fa126 --- /dev/null +++ b/examples/resources/coderd_organization_group_sync/import.sh @@ -0,0 +1,10 @@ +# The ID supplied must be an organization UUID +$ terraform import coderd_organization_group_sync.main_group_sync +``` +Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used: + +```terraform +import { + to = coderd_organization_group_sync.main_group_sync + id = "" +} diff --git a/examples/resources/coderd_organization_group_sync/resource.tf b/examples/resources/coderd_organization_group_sync/resource.tf new file mode 100644 index 0000000..acce998 --- /dev/null +++ b/examples/resources/coderd_organization_group_sync/resource.tf @@ -0,0 +1,12 @@ +resource "coderd_organization_group_sync" "test" { + organization_id = coderd_organization.test.id + field = "groups" + regex_filter = "test_.*|admin_.*" + auto_create_missing = false + + mapping = { + "test_developers" = [coderd_group.test.id] + "admin_users" = [coderd_group.admins.id] + "mixed_group" = [coderd_group.test.id, coderd_group.admins.id] + } +} diff --git a/integration/integration_test.go b/integration/integration_test.go index 79834a8..ad2f212 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -106,6 +106,45 @@ func TestIntegration(t *testing.T) { assert.Equal(t, group.QuotaAllowance, 100) }, }, + { + name: "org-group-sync-test", + preF: func(t testing.TB, c *codersdk.Client) {}, + assertF: func(t testing.TB, c *codersdk.Client) { + org, err := c.OrganizationByName(ctx, "test-org-group-sync") + assert.NoError(t, err) + assert.Equal(t, "test-org-group-sync", org.Name) + assert.Equal(t, "Test Organization for Group Sync", org.DisplayName) + + testGroup, err := c.GroupByOrgAndName(ctx, org.ID, "test-group") + assert.NoError(t, err) + assert.Equal(t, "test-group", testGroup.Name) + assert.Equal(t, "Test Group", testGroup.DisplayName) + assert.Equal(t, 50, testGroup.QuotaAllowance) + + adminGroup, err := c.GroupByOrgAndName(ctx, org.ID, "admin-group") + assert.NoError(t, err) + assert.Equal(t, "admin-group", adminGroup.Name) + assert.Equal(t, "Admin Group", adminGroup.DisplayName) + assert.Equal(t, 100, adminGroup.QuotaAllowance) + + // Verify group sync settings + groupSync, err := c.GroupIDPSyncSettings(ctx, org.ID.String()) + assert.NoError(t, err) + assert.Equal(t, "groups", groupSync.Field) + assert.NotNil(t, groupSync.RegexFilter) + assert.Equal(t, "test_.*|admin_.*", groupSync.RegexFilter.String()) + assert.False(t, groupSync.AutoCreateMissing) + + assert.Contains(t, groupSync.Mapping, "test_developers") + assert.Contains(t, groupSync.Mapping, "admin_users") + assert.Contains(t, groupSync.Mapping, "mixed_group") + + assert.Contains(t, groupSync.Mapping["test_developers"], testGroup.ID) + assert.Contains(t, groupSync.Mapping["admin_users"], adminGroup.ID) + assert.Contains(t, groupSync.Mapping["mixed_group"], testGroup.ID) + assert.Contains(t, groupSync.Mapping["mixed_group"], adminGroup.ID) + }, + }, { name: "template-test", preF: func(t testing.TB, c *codersdk.Client) {}, diff --git a/integration/org-group-sync-test/main.tf b/integration/org-group-sync-test/main.tf new file mode 100644 index 0000000..8fc4a92 --- /dev/null +++ b/integration/org-group-sync-test/main.tf @@ -0,0 +1,45 @@ +terraform { + required_providers { + coderd = { + source = "coder/coderd" + version = ">=0.0.0" + } + } +} + +resource "coderd_organization" "test" { + name = "test-org-group-sync" + display_name = "Test Organization for Group Sync" + description = "Organization created for testing group sync functionality" +} + +resource "coderd_group" "test" { + organization_id = coderd_organization.test.id + name = "test-group" + display_name = "Test Group" + quota_allowance = 50 +} + +resource "coderd_group" "admins" { + organization_id = coderd_organization.test.id + name = "admin-group" + display_name = "Admin Group" + quota_allowance = 100 +} + +resource "coderd_organization_group_sync" "test" { + organization_id = coderd_organization.test.id + field = "groups" + regex_filter = "test_.*|admin_.*" + auto_create_missing = false + + mapping = { + "test_developers" = [coderd_group.test.id] + "admin_users" = [coderd_group.admins.id] + "mixed_group" = [coderd_group.test.id, coderd_group.admins.id] + } +} + +data "coderd_organization" "test_data" { + id = coderd_organization.test.id +} diff --git a/internal/codersdkvalidator/regex.go b/internal/codersdkvalidator/regex.go index 0077616..7ad2120 100644 --- a/internal/codersdkvalidator/regex.go +++ b/internal/codersdkvalidator/regex.go @@ -7,7 +7,7 @@ import ( ) func checkRegexp(it string) error { - _, err := regexp.Compile("") + _, err := regexp.Compile(it) return err } diff --git a/internal/provider/organization_group_sync_resource.go b/internal/provider/organization_group_sync_resource.go new file mode 100644 index 0000000..9bb16ed --- /dev/null +++ b/internal/provider/organization_group_sync_resource.go @@ -0,0 +1,284 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + + "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "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/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &OrganizationGroupSyncResource{} +var _ resource.ResourceWithImportState = &OrganizationGroupSyncResource{} + +type OrganizationGroupSyncResource struct { + *CoderdProviderData +} + +// OrganizationGroupSyncResourceModel describes the resource data model. +type OrganizationGroupSyncResourceModel struct { + OrganizationID UUID `tfsdk:"organization_id"` + Field types.String `tfsdk:"field"` + RegexFilter types.String `tfsdk:"regex_filter"` + AutoCreateMissing types.Bool `tfsdk:"auto_create_missing"` + Mapping types.Map `tfsdk:"mapping"` +} + +func NewOrganizationGroupSyncResource() resource.Resource { + return &OrganizationGroupSyncResource{} +} + +func (r *OrganizationGroupSyncResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_organization_group_sync" +} + +func (r *OrganizationGroupSyncResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `Group sync settings for an organization on the Coder deployment. +Multiple instances of this resource for a single organization will conflict. + +~> **Warning** +This resource is only compatible with Coder version [2.16.0](https://github.com/coder/coder/releases/tag/v2.16.0) and later. +`, + Attributes: map[string]schema.Attribute{ + "organization_id": schema.StringAttribute{ + CustomType: UUIDType, + Required: true, + MarkdownDescription: "The ID of the organization to configure group sync for.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "field": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The claim field that specifies what groups a user should be in.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "regex_filter": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "A regular expression that will be used to filter the groups " + + "returned by the OIDC provider. Any group not matched will be ignored.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + codersdkvalidator.Regexp(), + }, + }, + "auto_create_missing": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Controls whether groups will be created if they are missing. Defaults to false.", + }, + "mapping": schema.MapAttribute{ + ElementType: types.ListType{ElemType: UUIDType}, + Required: true, + MarkdownDescription: "A map from OIDC group name to Coder group ID.", + }, + }, + } +} + +func (r *OrganizationGroupSyncResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + data, ok := req.ProviderData.(*CoderdProviderData) + + if !ok { + resp.Diagnostics.AddError( + "Unable to configure provider data", + fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.CoderdProviderData = data +} + +func (r *OrganizationGroupSyncResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform prior state data into the model + var data OrganizationGroupSyncResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + orgID := data.OrganizationID.ValueUUID() + + groupSync, err := r.Client.GroupIDPSyncSettings(ctx, orgID.String()) + if err != nil { + if isNotFound(err) { + resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Organization with ID %q not found. Marking resource as deleted.", orgID.String())) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization group sync settings, got error: %s", err)) + return + } + + data.Field = types.StringValue(groupSync.Field) + + if groupSync.RegexFilter != nil { + data.RegexFilter = types.StringValue(groupSync.RegexFilter.String()) + } else { + data.RegexFilter = types.StringNull() + } + + data.AutoCreateMissing = types.BoolValue(groupSync.AutoCreateMissing) + + elements := make(map[string][]string) + for key, ids := range groupSync.Mapping { + for _, id := range ids { + elements[key] = append(elements[key], id.String()) + } + } + + mapping, diags := types.MapValueFrom(ctx, types.ListType{ElemType: UUIDType}, elements) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.Mapping = mapping + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *OrganizationGroupSyncResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Read Terraform plan data into the model + var data OrganizationGroupSyncResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + orgID := data.OrganizationID.ValueUUID() + + tflog.Trace(ctx, "creating organization group sync", map[string]any{ + "organization_id": orgID, + "field": data.Field.ValueString(), + }) + + // Apply group sync settings + resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *OrganizationGroupSyncResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Read Terraform plan data into the model + var data OrganizationGroupSyncResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + orgID := data.OrganizationID.ValueUUID() + + tflog.Trace(ctx, "updating organization group sync", map[string]any{ + "organization_id": orgID, + "field": data.Field.ValueString(), + }) + + resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data)...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *OrganizationGroupSyncResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Read Terraform prior state data into the model + var data OrganizationGroupSyncResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + orgID := data.OrganizationID.ValueUUID() + + tflog.Trace(ctx, "deleting organization group sync", map[string]any{ + "organization_id": orgID, + }) + + // Sending all zero-values will delete the group sync configuration + _, err := r.Client.PatchGroupIDPSyncSettings(ctx, orgID.String(), codersdk.GroupSyncSettings{}) + if err != nil { + if isNotFound(err) { + // Organization doesn't exist, so group sync is already "deleted" + return + } + resp.Diagnostics.AddError("Group Sync Delete error", err.Error()) + return + } +} + +func (r *OrganizationGroupSyncResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Import using organization ID + resource.ImportStatePassthroughID(ctx, path.Root("organization_id"), req, resp) +} + +func (r *OrganizationGroupSyncResource) patchGroupSync( + ctx context.Context, + orgID uuid.UUID, + data OrganizationGroupSyncResourceModel, +) diag.Diagnostics { + var diags diag.Diagnostics + + groupSync := codersdk.GroupSyncSettings{ + Field: data.Field.ValueString(), + AutoCreateMissing: data.AutoCreateMissing.ValueBool(), + Mapping: make(map[string][]uuid.UUID), + } + + if !data.RegexFilter.IsNull() { + groupSync.RegexFilter = regexp.MustCompile(data.RegexFilter.ValueString()) + } + + // Mapping is required, so always process it (can be empty) + // Terraform doesn't know how to turn one our `UUID` Terraform values into a + // `uuid.UUID`, so we have to do the unwrapping manually here. + var mapping map[string][]UUID + diags.Append(data.Mapping.ElementsAs(ctx, &mapping, false)...) + if diags.HasError() { + return diags + } + for key, ids := range mapping { + for _, id := range ids { + groupSync.Mapping[key] = append(groupSync.Mapping[key], id.ValueUUID()) + } + } + + // Perform the PATCH + _, err := r.Client.PatchGroupIDPSyncSettings(ctx, orgID.String(), groupSync) + if err != nil { + diags.AddError("Group Sync Update error", err.Error()) + return diags + } + + return diags +} diff --git a/internal/provider/organization_group_sync_resource_test.go b/internal/provider/organization_group_sync_resource_test.go new file mode 100644 index 0000000..bd0a88c --- /dev/null +++ b/internal/provider/organization_group_sync_resource_test.go @@ -0,0 +1,196 @@ +package provider + +import ( + "os" + "regexp" + "strings" + "testing" + "text/template" + + "github.com/coder/coder/v2/coderd/util/ptr" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/integration" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/stretchr/testify/require" +) + +func TestAccOrganizationGroupSyncResource(t *testing.T) { + t.Parallel() + if os.Getenv("TF_ACC") == "" { + t.Skip("Acceptance tests are disabled.") + } + + ctx := t.Context() + client := integration.StartCoder(ctx, t, "organization_group_sync_acc", true) + _, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + // Create an organization first + org, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "test-org", + DisplayName: "Test Organization", + }) + require.NoError(t, err) + + cfg1 := testAccOrganizationGroupSyncResourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + OrganizationID: org.ID.String(), + Field: "groups", + Mapping: map[string][]string{}, // Empty mapping + } + + cfg2 := cfg1 + cfg2.Field = "updated_groups" + cfg2.RegexFilter = ptr.Ref(".*test.*") + cfg2.AutoCreateMissing = ptr.Ref(true) + cfg2.Mapping = map[string][]string{ + "test_group": {"6e57187f-6543-46ab-a62c-a10065dd4314"}, + } + + cfg3 := cfg2 + cfg3.Mapping = map[string][]string{ + "new_group": {"6e57187f-6543-46ab-a62c-a10065dd4314"}, + } + + t.Run("CreateImportUpdateReadOk", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read + { + Config: cfg1.String(t), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_organization_group_sync.test", tfjsonpath.New("organization_id"), knownvalue.StringExact(org.ID.String())), + statecheck.ExpectKnownValue("coderd_organization_group_sync.test", tfjsonpath.New("field"), knownvalue.StringExact("groups")), + }, + }, + // Import + { + Config: cfg1.String(t), + ResourceName: "coderd_organization_group_sync.test", + ImportState: true, + ImportStateVerify: true, + ImportStateId: org.ID.String(), + ImportStateVerifyIdentifierAttribute: "organization_id", + }, + // Update and Read + { + Config: cfg2.String(t), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_organization_group_sync.test", tfjsonpath.New("field"), knownvalue.StringExact("updated_groups")), + statecheck.ExpectKnownValue("coderd_organization_group_sync.test", tfjsonpath.New("regex_filter"), knownvalue.StringExact(".*test.*")), + statecheck.ExpectKnownValue("coderd_organization_group_sync.test", tfjsonpath.New("auto_create_missing"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("coderd_organization_group_sync.test", tfjsonpath.New("mapping").AtMapKey("test_group").AtSliceIndex(0), knownvalue.StringExact("6e57187f-6543-46ab-a62c-a10065dd4314")), + }, + }, + // Update mapping + { + Config: cfg3.String(t), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_organization_group_sync.test", tfjsonpath.New("mapping").AtMapKey("new_group").AtSliceIndex(0), knownvalue.StringExact("6e57187f-6543-46ab-a62c-a10065dd4314")), + }, + }, + }, + }) + }) + + t.Run("MinimalConfig", func(t *testing.T) { + minimalCfg := testAccOrganizationGroupSyncResourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + OrganizationID: org.ID.String(), + Field: "minimal", + Mapping: map[string][]string{}, // Empty mapping + } + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: minimalCfg.String(t), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_organization_group_sync.test", tfjsonpath.New("field"), knownvalue.StringExact("minimal")), + statecheck.ExpectKnownValue("coderd_organization_group_sync.test", tfjsonpath.New("organization_id"), knownvalue.StringExact(org.ID.String())), + }, + }, + }, + }) + }) + + t.Run("InvalidRegexFilter", func(t *testing.T) { + invalidRegexCfg := testAccOrganizationGroupSyncResourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + OrganizationID: org.ID.String(), + Field: "invalid_regex", + RegexFilter: ptr.Ref("[invalid"), + Mapping: map[string][]string{}, + } + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: invalidRegexCfg.String(t), + ExpectError: regexp.MustCompile("error parsing regexp"), + }, + }, + }) + }) +} + +type testAccOrganizationGroupSyncResourceConfig struct { + URL string + Token string + OrganizationID string + + Field string + RegexFilter *string + AutoCreateMissing *bool + Mapping map[string][]string +} + +func (c testAccOrganizationGroupSyncResourceConfig) String(t *testing.T) string { + t.Helper() + tpl := ` +provider coderd { + url = "{{.URL}}" + token = "{{.Token}}" +} + +resource "coderd_organization_group_sync" "test" { + organization_id = "{{.OrganizationID}}" + field = {{printf "%q" .Field}} + {{- if .RegexFilter}} + regex_filter = {{orNull .RegexFilter}} + {{- end}} + {{- if .AutoCreateMissing}} + auto_create_missing = {{.AutoCreateMissing}} + {{- end}} + mapping = { + {{- range $key, $value := .Mapping}} + {{$key}} = {{printf "%q" $value}} + {{- end}} + } +} +` + funcMap := template.FuncMap{ + "orNull": PrintOrNull, + } + + buf := strings.Builder{} + tmpl, err := template.New("organizationGroupSyncResource").Funcs(funcMap).Parse(tpl) + require.NoError(t, err) + + err = tmpl.Execute(&buf, c) + require.NoError(t, err) + return buf.String() +} diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index 8fbaba5..c1a40ae 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -145,7 +145,10 @@ This resource is only compatible with Coder version [2.16.0](https://github.com/ Blocks: map[string]schema.Block{ "group_sync": schema.SingleNestedBlock{ - MarkdownDescription: `Group sync settings to sync groups from an IdP.`, + MarkdownDescription: `Group sync settings to sync groups from an IdP. + +~> **Deprecated** This block is deprecated. Use the ` + "`coderd_organization_group_sync`" + ` resource instead.`, + DeprecationMessage: "Use the coderd_organization_group_sync resource instead.", Attributes: map[string]schema.Attribute{ "field": schema.StringAttribute{ Optional: true, diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8c65385..6e7f796 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -151,6 +151,7 @@ func (p *CoderdProvider) Resources(ctx context.Context) []func() resource.Resour NewOrganizationResource, NewProvisionerKeyResource, NewOrganizationSyncSettingsResource, + NewOrganizationGroupSyncResource, } } 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