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
7174type 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
8289type 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
637691type 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
736791func 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