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 @@ -56,6 +56,7 @@ resource "coderd_template" "debian-main" {
- `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))
- `cors_behavior` (String) The CORS behavior for workspace apps in this template. Requires a Coder deployment running v2.26.0 or later.
- `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.
Expand Down
1 change: 1 addition & 0 deletions docs/resources/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ resource "coderd_template" "ubuntu-main" {
- `allow_user_cancel_workspace_jobs` (Boolean) Whether users can cancel in-progress workspace jobs using this template. Defaults to true.
- `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))
- `cors_behavior` (String) The CORS behavior for workspace apps in this template. Valid values are `simple` (default CORS middleware) or `passthru` (bypass CORS middleware). Defaults to `simple`. Requires a Coder deployment running v2.26.0 or later.
- `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. Does nothing if set when the resource is created.
- `description` (String) A description of the template.
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 @@ -58,6 +58,7 @@ type TemplateDataSourceModel struct {

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

CreatedByUserID UUID `tfsdk:"created_by_user_id"`
CreatedAt types.Int64 `tfsdk:"created_at"` // Unix timestamp
Expand Down Expand Up @@ -188,6 +189,10 @@ func (d *TemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRe
MarkdownDescription: "The maximum port share level for workspaces created from the template.",
Computed: true,
},
"cors_behavior": schema.StringAttribute{
MarkdownDescription: "The CORS behavior for workspace apps in this template. Requires a Coder deployment running v2.26.0 or later.",
Computed: true,
},
"created_by_user_id": schema.StringAttribute{
MarkdownDescription: "ID of the user who created the template.",
CustomType: UUIDType,
Expand Down Expand Up @@ -330,6 +335,7 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques
data.TimeTilDormantAutoDeleteMillis = types.Int64Value(template.TimeTilDormantAutoDeleteMillis)
data.RequireActiveVersion = types.BoolValue(template.RequireActiveVersion)
data.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel))
data.CORSBehavior = stringValueOrNull(string(template.CORSBehavior))
data.CreatedByUserID = UUIDValue(template.CreatedByID)
data.CreatedAt = types.Int64Value(template.CreatedAt.Unix())
data.UpdatedAt = types.Int64Value(template.UpdatedAt.Unix())
Expand Down
1 change: 1 addition & 0 deletions internal/provider/template_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ func TestAccTemplateDataSource(t *testing.T) {
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", "cors_behavior", string(tpl.CORSBehavior)),
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()))),
Expand Down
20 changes: 20 additions & 0 deletions internal/provider/template_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type TemplateResourceModel struct {
RequireActiveVersion types.Bool `tfsdk:"require_active_version"`
DeprecationMessage types.String `tfsdk:"deprecation_message"`
MaxPortShareLevel types.String `tfsdk:"max_port_share_level"`
CORSBehavior types.String `tfsdk:"cors_behavior"`
UseClassicParameterFlow types.Bool `tfsdk:"use_classic_parameter_flow"`

// If null, we are not managing ACL via Terraform (such as for AGPL).
Expand Down Expand Up @@ -100,6 +101,7 @@ func (m *TemplateResourceModel) EqualTemplateMetadata(other *TemplateResourceMod
m.RequireActiveVersion.Equal(other.RequireActiveVersion) &&
m.DeprecationMessage.Equal(other.DeprecationMessage) &&
m.MaxPortShareLevel.Equal(other.MaxPortShareLevel) &&
m.CORSBehavior.Equal(other.CORSBehavior) &&
m.UseClassicParameterFlow.Equal(other.UseClassicParameterFlow)
}

Expand Down Expand Up @@ -398,6 +400,17 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
Computed: true,
Default: stringdefault.StaticString(""),
},
"cors_behavior": schema.StringAttribute{
MarkdownDescription: "The CORS behavior for workspace apps in this template. Valid values are `simple` (default CORS middleware) or `passthru` (bypass CORS middleware). Defaults to `simple`. Requires a Coder deployment running v2.26.0 or later.",
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(string(codersdk.CORSBehaviorSimple), string(codersdk.CORSBehaviorPassthru)),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"use_classic_parameter_flow": schema.BoolAttribute{
MarkdownDescription: "If true, the classic parameter flow will be used when creating workspaces from this template. Defaults to false.",
Optional: true,
Expand Down Expand Up @@ -602,6 +615,9 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
data.MaxPortShareLevel = types.StringValue(string(mpslResp.MaxPortShareLevel))
}

// Set cors_behavior from the response (it's set during create via toCreateRequest)
data.CORSBehavior = stringValueOrNull(string(templateResp.CORSBehavior))

// TODO: Remove this update call (and the attribute) once the provider
// requires a Coder version where this flag has been removed.
if data.UseClassicParameterFlow.IsUnknown() {
Expand Down Expand Up @@ -660,6 +676,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
return
}
data.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel))
data.CORSBehavior = stringValueOrNull(string(template.CORSBehavior))
data.UseClassicParameterFlow = types.BoolValue(template.UseClassicParameterFlow)

if !data.ACL.IsNull() {
Expand Down Expand Up @@ -836,6 +853,7 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
return
}
newState.MaxPortShareLevel = types.StringValue(string(templateResp.MaxPortShareLevel))
newState.CORSBehavior = stringValueOrNull(string(templateResp.CORSBehavior))

resp.Diagnostics.Append(newState.Versions.setPrivateState(ctx, resp.Private)...)
if resp.Diagnostics.HasError() {
Expand Down Expand Up @@ -1331,6 +1349,7 @@ func (r *TemplateResourceModel) toUpdateRequest(ctx context.Context, diag *diag.
RequireActiveVersion: r.RequireActiveVersion.ValueBool(),
DeprecationMessage: r.DeprecationMessage.ValueStringPointer(),
MaxPortShareLevel: ptr.Ref(codersdk.WorkspaceAgentPortShareLevel(r.MaxPortShareLevel.ValueString())),
CORSBehavior: corsPtr(r.CORSBehavior),
UseClassicParameterFlow: ptr.Ref(r.UseClassicParameterFlow.ValueBool()),
// If we're managing ACL, we want to delete the everyone group
DisableEveryoneGroupAccess: !r.ACL.IsNull(),
Expand Down Expand Up @@ -1377,6 +1396,7 @@ func (r *TemplateResourceModel) toCreateRequest(ctx context.Context, resp *resou
TimeTilDormantAutoDeleteMillis: r.TimeTilDormantAutoDeleteMillis.ValueInt64Pointer(),
RequireActiveVersion: r.RequireActiveVersion.ValueBool(),
UseClassicParameterFlow: r.UseClassicParameterFlow.ValueBoolPointer(),
CORSBehavior: corsPtr(r.CORSBehavior),
DisableEveryoneGroupAccess: !r.ACL.IsNull(),
}
}
Expand Down
51 changes: 51 additions & 0 deletions internal/provider/template_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -693,10 +693,12 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
cfg2 := cfg1
cfg2.ACL.GroupACL = slices.Clone(cfg2.ACL.GroupACL[1:])
cfg2.MaxPortShareLevel = ptr.Ref("owner")
cfg2.CORSBehavior = ptr.Ref("passthru")

cfg3 := cfg2
cfg3.ACL.null = true
cfg3.MaxPortShareLevel = ptr.Ref("public")
cfg3.CORSBehavior = ptr.Ref("simple")

cfg4 := cfg3
cfg4.AllowUserAutostart = ptr.Ref(false)
Expand All @@ -714,6 +716,7 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
Config: cfg1.String(t),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "owner"),
resource.TestCheckResourceAttr("coderd_template.test", "cors_behavior", "simple"),
resource.TestCheckResourceAttr("coderd_template.test", "acl.groups.#", "2"),
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.groups.*", map[string]*regexp.Regexp{
"id": regexp.MustCompile(firstUser.OrganizationIDs[0].String()),
Expand All @@ -734,6 +737,7 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
Config: cfg2.String(t),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "owner"),
resource.TestCheckResourceAttr("coderd_template.test", "cors_behavior", "passthru"),
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.users.*", map[string]*regexp.Regexp{
"id": regexp.MustCompile(firstUser.ID.String()),
"role": regexp.MustCompile("^admin$"),
Expand All @@ -744,6 +748,7 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
Config: cfg3.String(t),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "public"),
resource.TestCheckResourceAttr("coderd_template.test", "cors_behavior", "simple"),
resource.TestCheckNoResourceAttr("coderd_template.test", "acl"),
func(s *terraform.State) error {
templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
Expand Down Expand Up @@ -815,6 +820,50 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
})
}

func TestAccTemplateResourceBackCompat(t *testing.T) {
t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
ctx := t.Context()
// Coder 2.25 does not support cors_behavior. Verify that not setting it works.
client := integration.StartCoder(ctx, t, "tmpl_back_compat_acc", integration.CoderVersion("v2.25.0"))

exTemplateOne := t.TempDir()
err := cp.Copy("../../integration/template-test/example-template", exTemplateOne)
require.NoError(t, err)

cfg1 := testAccTemplateResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
Name: ptr.Ref("example-template"),
Versions: []testAccTemplateVersionConfig{
{
Directory: &exTemplateOne,
Active: ptr.Ref(true),
},
},
ACL: testAccTemplateACLConfig{
null: true,
},
}

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IsUnitTest: true,
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: cfg1.String(t),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("coderd_template.test", "id"),
resource.TestCheckNoResourceAttr("coderd_template.test", "cors_behavior"),
),
},
},
})
}

func TestAccTemplateResourceAGPL(t *testing.T) {
t.Parallel()
if os.Getenv("TF_ACC") == "" {
Expand Down Expand Up @@ -992,6 +1041,7 @@ type testAccTemplateResourceConfig struct {
RequireActiveVersion *bool
DeprecationMessage *string
MaxPortShareLevel *string
CORSBehavior *string
UseClassicParameterFlow *bool

Versions []testAccTemplateVersionConfig
Expand Down Expand Up @@ -1100,6 +1150,7 @@ resource "coderd_template" "test" {
require_active_version = {{orNull .RequireActiveVersion}}
deprecation_message = {{orNull .DeprecationMessage}}
max_port_share_level = {{orNull .MaxPortShareLevel}}
cors_behavior = {{orNull .CORSBehavior}}
use_classic_parameter_flow = {{orNull .UseClassicParameterFlow}}

acl = ` + c.ACL.String(t) + `
Expand Down
21 changes: 21 additions & 0 deletions internal/provider/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (

"github.com/coder/coder/v2/codersdk"
"github.com/google/uuid"

"github.com/hashicorp/terraform-plugin-framework/types"
)

func PrintOrNull(v any) string {
Expand Down Expand Up @@ -123,3 +125,22 @@ func isNotFound(err error) bool {
}
return false
}

// stringValueOrNull returns types.StringNull() if s is empty,
// otherwise types.StringValue(s).
func stringValueOrNull(s string) types.String {
if s == "" {
return types.StringNull()
}
return types.StringValue(s)
}

// corsPtr returns a pointer to a CORSBehavior if the value is known and not empty,
// otherwise returns nil (which will use the server default).
func corsPtr(v types.String) *codersdk.CORSBehavior {
if v.IsNull() || v.IsUnknown() || v.ValueString() == "" {
return nil
}
b := codersdk.CORSBehavior(v.ValueString())
return &b
}