Skip to content

Commit db62dcc

Browse files
committed
fix: validate resources against available features before creating
1 parent b02f6a8 commit db62dcc

11 files changed

+241
-14
lines changed

docs/resources/group.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
page_title: "coderd_group Resource - coderd"
44
subcategory: ""
55
description: |-
6-
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.
6+
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. Requires an Enterprise license.
77
---
88

99
# coderd_group (Resource)
1010

11-
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.
11+
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.
1212

1313

1414

docs/resources/template.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ A Coder template
3333
- `deprecation_message` (String) If set, the template will be marked as deprecated and users will be blocked from creating new workspaces from it.
3434
- `description` (String) A description of the template.
3535
- `display_name` (String) The display name of the template. Defaults to the template name.
36-
- `failure_ttl_ms` (Number) The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds.
36+
- `failure_ttl_ms` (Number) The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds. Requires an enterprise Coder deployment.
3737
- `icon` (String) Relative path or external URL that specifes an icon to be displayed in the dashboard.
3838
- `organization_id` (String) The ID of the organization. Defaults to the provider's default organization
39-
- `require_active_version` (Boolean) Whether workspaces must be created from the active version of this template. Defaults to false.
40-
- `time_til_dormant_autodelete_ms` (Number) The max lifetime before Coder permanently deletes dormant workspaces created from this template.
41-
- `time_til_dormant_ms` (Number) The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds.
39+
- `require_active_version` (Boolean) Whether workspaces must be created from the active version of this template. Defaults to false. Requires an enterprise Coder deployment.
40+
- `time_til_dormant_autodelete_ms` (Number) The max lifetime before Coder permanently deletes dormant workspaces created from this template. Requires an enterprise Coder deployment.
41+
- `time_til_dormant_ms` (Number) The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds. Requires an enterprise Coder deployment.
4242

4343
### Read-Only
4444

internal/provider/group_data_source.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ func (d *GroupDataSource) Read(ctx context.Context, req datasource.ReadRequest,
168168
return
169169
}
170170

171+
resp.Diagnostics.Append(CheckGroupEntitlements(ctx, d.data.Features)...)
172+
if resp.Diagnostics.HasError() {
173+
return
174+
}
175+
171176
client := d.data.Client
172177

