-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add group_sync
and role_sync
for coderd_organization_resource
#147
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
Changes from 20 commits
8b73a3e
69fab48
4743b9e
435032b
71dc51b
3397984
75c0858
cc2bb2e
d23168a
28b395a
f2d3e3c
236c11e
16d10e7
f3ff5fb
a2db0d6
93f476b
39da842
69ebed9
3bf1734
a185262
03bdf17
753eaa9
85891d6
68c6ad5
bd73bb4
8f3e1b9
d3f6e2c
33e09a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,9 +3,15 @@ | |
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/listvalidator" | ||
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" | ||
"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" | ||
|
@@ -14,6 +20,7 @@ | |
"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-framework/types/basetypes" | ||
"github.com/hashicorp/terraform-plugin-log/tflog" | ||
) | ||
|
||
|
@@ -33,6 +40,21 @@ | |
DisplayName types.String `tfsdk:"display_name"` | ||
Description types.String `tfsdk:"description"` | ||
Icon types.String `tfsdk:"icon"` | ||
|
||
GroupSync types.Object `tfsdk:"group_sync"` | ||
RoleSync types.Object `tfsdk:"role_sync"` | ||
} | ||
|
||
type GroupSyncModel struct { | ||
Field types.String `tfsdk:"field"` | ||
RegexFilter types.String `tfsdk:"regex_filter"` | ||
AutoCreateMissing types.Bool `tfsdk:"auto_create_missing"` | ||
Mapping types.Map `tfsdk:"mapping"` | ||
} | ||
|
||
type RoleSyncModel struct { | ||
Field types.String `tfsdk:"field"` | ||
Mapping types.Map `tfsdk:"mapping"` | ||
} | ||
|
||
func NewOrganizationResource() resource.Resource { | ||
|
@@ -83,6 +105,64 @@ | |
Default: stringdefault.StaticString(""), | ||
}, | ||
}, | ||
|
||
Blocks: map[string]schema.Block{ | ||
"group_sync": schema.SingleNestedBlock{ | ||
Attributes: map[string]schema.Attribute{ | ||
"field": schema.StringAttribute{ | ||
Optional: 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), | ||
}, | ||
}, | ||
"auto_create_missing": schema.BoolAttribute{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This defaults to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would that prevent There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, the default only gets computed if the parent is known, so:
would default any values within it, but omitting the block altogether would not. If you wanted to default the block to known, and the bool to false, then you'd also add a |
||
Optional: true, | ||
MarkdownDescription: "Controls whether groups will be created if " + | ||
"they are missing.", | ||
}, | ||
"mapping": schema.MapAttribute{ | ||
ElementType: types.ListType{ElemType: UUIDType}, | ||
Optional: true, | ||
MarkdownDescription: "A map from OIDC group name to Coder group ID.", | ||
Validators: []validator.Map{ | ||
mapvalidator.ValueListsAre(listvalidator.ValueStringsAre(stringvalidator.Any())), | ||
aslilac marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
}, | ||
}, | ||
}, | ||
"role_sync": schema.SingleNestedBlock{ | ||
Attributes: map[string]schema.Attribute{ | ||
"field": schema.StringAttribute{ | ||
Optional: true, | ||
MarkdownDescription: "The claim field that specifies what " + | ||
"organization roles a user should be given.", | ||
Validators: []validator.String{ | ||
stringvalidator.LengthAtLeast(1), | ||
}, | ||
}, | ||
"mapping": schema.MapAttribute{ | ||
ElementType: types.ListType{ElemType: UUIDType}, | ||
Optional: true, | ||
MarkdownDescription: "A map from OIDC group name to Coder " + | ||
"organization role.", | ||
Validators: []validator.Map{ | ||
mapvalidator.ValueListsAre(listvalidator.ValueStringsAre(stringvalidator.Any())), | ||
}, | ||
aslilac marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
|
@@ -133,6 +213,26 @@ | |
} | ||
} | ||
|
||
if !data.GroupSync.IsNull() { | ||
_, err := r.Client.GroupIDPSyncSettings(ctx, data.ID.ValueUUID().String()) | ||
if err != nil { | ||
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to get organization group sync settings, got error: %s", err)) | ||
return | ||
} | ||
|
||
// data.GroupSync = ??? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't recall if you can write to a config block (blocks can't be computed) like this. I suspect you can't. If you can, I assume it would just be to error if the post-apply plan isn't empty. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I figured something out that works 😄 |
||
} | ||
|
||
if !data.RoleSync.IsNull() { | ||
_, err := r.Client.RoleIDPSyncSettings(ctx, data.ID.ValueUUID().String()) | ||
if err != nil { | ||
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to get organization role sync settings, got error: %s", err)) | ||
return | ||
} | ||
|
||
// data.RoleSync = ??? | ||
} | ||
|
||
// We've fetched the organization ID from state, and the latest values for | ||
// everything else from the backend. Ensure that any mutable data is synced | ||
// with the backend. | ||
|
@@ -183,6 +283,21 @@ | |
// 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, | ||
}) | ||
if !data.GroupSync.IsNull() { | ||
r.patchGroupSync(ctx, orgID, data.GroupSync) | ||
} | ||
tflog.Trace(ctx, "updating role sync", map[string]any{ | ||
"orgID": orgID, | ||
}) | ||
if !data.RoleSync.IsNull() { | ||
r.patchRoleSync(ctx, orgID, data.RoleSync) | ||
} | ||
|
||
// Save data into Terraform state | ||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) | ||
} | ||
|
@@ -215,6 +330,7 @@ | |
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update organization %s, got error: %s", orgID, err)) | ||
return | ||
} | ||
|
||
tflog.Trace(ctx, "successfully updated organization", map[string]any{ | ||
"id": orgID, | ||
"name": org.Name, | ||
|
@@ -223,6 +339,19 @@ | |
"icon": org.Icon, | ||
}) | ||
|
||
tflog.Trace(ctx, "updating group sync", map[string]any{ | ||
"orgID": orgID, | ||
}) | ||
if !data.GroupSync.IsNull() { | ||
r.patchGroupSync(ctx, orgID, data.GroupSync) | ||
} | ||
tflog.Trace(ctx, "updating role sync", map[string]any{ | ||
"orgID": orgID, | ||
}) | ||
if !data.RoleSync.IsNull() { | ||
r.patchRoleSync(ctx, orgID, data.RoleSync) | ||
aslilac marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
// Save updated data into Terraform state | ||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) | ||
} | ||
|
@@ -260,3 +389,69 @@ | |
// set the `name` attribute. | ||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp) | ||
} | ||
|
||
func (r *OrganizationResource) patchGroupSync( | ||
ctx context.Context, | ||
orgID uuid.UUID, | ||
groupSyncObject types.Object, | ||
) diag.Diagnostics { | ||
var diags diag.Diagnostics | ||
|
||
// Read values from Terraform | ||
var groupSyncData GroupSyncModel | ||
diags.Append(groupSyncObject.As(ctx, &groupSyncData, basetypes.ObjectAsOptions{})...) | ||
if diags.HasError() { | ||
return diags | ||
} | ||
|
||
// Convert that into the type used to send the PATCH to the backend | ||
var groupSync codersdk.GroupSyncSettings | ||
groupSync.Field = groupSyncData.Field.ValueString() | ||
groupSync.RegexFilter = regexp.MustCompile(groupSyncData.RegexFilter.ValueString()) | ||
aslilac marked this conversation as resolved.
Show resolved
Hide resolved
|
||
groupSync.AutoCreateMissing = groupSyncData.AutoCreateMissing.ValueBool() | ||
diags.Append(groupSyncData.Mapping.ElementsAs(ctx, &groupSync.Mapping, false)...) | ||
if diags.HasError() { | ||
return diags | ||
} | ||
|
||
// 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 | ||
} | ||
|
||
func (r *OrganizationResource) patchRoleSync( | ||
ctx context.Context, | ||
orgID uuid.UUID, | ||
roleSyncObject types.Object, | ||
) diag.Diagnostics { | ||
var diags diag.Diagnostics | ||
|
||
// Read values from Terraform | ||
var roleSyncData RoleSyncModel | ||
diags.Append(roleSyncObject.As(ctx, &roleSyncData, basetypes.ObjectAsOptions{})...) | ||
if diags.HasError() { | ||
return diags | ||
} | ||
|
||
// Convert that into the type used to send the PATCH to the backend | ||
var roleSync codersdk.RoleSyncSettings | ||
roleSync.Field = roleSyncData.Field.ValueString() | ||
diags.Append(roleSyncData.Mapping.ElementsAs(ctx, &roleSync.Mapping, false)...) | ||
if diags.HasError() { | ||
return diags | ||
} | ||
|
||
// Perform the PATCH | ||
_, err := r.Client.PatchRoleIDPSyncSettings(ctx, orgID.String(), roleSync) | ||
if err != nil { | ||
diags.AddError("Role Sync Update error", err.Error()) | ||
return diags | ||
} | ||
|
||
return diags | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realised we're missing enterprise/premium entitlement checks on this and the provisioner key resource. We have them on the workspace proxy and the enterprise features of template resources, but they run during
apply
, when they should really run duringplan
. Let's not worry about it for now, and at some point I'll see if I can get them working at plan-time using a plan modifier (validators don't have the provider config we need).If that doesn't work we can just copy the ones at apply-time to these two new resources.