Skip to content

Commit afe3400

Browse files
committed
chore: allow pushing only inactive coderd_template versions
1 parent 19fbf75 commit afe3400

File tree

5 files changed

+382
-43
lines changed

5 files changed

+382
-43
lines changed

docs/resources/template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ resource "coderd_template" "ubuntu-main" {
7777
- `description` (String) A description of the template.
7878
- `display_name` (String) The display name of the template. Defaults to the template name.
7979
- `failure_ttl_ms` (Number) (Enterprise) The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds.
80-
- `icon` (String) Relative path or external URL that specifes an icon to be displayed in the dashboard.
80+
- `icon` (String) Relative path or external URL that specifies an icon to be displayed in the dashboard.
8181
- `max_port_share_level` (String) (Enterprise) The maximum port share level for workspaces created from this template. Defaults to `owner` on an Enterprise deployment, or `public` otherwise.
8282
- `organization_id` (String) The ID of the organization. Defaults to the provider's default organization
8383
- `require_active_version` (Boolean) (Enterprise) Whether workspaces must be created from the active version of this template. Defaults to false.

integration/integration.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ func StartCoder(ctx context.Context, t *testing.T, name string, useLicense bool)
5151
ctr, err := cli.ContainerCreate(ctx, &container.Config{
5252
Image: coderImg + ":" + coderVersion,
5353
Env: []string{
54-
"CODER_HTTP_ADDRESS=0.0.0.0:3000", // Listen on all interfaces inside the container
55-
"CODER_ACCESS_URL=http://localhost:3000", // Set explicitly to avoid creating try.coder.app URLs.
56-
"CODER_TELEMETRY_ENABLE=false", // Avoid creating noise.
54+
"CODER_HTTP_ADDRESS=0.0.0.0:3000", // Listen on all interfaces inside the container
55+
"CODER_ACCESS_URL=http://localhost:3000", // Set explicitly to avoid creating try.coder.app URLs.
56+
"CODER_TELEMETRY_ENABLE=false", // Avoid creating noise.
57+
"CODER_DANGEROUS_DISABLE_RATE_LIMITS=true", // Avoid hitting file rate limit in tests.
5758
},
5859
Labels: map[string]string{},
5960
ExposedPorts: map[nat.Port]struct{}{nat.Port("3000/tcp"): {}},

integration/integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func TestIntegration(t *testing.T) {
166166
tfCmd.Stderr = &buf
167167
tt.preF(t, client)
168168
if err := tfCmd.Run(); !assert.NoError(t, err) {
169-
t.Logf("%s", buf.String())
169+
t.Log(buf.String())
170170
t.FailNow()
171171
}
172172
tt.assertF(t, client)

internal/provider/template_resource.go

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
286286
},
287287
},
288288
"icon": schema.StringAttribute{
289-
MarkdownDescription: "Relative path or external URL that specifes an icon to be displayed in the dashboard.",
289+
MarkdownDescription: "Relative path or external URL that specifies an icon to be displayed in the dashboard.",
290290
Optional: true,
291291
Computed: true,
292292
Default: stringdefault.StaticString(""),
@@ -404,7 +404,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
404404
Required: true,
405405
Validators: []validator.List{
406406
listvalidator.SizeAtLeast(1),
407-
NewActiveVersionValidator(),
407+
NewVersionsValidator(),
408408
},
409409
NestedObject: schema.NestedAttributeObject{
410410
Attributes: map[string]schema.Attribute{
@@ -867,24 +867,24 @@ func (r *TemplateResource) ConfigValidators(context.Context) []resource.ConfigVa
867867
return []resource.ConfigValidator{}
868868
}
869869

870-
type activeVersionValidator struct{}
870+
type versionsValidator struct{}
871871

872-
func NewActiveVersionValidator() validator.List {
873-
return &activeVersionValidator{}
872+
func NewVersionsValidator() validator.List {
873+
return &versionsValidator{}
874874
}
875875

876876
// Description implements validator.List.
877-
func (a *activeVersionValidator) Description(ctx context.Context) string {
877+
func (a *versionsValidator) Description(ctx context.Context) string {
878878
return a.MarkdownDescription(ctx)
879879
}
880880

881881
// MarkdownDescription implements validator.List.
882-
func (a *activeVersionValidator) MarkdownDescription(context.Context) string {
883-
return "Validate that exactly one template version has active set to true."
882+
func (a *versionsValidator) MarkdownDescription(context.Context) string {
883+
return "Validate that template version names are unique and that at most one version is active."
884884
}
885885

886886
// ValidateList implements validator.List.
887-
func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
887+
func (a *versionsValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
888888
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
889889
return
890890
}
@@ -908,13 +908,13 @@ func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator
908908
uniqueNames[version.Name.ValueString()] = struct{}{}
909909
}
910910

911-
// Check if only one item in Version has active set to true
911+
// Ensure at most one version is active
912912
active := false
913913
for _, version := range data {
914-
// `active` is required, so if it's null or unknown, this is Terraform
914+
// `active` defaults to false, so if it's null or unknown, this is Terraform
915915
// requesting an early validation.
916916
if version.Active.IsNull() || version.Active.IsUnknown() {
917-
return
917+
continue
918918
}
919919
if version.Active.ValueBool() {
920920
if active {
@@ -924,12 +924,9 @@ func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator
924924
active = true
925925
}
926926
}
927-
if !active {
928-
resp.Diagnostics.AddError("Client Error", "At least one template version must be active.")
929-
}
930927
}
931928

932-
var _ validator.List = &activeVersionValidator{}
929+
var _ validator.List = &versionsValidator{}
933930

934931
type versionsPlanModifier struct{}
935932

@@ -956,6 +953,12 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
956953
return
957954
}
958955

956+
hasActiveVersion, diag := hasOneActiveVersion(configVersions)
957+
if diag.HasError() {
958+
resp.Diagnostics.Append(diag...)
959+
return
960+
}
961+
959962
for i := range planVersions {
960963
hash, err := computeDirectoryHash(planVersions[i].Directory.ValueString())
961964
if err != nil {
@@ -974,6 +977,13 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
974977
// If this is the first read, init the private state value
975978
if lvBytes == nil {
976979
lv = make(LastVersionsByHash)
980+
// If there's no prior private state, this might be resource creation,
981+
// in which case one version must be active.
982+
if !hasActiveVersion {
983+
resp.Diagnostics.AddError("Client Error", "At least one template version must be active when creating a"+
984+
" `coderd_template` resource.\n(Subsequent resource updates can be made without an active template in the list).")
985+
return
986+
}
977987
} else {
978988
err := json.Unmarshal(lvBytes, &lv)
979989
if err != nil {
@@ -982,9 +992,37 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
982992
}
983993
}
984994

985-
planVersions.reconcileVersionIDs(lv, configVersions)
995+
diag = planVersions.reconcileVersionIDs(lv, configVersions, hasActiveVersion)
996+
if diag.HasError() {
997+
resp.Diagnostics.Append(diag...)
998+
return
999+
}
1000+
1001+
resp.PlanValue, diag = types.ListValueFrom(ctx, req.PlanValue.ElementType(ctx), planVersions)
1002+
if diag.HasError() {
1003+
resp.Diagnostics.Append(diag...)
1004+
}
1005+
}
9861006

987-
resp.PlanValue, resp.Diagnostics = types.ListValueFrom(ctx, req.PlanValue.ElementType(ctx), planVersions)
1007+
func hasOneActiveVersion(data Versions) (hasActiveVersion bool, diags diag.Diagnostics) {
1008+
active := false
1009+
for _, version := range data {
1010+
if version.Active.IsNull() || version.Active.IsUnknown() {
1011+
// If null or unknown, the value will be defaulted to false
1012+
continue
1013+
}
1014+
if version.Active.ValueBool() {
1015+
if active {
1016+
diags.AddError("Client Error", "Only one template version can be active at a time.")
1017+
return
1018+
}
1019+
active = true
1020+
}
1021+
}
1022+
if !active {
1023+
return false, diags
1024+
}
1025+
return true, diags
9881026
}
9891027

9901028
func NewVersionsPlanModifier() planmodifier.List {
@@ -1309,6 +1347,7 @@ type PreviousTemplateVersion struct {
13091347
ID uuid.UUID `json:"id"`
13101348
Name string `json:"name"`
13111349
TFVars map[string]string `json:"tf_vars"`
1350+
Active bool `json:"active"`
13121351
}
13131352

13141353
type privateState interface {
@@ -1331,13 +1370,15 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
13311370
ID: version.ID.ValueUUID(),
13321371
Name: version.Name.ValueString(),
13331372
TFVars: tfVars,
1373+
Active: version.Active.ValueBool(),
13341374
})
13351375
} else {
13361376
lv[version.DirectoryHash.ValueString()] = []PreviousTemplateVersion{
13371377
{
13381378
ID: version.ID.ValueUUID(),
13391379
Name: version.Name.ValueString(),
13401380
TFVars: tfVars,
1381+
Active: version.Active.ValueBool(),
13411382
},
13421383
}
13431384
}
@@ -1350,7 +1391,7 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
13501391
return ps.SetKey(ctx, LastVersionsKey, lvBytes)
13511392
}
13521393

1353-
func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVersions Versions) {
1394+
func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVersions Versions, hasOneActiveVersion bool) (diag diag.Diagnostics) {
13541395
// We remove versions that we've matched from `lv`, so make a copy for
13551396
// resolving tfvar changes at the end.
13561397
fullLv := make(LastVersionsByHash)
@@ -1420,6 +1461,39 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe
14201461
}
14211462
}
14221463
}
1464+
1465+
// If a version was deactivated, and no active version was set, we need to
1466+
// return an error to avoid a post-apply plan being non-empty.
1467+
if !hasOneActiveVersion {
1468+
for i := range planVersions {
1469+
if !planVersions[i].ID.IsUnknown() {
1470+
prevs, ok := fullLv[planVersions[i].DirectoryHash.ValueString()]
1471+
if !ok {
1472+
continue
1473+
}
1474+
if versionDeactivated(prevs, &planVersions[i]) {
1475+
diag.AddError("Client Error", "Plan could not determine which version should be active.\n"+
1476+
"Either specify an active version or modify the contents of the previously active version before marking it as inactive.")
1477+
return diag
1478+
}
1479+
}
1480+
}
1481+
}
1482+
return diag
1483+
}
1484+
1485+
func versionDeactivated(prevs []PreviousTemplateVersion, planned *TemplateVersion) bool {
1486+
for _, prev := range prevs {
1487+
if prev.ID == planned.ID.ValueUUID() {
1488+
if prev.Active &&
1489+
!planned.Active.IsNull() &&
1490+
!planned.Active.IsUnknown() &&
1491+
!planned.Active.ValueBool() {
1492+
return true
1493+
}
1494+
}
1495+
}
1496+
return false
14231497
}
14241498

14251499
func tfVariablesChanged(prevs []PreviousTemplateVersion, planned *TemplateVersion) bool {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy