Skip to content

feat: add coderd_organization_sync_settings resource #173

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 12 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 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- run: go build -v .

- name: Run linters
uses: golangci/golangci-lint-action@e60da84bfae8c7920a47be973d75e15710aa8bd7 # v6.3.0
uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0
with:
version: latest

Expand Down
7 changes: 3 additions & 4 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
# Visit https://golangci-lint.run/ for usage documentation and information on
# other useful linters
issues:
max-per-linter: 0
max-issues-per-linter: 0
max-same-issues: 0

linters:
disable-all: true
enable:
- durationcheck
- errcheck
- exportloopref
- forcetypeassert
- godot
- gofmt
- gosimple
- govet
- ineffassign
- makezero
- misspell
- nilerr
- predeclared
- staticcheck
- tenv
- unconvert
- unparam
- unused
- vet
- usetesting
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ build: terraform-provider-coderd
terraform-provider-coderd: internal/provider/*.go main.go
CGO_ENABLED=0 go build .

test: testacc
.PHONY: test

# Run acceptance tests
.PHONY: testacc
testacc:
TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m
TF_ACC=1 go test ./... -v $(TESTARGS) -count 1 -timeout 120m
.PHONY: testacc
33 changes: 33 additions & 0 deletions docs/resources/organization_sync_settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "coderd_organization_sync_settings Resource - terraform-provider-coderd"
subcategory: ""
description: |-
IdP sync settings for organizations.
This resource can only be created once. Attempts to create multiple will fail.
~> Warning
This resource is only compatible with Coder version 2.19.0 https://github.com/coder/coder/releases/tag/v2.19.0 and later.
---

# coderd_organization_sync_settings (Resource)

IdP sync settings for organizations.

This resource can only be created once. Attempts to create multiple will fail.

~> **Warning**
This resource is only compatible with Coder version [2.19.0](https://github.com/coder/coder/releases/tag/v2.19.0) and later.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `assign_default` (Boolean) When true, every user will be added to the default organization, regardless of claims.
- `field` (String) The claim field that specifies what organizations a user should be in.

### Optional

- `mapping` (Map of List of String) A map from OIDC group name to Coder organization ID.
265 changes: 265 additions & 0 deletions internal/provider/organization_sync_settings_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package provider

import (
"context"
"fmt"

"github.com/coder/coder/v2/codersdk"
"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/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 = &OrganizationSyncSettingsResource{}
var _ resource.ResourceWithImportState = &OrganizationSyncSettingsResource{}

type OrganizationSyncSettingsResource struct {
*CoderdProviderData
}

// OrganizationSyncSettingsResourceModel describes the resource data model.
type OrganizationSyncSettingsResourceModel struct {
Field types.String `tfsdk:"field"`
AssignDefault types.Bool `tfsdk:"assign_default"`
Mapping types.Map `tfsdk:"mapping"`
}

func NewOrganizationSyncSettingsResource() resource.Resource {
return &OrganizationSyncSettingsResource{}
}

func (r *OrganizationSyncSettingsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_organization_sync_settings"
}

func (r *OrganizationSyncSettingsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: `IdP sync settings for organizations.

This resource can only be created once. Attempts to create multiple will fail.

~> **Warning**
This resource is only compatible with Coder version [2.19.0](https://github.com/coder/coder/releases/tag/v2.19.0) and later.
`,
Attributes: map[string]schema.Attribute{
"field": schema.StringAttribute{
Required: true,
MarkdownDescription: "The claim field that specifies what organizations " +
"a user should be in.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"assign_default": schema.BoolAttribute{
Required: true,
MarkdownDescription: "When true, every user will be added to the default " +
"organization, regardless of claims.",
},
"mapping": schema.MapAttribute{
ElementType: types.ListType{ElemType: UUIDType},
Optional: true,
MarkdownDescription: "A map from OIDC group name to Coder organization ID.",
},
},
}
}

func (r *OrganizationSyncSettingsResource) 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 *OrganizationSyncSettingsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Read Terraform prior state data into the model
var data OrganizationSyncSettingsResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

settings, err := r.Client.OrganizationIDPSyncSettings(ctx)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to get organization sync settings, got error: %s", err))
return
}

// Store the latest values that we just fetched.
data.Field = types.StringValue(settings.Field)
data.AssignDefault = types.BoolValue(settings.AssignDefault)

if !data.Mapping.IsNull() {
// Convert IDs to strings
elements := make(map[string][]string)
for key, ids := range settings.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
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *OrganizationSyncSettingsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Read Terraform plan data into the model
var data OrganizationSyncSettingsResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

tflog.Trace(ctx, "creating organization sync", map[string]any{
"field": data.Field.ValueString(),
"assign_default": data.AssignDefault.ValueBool(),
})

// Create and Update use a shared implementation
resp.Diagnostics.Append(r.patch(ctx, data)...)
if resp.Diagnostics.HasError() {
return
}

tflog.Trace(ctx, "successfully created organization sync", map[string]any{
"field": data.Field.ValueString(),
"assign_default": data.AssignDefault.ValueBool(),
})

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *OrganizationSyncSettingsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Read Terraform plan data into the model
var data OrganizationSyncSettingsResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

// Update the organization metadata
tflog.Trace(ctx, "updating organization", map[string]any{
"field": data.Field.ValueString(),
"assign_default": data.AssignDefault.ValueBool(),
})

// Create and Update use a shared implementation
resp.Diagnostics.Append(r.patch(ctx, data)...)
if resp.Diagnostics.HasError() {
return
}

tflog.Trace(ctx, "successfully updated organization", map[string]any{
"field": data.Field.ValueString(),
"assign_default": data.AssignDefault.ValueBool(),
})

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *OrganizationSyncSettingsResource) patch(
ctx context.Context,
data OrganizationSyncSettingsResourceModel,
) diag.Diagnostics {
var diags diag.Diagnostics
field := data.Field.ValueString()
assignDefault := data.AssignDefault.ValueBool()

if data.Mapping.IsNull() {
_, err := r.Client.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{
Field: field,
AssignDefault: assignDefault,
})

if err != nil {
diags.AddError("failed to create organization sync", err.Error())
return diags
}
} else {
settings := codersdk.OrganizationSyncSettings{
Field: field,
AssignDefault: assignDefault,
Mapping: map[string][]uuid.UUID{},
}

// 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 {
settings.Mapping[key] = append(settings.Mapping[key], id.ValueUUID())
}
}

_, err := r.Client.PatchOrganizationIDPSyncSettings(ctx, settings)
if err != nil {
diags.AddError("failed to create organization sync", err.Error())
return diags
}
}

return diags
}

func (r *OrganizationSyncSettingsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Read Terraform prior state data into the model
var data OrganizationSyncSettingsResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

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`.
Field: "",
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to delete organization sync, got error: %s", err))
return
}
tflog.Trace(ctx, "successfully deleted organization sync", map[string]any{})

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
}

func (r *OrganizationSyncSettingsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Any random string provided as the ID will work for importing.
resource.ImportStatePassthroughID(ctx, path.Root("field"), req, resp)
}
Loading