5
5
"context"
6
6
"fmt"
7
7
"io"
8
+ "strings"
8
9
9
10
"cdr.dev/slog"
10
11
"github.com/coder/coder/v2/codersdk"
@@ -25,6 +26,7 @@ import (
25
26
"github.com/hashicorp/terraform-plugin-framework/types"
26
27
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
27
28
"github.com/hashicorp/terraform-plugin-log/tflog"
29
+ "github.com/moby/moby/pkg/namesgenerator"
28
30
)
29
31
30
32
// Ensure provider defined types fully satisfy framework interfaces.
@@ -53,6 +55,7 @@ type TemplateResourceModel struct {
53
55
AllowUserAutoStart types.Bool `tfsdk:"allow_user_auto_start"`
54
56
AllowUserAutoStop types.Bool `tfsdk:"allow_user_auto_stop"`
55
57
58
+ // If null, we are not managing ACL via Terraform (such as for AGPL).
56
59
ACL types.Object `tfsdk:"acl"`
57
60
Versions Versions `tfsdk:"versions"`
58
61
}
@@ -69,21 +72,25 @@ func (m TemplateResourceModel) EqualTemplateMetadata(other TemplateResourceModel
69
72
}
70
73
71
74
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"`
74
78
Message types.String `tfsdk:"message"`
75
79
Directory types.String `tfsdk:"directory"`
76
80
DirectoryHash types.String `tfsdk:"directory_hash"`
77
81
Active types.Bool `tfsdk:"active"`
78
82
TerraformVariables []Variable `tfsdk:"tf_vars"`
79
83
ProvisionerTags []Variable `tfsdk:"provisioner_tags"`
84
+
85
+ RevisionNum types.Int64 `tfsdk:"revision_num"`
86
+ FullName types.String `tfsdk:"full_name"`
80
87
}
81
88
82
89
type Versions []TemplateVersion
83
90
84
- func (v Versions ) ByID ( id UUID ) * TemplateVersion {
91
+ func (v Versions ) ByNameSuffix ( nameSuffix types. String ) * TemplateVersion {
85
92
for _ , m := range v {
86
- if m .ID .Equal (id ) {
93
+ if m .NameSuffix .Equal (nameSuffix ) {
87
94
return & m
88
95
}
89
96
}
@@ -219,18 +226,20 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
219
226
Required : true ,
220
227
Validators : []validator.List {
221
228
listvalidator .SizeAtLeast (1 ),
222
- NewActiveVersionValidator (),
229
+ NewVersionsValidator (),
223
230
},
224
231
NestedObject : schema.NestedAttributeObject {
225
232
Attributes : map [string ]schema.Attribute {
226
233
"id" : schema.StringAttribute {
227
234
CustomType : UUIDType ,
228
235
Computed : true ,
236
+ // This ID may change as the version is re-created.
229
237
},
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 ." ,
232
240
Optional : true ,
233
241
Computed : true ,
242
+ Default : stringdefault .StaticString ("" ),
234
243
},
235
244
"message" : schema.StringAttribute {
236
245
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
261
270
Optional : true ,
262
271
NestedObject : variableNestedObject ,
263
272
},
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
+ },
264
281
},
265
282
PlanModifiers : []planmodifier.Object {
266
283
NewDirectoryHashPlanModifier (),
@@ -316,6 +333,7 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
316
333
newVersionRequest := newVersionRequest {
317
334
Version : & version ,
318
335
OrganizationID : orgID ,
336
+ RevisionNum : 0 ,
319
337
}
320
338
if idx > 0 {
321
339
newVersionRequest .TemplateID = & templateResp .ID
@@ -376,8 +394,9 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
376
394
}
377
395
tflog .Trace (ctx , "marked template version as active" )
378
396
}
397
+ data .Versions [idx ].FullName = types .StringValue (versionResp .Name )
379
398
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 )
381
400
}
382
401
data .ID = UUIDValue (templateResp .ID )
383
402
data .DisplayName = types .StringValue (templateResp .DisplayName )
@@ -437,7 +456,8 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
437
456
resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to get template version: %s" , err ))
438
457
return
439
458
}
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 ))
441
461
data .Versions [idx ].Message = types .StringValue (versionResp .Message )
442
462
active := false
443
463
if versionResp .ID == template .ActiveVersionID {
@@ -481,7 +501,8 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
481
501
482
502
client := r .data .Client
483
503
484
- if ! planState .EqualTemplateMetadata (curState ) {
504
+ templateMetadataChanged := ! planState .EqualTemplateMetadata (curState )
505
+ if templateMetadataChanged {
485
506
tflog .Trace (ctx , "change in template metadata detected, updating." )
486
507
_ , err := client .UpdateTemplateMeta (ctx , templateID , codersdk.UpdateTemplateMeta {
487
508
Name : planState .Name .ValueString (),
@@ -499,8 +520,9 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
499
520
tflog .Trace (ctx , "successfully updated template metadata" )
500
521
}
501
522
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 ) {
504
526
var acl ACL
505
527
resp .Diagnostics .Append (planState .ACL .As (ctx , & acl , basetypes.ObjectAsOptions {})... )
506
528
if resp .Diagnostics .HasError () {
@@ -515,31 +537,51 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
515
537
}
516
538
517
539
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 ) {
523
546
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
+ }
524
551
versionResp , err := newVersion (ctx , client , newVersionRequest {
525
552
Version : & plannedVersion ,
526
553
OrganizationID : orgID ,
527
554
TemplateID : & templateID ,
555
+ RevisionNum : curRevs ,
528
556
})
529
557
if err != nil {
530
558
resp .Diagnostics .AddError ("Client Error" , err .Error ())
531
559
return
532
560
}
533
- curVersionID = versionResp .ID
561
+ planState .Versions [idx ].RevisionNum = types .Int64Value (curRevs )
562
+ curVersionName = versionResp .Name
534
563
} 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 ()
537
567
}
538
- versionResp , err := client .TemplateVersion (ctx , curVersionID )
568
+ versionResp , err := client .TemplateVersionByName (ctx , templateID , curVersionName )
539
569
if err != nil {
540
570
resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to get template version: %s" , err ))
541
571
return
542
572
}
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
+
543
585
if plannedVersion .Active .ValueBool () {
544
586
tflog .Trace (ctx , "marking template version as active" , map [string ]any {
545
587
"version_id" : versionResp .ID ,
@@ -555,6 +597,7 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
555
597
tflog .Trace (ctx , "marked template version as active" )
556
598
}
557
599
planState .Versions [idx ].ID = UUIDValue (versionResp .ID )
600
+ planState .Versions [idx ].FullName = types .StringValue (versionResp .Name )
558
601
}
559
602
560
603
// Save updated data into Terraform state
@@ -592,24 +635,24 @@ func (r *TemplateResource) ConfigValidators(context.Context) []resource.ConfigVa
592
635
return []resource.ConfigValidator {}
593
636
}
594
637
595
- type activeVersionValidator struct {}
638
+ type versionsValidator struct {}
596
639
597
- func NewActiveVersionValidator () validator.List {
598
- return & activeVersionValidator {}
640
+ func NewVersionsValidator () validator.List {
641
+ return & versionsValidator {}
599
642
}
600
643
601
644
// Description implements validator.List.
602
- func (a * activeVersionValidator ) Description (ctx context.Context ) string {
645
+ func (a * versionsValidator ) Description (ctx context.Context ) string {
603
646
return a .MarkdownDescription (ctx )
604
647
}
605
648
606
649
// MarkdownDescription implements validator.List.
607
- func (a * activeVersionValidator ) MarkdownDescription (context.Context ) string {
650
+ func (a * versionsValidator ) MarkdownDescription (context.Context ) string {
608
651
return "Validate that exactly one template version has active set to true."
609
652
}
610
653
611
654
// 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 ) {
613
656
var data []TemplateVersion
614
657
resp .Diagnostics .Append (req .ConfigValue .ElementsAs (ctx , & data , false )... )
615
658
if resp .Diagnostics .HasError () {
@@ -630,9 +673,20 @@ func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator
630
673
if ! active {
631
674
resp .Diagnostics .AddError ("Client Error" , "At least one template version must be active." )
632
675
}
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
+ }
633
687
}
634
688
635
- var _ validator.List = & activeVersionValidator {}
689
+ var _ validator.List = & versionsValidator {}
636
690
637
691
type directoryHashPlanModifier struct {}
638
692
@@ -731,6 +785,7 @@ type newVersionRequest struct {
731
785
OrganizationID uuid.UUID
732
786
Version * TemplateVersion
733
787
TemplateID * uuid.UUID
788
+ RevisionNum int64
734
789
}
735
790
736
791
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
761
816
Value : variable .Value .ValueString (),
762
817
})
763
818
}
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
+ }
764
823
tmplVerReq := codersdk.CreateTemplateVersionRequest {
765
- Name : req . Version . Name . ValueString () ,
824
+ Name : versionName ,
766
825
Message : req .Version .Message .ValueString (),
767
826
StorageMethod : codersdk .ProvisionerStorageMethodFile ,
768
827
Provisioner : codersdk .ProvisionerTypeTerraform ,
@@ -821,3 +880,11 @@ func convertResponseToACL(acl codersdk.TemplateACL) ACL {
821
880
GroupPermissions : groupPerms ,
822
881
}
823
882
}
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