Skip to content

Commit 450beaa

Browse files
committed
fix: use partially randomised version names
1 parent 9aa27ba commit 450beaa

File tree

11 files changed

+187
-74
lines changed

11 files changed

+187
-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_suffix` (String) A suffix for the name of the template version. Prepended by a random string. 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
@@ -16,6 +16,7 @@ require (
1616
github.com/hashicorp/terraform-plugin-go v0.23.0
1717
github.com/hashicorp/terraform-plugin-log v0.9.0
1818
github.com/hashicorp/terraform-plugin-testing v1.9.0
19+
github.com/moby/moby v26.1.0+incompatible
1920
github.com/stretchr/testify v1.9.0
2021
)
2122

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
317317
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
318318
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
319319
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
320+
github.com/moby/moby v26.1.0+incompatible h1:mjepCwMH0KpCgPvrXjqqyCeTCHgzO7p9TwZ2nQMI2qU=
321+
github.com/moby/moby v26.1.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
320322
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
321323
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
322324
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

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(`^0_.*\.latest$`), 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_suffix = "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_suffix = "legacy"
56+
directory = "./version-two"
57+
active = false
5858
tf_vars = [
5959
{
6060
name = "name"

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+
NameSuffix: 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: 96 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77
"io"
8+
"strings"
89

910
"cdr.dev/slog"
1011
"github.com/coder/coder/v2/codersdk"
@@ -25,6 +26,7 @@ import (
2526
"github.com/hashicorp/terraform-plugin-framework/types"
2627
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
2728
"github.com/hashicorp/terraform-plugin-log/tflog"
29+
"github.com/moby/moby/pkg/namesgenerator"
2830
)
2931

3032
// Ensure provider defined types fully satisfy framework interfaces.
@@ -53,6 +55,7 @@ type TemplateResourceModel struct {
5355
AllowUserAutoStart types.Bool `tfsdk:"allow_user_auto_start"`
5456
AllowUserAutoStop types.Bool `tfsdk:"allow_user_auto_stop"`
5557

58+
// If null, we are not managing ACL via Terraform (such as for AGPL).
5659
ACL types.Object `tfsdk:"acl"`
5760
Versions Versions `tfsdk:"versions"`
5861
}
@@ -69,21 +72,25 @@ func (m TemplateResourceModel) EqualTemplateMetadata(other TemplateResourceModel
6972
}
7073

7174
type TemplateVersion struct {
72-
ID UUID `tfsdk:"id"`
73-
Name types.String `tfsdk:"name"`
75+
ID UUID `tfsdk:"id"`
76+
77+
NameSuffix types.String `tfsdk:"name_suffix"`
7478
Message types.String `tfsdk:"message"`
7579
Directory types.String `tfsdk:"directory"`
7680
DirectoryHash types.String `tfsdk:"directory_hash"`
7781
Active types.Bool `tfsdk:"active"`
7882
TerraformVariables []Variable `tfsdk:"tf_vars"`
7983
ProvisionerTags []Variable `tfsdk:"provisioner_tags"`
84+
85+
RevisionNum types.Int64 `tfsdk:"revision_num"`
86+
FullName types.String `tfsdk:"full_name"`
8087
}
8188

8289
type Versions []TemplateVersion
8390

84-
func (v Versions) ByID(id UUID) *TemplateVersion {
91+
func (v Versions) ByNameSuffix(nameSuffix types.String) *TemplateVersion {
8592
for _, m := range v {
86-
if m.ID.Equal(id) {
93+
if m.NameSuffix.Equal(nameSuffix) {
8794
return &m
8895
}
8996
}
@@ -219,18 +226,20 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
219226
Required: true,
220227
Validators: []validator.List{
221228
listvalidator.SizeAtLeast(1),
222-
NewActiveVersionValidator(),
229+
NewVersionsValidator(),
223230
},
224231
NestedObject: schema.NestedAttributeObject{
225232
Attributes: map[string]schema.Attribute{
226233
"id": schema.StringAttribute{
227234
CustomType: UUIDType,
228235
Computed: true,
236+
// This ID may change as the version is re-created.
229237
},
230-
"name": schema.StringAttribute{
231-
MarkdownDescription: "The name of the template version. Automatically generated if not provided.",
238+
"name_suffix": schema.StringAttribute{
239+
MarkdownDescription: "A suffix for the name of the template version. Prepended by a random string. Must be unique within the list of versions.",
232240
Optional: true,
233241
Computed: true,
242+
Default: stringdefault.StaticString(""),
234243
},
235244
"message": schema.StringAttribute{
236245
MarkdownDescription: "A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated.",
@@ -261,6 +270,14 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
261270
Optional: true,
262271
NestedObject: variableNestedObject,
263272
},
273+
"revision_num": schema.Int64Attribute{
274+
MarkdownDescription: "The ordinal appended to the name_prefix to generate a unique name for the template version.",
275+
Computed: true,
276+
},
277+
"full_name": schema.StringAttribute{
278+
MarkdownDescription: "The full name of the template version, as on the Coder deployment.",
279+
Computed: true,
280+
},
264281
},
265282
PlanModifiers: []planmodifier.Object{
266283
NewDirectoryHashPlanModifier(),
@@ -316,6 +333,7 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
316333
newVersionRequest := newVersionRequest{
317334
Version: &version,
318335
OrganizationID: orgID,
336+
RevisionNum: 0,
319337
}
320338
if idx > 0 {
321339
newVersionRequest.TemplateID = &templateResp.ID
@@ -376,8 +394,9 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
376394
}
377395
tflog.Trace(ctx, "marked template version as active")
378396
}
397+
data.Versions[idx].FullName = types.StringValue(versionResp.Name)
379398
data.Versions[idx].ID = UUIDValue(versionResp.ID)
380-
data.Versions[idx].Name = types.StringValue(versionResp.Name)
399+
data.Versions[idx].RevisionNum = types.Int64Value(newVersionRequest.RevisionNum)
381400
}
382401
data.ID = UUIDValue(templateResp.ID)
383402
data.DisplayName = types.StringValue(templateResp.DisplayName)
@@ -437,7 +456,8 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
437456
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template version: %s", err))
438457
return
439458
}
440-
data.Versions[idx].Name = types.StringValue(versionResp.Name)
459+
data.Versions[idx].FullName = types.StringValue(versionResp.Name)
460+
data.Versions[idx].NameSuffix = types.StringValue(extractNameSuffix(versionResp.Name))
441461
data.Versions[idx].Message = types.StringValue(versionResp.Message)
442462
active := false
443463
if versionResp.ID == template.ActiveVersionID {
@@ -481,7 +501,8 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
481501

482502
client := r.data.Client
483503

484-
if !planState.EqualTemplateMetadata(curState) {
504+
templateMetadataChanged := !planState.EqualTemplateMetadata(curState)
505+
if templateMetadataChanged {
485506
tflog.Trace(ctx, "change in template metadata detected, updating.")
486507
_, err := client.UpdateTemplateMeta(ctx, templateID, codersdk.UpdateTemplateMeta{
487508
Name: planState.Name.ValueString(),
@@ -499,8 +520,9 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
499520
tflog.Trace(ctx, "successfully updated template metadata")
500521
}
501522

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

517539
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 {
540+
var curVersionName string
541+
// All versions in the state are guaranteed to have known name suffixes
542+
foundVersion := curState.Versions.ByNameSuffix(plannedVersion.NameSuffix)
543+
// If the version is new (name suffix doesn't exist already), or if the directory hash has changed, create a
544+
// new version.
545+
if foundVersion == nil || !foundVersion.DirectoryHash.Equal(plannedVersion.DirectoryHash) {
523546
tflog.Trace(ctx, "discovered a new or modified template version")
547+
var curRevs int64 = 0
548+
if foundVersion != nil {
549+
curRevs = foundVersion.RevisionNum.ValueInt64() + 1
550+
}
524551
versionResp, err := newVersion(ctx, client, newVersionRequest{
525552
Version: &plannedVersion,
526553
OrganizationID: orgID,
527554
TemplateID: &templateID,
555+
RevisionNum: curRevs,
528556
})
529557
if err != nil {
530558
resp.Diagnostics.AddError("Client Error", err.Error())
531559
return
532560
}
533-
curVersionID = versionResp.ID
561+
planState.Versions[idx].RevisionNum = types.Int64Value(curRevs)
562+
curVersionName = versionResp.Name
534563
} else {
535-
// Or if it's an existing version, get the ID
536-
curVersionID = plannedVersion.ID.ValueUUID()
564+
// Or if it's an existing version, get the full name to look it up
565+
planState.Versions[idx].RevisionNum = foundVersion.RevisionNum
566+
curVersionName = foundVersion.FullName.ValueString()
537567
}
538-
versionResp, err := client.TemplateVersion(ctx, curVersionID)
568+
versionResp, err := client.TemplateVersionByName(ctx, templateID, curVersionName)
539569
if err != nil {
540570
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template version: %s", err))
541571
return
542572
}
573+
574+
if versionResp.Message != plannedVersion.Message.ValueString() {
575+
_, err := client.UpdateTemplateVersion(ctx, versionResp.ID, codersdk.PatchTemplateVersionRequest{
576+
Name: versionResp.Name,
577+
Message: plannedVersion.Message.ValueStringPointer(),
578+
})
579+
if err != nil {
580+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to update template version metadata: %s", err))
581+
return
582+
}
583+
}
584+
543585
if plannedVersion.Active.ValueBool() {
544586
tflog.Trace(ctx, "marking template version as active", map[string]any{
545587
"version_id": versionResp.ID,
@@ -555,6 +597,7 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
555597
tflog.Trace(ctx, "marked template version as active")
556598
}
557599
planState.Versions[idx].ID = UUIDValue(versionResp.ID)
600+
planState.Versions[idx].FullName = types.StringValue(versionResp.Name)
558601
}
559602

560603
// Save updated data into Terraform state
@@ -592,24 +635,24 @@ func (r *TemplateResource) ConfigValidators(context.Context) []resource.ConfigVa
592635
return []resource.ConfigValidator{}
593636
}
594637

595-
type activeVersionValidator struct{}
638+
type versionsValidator struct{}
596639

597-
func NewActiveVersionValidator() validator.List {
598-
return &activeVersionValidator{}
640+
func NewVersionsValidator() validator.List {
641+
return &versionsValidator{}
599642
}
600643

601644
// Description implements validator.List.
602-
func (a *activeVersionValidator) Description(ctx context.Context) string {
645+
func (a *versionsValidator) Description(ctx context.Context) string {
603646
return a.MarkdownDescription(ctx)
604647
}
605648

606649
// MarkdownDescription implements validator.List.
607-
func (a *activeVersionValidator) MarkdownDescription(context.Context) string {
650+
func (a *versionsValidator) MarkdownDescription(context.Context) string {
608651
return "Validate that exactly one template version has active set to true."
609652
}
610653

611654
// ValidateList implements validator.List.
612-
func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
655+
func (a *versionsValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
613656
var data []TemplateVersion
614657
resp.Diagnostics.Append(req.ConfigValue.ElementsAs(ctx, &data, false)...)
615658
if resp.Diagnostics.HasError() {
@@ -630,9 +673,20 @@ func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator
630673
if !active {
631674
resp.Diagnostics.AddError("Client Error", "At least one template version must be active.")
632675
}
676+
677+
// Check if all versions have unique name suffixes
678+
nameSuffixes := make(map[string]bool)
679+
for _, version := range data {
680+
nameSuffix := version.NameSuffix.ValueString()
681+
if _, ok := nameSuffixes[nameSuffix]; ok {
682+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Template version name suffixes must be unique, found duplicate: `%s`", nameSuffix))
683+
return
684+
}
685+
nameSuffixes[nameSuffix] = true
686+
}
633687
}
634688

635-
var _ validator.List = &activeVersionValidator{}
689+
var _ validator.List = &versionsValidator{}
636690

637691
type directoryHashPlanModifier struct{}
638692

@@ -731,6 +785,7 @@ type newVersionRequest struct {
731785
OrganizationID uuid.UUID
732786
Version *TemplateVersion
733787
TemplateID *uuid.UUID
788+
RevisionNum int64
734789
}
735790

736791
func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequest) (*codersdk.TemplateVersion, error) {
@@ -761,8 +816,12 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ
761816
Value: variable.Value.ValueString(),
762817
})
763818
}
819+
versionName := fmt.Sprintf("%d_%s", req.RevisionNum, namesgenerator.GetRandomName(1))
820+
if req.Version.NameSuffix.ValueString() != "" {
821+
versionName = fmt.Sprintf("%s.%s", versionName, req.Version.NameSuffix.ValueString())
822+
}
764823
tmplVerReq := codersdk.CreateTemplateVersionRequest{
765-
Name: req.Version.Name.ValueString(),
824+
Name: versionName,
766825
Message: req.Version.Message.ValueString(),
767826
StorageMethod: codersdk.ProvisionerStorageMethodFile,
768827
Provisioner: codersdk.ProvisionerTypeTerraform,
@@ -821,3 +880,11 @@ func convertResponseToACL(acl codersdk.TemplateACL) ACL {
821880
GroupPermissions: groupPerms,
822881
}
823882
}
883+
884+
func extractNameSuffix(name string) string {
885+
parts := strings.Split(name, ".")
886+
if len(parts) == 1 {
887+
return ""
888+
}
889+
return parts[1]
890+
}

0 commit comments

Comments
 (0)