From dfa7472214534c75aad86ba554c742b8644dcc17 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:38:48 +1000 Subject: [PATCH 1/3] chore: tflog trace -> info (#74) --- internal/provider/group_resource.go | 16 +++++----- internal/provider/template_resource.go | 42 +++++++++++++------------- internal/provider/user_resource.go | 28 ++++++++--------- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/internal/provider/group_resource.go b/internal/provider/group_resource.go index ab58c68..6c71b5b 100644 --- a/internal/provider/group_resource.go +++ b/internal/provider/group_resource.go @@ -158,7 +158,7 @@ func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest, orgID := data.OrganizationID.ValueUUID() - tflog.Trace(ctx, "creating group") + tflog.Info(ctx, "creating group") group, err := client.CreateGroup(ctx, orgID, codersdk.CreateGroupRequest{ Name: data.Name.ValueString(), DisplayName: data.DisplayName.ValueString(), @@ -169,13 +169,13 @@ func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest, resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create group, got error: %s", err)) return } - tflog.Trace(ctx, "successfully created group", map[string]any{ + tflog.Info(ctx, "successfully created group", map[string]any{ "id": group.ID.String(), }) data.ID = UUIDValue(group.ID) data.DisplayName = types.StringValue(group.DisplayName) - tflog.Trace(ctx, "setting group members") + tflog.Info(ctx, "setting group members") var members []string resp.Diagnostics.Append( data.Members.ElementsAs(ctx, &members, false)..., @@ -190,7 +190,7 @@ func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest, resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add members to group, got error: %s", err)) return } - tflog.Trace(ctx, "successfully set group members") + tflog.Info(ctx, "successfully set group members") // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -270,7 +270,7 @@ func (r *GroupResource) Update(ctx context.Context, req resource.UpdateRequest, } add, remove = memberDiff(curMembers, plannedMembers) } - tflog.Trace(ctx, "updating group", map[string]any{ + tflog.Info(ctx, "updating group", map[string]any{ "id": groupID, "new_members": add, "removed_members": remove, @@ -293,7 +293,7 @@ func (r *GroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update group, got error: %s", err)) return } - tflog.Trace(ctx, "successfully updated group") + tflog.Info(ctx, "successfully updated group") // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -312,7 +312,7 @@ func (r *GroupResource) Delete(ctx context.Context, req resource.DeleteRequest, client := r.data.Client groupID := data.ID.ValueUUID() - tflog.Trace(ctx, "deleting group", map[string]any{ + tflog.Info(ctx, "deleting group", map[string]any{ "id": groupID, }) err := client.DeleteGroup(ctx, groupID) @@ -320,7 +320,7 @@ func (r *GroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete group, got error: %s", err)) return } - tflog.Trace(ctx, "successfully deleted group") + tflog.Info(ctx, "successfully deleted group") } func (r *GroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { diff --git a/internal/provider/template_resource.go b/internal/provider/template_resource.go index 15c4932..8ef046f 100644 --- a/internal/provider/template_resource.go +++ b/internal/provider/template_resource.go @@ -492,7 +492,7 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques return } if idx == 0 { - tflog.Trace(ctx, "creating template") + tflog.Info(ctx, "creating template") createReq := data.toCreateRequest(ctx, resp, versionResp.ID) if resp.Diagnostics.HasError() { return @@ -502,7 +502,7 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to create template: %s", err)) return } - tflog.Trace(ctx, "successfully created template", map[string]any{ + tflog.Info(ctx, "successfully created template", map[string]any{ "id": templateResp.ID, }) @@ -514,7 +514,7 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques } if !data.ACL.IsNull() { - tflog.Trace(ctx, "updating template ACL") + tflog.Info(ctx, "updating template ACL") var acl ACL resp.Diagnostics.Append( data.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{})..., @@ -527,7 +527,7 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to create template ACL: %s", err)) return } - tflog.Trace(ctx, "successfully updated template ACL") + tflog.Info(ctx, "successfully updated template ACL") } } if version.Active.ValueBool() { @@ -578,7 +578,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r } if !data.ACL.IsNull() { - tflog.Trace(ctx, "reading template ACL") + tflog.Info(ctx, "reading template ACL") acl, err := client.TemplateACL(ctx, templateID) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template ACL: %s", err)) @@ -591,7 +591,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r return } data.ACL = aclObj - tflog.Trace(ctx, "read template ACL") + tflog.Info(ctx, "read template ACL") } for idx, version := range data.Versions { @@ -653,7 +653,7 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques templateMetadataChanged := !newState.EqualTemplateMetadata(&curState) // This is required, as the API will reject no-diff updates. if templateMetadataChanged { - tflog.Trace(ctx, "change in template metadata detected, updating.") + tflog.Info(ctx, "change in template metadata detected, updating.") updateReq := newState.toUpdateRequest(ctx, resp) if resp.Diagnostics.HasError() { return @@ -664,7 +664,7 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques return } - tflog.Trace(ctx, "successfully updated template metadata") + tflog.Info(ctx, "successfully updated template metadata") } // Since the everyone group always gets deleted by `DisableEveryoneGroupAccess`, we need to run this even if there @@ -680,12 +680,12 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to update template ACL: %s", err)) return } - tflog.Trace(ctx, "successfully updated template ACL") + tflog.Info(ctx, "successfully updated template ACL") } for idx := range newState.Versions { if newState.Versions[idx].ID.IsUnknown() { - tflog.Trace(ctx, "discovered a new or modified template version") + tflog.Info(ctx, "discovered a new or modified template version") uploadResp, err := newVersion(ctx, client, newVersionRequest{ Version: &newState.Versions[idx], OrganizationID: orgID, @@ -761,7 +761,7 @@ func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteReques templateID := data.ID.ValueUUID() - tflog.Trace(ctx, "deleting template") + tflog.Info(ctx, "deleting template") err := client.DeleteTemplate(ctx, templateID) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to delete template: %s", err)) @@ -927,7 +927,7 @@ func waitForJob(ctx context.Context, client *codersdk.Client, version *codersdk. if !ok { break } - tflog.Trace(ctx, logs.Output, map[string]interface{}{ + tflog.Info(ctx, logs.Output, map[string]interface{}{ "job_id": logs.ID, "job_stage": logs.Stage, "log_source": logs.Source, @@ -959,13 +959,13 @@ type newVersionRequest struct { func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequest) (*codersdk.TemplateVersion, error) { directory := req.Version.Directory.ValueString() - tflog.Trace(ctx, "uploading directory") + tflog.Info(ctx, "uploading directory") uploadResp, err := uploadDirectory(ctx, client, slog.Make(newTFLogSink(ctx)), directory) if err != nil { return nil, fmt.Errorf("failed to upload directory: %s", err) } - tflog.Trace(ctx, "successfully uploaded directory") - tflog.Trace(ctx, "discovering and parsing vars files") + tflog.Info(ctx, "successfully uploaded directory") + tflog.Info(ctx, "discovering and parsing vars files") varFiles, err := codersdk.DiscoverVarsFiles(directory) if err != nil { return nil, fmt.Errorf("failed to discover vars files: %s", err) @@ -974,7 +974,7 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ if err != nil { return nil, fmt.Errorf("failed to parse user variable values: %s", err) } - tflog.Trace(ctx, "discovered and parsed vars files", map[string]any{ + tflog.Info(ctx, "discovered and parsed vars files", map[string]any{ "vars": vars, }) for _, variable := range req.Version.TerraformVariables { @@ -994,22 +994,22 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ if req.TemplateID != nil { tmplVerReq.TemplateID = *req.TemplateID } - tflog.Trace(ctx, "creating template version") + tflog.Info(ctx, "creating template version") versionResp, err := client.CreateTemplateVersion(ctx, req.OrganizationID, tmplVerReq) if err != nil { return nil, fmt.Errorf("failed to create template version: %s", err) } - tflog.Trace(ctx, "waiting for template version import job.") + tflog.Info(ctx, "waiting for template version import job.") err = waitForJob(ctx, client, &versionResp) if err != nil { return nil, fmt.Errorf("failed to wait for job: %s", err) } - tflog.Trace(ctx, "successfully created template version") + tflog.Info(ctx, "successfully created template version") return &versionResp, nil } func markActive(ctx context.Context, client *codersdk.Client, templateID uuid.UUID, versionID uuid.UUID) error { - tflog.Trace(ctx, "marking template version as active", map[string]any{ + tflog.Info(ctx, "marking template version as active", map[string]any{ "version_id": versionID.String(), "template_id": templateID.String(), }) @@ -1019,7 +1019,7 @@ func markActive(ctx context.Context, client *codersdk.Client, templateID uuid.UU if err != nil { return fmt.Errorf("Failed to update active template version: %s", err) } - tflog.Trace(ctx, "marked template version as active") + tflog.Info(ctx, "marked template version as active") return nil } diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index e92e2c4..3eee654 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -159,7 +159,7 @@ func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, r return } - tflog.Trace(ctx, "creating user") + tflog.Info(ctx, "creating user") loginType := codersdk.LoginType(data.LoginType.ValueString()) if loginType == codersdk.LoginTypePassword && data.Password.IsNull() { resp.Diagnostics.AddError("Data Error", "Password is required when login_type is 'password'") @@ -180,12 +180,12 @@ func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, r resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create user, got error: %s", err)) return } - tflog.Trace(ctx, "successfully created user", map[string]any{ + tflog.Info(ctx, "successfully created user", map[string]any{ "id": user.ID.String(), }) data.ID = UUIDValue(user.ID) - tflog.Trace(ctx, "updating user profile") + tflog.Info(ctx, "updating user profile") name := data.Username if data.Name.ValueString() != "" { name = data.Name @@ -198,14 +198,14 @@ func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, r resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update newly created user profile, got error: %s", err)) return } - tflog.Trace(ctx, "successfully updated user profile") + tflog.Info(ctx, "successfully updated user profile") data.Name = types.StringValue(user.Name) var roles []string resp.Diagnostics.Append( data.Roles.ElementsAs(ctx, &roles, false)..., ) - tflog.Trace(ctx, "updating user roles", map[string]any{ + tflog.Info(ctx, "updating user roles", map[string]any{ "new_roles": roles, }) user, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{ @@ -215,7 +215,7 @@ func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, r resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update newly created user roles, got error: %s", err)) return } - tflog.Trace(ctx, "successfully updated user roles") + tflog.Info(ctx, "successfully updated user roles") if data.Suspended.ValueBool() { _, err = client.UpdateUserStatus(ctx, data.ID.ValueString(), codersdk.UserStatus("suspended")) @@ -291,7 +291,7 @@ func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, r if data.Name.ValueString() != "" { name = data.Name } - tflog.Trace(ctx, "updating user", map[string]any{ + tflog.Info(ctx, "updating user", map[string]any{ "new_username": data.Username.ValueString(), "new_name": name.ValueString(), }) @@ -304,13 +304,13 @@ func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, r return } data.Name = name - tflog.Trace(ctx, "successfully updated user profile") + tflog.Info(ctx, "successfully updated user profile") var roles []string resp.Diagnostics.Append( data.Roles.ElementsAs(ctx, &roles, false)..., ) - tflog.Trace(ctx, "updating user roles", map[string]any{ + tflog.Info(ctx, "updating user roles", map[string]any{ "new_roles": roles, }) _, err = client.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{ @@ -320,10 +320,10 @@ func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, r resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update user roles, got error: %s", err)) return } - tflog.Trace(ctx, "successfully updated user roles") + tflog.Info(ctx, "successfully updated user roles") if data.LoginType.ValueString() == string(codersdk.LoginTypePassword) && !data.Password.IsNull() { - tflog.Trace(ctx, "updating password") + tflog.Info(ctx, "updating password") err = client.UpdateUserPassword(ctx, user.ID.String(), codersdk.UpdateUserPasswordRequest{ Password: data.Password.ValueString(), }) @@ -331,7 +331,7 @@ func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, r resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update password, got error: %s", err)) return } - tflog.Trace(ctx, "successfully updated password") + tflog.Info(ctx, "successfully updated password") } var statusErr error @@ -362,13 +362,13 @@ func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, r client := r.data.Client - tflog.Trace(ctx, "deleting user") + tflog.Info(ctx, "deleting user") err := client.DeleteUser(ctx, data.ID.ValueUUID()) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete user, got error: %s", err)) return } - tflog.Trace(ctx, "successfully deleted user") + tflog.Info(ctx, "successfully deleted user") } func (r *UserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { From 3bfb793dee7d7665004ff93d1893893261c0ad29 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:58:24 +1000 Subject: [PATCH 2/3] docs: add examples for all resources & data sources (#78) --- docs/data-sources/group.md | 31 +++++++++ docs/data-sources/organization.md | 25 ++++++- docs/data-sources/template.md | 24 +++++++ docs/data-sources/user.md | 24 ++++++- docs/index.md | 69 ++++++++++++++++++- docs/resources/group.md | 9 ++- docs/resources/template.md | 12 +++- docs/resources/user.md | 1 + docs/resources/workspace_proxy.md | 33 ++++++++- .../data-sources/coderd_group/data-source.tf | 28 ++++++++ .../coderd_organization/data-source.tf | 20 ++++++ .../coderd_template/data-source.tf | 21 ++++++ .../data-sources/coderd_user/data-source.tf | 19 +++++ examples/provider/provider.tf | 59 ++++++++++++++++ .../resources/coderd_template/resource.tf | 6 ++ examples/resources/coderd_user/resource.tf | 1 + .../coderd_workspace_proxy/resource.tf | 28 ++++++++ internal/provider/group_resource.go | 4 +- internal/provider/provider.go | 6 +- internal/provider/template_resource.go | 6 +- 20 files changed, 408 insertions(+), 18 deletions(-) create mode 100644 examples/data-sources/coderd_group/data-source.tf create mode 100644 examples/data-sources/coderd_organization/data-source.tf create mode 100644 examples/data-sources/coderd_template/data-source.tf create mode 100644 examples/data-sources/coderd_user/data-source.tf create mode 100644 examples/provider/provider.tf create mode 100644 examples/resources/coderd_workspace_proxy/resource.tf diff --git a/docs/data-sources/group.md b/docs/data-sources/group.md index 37d3f7a..8a50e9e 100644 --- a/docs/data-sources/group.md +++ b/docs/data-sources/group.md @@ -10,7 +10,38 @@ description: |- An existing group on the Coder deployment. +## Example Usage +```terraform +// Get a group on the provider default organization by `id` +data "coderd_group" "employees" { + id = "abcd-efg-hijk" +} + +// Get a group on the provider default organization by `name` + `organization_id` +data "coderd_group" "bosses" { + name = "bosses" +} + +// Use them to apply ACL to a template +resource "coderd_template" "example" { + name = "example-template" + versions = [/* ... */] + acl = { + groups = [ + { + id = data.coderd_group.employees.id + role = "use" + }, + { + id = data.coderd_group.bosses.id + role = "admin" + } + ] + users = [] + } +} +``` ## Schema diff --git a/docs/data-sources/organization.md b/docs/data-sources/organization.md index 26b8cdd..f8cf537 100644 --- a/docs/data-sources/organization.md +++ b/docs/data-sources/organization.md @@ -15,7 +15,30 @@ An existing organization on the Coder deployment. ~> **Warning** This data source is only compatible with Coder version [2.13.0](https://github.com/coder/coder/releases/tag/v2.13.0) and later. - +## Example Usage + +```terraform +// Get the default (first) organization for the coder deployment +data "coderd_organization" "default" { + is_default = true +} + +// Get another organization by `id` +data "coderd_organization" "example" { + id = "abcd-efg-hijk" +} + +// Or get by name +data "coderd_organization" "example2" { + name = "example-organization-2" +} + +// Create a group on a specific organization +resource "coderd_group" "example" { + name = "example-group" + organization_id = data.coderd_organization.example.id +} +``` ## Schema diff --git a/docs/data-sources/template.md b/docs/data-sources/template.md index 33128fa..c07fd38 100644 --- a/docs/data-sources/template.md +++ b/docs/data-sources/template.md @@ -10,7 +10,31 @@ description: |- An existing template on the Coder deployment. +## Example Usage +```terraform +// Get a template on the provider's default organization by `id` +data "coderd_template" "ubuntu-main" { + id = "abcd-efg-hijk" +} + +// Get a template on the provider's default organization by `name` +data "coderd_template" "windows-main" { + name = "windows-main" +} + +// Manage a template resource with the same permissions & settings as an existing template +resource "coderd_template" "debian-main" { + name = "debian-main" + versions = [/* ... */] + acl = data.coderd_template.ubuntu-main.acl + allow_user_auto_start = data.coderd_template.ubuntu-main.allow_user_auto_start + auto_start_permitted_days_of_week = data.coderd_template.ubuntu-main.auto_start_permitted_days_of_week + allow_user_auto_stop = data.coderd_template.ubuntu-main.allow_user_auto_stop + auto_stop_requirement = data.coderd_template.ubuntu-main.auto_stop_requirement + allow_user_cancel_workspace_jobs = data.coderd_template.ubuntu-main.allow_user_cancel_workspace_jobs +} +``` ## Schema diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index 35557e7..366c16a 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -10,7 +10,29 @@ description: |- An existing user on the Coder deployment - +## Example Usage + +```terraform +// Get a user on the Coder deployment by `id` +data "coderd_user" "manager" { + id = "abcd-efg-hijk" +} + +// Get a user on the Coder deployment by `username` +data "coderd_user" "admin" { + username = "admin" +} + + +// Use them to create a group +resource "coderd_group" "bosses" { + name = "group" + members = [ + data.coderd_user.admin.id, + data.coderd_user.manager.id + ] +} +``` ## Schema diff --git a/docs/index.md b/docs/index.md index b9d4dc7..3d9be49 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,16 +3,81 @@ page_title: "coderd Provider" subcategory: "" description: |- + The coderd provider can be used to manage resources on a Coder deployment. The provider exposes resources and data sources for users, groups, templates, and workspace proxies. ~> Warning This provider is only compatible with Coder version 2.10.1 https://github.com/coder/coder/releases/tag/v2.10.1 and later. --- # coderd Provider +The coderd provider can be used to manage resources on a Coder deployment. The provider exposes resources and data sources for users, groups, templates, and workspace proxies. + ~> **Warning** This provider is only compatible with Coder version [2.10.1](https://github.com/coder/coder/releases/tag/v2.10.1) and later. +## Example Usage + +```terraform +terraform { + required_providers { + coderd = { + source = "coder/coderd" + } + } +} + +provider "coderd" { + url = "coder.example.com" + token = "****" +} + + +data "coderd_organization" "default" { + is_default = true +} + +data "coderd_user" "admin" { + username = "admin" +} + +resource "coderd_user" "manager" { + username = "Manager" + email = "manager@example.com" +} + +resource "coderd_group" "bosses" { + name = "group" + members = [ + data.coderd_user.admin.id, + resource.coderd_user.manager.id + ] +} +resource "coderd_template" "example" { + name = "example-template" + versions = [{ + directory = "./example-template" + active = true + tf_vars = [{ + name = "image_id" + value = "ami-12345678" + }] + # Version names can be randomly generated if null/omitted + }] + acl = { + groups = [{ + id = data.coderd_organization.default.id + role = "use" + }, + { + id = resource.coderd_group.bosses.id + role = "admin" + }] + users = [] + } + allow_user_cancel_workspace_jobs = false +} +``` ## Schema @@ -20,5 +85,5 @@ This provider is only compatible with Coder version [2.10.1](https://github.com/ ### Optional - `default_organization_id` (String) Default organization ID to use when creating resources. Defaults to the first organization the token has access to. -- `token` (String) API token for communicating with the deployment. Most resource types require elevated permissions. Defaults to $CODER_SESSION_TOKEN. -- `url` (String) URL to the Coder deployment. Defaults to $CODER_URL. +- `token` (String) API token for communicating with the deployment. Most resource types require elevated permissions. Defaults to `$CODER_SESSION_TOKEN`. +- `url` (String) URL to the Coder deployment. Defaults to `$CODER_URL`. diff --git a/docs/resources/group.md b/docs/resources/group.md index 3e7220a..5579753 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -3,12 +3,15 @@ page_title: "coderd_group Resource - terraform-provider-coderd" subcategory: "" description: |- - A group on the Coder deployment. If you want to have a group resource with unmanaged members, but still want to read the members in Terraform, use the data.coderd_group data source. Creating groups requires an Enterprise license. + A group on the Coder deployment. + Creating groups requires an Enterprise license. --- # coderd_group (Resource) -A group on the Coder deployment. If you want to have a group resource with unmanaged members, but still want to read the members in Terraform, use the `data.coderd_group` data source. Creating groups requires an Enterprise license. +A group on the Coder deployment. + +Creating groups requires an Enterprise license. ## Example Usage @@ -49,7 +52,7 @@ resource "coderd_group" "group1" { - `avatar_url` (String) The URL of the group's avatar. - `display_name` (String) The display name of the group. Defaults to the group name. -- `members` (Set of String) Members of the group, by ID. If null, members will not be added or removed by Terraform. +- `members` (Set of String) Members of the group, by ID. If null, members will not be added or removed by Terraform. To have a group resource with unmanaged members, but be able to read the members in Terraform, use `data.coderd_group` - `organization_id` (String) The organization ID that the group belongs to. Defaults to the provider default organization ID. - `quota_allowance` (Number) The number of quota credits to allocate to each user in the group. diff --git a/docs/resources/template.md b/docs/resources/template.md index 185e52e..2e8dc9a 100644 --- a/docs/resources/template.md +++ b/docs/resources/template.md @@ -42,6 +42,12 @@ resource "coderd_template" "ubuntu-main" { directory = "./staging-template" } ] + acl = { + users = [{ + id = coderd_user.coder1.id + role = "admin" + }] + } } ``` @@ -55,7 +61,7 @@ resource "coderd_template" "ubuntu-main" { ### Optional -- `acl` (Attributes) (Enterprise) Access control list for the template. If null, ACL policies will not be added or removed by Terraform. (see [below for nested schema](#nestedatt--acl)) +- `acl` (Attributes) (Enterprise) Access control list for the template. If null, ACL policies will not be added, removed, or read by Terraform. (see [below for nested schema](#nestedatt--acl)) - `activity_bump_ms` (Number) The activity bump duration for all workspaces created from this template, in milliseconds. Defaults to one hour. - `allow_user_auto_start` (Boolean) (Enterprise) Whether users can auto-start workspaces created from this template. Defaults to true. - `allow_user_auto_stop` (Boolean) (Enterprise) Whether users can auto-start workspaces created from this template. Defaults to true. @@ -63,7 +69,7 @@ resource "coderd_template" "ubuntu-main" { - `auto_start_permitted_days_of_week` (Set of String) (Enterprise) List of days of the week in which autostart is allowed to happen, for all workspaces created from this template. Defaults to all days. If no days are specified, autostart is not allowed. - `auto_stop_requirement` (Attributes) (Enterprise) The auto-stop requirement for all workspaces created from this template. (see [below for nested schema](#nestedatt--auto_stop_requirement)) - `default_ttl_ms` (Number) The default time-to-live for all workspaces created from this template, in milliseconds. -- `deprecation_message` (String) If set, the template will be marked as deprecated and users will be blocked from creating new workspaces from it. +- `deprecation_message` (String) If set, the template will be marked as deprecated with the provided message and users will be blocked from creating new workspaces from it. - `description` (String) A description of the template. - `display_name` (String) The display name of the template. Defaults to the template name. - `failure_ttl_ms` (Number) (Enterprise) The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds. @@ -71,7 +77,7 @@ resource "coderd_template" "ubuntu-main" { - `organization_id` (String) The ID of the organization. Defaults to the provider's default organization - `require_active_version` (Boolean) (Enterprise) Whether workspaces must be created from the active version of this template. Defaults to false. - `time_til_dormant_autodelete_ms` (Number) (Enterprise) The max lifetime before Coder permanently deletes dormant workspaces created from this template. -- `time_til_dormant_ms` (Number) Enterprise) The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds. +- `time_til_dormant_ms` (Number) (Enterprise) The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds. ### Read-Only diff --git a/docs/resources/user.md b/docs/resources/user.md index dd54af7..e1c6b68 100644 --- a/docs/resources/user.md +++ b/docs/resources/user.md @@ -39,6 +39,7 @@ resource "coderd_user" "audit" { resource "coderd_user" "admin" { username = "admin" suspended = true + email = "admin@example.com" } ``` diff --git a/docs/resources/workspace_proxy.md b/docs/resources/workspace_proxy.md index 53f2685..ad77a82 100644 --- a/docs/resources/workspace_proxy.md +++ b/docs/resources/workspace_proxy.md @@ -10,7 +10,38 @@ description: |- A Workspace Proxy for the Coder deployment. - +## Example Usage + +```terraform +resource "coderd_workspace_proxy" "sydney-wsp" { + name = "sydney-wsp" + display_name = "Australia (Sydney)" + icon = "/emojis/1f1e6-1f1fa.png" +} + +resource "kubernetes_deployment" "syd_wsproxy" { + metadata { /* ... */ } + spec { + template { + metadata { /* ... */ } + spec { + container { + name = "syd-wsp" + image = "ghcr.io/coder/coder:latest" + args = ["wsproxy", "server"] + env { + name = "CODER_PROXY_SESSION_TOKEN" + value = coderd_workspace_proxy.sydney-wsp.session_token + } + /* ... */ + } + /* ... */ + } + } + /* ... */ + } +} +``` ## Schema diff --git a/examples/data-sources/coderd_group/data-source.tf b/examples/data-sources/coderd_group/data-source.tf new file mode 100644 index 0000000..572e264 --- /dev/null +++ b/examples/data-sources/coderd_group/data-source.tf @@ -0,0 +1,28 @@ +// Get a group on the provider default organization by `id` +data "coderd_group" "employees" { + id = "abcd-efg-hijk" +} + +// Get a group on the provider default organization by `name` + `organization_id` +data "coderd_group" "bosses" { + name = "bosses" +} + +// Use them to apply ACL to a template +resource "coderd_template" "example" { + name = "example-template" + versions = [/* ... */] + acl = { + groups = [ + { + id = data.coderd_group.employees.id + role = "use" + }, + { + id = data.coderd_group.bosses.id + role = "admin" + } + ] + users = [] + } +} diff --git a/examples/data-sources/coderd_organization/data-source.tf b/examples/data-sources/coderd_organization/data-source.tf new file mode 100644 index 0000000..37cb8ff --- /dev/null +++ b/examples/data-sources/coderd_organization/data-source.tf @@ -0,0 +1,20 @@ +// Get the default (first) organization for the coder deployment +data "coderd_organization" "default" { + is_default = true +} + +// Get another organization by `id` +data "coderd_organization" "example" { + id = "abcd-efg-hijk" +} + +// Or get by name +data "coderd_organization" "example2" { + name = "example-organization-2" +} + +// Create a group on a specific organization +resource "coderd_group" "example" { + name = "example-group" + organization_id = data.coderd_organization.example.id +} diff --git a/examples/data-sources/coderd_template/data-source.tf b/examples/data-sources/coderd_template/data-source.tf new file mode 100644 index 0000000..3255747 --- /dev/null +++ b/examples/data-sources/coderd_template/data-source.tf @@ -0,0 +1,21 @@ +// Get a template on the provider's default organization by `id` +data "coderd_template" "ubuntu-main" { + id = "abcd-efg-hijk" +} + +// Get a template on the provider's default organization by `name` +data "coderd_template" "windows-main" { + name = "windows-main" +} + +// Manage a template resource with the same permissions & settings as an existing template +resource "coderd_template" "debian-main" { + name = "debian-main" + versions = [/* ... */] + acl = data.coderd_template.ubuntu-main.acl + allow_user_auto_start = data.coderd_template.ubuntu-main.allow_user_auto_start + auto_start_permitted_days_of_week = data.coderd_template.ubuntu-main.auto_start_permitted_days_of_week + allow_user_auto_stop = data.coderd_template.ubuntu-main.allow_user_auto_stop + auto_stop_requirement = data.coderd_template.ubuntu-main.auto_stop_requirement + allow_user_cancel_workspace_jobs = data.coderd_template.ubuntu-main.allow_user_cancel_workspace_jobs +} diff --git a/examples/data-sources/coderd_user/data-source.tf b/examples/data-sources/coderd_user/data-source.tf new file mode 100644 index 0000000..64a0345 --- /dev/null +++ b/examples/data-sources/coderd_user/data-source.tf @@ -0,0 +1,19 @@ +// Get a user on the Coder deployment by `id` +data "coderd_user" "manager" { + id = "abcd-efg-hijk" +} + +// Get a user on the Coder deployment by `username` +data "coderd_user" "admin" { + username = "admin" +} + + +// Use them to create a group +resource "coderd_group" "bosses" { + name = "group" + members = [ + data.coderd_user.admin.id, + data.coderd_user.manager.id + ] +} diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf new file mode 100644 index 0000000..fe3b9dc --- /dev/null +++ b/examples/provider/provider.tf @@ -0,0 +1,59 @@ +terraform { + required_providers { + coderd = { + source = "coder/coderd" + } + } +} + +provider "coderd" { + url = "coder.example.com" + token = "****" +} + + +data "coderd_organization" "default" { + is_default = true +} + +data "coderd_user" "admin" { + username = "admin" +} + +resource "coderd_user" "manager" { + username = "Manager" + email = "manager@example.com" +} + +resource "coderd_group" "bosses" { + name = "group" + members = [ + data.coderd_user.admin.id, + resource.coderd_user.manager.id + ] +} + +resource "coderd_template" "example" { + name = "example-template" + versions = [{ + directory = "./example-template" + active = true + tf_vars = [{ + name = "image_id" + value = "ami-12345678" + }] + # Version names can be randomly generated if null/omitted + }] + acl = { + groups = [{ + id = data.coderd_organization.default.id + role = "use" + }, + { + id = resource.coderd_group.bosses.id + role = "admin" + }] + users = [] + } + allow_user_cancel_workspace_jobs = false +} diff --git a/examples/resources/coderd_template/resource.tf b/examples/resources/coderd_template/resource.tf index b95f23d..b8d05bd 100644 --- a/examples/resources/coderd_template/resource.tf +++ b/examples/resources/coderd_template/resource.tf @@ -27,4 +27,10 @@ resource "coderd_template" "ubuntu-main" { directory = "./staging-template" } ] + acl = { + users = [{ + id = coderd_user.coder1.id + role = "admin" + }] + } } diff --git a/examples/resources/coderd_user/resource.tf b/examples/resources/coderd_user/resource.tf index ed6e380..273d383 100644 --- a/examples/resources/coderd_user/resource.tf +++ b/examples/resources/coderd_user/resource.tf @@ -24,4 +24,5 @@ resource "coderd_user" "audit" { resource "coderd_user" "admin" { username = "admin" suspended = true + email = "admin@example.com" } diff --git a/examples/resources/coderd_workspace_proxy/resource.tf b/examples/resources/coderd_workspace_proxy/resource.tf new file mode 100644 index 0000000..9780854 --- /dev/null +++ b/examples/resources/coderd_workspace_proxy/resource.tf @@ -0,0 +1,28 @@ +resource "coderd_workspace_proxy" "sydney-wsp" { + name = "sydney-wsp" + display_name = "Australia (Sydney)" + icon = "/emojis/1f1e6-1f1fa.png" +} + +resource "kubernetes_deployment" "syd_wsproxy" { + metadata { /* ... */ } + spec { + template { + metadata { /* ... */ } + spec { + container { + name = "syd-wsp" + image = "ghcr.io/coder/coder:latest" + args = ["wsproxy", "server"] + env { + name = "CODER_PROXY_SESSION_TOKEN" + value = coderd_workspace_proxy.sydney-wsp.session_token + } + /* ... */ + } + /* ... */ + } + } + /* ... */ + } +} diff --git a/internal/provider/group_resource.go b/internal/provider/group_resource.go index 6c71b5b..2599f9f 100644 --- a/internal/provider/group_resource.go +++ b/internal/provider/group_resource.go @@ -60,7 +60,7 @@ func (r *GroupResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "A group on the Coder deployment. If you want to have a group resource with unmanaged members, but still want to read the members in Terraform, use the `data.coderd_group` data source. Creating groups requires an Enterprise license.", + MarkdownDescription: "A group on the Coder deployment.\n\nCreating groups requires an Enterprise license.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -107,7 +107,7 @@ func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, }, }, "members": schema.SetAttribute{ - MarkdownDescription: "Members of the group, by ID. If null, members will not be added or removed by Terraform.", + MarkdownDescription: "Members of the group, by ID. If null, members will not be added or removed by Terraform. To have a group resource with unmanaged members, but be able to read the members in Terraform, use `data.coderd_group`", ElementType: UUIDType, Optional: true, }, diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5c8bb43..651dcb4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -53,16 +53,18 @@ func (p *CoderdProvider) Metadata(ctx context.Context, req provider.MetadataRequ func (p *CoderdProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: ` +The coderd provider can be used to manage resources on a Coder deployment. The provider exposes resources and data sources for users, groups, templates, and workspace proxies. + ~> **Warning** This provider is only compatible with Coder version [2.10.1](https://github.com/coder/coder/releases/tag/v2.10.1) and later. `, Attributes: map[string]schema.Attribute{ "url": schema.StringAttribute{ - MarkdownDescription: "URL to the Coder deployment. Defaults to $CODER_URL.", + MarkdownDescription: "URL to the Coder deployment. Defaults to `$CODER_URL`.", Optional: true, }, "token": schema.StringAttribute{ - MarkdownDescription: "API token for communicating with the deployment. Most resource types require elevated permissions. Defaults to $CODER_SESSION_TOKEN.", + MarkdownDescription: "API token for communicating with the deployment. Most resource types require elevated permissions. Defaults to `$CODER_SESSION_TOKEN`.", Optional: true, }, "default_organization_id": schema.StringAttribute{ diff --git a/internal/provider/template_resource.go b/internal/provider/template_resource.go index 8ef046f..b643cb3 100644 --- a/internal/provider/template_resource.go +++ b/internal/provider/template_resource.go @@ -343,7 +343,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques Default: int64default.StaticInt64(0), }, "time_til_dormant_ms": schema.Int64Attribute{ - MarkdownDescription: "Enterprise) The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds.", + MarkdownDescription: "(Enterprise) The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds.", Optional: true, Computed: true, Default: int64default.StaticInt64(0), @@ -361,13 +361,13 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques Default: booldefault.StaticBool(false), }, "deprecation_message": schema.StringAttribute{ - MarkdownDescription: "If set, the template will be marked as deprecated and users will be blocked from creating new workspaces from it.", + MarkdownDescription: "If set, the template will be marked as deprecated with the provided message and users will be blocked from creating new workspaces from it.", Optional: true, Computed: true, Default: stringdefault.StaticString(""), }, "acl": schema.SingleNestedAttribute{ - MarkdownDescription: "(Enterprise) Access control list for the template. If null, ACL policies will not be added or removed by Terraform.", + MarkdownDescription: "(Enterprise) Access control list for the template. If null, ACL policies will not be added, removed, or read by Terraform.", Optional: true, Attributes: map[string]schema.Attribute{ "users": permissionAttribute, From 0ee6dc09d5b56750431c34991fa42f5c67633504 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:01:25 +1000 Subject: [PATCH 3/3] chore: add missing template data source fields (#79) --- .gitignore | 7 ++ docs/data-sources/template.md | 42 +++++++++ docs/resources/template.md | 2 +- internal/provider/template_data_source.go | 91 +++++++++++++++++-- .../provider/template_data_source_test.go | 25 +++++ internal/provider/template_resource.go | 4 +- 6 files changed, 161 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 725d809..952b275 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,10 @@ terraform-provider-coderd integration/integration.tfrc *.tfstate + +# Local .terraform directories +**/.terraform/* + + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info diff --git a/docs/data-sources/template.md b/docs/data-sources/template.md index c07fd38..28cfcad 100644 --- a/docs/data-sources/template.md +++ b/docs/data-sources/template.md @@ -47,12 +47,15 @@ resource "coderd_template" "debian-main" { ### Read-Only +- `acl` (Attributes) (Enterprise) Access control list for the template. (see [below for nested schema](#nestedatt--acl)) - `active_user_count` (Number) Number of active users using the template. - `active_version_id` (String) ID of the active version of the template. - `activity_bump_ms` (Number) Duration to bump the deadline of a workspace when it receives activity. - `allow_user_autostart` (Boolean) Whether users can autostart workspaces created from the template. - `allow_user_autostop` (Boolean) Whether users can customize autostop behavior for workspaces created from the template. - `allow_user_cancel_workspace_jobs` (Boolean) Whether users can cancel jobs in workspaces created from the template. +- `auto_start_permitted_days_of_week` (Set of String) List of days of the week in which autostart is allowed to happen, for all workspaces created from this template. Defaults to all days. If no days are specified, autostart is not allowed. +- `auto_stop_requirement` (Attributes) The auto-stop requirement for all workspaces created from this template. (see [below for nested schema](#nestedatt--auto_stop_requirement)) - `created_at` (Number) Unix timestamp of when the template was created. - `created_by_user_id` (String) ID of the user who created the template. - `default_ttl_ms` (Number) Default time-to-live for workspaces created from the template. @@ -62,7 +65,46 @@ resource "coderd_template" "debian-main" { - `display_name` (String) Display name of the template. - `failure_ttl_ms` (Number) Automatic cleanup TTL for failed workspace builds. - `icon` (String) URL of the template's icon. +- `max_port_share_level` (String) The maximum port share level for workspaces created from the template. - `require_active_version` (Boolean) Whether workspaces created from the template must be up-to-date on the latest active version. - `time_til_dormant_autodelete_ms` (Number) Duration of inactivity after the workspace becomes dormant before a workspace is automatically deleted. - `time_til_dormant_ms` (Number) Duration of inactivity before a workspace is considered dormant. - `updated_at` (Number) Unix timestamp of when the template was last updated. + + +### Nested Schema for `acl` + +Read-Only: + +- `groups` (Attributes Set) (see [below for nested schema](#nestedatt--acl--groups)) +- `users` (Attributes Set) (see [below for nested schema](#nestedatt--acl--users)) + + +### Nested Schema for `acl.groups` + +Read-Only: + +- `id` (String) +- `role` (String) + + + +### Nested Schema for `acl.users` + +Read-Only: + +- `id` (String) +- `role` (String) + + + + +### Nested Schema for `auto_stop_requirement` + +Optional: + +- `weeks` (Number) Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc. + +Read-Only: + +- `days_of_week` (Set of String) List of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required. diff --git a/docs/resources/template.md b/docs/resources/template.md index 2e8dc9a..4300c55 100644 --- a/docs/resources/template.md +++ b/docs/resources/template.md @@ -69,7 +69,7 @@ resource "coderd_template" "ubuntu-main" { - `auto_start_permitted_days_of_week` (Set of String) (Enterprise) List of days of the week in which autostart is allowed to happen, for all workspaces created from this template. Defaults to all days. If no days are specified, autostart is not allowed. - `auto_stop_requirement` (Attributes) (Enterprise) The auto-stop requirement for all workspaces created from this template. (see [below for nested schema](#nestedatt--auto_stop_requirement)) - `default_ttl_ms` (Number) The default time-to-live for all workspaces created from this template, in milliseconds. -- `deprecation_message` (String) If set, the template will be marked as deprecated with the provided message and users will be blocked from creating new workspaces from it. +- `deprecation_message` (String) If set, the template will be marked as deprecated with the provided message and users will be blocked from creating new workspaces from it. Does nothing if set when the resource is created. - `description` (String) A description of the template. - `display_name` (String) The display name of the template. Defaults to the template name. - `failure_ttl_ms` (Number) (Enterprise) The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds. diff --git a/internal/provider/template_data_source.go b/internal/provider/template_data_source.go index 9b5d4a2..406ef33 100644 --- a/internal/provider/template_data_source.go +++ b/internal/provider/template_data_source.go @@ -7,6 +7,7 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/path" @@ -42,10 +43,10 @@ type TemplateDataSourceModel struct { DeprecationMessage types.String `tfsdk:"deprecation_message"` Icon types.String `tfsdk:"icon"` - DefaultTTLMillis types.Int64 `tfsdk:"default_ttl_ms"` - ActivityBumpMillis types.Int64 `tfsdk:"activity_bump_ms"` - // TODO: AutostopRequirement - // TODO: AutostartRequirement + DefaultTTLMillis types.Int64 `tfsdk:"default_ttl_ms"` + ActivityBumpMillis types.Int64 `tfsdk:"activity_bump_ms"` + AutostopRequirement types.Object `tfsdk:"auto_stop_requirement"` + AutostartPermittedDaysOfWeek types.Set `tfsdk:"auto_start_permitted_days_of_week"` AllowUserAutostart types.Bool `tfsdk:"allow_user_autostart"` AllowUserAutostop types.Bool `tfsdk:"allow_user_autostop"` @@ -55,14 +56,14 @@ type TemplateDataSourceModel struct { TimeTilDormantMillis types.Int64 `tfsdk:"time_til_dormant_ms"` TimeTilDormantAutoDeleteMillis types.Int64 `tfsdk:"time_til_dormant_autodelete_ms"` - RequireActiveVersion types.Bool `tfsdk:"require_active_version"` - // TODO: MaxPortShareLevel + RequireActiveVersion types.Bool `tfsdk:"require_active_version"` + MaxPortShareLevel types.String `tfsdk:"max_port_share_level"` CreatedByUserID UUID `tfsdk:"created_by_user_id"` CreatedAt types.Int64 `tfsdk:"created_at"` // Unix timestamp UpdatedAt types.Int64 `tfsdk:"updated_at"` // Unix timestamp - // TODO: ACL-related stuff + ACL types.Object `tfsdk:"acl"` } func (d *TemplateDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -134,6 +135,27 @@ func (d *TemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRe MarkdownDescription: "Duration to bump the deadline of a workspace when it receives activity.", Computed: true, }, + "auto_stop_requirement": schema.SingleNestedAttribute{ + MarkdownDescription: "The auto-stop requirement for all workspaces created from this template.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "days_of_week": schema.SetAttribute{ + MarkdownDescription: "List of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required.", + Computed: true, + ElementType: types.StringType, + }, + "weeks": schema.Int64Attribute{ + MarkdownDescription: "Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc.", + Optional: true, + Computed: true, + }, + }, + }, + "auto_start_permitted_days_of_week": schema.SetAttribute{ + MarkdownDescription: "List of days of the week in which autostart is allowed to happen, for all workspaces created from this template. Defaults to all days. If no days are specified, autostart is not allowed.", + Computed: true, + ElementType: types.StringType, + }, "allow_user_autostart": schema.BoolAttribute{ MarkdownDescription: "Whether users can autostart workspaces created from the template.", Computed: true, @@ -162,6 +184,10 @@ func (d *TemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRe MarkdownDescription: "Whether workspaces created from the template must be up-to-date on the latest active version.", Computed: true, }, + "max_port_share_level": schema.StringAttribute{ + MarkdownDescription: "The maximum port share level for workspaces created from the template.", + Computed: true, + }, "created_by_user_id": schema.StringAttribute{ MarkdownDescription: "ID of the user who created the template.", CustomType: UUIDType, @@ -175,6 +201,14 @@ func (d *TemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRe MarkdownDescription: "Unix timestamp of when the template was last updated.", Computed: true, }, + "acl": schema.SingleNestedAttribute{ + MarkdownDescription: "(Enterprise) Access control list for the template.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "users": computedPermissionAttribute, + "groups": computedPermissionAttribute, + }, + }, }, } } @@ -244,6 +278,33 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques return } + acl, err := client.TemplateACL(ctx, template.ID) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template ACL: %s", err)) + return + } + tfACL := convertResponseToACL(acl) + aclObj, diag := types.ObjectValueFrom(ctx, aclTypeAttr, tfACL) + if diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + + asrObj, diag := types.ObjectValueFrom(ctx, autostopRequirementTypeAttr, AutostopRequirement{ + DaysOfWeek: template.AutostopRequirement.DaysOfWeek, + Weeks: template.AutostopRequirement.Weeks, + }) + resp.Diagnostics.Append(diag...) + if resp.Diagnostics.HasError() { + return + } + autoStartDays := make([]attr.Value, 0, len(template.AutostartRequirement.DaysOfWeek)) + for _, day := range template.AutostartRequirement.DaysOfWeek { + autoStartDays = append(autoStartDays, types.StringValue(day)) + } + data.ACL = aclObj + data.AutostartPermittedDaysOfWeek = types.SetValueMust(types.StringType, autoStartDays) + data.AutostopRequirement = asrObj data.OrganizationID = UUIDValue(template.OrganizationID) data.ID = UUIDValue(template.ID) data.Name = types.StringValue(template.Name) @@ -263,6 +324,7 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques data.TimeTilDormantMillis = types.Int64Value(template.TimeTilDormantMillis) data.TimeTilDormantAutoDeleteMillis = types.Int64Value(template.TimeTilDormantAutoDeleteMillis) data.RequireActiveVersion = types.BoolValue(template.RequireActiveVersion) + data.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel)) data.CreatedByUserID = UUIDValue(template.CreatedByID) data.CreatedAt = types.Int64Value(template.CreatedAt.Unix()) data.UpdatedAt = types.Int64Value(template.UpdatedAt.Unix()) @@ -270,3 +332,18 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } + +// computedPermissionAttribute is the attribute schema for a computed instance of `[]Permission`. +var computedPermissionAttribute = schema.SetNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "role": schema.StringAttribute{ + Computed: true, + }, + }, + }, +} diff --git a/internal/provider/template_data_source_test.go b/internal/provider/template_data_source_test.go index 0c1e40d..79c1f80 100644 --- a/internal/provider/template_data_source_test.go +++ b/internal/provider/template_data_source_test.go @@ -99,6 +99,16 @@ func TestAccTemplateDataSource(t *testing.T) { }) require.NoError(t, err) + err = client.UpdateTemplateACL(ctx, tpl.ID, codersdk.UpdateTemplateACL{ + UserPerms: map[string]codersdk.TemplateRole{ + firstUser.ID.String(): codersdk.TemplateRoleAdmin, + }, + GroupPerms: map[string]codersdk.TemplateRole{ + firstUser.OrganizationIDs[0].String(): codersdk.TemplateRoleUse, + }, + }) + require.NoError(t, err) + checkFn := resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("data.coderd_template.test", "organization_id", tpl.OrganizationID.String()), resource.TestCheckResourceAttr("data.coderd_template.test", "id", tpl.ID.String()), @@ -112,6 +122,10 @@ func TestAccTemplateDataSource(t *testing.T) { resource.TestCheckResourceAttr("data.coderd_template.test", "icon", tpl.Icon), resource.TestCheckResourceAttr("data.coderd_template.test", "default_ttl_ms", strconv.FormatInt(tpl.DefaultTTLMillis, 10)), resource.TestCheckResourceAttr("data.coderd_template.test", "activity_bump_ms", strconv.FormatInt(tpl.ActivityBumpMillis, 10)), + resource.TestCheckResourceAttr("data.coderd_template.test", "auto_stop_requirement.days_of_week.#", strconv.FormatInt(int64(len(tpl.AutostopRequirement.DaysOfWeek)), 10)), + resource.TestCheckResourceAttr("data.coderd_template.test", "auto_stop_requirement.weeks", strconv.FormatInt(tpl.AutostopRequirement.Weeks, 10)), + resource.TestCheckResourceAttr("data.coderd_template.test", "auto_start_permitted_days_of_week.#", strconv.FormatInt(int64(len(tpl.AutostartRequirement.DaysOfWeek)), 10)), + resource.TestCheckResourceAttr("data.coderd_template.test", "allow_user_cancel_workspace_jobs", "true"), resource.TestCheckResourceAttr("data.coderd_template.test", "allow_user_autostart", strconv.FormatBool(tpl.AllowUserAutostart)), resource.TestCheckResourceAttr("data.coderd_template.test", "allow_user_autostop", strconv.FormatBool(tpl.AllowUserAutostop)), resource.TestCheckResourceAttr("data.coderd_template.test", "allow_user_cancel_workspace_jobs", strconv.FormatBool(tpl.AllowUserCancelWorkspaceJobs)), @@ -119,9 +133,20 @@ func TestAccTemplateDataSource(t *testing.T) { resource.TestCheckResourceAttr("data.coderd_template.test", "time_til_dormant_ms", strconv.FormatInt(tpl.TimeTilDormantMillis, 10)), resource.TestCheckResourceAttr("data.coderd_template.test", "time_til_dormant_autodelete_ms", strconv.FormatInt(tpl.TimeTilDormantAutoDeleteMillis, 10)), resource.TestCheckResourceAttr("data.coderd_template.test", "require_active_version", strconv.FormatBool(tpl.RequireActiveVersion)), + resource.TestCheckResourceAttr("data.coderd_template.test", "max_port_share_level", string(tpl.MaxPortShareLevel)), resource.TestCheckResourceAttr("data.coderd_template.test", "created_by_user_id", firstUser.ID.String()), resource.TestCheckResourceAttr("data.coderd_template.test", "created_at", strconv.Itoa(int(tpl.CreatedAt.Unix()))), resource.TestCheckResourceAttr("data.coderd_template.test", "updated_at", strconv.Itoa(int(tpl.UpdatedAt.Unix()))), + resource.TestCheckResourceAttr("data.coderd_template.test", "acl.groups.#", "1"), + resource.TestCheckResourceAttr("data.coderd_template.test", "acl.users.#", "1"), + resource.TestMatchTypeSetElemNestedAttrs("data.coderd_template.test", "acl.groups.*", map[string]*regexp.Regexp{ + "id": regexp.MustCompile(firstUser.OrganizationIDs[0].String()), + "role": regexp.MustCompile("^use$"), + }), + resource.TestMatchTypeSetElemNestedAttrs("data.coderd_template.test", "acl.users.*", map[string]*regexp.Regexp{ + "id": regexp.MustCompile(firstUser.ID.String()), + "role": regexp.MustCompile("^admin$"), + }), ) t.Run("TemplateByOrgAndNameOK", func(t *testing.T) { diff --git a/internal/provider/template_resource.go b/internal/provider/template_resource.go index b643cb3..3bdde6d 100644 --- a/internal/provider/template_resource.go +++ b/internal/provider/template_resource.go @@ -361,7 +361,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques Default: booldefault.StaticBool(false), }, "deprecation_message": schema.StringAttribute{ - MarkdownDescription: "If set, the template will be marked as deprecated with the provided message and users will be blocked from creating new workspaces from it.", + MarkdownDescription: "If set, the template will be marked as deprecated with the provided message and users will be blocked from creating new workspaces from it. Does nothing if set when the resource is created.", Optional: true, Computed: true, Default: stringdefault.StaticString(""), @@ -586,8 +586,8 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r } tfACL := convertResponseToACL(acl) aclObj, diag := types.ObjectValueFrom(ctx, aclTypeAttr, tfACL) - diag.Append(diag...) if diag.HasError() { + resp.Diagnostics.Append(diag...) return } data.ACL = aclObj