Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit aad8947

Browse files
committed
fix: validate resources against available features before creating
1 parent ef38461 commit aad8947

11 files changed

+241
-14
lines changed

docs/resources/group.md

+2-2
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

+4-4
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

+5
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

+15-1
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

+34
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

+1-1
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

+7
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

+57-6
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ type TemplateResourceModel struct {
7575
}
7676

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

97+
func (m *TemplateResourceModel) CheckEntitlements(ctx context.Context, features map[codersdk.FeatureName]codersdk.Feature) (diags diag.Diagnostics) {
98+
var autoStop AutostopRequirement
99+
diags.Append(m.AutostopRequirement.As(ctx, &autoStop, basetypes.ObjectAsOptions{})...)
100+
if diags.HasError() {
101+
return diags
102+
}
103+
requiresScheduling := len(autoStop.DaysOfWeek) > 0 ||
104+
!m.AllowUserAutostart.ValueBool() ||
105+
!m.AllowUserAutostop.ValueBool() ||
106+
m.FailureTTLMillis.ValueInt64() != 0 ||
107+
m.TimeTilDormantAutoDeleteMillis.ValueInt64() != 0 ||
108+
m.TimeTilDormantMillis.ValueInt64() != 0 ||
109+
len(m.AutostartPermittedDaysOfWeek.Elements()) != 7
110+
requiresActiveVersion := m.RequireActiveVersion.ValueBool()
111+
requiresACL := !m.ACL.IsNull()
112+
if requiresScheduling || requiresActiveVersion || requiresACL {
113+
if requiresScheduling && !features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
114+
diags.AddError(
115+
"Feature not enabled",
116+
"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.",
117+
)
118+
return
119+
}
120+
if requiresActiveVersion && !features[codersdk.FeatureAccessControl].Enabled {
121+
diags.AddError(
122+
"Feature not enabled",
123+
"Your license is not entitled to use access control, so you cannot set require_active_version.",
124+
)
125+
return
126+
}
127+
if requiresACL && !features[codersdk.FeatureTemplateRBAC].Enabled {
128+
diags.AddError(
129+
"Feature not enabled",
130+
"Your license is not entitled to use template access control, so you cannot set acl.",
131+
)
132+
return
133+
}
134+
}
135+
return
136+
}
137+
97138
type TemplateVersion struct {
98139
ID UUID `tfsdk:"id"`
99140
Name types.String `tfsdk:"name"`
@@ -297,25 +338,25 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
297338
Default: booldefault.StaticBool(true),
298339
},
299340
"failure_ttl_ms": schema.Int64Attribute{
300-
MarkdownDescription: "The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds.",
341+
MarkdownDescription: "The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds. Requires an enterprise Coder deployment.",
301342
Optional: true,
302343
Computed: true,
303344
Default: int64default.StaticInt64(0),
304345
},
305346
"time_til_dormant_ms": schema.Int64Attribute{
306-
MarkdownDescription: "The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds.",
347+
MarkdownDescription: "The max lifetime before Coder locks inactive workspaces created from this template, in milliseconds. Requires an enterprise Coder deployment.",
307348
Optional: true,
308349
Computed: true,
309350
Default: int64default.StaticInt64(0),
310351
},
311352
"time_til_dormant_autodelete_ms": schema.Int64Attribute{
312-
MarkdownDescription: "The max lifetime before Coder permanently deletes dormant workspaces created from this template.",
353+
MarkdownDescription: "The max lifetime before Coder permanently deletes dormant workspaces created from this template. Requires an enterprise Coder deployment.",
313354
Optional: true,
314355
Computed: true,
315356
Default: int64default.StaticInt64(0),
316357
},
317358
"require_active_version": schema.BoolAttribute{
318-
MarkdownDescription: "Whether workspaces must be created from the active version of this template. Defaults to false.",
359+
MarkdownDescription: "Whether workspaces must be created from the active version of this template. Defaults to false. Requires an enterprise Coder deployment.",
319360
Optional: true,
320361
Computed: true,
321362
Default: booldefault.StaticBool(false),
@@ -430,6 +471,11 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
430471
data.DisplayName = data.Name
431472
}
432473

474+
resp.Diagnostics.Append(data.CheckEntitlements(ctx, r.data.Features)...)
475+
if resp.Diagnostics.HasError() {
476+
return
477+
}
478+
433479
client := r.data.Client
434480
orgID := data.OrganizationID.ValueUUID()
435481
var templateResp codersdk.Template
@@ -601,13 +647,18 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
601647
newState.DisplayName = newState.Name
602648
}
603649

650+
resp.Diagnostics.Append(newState.CheckEntitlements(ctx, r.data.Features)...)
651+
if resp.Diagnostics.HasError() {
652+
return
653+
}
654+
604655
orgID := newState.OrganizationID.ValueUUID()
605656

606657
templateID := newState.ID.ValueUUID()
607658

608659
client := r.data.Client
609660

610-
templateMetadataChanged := !newState.EqualTemplateMetadata(curState)
661+
templateMetadataChanged := !newState.EqualTemplateMetadata(&curState)
611662
// This is required, as the API will reject no-diff updates.
612663
if templateMetadataChanged {
613664
tflog.Trace(ctx, "change in template metadata detected, updating.")

internal/provider/template_resource_test.go

+81
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,87 @@ func TestAccTemplateResource(t *testing.T) {
372372
})
373373
}
374374

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

internal/provider/workspace_proxy_resource.go

+5
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)