Skip to content

Commit b768a21

Browse files
fix: validate resources against available features before creating (#62)
1 parent 4d75741 commit b768a21

11 files changed

+251
-24
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 - terraform-provider-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. Creating groups 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: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,23 @@ A Coder template
2222

2323
### Optional
2424

25-
- `acl` (Attributes) Access control list for the template. Requires an enterprise Coder deployment. If null, ACL policies will not be added or removed by Terraform. (see [below for nested schema](#nestedatt--acl))
25+
- `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))
2626
- `activity_bump_ms` (Number) The activity bump duration for all workspaces created from this template, in milliseconds. Defaults to one hour.
27-
- `allow_user_auto_start` (Boolean) Whether users can auto-start workspaces created from this template. Defaults to true. Requires an enterprise Coder deployment.
28-
- `allow_user_auto_stop` (Boolean) Whether users can auto-start workspaces created from this template. Defaults to true. Requires an enterprise Coder deployment.
27+
- `allow_user_auto_start` (Boolean) (Enterprise) Whether users can auto-start workspaces created from this template. Defaults to true.
28+
- `allow_user_auto_stop` (Boolean) (Enterprise) Whether users can auto-start workspaces created from this template. Defaults to true.
2929
- `allow_user_cancel_workspace_jobs` (Boolean) Whether users can cancel in-progress workspace jobs using this template. Defaults to true.
30-
- `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. Requires an enterprise Coder deployment.
31-
- `auto_stop_requirement` (Attributes) The auto-stop requirement for all workspaces created from this template. Requires an enterprise Coder deployment. (see [below for nested schema](#nestedatt--auto_stop_requirement))
30+
- `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.
31+
- `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))
3232
- `default_ttl_ms` (Number) The default time-to-live for all workspaces created from this template, in milliseconds.
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) (Enterprise) The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds.
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) (Enterprise) Whether workspaces must be created from the active version of this template. Defaults to false.
40+
- `time_til_dormant_autodelete_ms` (Number) (Enterprise) The max lifetime before Coder permanently deletes dormant workspaces created from this template.
41+
- `time_til_dormant_ms` (Number) Enterprise) The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds.
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
@@ -8,6 +8,7 @@ import (
88
"github.com/google/uuid"
99
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1010
"github.com/hashicorp/terraform-plugin-framework/attr"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
1112
"github.com/hashicorp/terraform-plugin-framework/path"
1213
"github.com/hashicorp/terraform-plugin-framework/resource"
1314
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -45,13 +46,21 @@ type GroupResourceModel struct {
4546
Members types.Set `tfsdk:"members"`
4647
}
4748

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

5261
func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
5362
resp.Schema = schema.Schema{
54-
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.",
63+
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.",
5564

5665
Attributes: map[string]schema.Attribute{
5766
"id": schema.StringAttribute{
@@ -136,6 +145,11 @@ func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest,
136145
return
137146
}
138147

148+
resp.Diagnostics.Append(CheckGroupEntitlements(ctx, r.data.Features)...)
149+
if resp.Diagnostics.HasError() {
150+
return
151+
}
152+
139153
client := r.data.Client
140154

141155
if data.OrganizationID.IsUnknown() {

internal/provider/group_resource_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package provider
33
import (
44
"context"
55
"os"
6+
"regexp"
67
"strings"
78
"testing"
89
"text/template"
@@ -124,6 +125,39 @@ func TestAccGroupResource(t *testing.T) {
124125
})
125126
}
126127

128+
func TestAccGroupResourceAGPL(t *testing.T) {
129+
if os.Getenv("TF_ACC") == "" {
130+
t.Skip("Acceptance tests are disabled.")
131+
}
132+
ctx := context.Background()
133+
client := integration.StartCoder(ctx, t, "group_acc_agpl", false)
134+
firstUser, err := client.User(ctx, codersdk.Me)
135+
require.NoError(t, err)
136+
137+
cfg1 := testAccGroupResourceconfig{
138+
URL: client.URL.String(),
139+
Token: client.SessionToken(),
140+
Name: PtrTo("example-group"),
141+
DisplayName: PtrTo("Example Group"),
142+
AvatarUrl: PtrTo("https://google.com"),
143+
QuotaAllowance: PtrTo(int32(100)),
144+
Members: PtrTo([]string{firstUser.ID.String()}),
145+
}
146+
147+
resource.Test(t, resource.TestCase{
148+
IsUnitTest: true,
149+
PreCheck: func() { testAccPreCheck(t) },
150+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
151+
Steps: []resource.TestStep{
152+
{
153+
Config: cfg1.String(t),
154+
ExpectError: regexp.MustCompile("Your license is not entitled to use groups."),
155+
},
156+
},
157+
})
158+
159+
}
160+
127161
type testAccGroupResourceconfig struct {
128162
URL string
129163
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: 62 additions & 11 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"`
@@ -245,7 +286,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
245286
Default: int64default.StaticInt64(3600000),
246287
},
247288
"auto_stop_requirement": schema.SingleNestedAttribute{
248-
MarkdownDescription: "The auto-stop requirement for all workspaces created from this template. Requires an enterprise Coder deployment.",
289+
MarkdownDescription: "(Enterprise) The auto-stop requirement for all workspaces created from this template.",
249290
Optional: true,
250291
Computed: true,
251292
Attributes: map[string]schema.Attribute{
@@ -270,7 +311,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
270311
})),
271312
},
272313
"auto_start_permitted_days_of_week": schema.SetAttribute{
273-
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. Requires an enterprise Coder deployment.",
314+
MarkdownDescription: "(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.",
274315
Optional: true,
275316
Computed: true,
276317
ElementType: types.StringType,
@@ -284,37 +325,37 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
284325
Default: booldefault.StaticBool(true),
285326
},
286327
"allow_user_auto_start": schema.BoolAttribute{
287-
MarkdownDescription: "Whether users can auto-start workspaces created from this template. Defaults to true. Requires an enterprise Coder deployment.",
328+
MarkdownDescription: "(Enterprise) Whether users can auto-start workspaces created from this template. Defaults to true.",
288329
Optional: true,
289330
Computed: true,
290331
Default: booldefault.StaticBool(true),
291332
},
292333
"allow_user_auto_stop": schema.BoolAttribute{
293-
MarkdownDescription: "Whether users can auto-start workspaces created from this template. Defaults to true. Requires an enterprise Coder deployment.",
334+
MarkdownDescription: "(Enterprise) Whether users can auto-start workspaces created from this template. Defaults to true.",
294335
Optional: true,
295336
Computed: true,
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: "(Enterprise) The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds.",
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: "Enterprise) The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds.",
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: "(Enterprise) The max lifetime before Coder permanently deletes dormant workspaces created from this template.",
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: "(Enterprise) Whether workspaces must be created from the active version of this template. Defaults to false.",
318359
Optional: true,
319360
Computed: true,
320361
Default: booldefault.StaticBool(false),
@@ -326,7 +367,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
326367
Default: stringdefault.StaticString(""),
327368
},
328369
"acl": schema.SingleNestedAttribute{
329-
MarkdownDescription: "Access control list for the template. Requires an enterprise Coder deployment. If null, ACL policies will not be added or removed by Terraform.",
370+
MarkdownDescription: "(Enterprise) Access control list for the template. If null, ACL policies will not be added or removed by Terraform.",
330371
Optional: true,
331372
Attributes: map[string]schema.Attribute{
332373
"users": permissionAttribute,
@@ -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.")

0 commit comments

Comments
 (0)