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
7173type 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
8288type 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
637692type 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
736792func 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