@@ -3,6 +3,7 @@ package provider
3
3
import (
4
4
"bufio"
5
5
"context"
6
+ "encoding/json"
6
7
"fmt"
7
8
"io"
8
9
@@ -346,7 +347,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
346
347
Computed : true ,
347
348
},
348
349
"name" : schema.StringAttribute {
349
- MarkdownDescription : "The name of the template version. Automatically generated if not provided." ,
350
+ MarkdownDescription : "The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents are updated. " ,
350
351
Optional : true ,
351
352
Computed : true ,
352
353
},
@@ -502,6 +503,17 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
502
503
data .ID = UUIDValue (templateResp .ID )
503
504
data .DisplayName = types .StringValue (templateResp .DisplayName )
504
505
506
+ // We have to init the private state again since the PlanModifyObject private
507
+ // state is not accessible in Create
508
+ resp .Diagnostics .Append (setEmptyPrivateState (ctx , resp .Private )... )
509
+ if resp .Diagnostics .HasError () {
510
+ return
511
+ }
512
+ resp .Diagnostics .Append (data .Versions .writePrivateState (ctx , resp .Private )... )
513
+ if resp .Diagnostics .HasError () {
514
+ return
515
+ }
516
+
505
517
// Save data into Terraform sutate
506
518
resp .Diagnostics .Append (resp .State .Set (ctx , & data )... )
507
519
}
@@ -569,11 +581,11 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
569
581
}
570
582
571
583
func (r * TemplateResource ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
572
- var planState TemplateResourceModel
584
+ var newState TemplateResourceModel
573
585
var curState TemplateResourceModel
574
586
575
587
// Read Terraform plan data into the model
576
- resp .Diagnostics .Append (req .Plan .Get (ctx , & planState )... )
588
+ resp .Diagnostics .Append (req .Plan .Get (ctx , & newState )... )
577
589
578
590
if resp .Diagnostics .HasError () {
579
591
return
@@ -585,25 +597,25 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
585
597
return
586
598
}
587
599
588
- if planState .OrganizationID .IsUnknown () {
589
- planState .OrganizationID = UUIDValue (r .data .DefaultOrganizationID )
600
+ if newState .OrganizationID .IsUnknown () {
601
+ newState .OrganizationID = UUIDValue (r .data .DefaultOrganizationID )
590
602
}
591
603
592
- if planState .DisplayName .IsUnknown () {
593
- planState .DisplayName = planState .Name
604
+ if newState .DisplayName .IsUnknown () {
605
+ newState .DisplayName = newState .Name
594
606
}
595
607
596
- orgID := planState .OrganizationID .ValueUUID ()
608
+ orgID := newState .OrganizationID .ValueUUID ()
597
609
598
- templateID := planState .ID .ValueUUID ()
610
+ templateID := newState .ID .ValueUUID ()
599
611
600
612
client := r .data .Client
601
613
602
- templateMetadataChanged := ! planState .EqualTemplateMetadata (curState )
614
+ templateMetadataChanged := ! newState .EqualTemplateMetadata (curState )
603
615
// This is required, as the API will reject no-diff updates.
604
616
if templateMetadataChanged {
605
617
tflog .Trace (ctx , "change in template metadata detected, updating." )
606
- updateReq := planState .toUpdateRequest (ctx , resp )
618
+ updateReq := newState .toUpdateRequest (ctx , resp )
607
619
if resp .Diagnostics .HasError () {
608
620
return
609
621
}
@@ -618,9 +630,9 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
618
630
619
631
// Since the everyone group always gets deleted by `DisableEveryoneGroupAccess`, we need to run this even if there
620
632
// were no ACL changes but the template metadata was updated.
621
- if ! planState .ACL .IsNull () && (! curState .ACL .Equal (planState .ACL ) || templateMetadataChanged ) {
633
+ if ! newState .ACL .IsNull () && (! curState .ACL .Equal (newState .ACL ) || templateMetadataChanged ) {
622
634
var acl ACL
623
- resp .Diagnostics .Append (planState .ACL .As (ctx , & acl , basetypes.ObjectAsOptions {})... )
635
+ resp .Diagnostics .Append (newState .ACL .As (ctx , & acl , basetypes.ObjectAsOptions {})... )
624
636
if resp .Diagnostics .HasError () {
625
637
return
626
638
}
@@ -632,51 +644,64 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
632
644
tflog .Trace (ctx , "successfully updated template ACL" )
633
645
}
634
646
635
- for idx , plannedVersion := range planState .Versions {
636
- var curVersionID uuid.UUID
637
- // All versions in the state are guaranteed to have known IDs
638
- foundVersion := curState .Versions .ByID (plannedVersion .ID )
639
- // If the version is new, or if the directory hash has changed, create a new version
640
- if foundVersion == nil || foundVersion .DirectoryHash != plannedVersion .DirectoryHash {
647
+ for idx := range newState .Versions {
648
+ if newState .Versions [idx ].ID .IsUnknown () {
641
649
tflog .Trace (ctx , "discovered a new or modified template version" )
642
- versionResp , err := newVersion (ctx , client , newVersionRequest {
643
- Version : & plannedVersion ,
650
+ uploadResp , err := newVersion (ctx , client , newVersionRequest {
651
+ Version : & newState . Versions [ idx ] ,
644
652
OrganizationID : orgID ,
645
653
TemplateID : & templateID ,
646
654
})
647
655
if err != nil {
648
656
resp .Diagnostics .AddError ("Client Error" , err .Error ())
649
657
return
650
658
}
651
- curVersionID = versionResp .ID
659
+ versionResp , err := client .TemplateVersion (ctx , uploadResp .ID )
660
+ if err != nil {
661
+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to get template version: %s" , err ))
662
+ return
663
+ }
664
+ newState .Versions [idx ].ID = UUIDValue (versionResp .ID )
665
+ newState .Versions [idx ].Name = types .StringValue (versionResp .Name )
652
666
} else {
653
- // Or if it's an existing version, get the ID
654
- curVersionID = plannedVersion .ID .ValueUUID ()
655
- }
656
- versionResp , err := client .TemplateVersion (ctx , curVersionID )
657
- if err != nil {
658
- resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to get template version: %s" , err ))
659
- return
667
+ versionResp , err := client .UpdateTemplateVersion (ctx , newState .Versions [idx ].ID .ValueUUID (), codersdk.PatchTemplateVersionRequest {
668
+ Name : newState .Versions [idx ].Name .ValueString (),
669
+ Message : newState .Versions [idx ].Message .ValueStringPointer (),
670
+ })
671
+ if err != nil {
672
+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to update template version metadata: %s" , err ))
673
+ return
674
+ }
675
+ // If the name was not provided on an update we set it to the patch result, which is the previous name.
676
+ // There's no way to go back to an auto-generated name unless the template version files itself change.
677
+ newState .Versions [idx ].Name = types .StringValue (versionResp .Name )
660
678
}
661
- if plannedVersion .Active .ValueBool () {
679
+ if newState . Versions [ idx ] .Active .ValueBool () {
662
680
tflog .Trace (ctx , "marking template version as active" , map [string ]any {
663
- "version_id" : versionResp . ID ,
664
- "template_id" : templateID ,
681
+ "version_id" : newState . Versions [ idx ]. ID . ValueString () ,
682
+ "template_id" : templateID . String () ,
665
683
})
666
684
err := client .UpdateActiveTemplateVersion (ctx , templateID , codersdk.UpdateActiveTemplateVersion {
667
- ID : versionResp . ID ,
685
+ ID : newState . Versions [ idx ]. ID . ValueUUID () ,
668
686
})
669
687
if err != nil {
670
688
resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to update active template version: %s" , err ))
671
689
return
672
690
}
673
691
tflog .Trace (ctx , "marked template version as active" )
674
692
}
675
- planState .Versions [idx ].ID = UUIDValue (versionResp .ID )
693
+ }
694
+
695
+ // We only want the previous apply in the state at any given time
696
+ resp .Diagnostics .Append (setEmptyPrivateState (ctx , resp .Private )... )
697
+
698
+ resp .Diagnostics .Append (newState .Versions .writePrivateState (ctx , resp .Private )... )
699
+ if resp .Diagnostics .HasError () {
700
+ return
676
701
}
677
702
678
703
// Save updated data into Terraform state
679
- resp .Diagnostics .Append (resp .State .Set (ctx , & planState )... )
704
+ resp .Diagnostics .Append (resp .State .Set (ctx , & newState )... )
680
705
}
681
706
682
707
func (r * TemplateResource ) Delete (ctx context.Context , req resource.DeleteRequest , resp * resource.DeleteResponse ) {
@@ -766,25 +791,26 @@ func (d *directoryHashPlanModifier) MarkdownDescription(context.Context) string
766
791
767
792
// PlanModifyObject implements planmodifier.Object.
768
793
func (d * directoryHashPlanModifier ) PlanModifyObject (ctx context.Context , req planmodifier.ObjectRequest , resp * planmodifier.ObjectResponse ) {
769
- attributes := req .PlanValue .Attributes ()
770
- directory , ok := attributes ["directory" ].(types.String )
771
- if ! ok {
772
- resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("unexpected type for directory, got: %T" , directory ))
794
+ var data TemplateVersion
795
+ resp .Diagnostics .Append (req .PlanValue .As (ctx , & data , basetypes.ObjectAsOptions {})... )
796
+ if resp .Diagnostics .HasError () {
773
797
return
774
798
}
775
799
776
- hash , err := computeDirectoryHash (directory .ValueString ())
800
+ hash , err := computeDirectoryHash (data . Directory .ValueString ())
777
801
if err != nil {
778
802
resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to compute directory hash: %s" , err ))
779
803
return
780
804
}
781
- attributes ["directory_hash" ] = types .StringValue (hash )
782
- out , diag := types .ObjectValue (req .PlanValue .AttributeTypes (ctx ), attributes )
783
- if diag .HasError () {
784
- resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to create plan object: %s" , diag ))
805
+
806
+ data .DirectoryHash = types .StringValue (hash )
807
+ // Populate version IDs or mark them as unknown if the hash has changed
808
+ resp .Diagnostics .Append (data .readFromPrivateState (ctx , req .Private )... )
809
+ if resp .Diagnostics .HasError () {
785
810
return
786
811
}
787
- resp .PlanValue = out
812
+
813
+ resp .PlanValue , resp .Diagnostics = types .ObjectValueFrom (ctx , req .PlanValue .AttributeTypes (ctx ), data )
788
814
}
789
815
790
816
func NewDirectoryHashPlanModifier () planmodifier.Object {
@@ -1062,3 +1088,81 @@ func (r *TemplateResourceModel) toCreateRequest(ctx context.Context, resp *resou
1062
1088
DisableEveryoneGroupAccess : ! r .ACL .IsNull (),
1063
1089
}
1064
1090
}
1091
+
1092
+ type LastVersionsByHash = map [string ]PreviousTemplateVersion
1093
+
1094
+ var LastVersionsKey = "last_versions"
1095
+
1096
+ type PreviousTemplateVersion struct {
1097
+ ID uuid.UUID `json:"id"`
1098
+ Name string `json:"name"`
1099
+ }
1100
+
1101
+ type privateState interface {
1102
+ GetKey (ctx context.Context , key string ) ([]byte , diag.Diagnostics )
1103
+ SetKey (ctx context.Context , key string , value []byte ) diag.Diagnostics
1104
+ }
1105
+
1106
+ func (v Versions ) writePrivateState (ctx context.Context , ps privateState ) (diags diag.Diagnostics ) {
1107
+ var lv LastVersionsByHash
1108
+ lvBytes , diag := ps .GetKey (ctx , LastVersionsKey )
1109
+ if diag .HasError () {
1110
+ return diag
1111
+ }
1112
+ err := json .Unmarshal (lvBytes , & lv )
1113
+ if err != nil {
1114
+ diags .AddError ("Client Error" , fmt .Sprintf ("Failed to unmarshal private state when writing: %s" , err ))
1115
+ return diags
1116
+ }
1117
+ for _ , version := range v {
1118
+ lv [version .DirectoryHash .ValueString ()] = PreviousTemplateVersion {
1119
+ ID : version .ID .ValueUUID (),
1120
+ Name : version .ID .ValueString (),
1121
+ }
1122
+ lvBytes , err = json .Marshal (lv )
1123
+ if err != nil {
1124
+ diags .AddError ("Client Error" , fmt .Sprintf ("Failed to marshal private state: %s" , err ))
1125
+ return diags
1126
+ }
1127
+ }
1128
+ return ps .SetKey (ctx , LastVersionsKey , lvBytes )
1129
+ }
1130
+
1131
+ func (v * TemplateVersion ) readFromPrivateState (ctx context.Context , ps privateState ) (diags diag.Diagnostics ) {
1132
+ var lv LastVersionsByHash
1133
+ lvBytes , diag := ps .GetKey (ctx , LastVersionsKey )
1134
+ if diag .HasError () {
1135
+ diags .Append (diag ... )
1136
+ return
1137
+ }
1138
+ // If this is the first read, init the private state value
1139
+ if lvBytes == nil {
1140
+ setEmptyPrivateState (ctx , ps )
1141
+ return
1142
+ }
1143
+ err := json .Unmarshal (lvBytes , & lv )
1144
+ if err != nil {
1145
+ diags .AddError ("Client Error" , fmt .Sprintf ("Failed to unmarshal private state when reading: %s" , err ))
1146
+ return
1147
+ }
1148
+
1149
+ prev , ok := lv [v .DirectoryHash .ValueString ()]
1150
+ // If not in state, mark as known after apply since we'll create a new version.
1151
+ // Versions who's Terraform configuration has not changed will have known
1152
+ // IDs at this point, so we need to set this manually.
1153
+ if ! ok {
1154
+ v .ID = NewUUIDUnknown ()
1155
+ return
1156
+ }
1157
+ // Otherwise, use the existing ID for this hash
1158
+ v .ID = UUIDValue (prev .ID )
1159
+ return
1160
+ }
1161
+
1162
+ func setEmptyPrivateState (ctx context.Context , ps privateState ) (diags diag.Diagnostics ) {
1163
+ pvBytes , err := json .Marshal (make (LastVersionsByHash ))
1164
+ if err != nil {
1165
+ panic ("failed to marshal empty private state" )
1166
+ }
1167
+ return ps .SetKey (ctx , LastVersionsKey , pvBytes )
1168
+ }
0 commit comments