Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/data-sources/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ resource "coderd_template" "debian-main" {
- `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.
- `agents_allowed` (Boolean) Whether Coder Agents can create workspaces from the template. Requires a Coder deployment running v2.37.0 or later.
- `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.
Expand Down
1 change: 1 addition & 0 deletions docs/resources/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ resource "coderd_template" "ubuntu-main" {

- `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.
- `agents_allowed` (Boolean) Whether Coder Agents can create workspaces from this template. Coder defaults this setting to true. Requires a Coder deployment running v2.37.0 or later.
- `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-stop workspaces created from this template. Defaults to true.
- `allow_user_cancel_workspace_jobs` (Boolean) Whether users can cancel in-progress workspace jobs using this template. Defaults to true.
Expand Down
6 changes: 6 additions & 0 deletions internal/provider/template_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type TemplateDataSourceModel struct {
TimeTilDormantAutoDeleteMillis types.Int64 `tfsdk:"time_til_dormant_autodelete_ms"`

RequireActiveVersion types.Bool `tfsdk:"require_active_version"`
AgentsAllowed types.Bool `tfsdk:"agents_allowed"`
MaxPortShareLevel types.String `tfsdk:"max_port_share_level"`
CORSBehavior types.String `tfsdk:"cors_behavior"`

Expand Down Expand Up @@ -185,6 +186,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,
},
"agents_allowed": schema.BoolAttribute{
MarkdownDescription: fmt.Sprintf("Whether Coder Agents can create workspaces from the template. Requires a Coder deployment running v%s or later.", templateAgentsAllowedMinVersion),
Comment thread
ethanndickson marked this conversation as resolved.
Computed: true,
},
"max_port_share_level": schema.StringAttribute{
MarkdownDescription: "The maximum port share level for workspaces created from the template.",
Computed: true,
Expand Down Expand Up @@ -334,6 +339,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.AgentsAllowed = types.BoolValue(template.AgentsAllowed)
Comment thread
ethanndickson marked this conversation as resolved.
data.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel))
data.CORSBehavior = stringValueOrNull(string(template.CORSBehavior))
data.CreatedByUserID = UUIDValue(template.CreatedByID)
Expand Down
14 changes: 12 additions & 2 deletions internal/provider/template_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"

"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
Expand All @@ -29,6 +30,10 @@ func TestAccTemplateDataSource(t *testing.T) {
require.NoError(t, err)
orgID := firstUser.OrganizationIDs[0]

buildInfo, err := client.BuildInfo(ctx)
require.NoError(t, err, "fetch buildinfo")
supportsAgentsAllowed := semver.Compare(buildInfo.CanonicalVersion(), "v"+templateAgentsAllowedMinVersion) >= 0

version, _, err := newVersion(ctx, client, newVersionRequest{
OrganizationID: orgID,
Version: &TemplateVersion{
Expand Down Expand Up @@ -67,6 +72,7 @@ func TestAccTemplateDataSource(t *testing.T) {
TimeTilDormantAutoDeleteMillis: ptr.Ref((30 * 24 * time.Hour).Milliseconds()),
DisableEveryoneGroupAccess: true,
RequireActiveVersion: true,
AgentsAllowed: ptr.Ref(false),
})
require.NoError(t, err)

Expand Down Expand Up @@ -110,7 +116,7 @@ func TestAccTemplateDataSource(t *testing.T) {
})
require.NoError(t, err)

checkFn := resource.ComposeAggregateTestCheckFunc(
checks := []resource.TestCheckFunc{
resource.TestCheckResourceAttr("data.coderd_template.test", "organization_id", tpl.OrganizationID.String()),
resource.TestCheckResourceAttr("data.coderd_template.test", "id", tpl.ID.String()),
resource.TestCheckResourceAttr("data.coderd_template.test", "name", tpl.Name),
Expand Down Expand Up @@ -149,7 +155,11 @@ func TestAccTemplateDataSource(t *testing.T) {
"id": regexp.MustCompile(firstUser.ID.String()),
"role": regexp.MustCompile("^admin$"),
}),
)
}
if supportsAgentsAllowed {
checks = append(checks, resource.TestCheckResourceAttr("data.coderd_template.test", "agents_allowed", strconv.FormatBool(tpl.AgentsAllowed)))
}
checkFn := resource.ComposeAggregateTestCheckFunc(checks...)

t.Run("TemplateByOrgAndNameOK", func(t *testing.T) {
cfg := testAccTemplateDataSourceConfig{
Expand Down
45 changes: 37 additions & 8 deletions internal/provider/template_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ var (
_ resource.ResourceWithConfigValidators = &TemplateResource{}
)

const templateAgentsAllowedMinVersion = "2.37.0"

func NewTemplateResource() resource.Resource {
return &TemplateResource{}
}
Expand Down Expand Up @@ -79,6 +81,7 @@ type TemplateResourceModel struct {
MaxPortShareLevel types.String `tfsdk:"max_port_share_level"`
CORSBehavior types.String `tfsdk:"cors_behavior"`
UseClassicParameterFlow types.Bool `tfsdk:"use_classic_parameter_flow"`
AgentsAllowed types.Bool `tfsdk:"agents_allowed"`

// If null, we are not managing ACL via Terraform (such as for AGPL).
ACL types.Object `tfsdk:"acl"`
Expand Down Expand Up @@ -106,7 +109,8 @@ func (m *TemplateResourceModel) EqualTemplateMetadata(other *TemplateResourceMod
m.DeprecationMessage.Equal(other.DeprecationMessage) &&
m.MaxPortShareLevel.Equal(other.MaxPortShareLevel) &&
m.CORSBehavior.Equal(other.CORSBehavior) &&
m.UseClassicParameterFlow.Equal(other.UseClassicParameterFlow)
m.UseClassicParameterFlow.Equal(other.UseClassicParameterFlow) &&
m.AgentsAllowed.Equal(other.AgentsAllowed)
}

func (m *TemplateResourceModel) CheckEntitlements(ctx context.Context, features map[codersdk.FeatureName]codersdk.Feature) (diags diag.Diagnostics) {
Expand Down Expand Up @@ -467,6 +471,14 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
boolplanmodifier.UseStateForUnknown(),
},
},
"agents_allowed": schema.BoolAttribute{
MarkdownDescription: fmt.Sprintf("Whether Coder Agents can create workspaces from this template. Coder defaults this setting to true. Requires a Coder deployment running v%s or later.", templateAgentsAllowedMinVersion),
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"acl": schema.SingleNestedAttribute{
MarkdownDescription: "(Enterprise) Access control list for the template. If null, ACL policies will not be added, removed, or read by Terraform.",
Optional: true,
Expand Down Expand Up @@ -690,6 +702,14 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
data.UseClassicParameterFlow = types.BoolValue(ucpfResp.UseClassicParameterFlow)
}

// Fetch the authoritative values after the compatibility updates.
authoritativeTemplate, err := client.Template(ctx, data.ID.ValueUUID())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template: %s", err))
return
}
data.reconcileVersionedMetadata(&authoritativeTemplate)

resp.Diagnostics.Append(data.Versions.setPrivateState(ctx, resp.Private)...)
if resp.Diagnostics.HasError() {
return
Expand Down Expand Up @@ -728,9 +748,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
resp.Diagnostics.Append(diag...)
return
}
data.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel))
data.CORSBehavior = stringValueOrNull(string(template.CORSBehavior))
data.UseClassicParameterFlow = types.BoolValue(template.UseClassicParameterFlow)
data.reconcileVersionedMetadata(&template)

if !data.ACL.IsNull() {
tflog.Info(ctx, "reading template ACL")
Expand Down Expand Up @@ -905,8 +923,7 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template: %s", err))
return
}
newState.MaxPortShareLevel = types.StringValue(string(templateResp.MaxPortShareLevel))
newState.CORSBehavior = stringValueOrNull(string(templateResp.CORSBehavior))
newState.reconcileVersionedMetadata(&templateResp)
Comment thread
ethanndickson marked this conversation as resolved.

resp.Diagnostics.Append(newState.Versions.setPrivateState(ctx, resp.Private)...)
if resp.Diagnostics.HasError() {
Expand Down Expand Up @@ -1416,6 +1433,16 @@ func convertResponseToACL(acl codersdk.TemplateACL) ACL {
}
}

// reconcileVersionedMetadata overwrites metadata fields that older Coder
// servers can omit from mutation responses. Call it with an authoritative
// template response before writing state.
func (r *TemplateResourceModel) reconcileVersionedMetadata(template *codersdk.Template) {
Comment thread
ethanndickson marked this conversation as resolved.
r.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel))
r.CORSBehavior = stringValueOrNull(string(template.CORSBehavior))
r.UseClassicParameterFlow = types.BoolValue(template.UseClassicParameterFlow)
r.AgentsAllowed = types.BoolValue(template.AgentsAllowed)
}

func (r *TemplateResourceModel) readResponse(ctx context.Context, template *codersdk.Template) diag.Diagnostics {
r.Name = types.StringValue(template.Name)
r.DisplayName = types.StringValue(template.DisplayName)
Expand Down Expand Up @@ -1491,7 +1518,8 @@ func (r *TemplateResourceModel) toUpdateRequest(ctx context.Context, diag *diag.
DeprecationMessage: r.DeprecationMessage.ValueStringPointer(),
MaxPortShareLevel: ptr.Ref(codersdk.WorkspaceAgentPortShareLevel(r.MaxPortShareLevel.ValueString())),
CORSBehavior: corsPtr(r.CORSBehavior),
UseClassicParameterFlow: r.UseClassicParameterFlow.ValueBoolPointer(),
UseClassicParameterFlow: boolPtrOrNil(r.UseClassicParameterFlow),
AgentsAllowed: boolPtrOrNil(r.AgentsAllowed),
Comment thread
ethanndickson marked this conversation as resolved.
// If we're managing ACL, we want to delete the everyone group.
DisableEveryoneGroupAccess: ptr.Ref(!r.ACL.IsNull()),
}
Expand Down Expand Up @@ -1536,7 +1564,8 @@ func (r *TemplateResourceModel) toCreateRequest(ctx context.Context, resp *resou
TimeTilDormantMillis: r.TimeTilDormantMillis.ValueInt64Pointer(),
TimeTilDormantAutoDeleteMillis: r.TimeTilDormantAutoDeleteMillis.ValueInt64Pointer(),
RequireActiveVersion: r.RequireActiveVersion.ValueBool(),
UseClassicParameterFlow: r.UseClassicParameterFlow.ValueBoolPointer(),
UseClassicParameterFlow: boolPtrOrNil(r.UseClassicParameterFlow),
AgentsAllowed: boolPtrOrNil(r.AgentsAllowed),
CORSBehavior: corsPtr(r.CORSBehavior),
DisableEveryoneGroupAccess: !r.ACL.IsNull(),
}
Expand Down
Loading