173178
if data.OrganizationID.IsNull() {

internal/provider/group_resource.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/coder/coder/v2/codersdk"
1111
"github.com/google/uuid"
1212
"github.com/hashicorp/terraform-plugin-framework/attr"
13+
"github.com/hashicorp/terraform-plugin-framework/diag"
1314
"github.com/hashicorp/terraform-plugin-framework/path"
1415
"github.com/hashicorp/terraform-plugin-framework/resource"
1516
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -46,13 +47,21 @@ type GroupResourceModel struct {
4647
Members types.Set `tfsdk:"members"`
4748
}
4849

50+
func CheckGroupEntitlements(ctx context.Context, features map[codersdk.FeatureName]codersdk.Feature) (diags diag.Diagnostics) {
51+
if !features[codersdk.FeatureTemplateRBAC].Enabled {
52+
diags.AddError("Feature not enabled", "Your license is not entitled to create groups.")
53+
return
54+
}
55+
return nil
56+
}
57+
4958
func (r *GroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
5059
resp.TypeName = req.ProviderTypeName + "_group"
5160
}
5261

5362
func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
5463
resp.Schema = schema.Schema{
55-
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.",
64+
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. Requires an Enterprise license.",
5665

5766
Attributes: map[string]schema.Attribute{
5867
"id": schema.StringAttribute{
@@ -134,6 +143,11 @@ func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest,
134143
return
135144
}
136145

146+
resp.Diagnostics.Append(CheckGroupEntitlements(ctx, r.data.Features)...)
147+
if resp.Diagnostics.HasError() {
148+
return
149+
}
150+
137151
client := r.data.Client
138152

139153
if data.OrganizationID.IsUnknown() {

internal/provider/group_resource_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package provider
66
import (
77
"context"
88
"os"
9+
"regexp"
910
"strings"
1011
"testing"
1112
"text/template"
@@ -127,6 +128,39 @@ func TestAccGroupResource(t *testing.T) {
127128
})
128129
}
129130

131+
func TestAccGroupResourceAGPL(t *testing.T) {
132+
if os.Getenv("TF_ACC") == "" {
133+
t.Skip("Acceptance tests are disabled.")
134+
}
135+
ctx := context.Background()
136+
client := integration.StartCoder(ctx, t, "group_acc_agpl", false)
137+
firstUser, err := client.User(ctx, codersdk.Me)
138+
require.NoError(t, err)
139+
140+
cfg1 := testAccGroupResourceconfig{
141+
URL: client.URL.String(),
142+
Token: client.SessionToken(),
143+
Name: PtrTo("example-group"),
144+
DisplayName: PtrTo("Example Group"),
145+
AvatarUrl: PtrTo("https://google.com"),
146+
QuotaAllowance: PtrTo(int32(100)),
147+
Members: PtrTo([]string{firstUser.ID.String()}),
148+
}
149+
150+
resource.Test(t, resource.TestCase{
151+
IsUnitTest: true,
152+
PreCheck: func() { testAccPreCheck(t) },
153+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
154+
Steps: []resource.TestStep{
155+
{
156+
Config: cfg1.String(t),
157+
ExpectError: regexp.MustCompile("Your license is not entitled to create groups."),
158+
},
159+
},
160+
})
161+
162+
}
163+
130164
type testAccGroupResourceconfig struct {
131165
URL string
132166
Token string

internal/provider/organization_data_source_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestAccOrganizationDataSource(t *testing.T) {
1919
t.Skip("Acceptance tests are disabled.")
2020
}
2121
ctx := context.Background()
22-
client := integration.StartCoder(ctx, t, "org_data_acc", true)
22+
client := integration.StartCoder(ctx, t, "org_data_acc", false)
2323
firstUser, err := client.User(ctx, codersdk.Me)
2424
require.NoError(t, err)
2525

internal/provider/provider.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type CoderdProvider struct {
3434
type CoderdProviderData struct {
3535
Client *codersdk.Client
3636
DefaultOrganizationID uuid.UUID
37+
Features map[codersdk.FeatureName]codersdk.Feature
3738
}
3839

3940
// CoderdProviderModel describes the provider data model.
@@ -111,9 +112,15 @@ func (p *CoderdProvider) Configure(ctx context.Context, req provider.ConfigureRe
111112
}
112113
data.DefaultOrganizationID = UUIDValue(user.OrganizationIDs[0])
113114
}
115+
entitlements, err := client.Entitlements(ctx)
116+
if err != nil {
117+
resp.Diagnostics.AddError("Client Error", "failed to get deployment entitlements: "+err.Error())
118+
}
119+
114120
providerData := &CoderdProviderData{
115121
Client: client,
116122
DefaultOrganizationID: data.DefaultOrganizationID.ValueUUID(),
123+
Features: entitlements.Features,
117124
}
118125
resp.DataSourceData = providerData
119126
resp.ResourceData = providerData

internal/provider/template_resource.go

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ type TemplateResourceModel struct {
7474
}
7575

7676
// EqualTemplateMetadata returns true if two templates have identical metadata (excluding ACL).
77-
func (m TemplateResourceModel) EqualTemplateMetadata(other TemplateResourceModel) bool {
77+
func (m *TemplateResourceModel) EqualTemplateMetadata(other *TemplateResourceModel) bool {
7878
return m.Name.Equal(other.Name) &&
7979
m.DisplayName.Equal(other.DisplayName) &&
8080
m.Description.Equal(other.Description) &&
@@ -93,6 +93,47 @@ func (m TemplateResourceModel) EqualTemplateMetadata(other TemplateResourceModel
9393
m.RequireActiveVersion.Equal(other.RequireActiveVersion)
9494
}
9595

96+
func (m *TemplateResourceModel) CheckEntitlements(ctx context.Context, features map[codersdk.FeatureName]codersdk.Feature) (diags diag.Diagnostics) {
97+
var autoStop AutostopRequirement
98+
diags.Append(m.AutostopRequirement.As(ctx, &autoStop, basetypes.ObjectAsOptions{})...)
99+
if diags.HasError() {
100+
return diags
101+
}
102+
requiresScheduling := len(autoStop.DaysOfWeek) > 0 ||
103+
!m.AllowUserAutostart.ValueBool() ||
104+
!m.AllowUserAutostop.ValueBool() ||
105+
m.FailureTTLMillis.ValueInt64() != 0 ||
106+
m.TimeTilDormantAutoDeleteMillis.ValueInt64() != 0 ||
107+
m.TimeTilDormantMillis.ValueInt64() != 0 ||
108+
len(m.AutostartPermittedDaysOfWeek.Elements()) != 7
109+
requiresActiveVersion := m.RequireActiveVersion.ValueBool()
110+
requiresACL := !m.ACL.IsNull()
111+
if requiresScheduling || requiresActiveVersion || requiresACL {
112+
if requiresScheduling && !features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
113+
diags.AddError(
114+
"Feature not enabled",
115+
"Your license is not entitled to use advanced template scheduling, so you cannot modify any of the following fields from their defaults: auto_stop_requirement, auto_start_permitted_days_of_week, allow_user_auto_start, allow_user_auto_stop, failure_ttl_ms, time_til_dormant_ms, time_til_dormant_autodelete_ms.",
116+
)
117+
return
118+
}
119+
if requiresActiveVersion && !features[codersdk.FeatureAccessControl].Enabled {
120+
diags.AddError(
121+
"Feature not enabled",
122+
"Your license is not entitled to use access control, so you cannot set require_active_version.",
123+
)
124+
return
125+
}
126+
if requiresACL && !features[codersdk.FeatureTemplateRBAC].Enabled {
127+
diags.AddError(
128+
"Feature not enabled",
129+
"Your license is not entitled to use template access control, so you cannot set acl.",
130+
)
131+
return
132+
}
133+
}
134+
return
135+
}
136+
96137
type TemplateVersion struct {
97138
ID UUID `tfsdk:"id"`
98139
Name types.String `tfsdk:"name"`
@@ -296,25 +337,25 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
296337
Default: booldefault.StaticBool(true),
297338
},
298339
"failure_ttl_ms": schema.Int64Attribute{
299-
MarkdownDescription: "The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds.",
340+
MarkdownDescription: "The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds. Requires an enterprise Coder deployment.",
300341
Optional: true,
301342
Computed: true,
302343
Default: int64default.StaticInt64(0),
303344
},
304345
"time_til_dormant_ms": schema.Int64Attribute{
305-
MarkdownDescription: "The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds.",
346+
MarkdownDescription: "The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds. Requires an enterprise Coder deployment.",
306347
Optional: true,
307348
Computed: true,
308349
Default: int64default.StaticInt64(0),
309350
},
310351
"time_til_dormant_autodelete_ms": schema.Int64Attribute{
311-
MarkdownDescription: "The max lifetime before Coder permanently deletes dormant workspaces created from this template.",
352+
MarkdownDescription: "The max lifetime before Coder permanently deletes dormant workspaces created from this template. Requires an enterprise Coder deployment.",
312353
Optional: true,
313354
Computed: true,
314355
Default: int64default.StaticInt64(0),
315356
},
316357
"require_active_version": schema.BoolAttribute{
317-
MarkdownDescription: "Whether workspaces must be created from the active version of this template. Defaults to false.",
358+
MarkdownDescription: "Whether workspaces must be created from the active version of this template. Defaults to false. Requires an enterprise Coder deployment.",
318359
Optional: true,
319360
Computed: true,
320361
Default: booldefault.StaticBool(false),
@@ -429,6 +470,11 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
429470
data.DisplayName = data.Name
430471
}
431472

473+
resp.Diagnostics.Append(data.CheckEntitlements(ctx, r.data.Features)...)
474+
if resp.Diagnostics.HasError() {
475+
return
476+
}
477+
432478
client := r.data.Client
433479
orgID := data.OrganizationID.ValueUUID()
434480
var templateResp codersdk.Template
@@ -593,13 +639,18 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
593639
newState.DisplayName = newState.Name
594640
}
595641

642+
resp.Diagnostics.Append(newState.CheckEntitlements(ctx, r.data.Features)...)
643+
if resp.Diagnostics.HasError() {
644+
return
645+
}
646+
596647
orgID := newState.OrganizationID.ValueUUID()
597648

598649
templateID := newState.ID.ValueUUID()
599650

600651
client := r.data.Client
601652

602-
templateMetadataChanged := !newState.EqualTemplateMetadata(curState)
653+
templateMetadataChanged := !newState.EqualTemplateMetadata(&curState)
603654
// This is required, as the API will reject no-diff updates.
604655
if templateMetadataChanged {
605656
tflog.Trace(ctx, "change in template metadata detected, updating.")

internal/provider/template_resource_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,87 @@ func TestAccTemplateResource(t *testing.T) {
374374
})
375375
}
376376

377+
func TestAccTemplateResourceAGPL(t *testing.T) {
378+
if os.Getenv("TF_ACC") == "" {
379+
t.Skip("Acceptance tests are disabled.")
380+
}
381+
ctx := context.Background()
382+
client := integration.StartCoder(ctx, t, "template_acc", false)
383+
firstUser, err := client.User(ctx, codersdk.Me)
384+
require.NoError(t, err)
385+
386+
cfg1 := testAccTemplateResourceConfig{
387+
URL: client.URL.String(),
388+
Token: client.SessionToken(),
389+
Name: PtrTo("example-template"),
390+
Versions: []testAccTemplateVersionConfig{
391+
{
392+
// Auto-generated version name
393+
Directory: PtrTo("../../integration/template-test/example-template/"),
394+
Active: PtrTo(true),
395+
},
396+
},
397+
AllowUserAutostart: PtrTo(false),
398+
}
399+
400+
cfg2 := cfg1
401+
cfg2.AllowUserAutostart = nil
402+
cfg2.AutostopRequirement.DaysOfWeek = PtrTo([]string{"monday", "tuesday"})
403+
404+
cfg3 := cfg2
405+
cfg3.AutostopRequirement.null = true
406+
cfg3.AutostartRequirement = PtrTo([]string{})
407+
408+
cfg4 := cfg3
409+
cfg4.FailureTTL = PtrTo(int64(1))
410+
411+
cfg5 := cfg4
412+
cfg5.FailureTTL = nil
413+
cfg5.AutostartRequirement = nil
414+
cfg5.RequireActiveVersion = PtrTo(true)
415+
416+
cfg6 := cfg5
417+
cfg6.RequireActiveVersion = nil
418+
cfg6.ACL = testAccTemplateACLConfig{
419+
GroupACL: []testAccTemplateKeyValueConfig{
420+
{
421+
Key: PtrTo(firstUser.OrganizationIDs[0].String()),
422+
Value: PtrTo("use"),
423+
},
424+
},
425+
}
426+
427+
for _, cfg := range []testAccTemplateResourceConfig{cfg1, cfg2, cfg3, cfg4} {
428+
resource.Test(t, resource.TestCase{
429+
PreCheck: func() { testAccPreCheck(t) },
430+
IsUnitTest: true,
431+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
432+
Steps: []resource.TestStep{
433+
{
434+
Config: cfg.String(t),
435+
ExpectError: regexp.MustCompile("Your license is not entitled to use advanced template scheduling"),
436+
},
437+
},
438+
})
439+
}
440+
441+
resource.Test(t, resource.TestCase{
442+
PreCheck: func() { testAccPreCheck(t) },
443+
IsUnitTest: true,
444+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
445+
Steps: []resource.TestStep{
446+
{
447+
Config: cfg5.String(t),
448+
ExpectError: regexp.MustCompile("Your license is not entitled to use access control"),
449+
},
450+
{
451+
Config: cfg6.String(t),
452+
ExpectError: regexp.MustCompile("Your license is not entitled to use template access control"),
453+
},
454+
},
455+
})
456+
}
457+
377458
type testAccTemplateResourceConfig struct {
378459
URL string
379460
Token string

internal/provider/workspace_proxy_resource.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ func (r *WorkspaceProxyResource) Create(ctx context.Context, req resource.Create
103103
return
104104
}
105105

106+
if !r.data.Features[codersdk.FeatureWorkspaceProxy].Enabled {
107+
resp.Diagnostics.AddError("Feature not enabled", "Your license is not entitled to create workspace proxies.")
108+
return
109+
}
110+
106111
client := r.data.Client
107112
wsp, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
108113
Name: data.Name.ValueString(),

0 commit comments

Comments
 (0)