Skip to content

Commit 5ec8b26

Browse files
committed
fix: use partially randomised version names
1 parent 9aa27ba commit 5ec8b26

File tree

11 files changed

+185
-74
lines changed

11 files changed

+185
-74
lines changed

docs/resources/template.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,16 @@ Optional:
4545

4646
- `active` (Boolean) Whether this version is the active version of the template. Only one version can be active at a time.
4747
- `message` (String) A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated.
48-
- `name` (String) The name of the template version. Automatically generated if not provided.
48+
- `name_prefix` (String) A prefix for the name of the template version. Must be unique within the list of versions.
4949
- `provisioner_tags` (Attributes Set) Provisioner tags for the template version. (see [below for nested schema](#nestedatt--versions--provisioner_tags))
5050
- `tf_vars` (Attributes Set) Terraform variables for the template version. (see [below for nested schema](#nestedatt--versions--tf_vars))
5151

5252
Read-Only:
5353

5454
- `directory_hash` (String)
55+
- `full_name` (String) The full name of the template version, as on the Coder deployment.
5556
- `id` (String)
57+
- `revision_num` (Number) The ordinal appended to the name_prefix to generate a unique name for the template version.
5658

5759
<a id="nestedatt--versions--provisioner_tags"></a>
5860
### Nested Schema for `versions.provisioner_tags`

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ toolchain go1.22.5
66

77
require (
88
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6
9+
github.com/coder/coder v0.27.3
910
github.com/coder/coder/v2 v2.13.1
1011
github.com/docker/docker v27.0.3+incompatible
1112
github.com/docker/go-connections v0.4.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo
8181
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
8282
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
8383
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
84+
github.com/coder/coder v0.27.3 h1:8RIaBIXp1xCR5pwsGRveGYdIu/wX4DwYVkaeSCPL2Ng=
85+
github.com/coder/coder v0.27.3/go.mod h1:i/vLWfbLhIho40eYsLLZx+TLZnHxdqwGmyhPl0/v7rE=
8486
github.com/coder/coder/v2 v2.13.1 h1:tCd8ljqIAufbVcBr8ODS1QbsrjJbmOIvgDkvdd/JMXc=
8587
github.com/coder/coder/v2 v2.13.1/go.mod h1:Gxc79InMB6b+sncuDUORtFLWi7aKshvis3QrMUhpq5Q=
8688
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=

integration/integration_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/exec"
99
"path/filepath"
10+
"regexp"
1011
"strconv"
1112
"testing"
1213
"time"
@@ -128,7 +129,7 @@ func TestIntegration(t *testing.T) {
128129
})
129130
require.NoError(t, err)
130131
require.Len(t, versions, 2)
131-
require.Equal(t, "latest", versions[0].Name)
132+
require.Regexp(t, regexp.MustCompile(`^latest-0-.*$`), versions[0].Name)
132133
require.NotEmpty(t, versions[0].ID)
133134
require.Equal(t, templates[0].ID, *versions[0].TemplateID)
134135
require.Equal(t, templates[0].ActiveVersionID, versions[0].ID)

integration/template-test/main.tf

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ resource "coderd_template" "sample" {
4141
}
4242
versions = [
4343
{
44-
name = "latest"
45-
directory = "./example-template"
46-
active = true
44+
name_prefix = "latest"
45+
directory = "./version-one"
46+
active = true
4747
tf_vars = [
4848
{
4949
name = "name"
@@ -52,9 +52,9 @@ resource "coderd_template" "sample" {
5252
]
5353
},
5454
{
55-
name = "legacy"
56-
directory = "./example-template-2"
57-
active = false
55+
name_prefix = "legacy"
56+
directory = "./version-two"
57+
active = false
5858
tf_vars = [
5959
{
6060
name = "name"
File renamed without changes.

integration/template-test/example-template/terraform.tfvars renamed to integration/template-test/version-one/terraform.tfvars

File renamed without changes.
File renamed without changes.

internal/provider/template_data_source_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ func TestAccTemplateDataSource(t *testing.T) {
3131
version, err := newVersion(ctx, client, newVersionRequest{
3232
OrganizationID: orgID,
3333
Version: &TemplateVersion{
34-
Name: types.StringValue("main"),
35-
Message: types.StringValue("Initial commit"),
36-
Directory: types.StringValue("../../integration/template-test/example-template/"),
34+
NamePrefix: types.StringValue("version-one"),
35+
Message: types.StringValue("Initial commit"),
36+
Directory: types.StringValue("../../integration/template-test/version-one/"),
3737
TerraformVariables: []Variable{
3838
{
3939
Name: types.StringValue("name"),

internal/provider/template_resource.go

Lines changed: 94 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88

99
"cdr.dev/slog"
10+
"github.com/coder/coder/cryptorand"
1011
"github.com/coder/coder/v2/codersdk"
1112
"github.com/coder/coder/v2/provisionersdk"
1213
"github.com/google/uuid"
@@ -53,6 +54,7 @@ type TemplateResourceModel struct {
5354
AllowUserAutoStart types.Bool `tfsdk:"allow_user_auto_start"`
5455
AllowUserAutoStop types.Bool `tfsdk:"allow_user_auto_stop"`
5556

57+
// If null, we are not managing ACL via Terraform (such as for AGPL).
5658
ACL types.Object `tfsdk:"acl"`
5759
Versions Versions `tfsdk:"versions"`
5860
}
@@ -69,21 +71,25 @@ func (m TemplateResourceModel) EqualTemplateMetadata(other TemplateResourceModel
6971
}
7072

7173
type TemplateVersion struct {
72-
ID UUID `tfsdk:"id"`
73-
Name types.String `tfsdk:"name"`
74+
ID UUID `tfsdk:"id"`
75+
76+
NamePrefix types.String `tfsdk:"name_prefix"`
7477
Message types.String `tfsdk:"message"`
7578
Directory types.String `tfsdk:"directory"`
7679
DirectoryHash types.String `tfsdk:"directory_hash"`
7780
Active types.Bool `tfsdk:"active"`
7881
TerraformVariables []Variable `tfsdk:"tf_vars"`
7982
ProvisionerTags []Variable `tfsdk:"provisioner_tags"`
83+
84+
RevisionNum types.Int64 `tfsdk:"revision_num"`
85+
FullName types.String `tfsdk:"full_name"`
8086
}
8187

8288
type Versions []TemplateVersion
8389

84-
func (v Versions) ByID(id UUID) *TemplateVersion {
90+
func (v Versions) ByNamePrefix(namePrefix types.String) *TemplateVersion {
8591
for _, m := range v {
86-
if m.ID.Equal(id) {
92+
if m.NamePrefix.Equal(namePrefix) {
8793
return &m
8894
}
8995
}
@@ -219,18 +225,23 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
219225
Required: true,
220226
Validators: []validator.List{
221227
listvalidator.SizeAtLeast(1),
222-
NewActiveVersionValidator(),
228+
NewVersionsValidator(),
223229
},
224230
NestedObject: schema.NestedAttributeObject{
225231
Attributes: map[string]schema.Attribute{
226232
"id": schema.StringAttribute{
227233
CustomType: UUIDType,
228234
Computed: true,
235+
// This ID may change as the version is re-created.
229236
},
230-
"name": schema.StringAttribute{
231-
MarkdownDescription: "The name of the template version. Automatically generated if not provided.",
237+
"name_prefix": schema.StringAttribute{
238+
MarkdownDescription: "A prefix for the name of the template version. Must be unique within the list of versions.",
232239
Optional: true,
233240
Computed: true,
241+
Default: stringdefault.StaticString(""),
242+
PlanModifiers: []planmodifier.String{
243+
stringplanmodifier.UseStateForUnknown(),
244+
},
234245
},
235246
"message": schema.StringAttribute{
236247
MarkdownDescription: "A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated.",
@@ -261,6 +272,14 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
261272
Optional: true,
262273
NestedObject: variableNestedObject,
263274
},
275+
"revision_num": schema.Int64Attribute{
276+
MarkdownDescription: "The ordinal appended to the name_prefix to generate a unique name for the template version.",
277+
Computed: true,
278+
},
279+
"full_name": schema.StringAttribute{
280+
MarkdownDescription: "The full name of the template version, as on the Coder deployment.",
281+
Computed: true,
282+
},
264283
},
265284
PlanModifiers: []planmodifier.Object{
266285
NewDirectoryHashPlanModifier(),
@@ -316,6 +335,7 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
316335
newVersionRequest := newVersionRequest{
317336
Version: &version,
318337
OrganizationID: orgID,
338+
RevisionNum: 0,
319339
}
320340
if idx > 0 {
321341
newVersionRequest.TemplateID = &templateResp.ID
@@ -376,8 +396,9 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
376396
}
377397
tflog.Trace(ctx, "marked template version as active")
378398
}
399+
data.Versions[idx].FullName = types.StringValue(versionResp.Name)
379400
data.Versions[idx].ID = UUIDValue(versionResp.ID)
380-
data.Versions[idx].Name = types.StringValue(versionResp.Name)
401+
data.Versions[idx].RevisionNum = types.Int64Value(newVersionRequest.RevisionNum)
381402
}
382403
data.ID = UUIDValue(templateResp.ID)
383404
data.DisplayName = types.StringValue(templateResp.DisplayName)
@@ -437,7 +458,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
437458
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template version: %s", err))
438459
return
439460
}
440-
data.Versions[idx].Name = types.StringValue(versionResp.Name)
461+
data.Versions[idx].FullName = types.StringValue(versionResp.Name)
441462
data.Versions[idx].Message = types.StringValue(versionResp.Message)
442463
active := false
443464
if versionResp.ID == template.ActiveVersionID {
@@ -481,7 +502,8 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
481502

482503
client := r.data.Client
483504

484-
if !planState.EqualTemplateMetadata(curState) {
505+
templateMetadataChanged := !planState.EqualTemplateMetadata(curState)
506+
if templateMetadataChanged {
485507
tflog.Trace(ctx, "change in template metadata detected, updating.")
486508
_, err := client.UpdateTemplateMeta(ctx, templateID, codersdk.UpdateTemplateMeta{
487509
Name: planState.Name.ValueString(),
@@ -499,8 +521,9 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
499521
tflog.Trace(ctx, "successfully updated template metadata")
500522
}
501523

502-
// If there's a change, and we're still managing ACL
503-
if !planState.ACL.Equal(curState.ACL) && !planState.ACL.IsNull() {
524+
// Since the everyone group always gets deleted by `DisableEveryoneGroupAccess`, we need to run this even if there
525+
// were no ACL changes, in case the template metadata was updated.
526+
if !planState.ACL.IsNull() && (!curState.ACL.Equal(planState.ACL) || templateMetadataChanged) {
504527
var acl ACL
505528
resp.Diagnostics.Append(planState.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{})...)
506529
if resp.Diagnostics.HasError() {
@@ -515,31 +538,51 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
515538
}
516539

517540
for idx, plannedVersion := range planState.Versions {
518-
var curVersionID uuid.UUID
519-
// All versions in the state are guaranteed to have known IDs
520-
foundVersion := curState.Versions.ByID(plannedVersion.ID)
521-
// If the version is new, or if the directory hash has changed, create a new version
522-
if foundVersion == nil || foundVersion.DirectoryHash != plannedVersion.DirectoryHash {
541+
var curVersionName string
542+
// All versions in the state are guaranteed to have known name prefixes
543+
foundVersion := curState.Versions.ByNamePrefix(plannedVersion.NamePrefix)
544+
// If the version is new (name prefix doesn't exist already), or if the directory hash has changed, create a
545+
// new version.
546+
if foundVersion == nil || !foundVersion.DirectoryHash.Equal(plannedVersion.DirectoryHash) {
523547
tflog.Trace(ctx, "discovered a new or modified template version")
548+
var curRevs int64 = 0
549+
if foundVersion != nil {
550+
curRevs = foundVersion.RevisionNum.ValueInt64() + 1
551+
}
524552
versionResp, err := newVersion(ctx, client, newVersionRequest{
525553
Version: &plannedVersion,
526554
OrganizationID: orgID,
527555
TemplateID: &templateID,
556+
RevisionNum: curRevs,
528557
})
529558
if err != nil {
530559
resp.Diagnostics.AddError("Client Error", err.Error())
531560
return
532561
}
533-
curVersionID = versionResp.ID
562+
planState.Versions[idx].RevisionNum = types.Int64Value(curRevs)
563+
curVersionName = versionResp.Name
534564
} else {
535-
// Or if it's an existing version, get the ID
536-
curVersionID = plannedVersion.ID.ValueUUID()
565+
// Or if it's an existing version, get the full name to look it up
566+
planState.Versions[idx].RevisionNum = foundVersion.RevisionNum
567+
curVersionName = foundVersion.FullName.ValueString()
537568
}
538-
versionResp, err := client.TemplateVersion(ctx, curVersionID)
569+
versionResp, err := client.TemplateVersionByName(ctx, templateID, curVersionName)
539570
if err != nil {
540571
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template version: %s", err))
541572
return
542573
}
574+
575+
if versionResp.Message != plannedVersion.Message.ValueString() {
576+
_, err := client.UpdateTemplateVersion(ctx, versionResp.ID, codersdk.PatchTemplateVersionRequest{
577+
Name: versionResp.Name,
578+
Message: plannedVersion.Message.ValueStringPointer(),
579+
})
580+
if err != nil {
581+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to update template version metadata: %s", err))
582+
return
583+
}
584+
}
585+
543586
if plannedVersion.Active.ValueBool() {
544587
tflog.Trace(ctx, "marking template version as active", map[string]any{
545588
"version_id": versionResp.ID,
@@ -555,6 +598,7 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
555598
tflog.Trace(ctx, "marked template version as active")
556599
}
557600
planState.Versions[idx].ID = UUIDValue(versionResp.ID)
601+
planState.Versions[idx].FullName = types.StringValue(versionResp.Name)
558602
}
559603

560604
// Save updated data into Terraform state
@@ -592,24 +636,24 @@ func (r *TemplateResource) ConfigValidators(context.Context) []resource.ConfigVa
592636
return []resource.ConfigValidator{}
593637
}
594638

595-
type activeVersionValidator struct{}
639+
type versionsValidator struct{}
596640

597-
func NewActiveVersionValidator() validator.List {
598-
return &activeVersionValidator{}
641+
func NewVersionsValidator() validator.List {
642+
return &versionsValidator{}
599643
}
600644

601645
// Description implements validator.List.
602-
func (a *activeVersionValidator) Description(ctx context.Context) string {
646+
func (a *versionsValidator) Description(ctx context.Context) string {
603647
return a.MarkdownDescription(ctx)
604648
}
605649

606650
// MarkdownDescription implements validator.List.
607-
func (a *activeVersionValidator) MarkdownDescription(context.Context) string {
651+
func (a *versionsValidator) MarkdownDescription(context.Context) string {
608652
return "Validate that exactly one template version has active set to true."
609653
}
610654

611655
// ValidateList implements validator.List.
612-
func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
656+
func (a *versionsValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
613657
var data []TemplateVersion
614658
resp.Diagnostics.Append(req.ConfigValue.ElementsAs(ctx, &data, false)...)
615659
if resp.Diagnostics.HasError() {
@@ -630,9 +674,20 @@ func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator
630674
if !active {
631675
resp.Diagnostics.AddError("Client Error", "At least one template version must be active.")
632676
}
677+
678+
// Check if all versions have unique name prefixes
679+
namePrefixes := make(map[string]bool)
680+
for _, version := range data {
681+
namePrefix := version.NamePrefix.ValueString()
682+
if _, ok := namePrefixes[namePrefix]; ok {
683+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Template version name prefix must be unique, found duplicate: `%s`", namePrefix))
684+
return
685+
}
686+
namePrefixes[namePrefix] = true
687+
}
633688
}
634689

635-
var _ validator.List = &activeVersionValidator{}
690+
var _ validator.List = &versionsValidator{}
636691

637692
type directoryHashPlanModifier struct{}
638693

@@ -731,6 +786,7 @@ type newVersionRequest struct {
731786
OrganizationID uuid.UUID
732787
Version *TemplateVersion
733788
TemplateID *uuid.UUID
789+
RevisionNum int64
734790
}
735791

736792
func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequest) (*codersdk.TemplateVersion, error) {
@@ -761,8 +817,17 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ
761817
Value: variable.Value.ValueString(),
762818
})
763819
}
820+
randPart, err := cryptorand.String(6)
821+
if err != nil {
822+
return nil, fmt.Errorf("failed to generate random string: %s", err)
823+
}
824+
825+
versionName := fmt.Sprintf("%d-%s", req.RevisionNum, randPart)
826+
if req.Version.NamePrefix.ValueString() != "" {
827+
versionName = fmt.Sprintf("%s-%s", req.Version.NamePrefix.ValueString(), versionName)
828+
}
764829
tmplVerReq := codersdk.CreateTemplateVersionRequest{
765-
Name: req.Version.Name.ValueString(),
830+
Name: versionName,
766831
Message: req.Version.Message.ValueString(),
767832
StorageMethod: codersdk.ProvisionerStorageMethodFile,
768833
Provisioner: codersdk.ProvisionerTypeTerraform,

0 commit comments

Comments
 (0)