Skip to content

Commit 064e64e

Browse files
blinkagent[bot]blink-so[bot]ethanndickson
authored
feat: add cors_behavior support to template resource (#294)
## Summary Adds support for the `cors_behavior` field in the `coderd_template` resource to allow configuring CORS settings for workspace apps via Terraform. ## Changes ### Template Resource - Added `cors_behavior` field to `TemplateResourceModel` struct - Added schema attribute with validation (`simple` or `passthru`) - Handle `cors_behavior` in Create, Read, and Update operations - Added `cors_behavior` to `toUpdateRequest` for API calls - Added `cors_behavior` to `EqualTemplateMetadata` comparison ### Template Data Source - Added `cors_behavior` field to data source struct and schema - Added reading `cors_behavior` in the Read function ## Usage ```hcl resource "coderd_template" "example" { name = "example" cors_behavior = "passthru" # or "simple" (default) versions { # ... } } ``` ## Background The Coder backend supports a `cors_behavior` field on templates with two possible values: - `simple` - uses the default CORS middleware - `passthru` - bypasses CORS middleware for workspace apps This setting is available in the Template Settings page in the Coder UI but was not previously configurable via the Terraform provider. Fixes #293 --- Created on behalf of @matifali --------- Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: ethan <ethan@coder.com>
1 parent 96ca475 commit 064e64e

7 files changed

Lines changed: 101 additions & 0 deletions

File tree

docs/data-sources/template.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ resource "coderd_template" "debian-main" {
5656
- `allow_user_cancel_workspace_jobs` (Boolean) Whether users can cancel jobs in workspaces created from the template.
5757
- `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.
5858
- `auto_stop_requirement` (Attributes) The auto-stop requirement for all workspaces created from this template. (see [below for nested schema](#nestedatt--auto_stop_requirement))
59+
- `cors_behavior` (String) The CORS behavior for workspace apps in this template. Requires a Coder deployment running v2.26.0 or later.
5960
- `created_at` (Number) Unix timestamp of when the template was created.
6061
- `created_by_user_id` (String) ID of the user who created the template.
6162
- `default_ttl_ms` (Number) Default time-to-live for workspaces created from the template.

docs/resources/template.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ resource "coderd_template" "ubuntu-main" {
7272
- `allow_user_cancel_workspace_jobs` (Boolean) Whether users can cancel in-progress workspace jobs using this template. Defaults to true.
7373
- `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.
7474
- `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))
75+
- `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.
7576
- `default_ttl_ms` (Number) The default time-to-live for all workspaces created from this template, in milliseconds.
7677
- `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.
7778
- `description` (String) A description of the template.

internal/provider/template_data_source.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type TemplateDataSourceModel struct {
5858

5959
RequireActiveVersion types.Bool `tfsdk:"require_active_version"`
6060
MaxPortShareLevel types.String `tfsdk:"max_port_share_level"`
61+
CORSBehavior types.String `tfsdk:"cors_behavior"`
6162

6263
CreatedByUserID UUID `tfsdk:"created_by_user_id"`
6364
CreatedAt types.Int64 `tfsdk:"created_at"` // Unix timestamp
@@ -188,6 +189,10 @@ func (d *TemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRe
188189
MarkdownDescription: "The maximum port share level for workspaces created from the template.",
189190
Computed: true,
190191
},
192+
"cors_behavior": schema.StringAttribute{
193+
MarkdownDescription: "The CORS behavior for workspace apps in this template. Requires a Coder deployment running v2.26.0 or later.",
194+
Computed: true,
195+
},
191196
"created_by_user_id": schema.StringAttribute{
192197
MarkdownDescription: "ID of the user who created the template.",
193198
CustomType: UUIDType,
@@ -330,6 +335,7 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques
330335
data.TimeTilDormantAutoDeleteMillis = types.Int64Value(template.TimeTilDormantAutoDeleteMillis)
331336
data.RequireActiveVersion = types.BoolValue(template.RequireActiveVersion)
332337
data.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel))
338+
data.CORSBehavior = stringValueOrNull(string(template.CORSBehavior))
333339
data.CreatedByUserID = UUIDValue(template.CreatedByID)
334340
data.CreatedAt = types.Int64Value(template.CreatedAt.Unix())
335341
data.UpdatedAt = types.Int64Value(template.UpdatedAt.Unix())

internal/provider/template_data_source_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ func TestAccTemplateDataSource(t *testing.T) {
135135
resource.TestCheckResourceAttr("data.coderd_template.test", "time_til_dormant_autodelete_ms", strconv.FormatInt(tpl.TimeTilDormantAutoDeleteMillis, 10)),
136136
resource.TestCheckResourceAttr("data.coderd_template.test", "require_active_version", strconv.FormatBool(tpl.RequireActiveVersion)),
137137
resource.TestCheckResourceAttr("data.coderd_template.test", "max_port_share_level", string(tpl.MaxPortShareLevel)),
138+
resource.TestCheckResourceAttr("data.coderd_template.test", "cors_behavior", string(tpl.CORSBehavior)),
138139
resource.TestCheckResourceAttr("data.coderd_template.test", "created_by_user_id", firstUser.ID.String()),
139140
resource.TestCheckResourceAttr("data.coderd_template.test", "created_at", strconv.Itoa(int(tpl.CreatedAt.Unix()))),
140141
resource.TestCheckResourceAttr("data.coderd_template.test", "updated_at", strconv.Itoa(int(tpl.UpdatedAt.Unix()))),

internal/provider/template_resource.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type TemplateResourceModel struct {
7373
RequireActiveVersion types.Bool `tfsdk:"require_active_version"`
7474
DeprecationMessage types.String `tfsdk:"deprecation_message"`
7575
MaxPortShareLevel types.String `tfsdk:"max_port_share_level"`
76+
CORSBehavior types.String `tfsdk:"cors_behavior"`
7677
UseClassicParameterFlow types.Bool `tfsdk:"use_classic_parameter_flow"`
7778

7879
// If null, we are not managing ACL via Terraform (such as for AGPL).
@@ -100,6 +101,7 @@ func (m *TemplateResourceModel) EqualTemplateMetadata(other *TemplateResourceMod
100101
m.RequireActiveVersion.Equal(other.RequireActiveVersion) &&
101102
m.DeprecationMessage.Equal(other.DeprecationMessage) &&
102103
m.MaxPortShareLevel.Equal(other.MaxPortShareLevel) &&
104+
m.CORSBehavior.Equal(other.CORSBehavior) &&
103105
m.UseClassicParameterFlow.Equal(other.UseClassicParameterFlow)
104106
}
105107

@@ -398,6 +400,17 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
398400
Computed: true,
399401
Default: stringdefault.StaticString(""),
400402
},
403+
"cors_behavior": schema.StringAttribute{
404+
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.",
405+
Optional: true,
406+
Computed: true,
407+
Validators: []validator.String{
408+
stringvalidator.OneOfCaseInsensitive(string(codersdk.CORSBehaviorSimple), string(codersdk.CORSBehaviorPassthru)),
409+
},
410+
PlanModifiers: []planmodifier.String{
411+
stringplanmodifier.UseStateForUnknown(),
412+
},
413+
},
401414
"use_classic_parameter_flow": schema.BoolAttribute{
402415
MarkdownDescription: "If true, the classic parameter flow will be used when creating workspaces from this template. Defaults to false.",
403416
Optional: true,
@@ -602,6 +615,9 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
602615
data.MaxPortShareLevel = types.StringValue(string(mpslResp.MaxPortShareLevel))
603616
}
604617

618+
// Set cors_behavior from the response (it's set during create via toCreateRequest)
619+
data.CORSBehavior = stringValueOrNull(string(templateResp.CORSBehavior))
620+
605621
// TODO: Remove this update call (and the attribute) once the provider
606622
// requires a Coder version where this flag has been removed.
607623
if data.UseClassicParameterFlow.IsUnknown() {
@@ -660,6 +676,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
660676
return
661677
}
662678
data.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel))
679+
data.CORSBehavior = stringValueOrNull(string(template.CORSBehavior))
663680
data.UseClassicParameterFlow = types.BoolValue(template.UseClassicParameterFlow)
664681

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

840858
resp.Diagnostics.Append(newState.Versions.setPrivateState(ctx, resp.Private)...)
841859
if resp.Diagnostics.HasError() {
@@ -1331,6 +1349,7 @@ func (r *TemplateResourceModel) toUpdateRequest(ctx context.Context, diag *diag.
13311349
RequireActiveVersion: r.RequireActiveVersion.ValueBool(),
13321350
DeprecationMessage: r.DeprecationMessage.ValueStringPointer(),
13331351
MaxPortShareLevel: ptr.Ref(codersdk.WorkspaceAgentPortShareLevel(r.MaxPortShareLevel.ValueString())),
1352+
CORSBehavior: corsPtr(r.CORSBehavior),
13341353
UseClassicParameterFlow: ptr.Ref(r.UseClassicParameterFlow.ValueBool()),
13351354
// If we're managing ACL, we want to delete the everyone group
13361355
DisableEveryoneGroupAccess: !r.ACL.IsNull(),
@@ -1377,6 +1396,7 @@ func (r *TemplateResourceModel) toCreateRequest(ctx context.Context, resp *resou
13771396
TimeTilDormantAutoDeleteMillis: r.TimeTilDormantAutoDeleteMillis.ValueInt64Pointer(),
13781397
RequireActiveVersion: r.RequireActiveVersion.ValueBool(),
13791398
UseClassicParameterFlow: r.UseClassicParameterFlow.ValueBoolPointer(),
1399+
CORSBehavior: corsPtr(r.CORSBehavior),
13801400
DisableEveryoneGroupAccess: !r.ACL.IsNull(),
13811401
}
13821402
}

internal/provider/template_resource_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,10 +693,12 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
693693
cfg2 := cfg1
694694
cfg2.ACL.GroupACL = slices.Clone(cfg2.ACL.GroupACL[1:])
695695
cfg2.MaxPortShareLevel = ptr.Ref("owner")
696+
cfg2.CORSBehavior = ptr.Ref("passthru")
696697

697698
cfg3 := cfg2
698699
cfg3.ACL.null = true
699700
cfg3.MaxPortShareLevel = ptr.Ref("public")
701+
cfg3.CORSBehavior = ptr.Ref("simple")
700702

701703
cfg4 := cfg3
702704
cfg4.AllowUserAutostart = ptr.Ref(false)
@@ -714,6 +716,7 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
714716
Config: cfg1.String(t),
715717
Check: resource.ComposeAggregateTestCheckFunc(
716718
resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "owner"),
719+
resource.TestCheckResourceAttr("coderd_template.test", "cors_behavior", "simple"),
717720
resource.TestCheckResourceAttr("coderd_template.test", "acl.groups.#", "2"),
718721
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.groups.*", map[string]*regexp.Regexp{
719722
"id": regexp.MustCompile(firstUser.OrganizationIDs[0].String()),
@@ -734,6 +737,7 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
734737
Config: cfg2.String(t),
735738
Check: resource.ComposeAggregateTestCheckFunc(
736739
resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "owner"),
740+
resource.TestCheckResourceAttr("coderd_template.test", "cors_behavior", "passthru"),
737741
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.users.*", map[string]*regexp.Regexp{
738742
"id": regexp.MustCompile(firstUser.ID.String()),
739743
"role": regexp.MustCompile("^admin$"),
@@ -744,6 +748,7 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
744748
Config: cfg3.String(t),
745749
Check: resource.ComposeAggregateTestCheckFunc(
746750
resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "public"),
751+
resource.TestCheckResourceAttr("coderd_template.test", "cors_behavior", "simple"),
747752
resource.TestCheckNoResourceAttr("coderd_template.test", "acl"),
748753
func(s *terraform.State) error {
749754
templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
@@ -815,6 +820,50 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
815820
})
816821
}
817822

823+
func TestAccTemplateResourceBackCompat(t *testing.T) {
824+
t.Parallel()
825+
if os.Getenv("TF_ACC") == "" {
826+
t.Skip("Acceptance tests are disabled.")
827+
}
828+
ctx := t.Context()
829+
// Coder 2.25 does not support cors_behavior. Verify that not setting it works.
830+
client := integration.StartCoder(ctx, t, "tmpl_back_compat_acc", integration.CoderVersion("v2.25.0"))
831+
832+
exTemplateOne := t.TempDir()
833+
err := cp.Copy("../../integration/template-test/example-template", exTemplateOne)
834+
require.NoError(t, err)
835+
836+
cfg1 := testAccTemplateResourceConfig{
837+
URL: client.URL.String(),
838+
Token: client.SessionToken(),
839+
Name: ptr.Ref("example-template"),
840+
Versions: []testAccTemplateVersionConfig{
841+
{
842+
Directory: &exTemplateOne,
843+
Active: ptr.Ref(true),
844+
},
845+
},
846+
ACL: testAccTemplateACLConfig{
847+
null: true,
848+
},
849+
}
850+
851+
resource.Test(t, resource.TestCase{
852+
PreCheck: func() { testAccPreCheck(t) },
853+
IsUnitTest: true,
854+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
855+
Steps: []resource.TestStep{
856+
{
857+
Config: cfg1.String(t),
858+
Check: resource.ComposeTestCheckFunc(
859+
resource.TestCheckResourceAttrSet("coderd_template.test", "id"),
860+
resource.TestCheckNoResourceAttr("coderd_template.test", "cors_behavior"),
861+
),
862+
},
863+
},
864+
})
865+
}
866+
818867
func TestAccTemplateResourceAGPL(t *testing.T) {
819868
t.Parallel()
820869
if os.Getenv("TF_ACC") == "" {
@@ -992,6 +1041,7 @@ type testAccTemplateResourceConfig struct {
9921041
RequireActiveVersion *bool
9931042
DeprecationMessage *string
9941043
MaxPortShareLevel *string
1044+
CORSBehavior *string
9951045
UseClassicParameterFlow *bool
9961046

9971047
Versions []testAccTemplateVersionConfig
@@ -1100,6 +1150,7 @@ resource "coderd_template" "test" {
11001150
require_active_version = {{orNull .RequireActiveVersion}}
11011151
deprecation_message = {{orNull .DeprecationMessage}}
11021152
max_port_share_level = {{orNull .MaxPortShareLevel}}
1153+
cors_behavior = {{orNull .CORSBehavior}}
11031154
use_classic_parameter_flow = {{orNull .UseClassicParameterFlow}}
11041155
11051156
acl = ` + c.ACL.String(t) + `

internal/provider/util.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212

1313
"github.com/coder/coder/v2/codersdk"
1414
"github.com/google/uuid"
15+
16+
"github.com/hashicorp/terraform-plugin-framework/types"
1517
)
1618

1719
func PrintOrNull(v any) string {
@@ -123,3 +125,22 @@ func isNotFound(err error) bool {
123125
}
124126
return false
125127
}
128+
129+
// stringValueOrNull returns types.StringNull() if s is empty,
130+
// otherwise types.StringValue(s).
131+
func stringValueOrNull(s string) types.String {
132+
if s == "" {
133+
return types.StringNull()
134+
}
135+
return types.StringValue(s)
136+
}
137+
138+
// corsPtr returns a pointer to a CORSBehavior if the value is known and not empty,
139+
// otherwise returns nil (which will use the server default).
140+
func corsPtr(v types.String) *codersdk.CORSBehavior {
141+
if v.IsNull() || v.IsUnknown() || v.ValueString() == "" {
142+
return nil
143+
}
144+
b := codersdk.CORSBehavior(v.ValueString())
145+
return &b
146+
}

0 commit comments

Comments
 (0)