@@ -3,6 +3,7 @@ package provider
33import (
44 "bufio"
55 "context"
6+ "encoding/json"
67 "fmt"
78 "io"
89
@@ -346,7 +347,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
346347 Computed : true ,
347348 },
348349 "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. " ,
350351 Optional : true ,
351352 Computed : true ,
352353 },
@@ -502,6 +503,17 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
502503 data .ID = UUIDValue (templateResp .ID )
503504 data .DisplayName = types .StringValue (templateResp .DisplayName )
504505
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+
505517 // Save data into Terraform sutate
506518 resp .Diagnostics .Append (resp .State .Set (ctx , & data )... )
507519}
@@ -569,11 +581,11 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
569581}
570582
571583func (r * TemplateResource ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
572- var planState TemplateResourceModel
584+ var newState TemplateResourceModel
573585 var curState TemplateResourceModel
574586
575587 // 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 )... )
577589
578590 if resp .Diagnostics .HasError () {
579591 return
@@ -585,25 +597,25 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
585597 return
586598 }
587599
588- if planState .OrganizationID .IsUnknown () {
589- planState .OrganizationID = UUIDValue (r .data .DefaultOrganizationID )
600+ if newState .OrganizationID .IsUnknown () {
601+ newState .OrganizationID = UUIDValue (r .data .DefaultOrganizationID )
590602 }
591603
592- if planState .DisplayName .IsUnknown () {
593- planState .DisplayName = planState .Name
604+ if newState .DisplayName .IsUnknown () {
605+ newState .DisplayName = newState .Name
594606 }
595607
596- orgID := planState .OrganizationID .ValueUUID ()
608+ orgID := newState .OrganizationID .ValueUUID ()
597609
598- templateID := planState .ID .ValueUUID ()
610+ templateID := newState .ID .ValueUUID ()
599611
600612 client := r .data .Client
601613
602- templateMetadataChanged := ! planState .EqualTemplateMetadata (curState )
614+ templateMetadataChanged := ! newState .EqualTemplateMetadata (curState )
603615 // This is required, as the API will reject no-diff updates.
604616 if templateMetadataChanged {
605617 tflog .Trace (ctx , "change in template metadata detected, updating." )
606- updateReq := planState .toUpdateRequest (ctx , resp )
618+ updateReq := newState .toUpdateRequest (ctx , resp )
607619 if resp .Diagnostics .HasError () {
608620 return
609621 }
@@ -618,9 +630,9 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
618630
619631 // Since the everyone group always gets deleted by `DisableEveryoneGroupAccess`, we need to run this even if there
620632 // 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 ) {
622634 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 {})... )
624636 if resp .Diagnostics .HasError () {
625637 return
626638 }
@@ -632,51 +644,64 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
632644 tflog .Trace (ctx , "successfully updated template ACL" )
633645 }
634646
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 () {
641649 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 ] ,
644652 OrganizationID : orgID ,
645653 TemplateID : & templateID ,
646654 })
647655 if err != nil {
648656 resp .Diagnostics .AddError ("Client Error" , err .Error ())
649657 return
650658 }
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 )
652666 } 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 )
660678 }
661- if plannedVersion .Active .ValueBool () {
679+ if newState . Versions [ idx ] .Active .ValueBool () {
662680 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 () ,
665683 })
666684 err := client .UpdateActiveTemplateVersion (ctx , templateID , codersdk.UpdateActiveTemplateVersion {
667- ID : versionResp . ID ,
685+ ID : newState . Versions [ idx ]. ID . ValueUUID () ,
668686 })
669687 if err != nil {
670688 resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to update active template version: %s" , err ))
671689 return
672690 }
673691 tflog .Trace (ctx , "marked template version as active" )
674692 }
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
676701 }
677702
678703 // Save updated data into Terraform state
679- resp .Diagnostics .Append (resp .State .Set (ctx , & planState )... )
704+ resp .Diagnostics .Append (resp .State .Set (ctx , & newState )... )
680705}
681706
682707func (r * TemplateResource ) Delete (ctx context.Context , req resource.DeleteRequest , resp * resource.DeleteResponse ) {
@@ -766,25 +791,26 @@ func (d *directoryHashPlanModifier) MarkdownDescription(context.Context) string
766791
767792// PlanModifyObject implements planmodifier.Object.
768793func (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 () {
773797 return
774798 }
775799
776- hash , err := computeDirectoryHash (directory .ValueString ())
800+ hash , err := computeDirectoryHash (data . Directory .ValueString ())
777801 if err != nil {
778802 resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Failed to compute directory hash: %s" , err ))
779803 return
780804 }
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 () {
785810 return
786811 }
787- resp .PlanValue = out
812+
813+ resp .PlanValue , resp .Diagnostics = types .ObjectValueFrom (ctx , req .PlanValue .AttributeTypes (ctx ), data )
788814}
789815
790816func NewDirectoryHashPlanModifier () planmodifier.Object {
@@ -1062,3 +1088,95 @@ func (r *TemplateResourceModel) toCreateRequest(ctx context.Context, resp *resou
10621088 DisableEveryoneGroupAccess : ! r .ACL .IsNull (),
10631089 }
10641090}
1091+
1092+ type LastVersionsByHash map [string ]PreviousTemplateVersion
1093+
1094+ func (lv LastVersionsByHash ) MarshalJSON () ([]byte , error ) {
1095+ return json .Marshal (map [string ]PreviousTemplateVersion (lv ))
1096+ }
1097+
1098+ func (lv * LastVersionsByHash ) UnmarshalJSON (data []byte ) error {
1099+ var m map [string ]PreviousTemplateVersion
1100+ err := json .Unmarshal (data , & m )
1101+ if err != nil {
1102+ return err
1103+ }
1104+ * lv = LastVersionsByHash (m )
1105+ return nil
1106+ }
1107+
1108+ var LastVersionsKey = "last_versions"
1109+
1110+ type PreviousTemplateVersion struct {
1111+ ID uuid.UUID `json:"id"`
1112+ Name string `json:"name"`
1113+ }
1114+
1115+ type privateState interface {
1116+ GetKey (ctx context.Context , key string ) ([]byte , diag.Diagnostics )
1117+ SetKey (ctx context.Context , key string , value []byte ) diag.Diagnostics
1118+ }
1119+
1120+ func (v Versions ) writePrivateState (ctx context.Context , ps privateState ) (diags diag.Diagnostics ) {
1121+ var lv LastVersionsByHash
1122+ lvBytes , diag := ps .GetKey (ctx , LastVersionsKey )
1123+ if diag .HasError () {
1124+ return diag
1125+ }
1126+ err := lv .UnmarshalJSON (lvBytes )
1127+ if err != nil {
1128+ diags .AddError ("Client Error" , fmt .Sprintf ("Failed to unmarshal private state when writing: %s" , err ))
1129+ return diags
1130+ }
1131+ for _ , version := range v {
1132+ lv [version .DirectoryHash .ValueString ()] = PreviousTemplateVersion {
1133+ ID : version .ID .ValueUUID (),
1134+ Name : version .ID .ValueString (),
1135+ }
1136+ lvBytes , err = lv .MarshalJSON ()
1137+ if err != nil {
1138+ diags .AddError ("Client Error" , fmt .Sprintf ("Failed to marshal private state: %s" , err ))
1139+ return diags
1140+ }
1141+ }
1142+ return ps .SetKey (ctx , LastVersionsKey , lvBytes )
1143+ }
1144+
1145+ func (v * TemplateVersion ) readFromPrivateState (ctx context.Context , ps privateState ) (diags diag.Diagnostics ) {
1146+ var lv LastVersionsByHash
1147+ lvBytes , diag := ps .GetKey (ctx , LastVersionsKey )
1148+ if diag .HasError () {
1149+ diags .Append (diag ... )
1150+ return
1151+ }
1152+ // If this is the first read, init the private state value
1153+ if lvBytes == nil {
1154+ setEmptyPrivateState (ctx , ps )
1155+ return
1156+ }
1157+ err := lv .UnmarshalJSON (lvBytes )
1158+ if err != nil {
1159+ diags .AddError ("Client Error" , fmt .Sprintf ("Failed to unmarshal private state when reading: %s" , err ))
1160+ return
1161+ }
1162+
1163+ prev , ok := lv [v .DirectoryHash .ValueString ()]
1164+ // If not in state, mark as known after apply since we'll create a new version.
1165+ // Versions who's Terraform configuration has not changed will have known
1166+ // IDs at this point, so we need to set this manually.
1167+ if ! ok {
1168+ v .ID = NewUUIDUnknown ()
1169+ return
1170+ }
1171+ // Otherwise, use the existing ID for this hash
1172+ v .ID = UUIDValue (prev .ID )
1173+ return
1174+ }
1175+
1176+ func setEmptyPrivateState (ctx context.Context , ps privateState ) (diags diag.Diagnostics ) {
1177+ pvBytes , err := make (LastVersionsByHash ).MarshalJSON ()
1178+ if err != nil {
1179+ panic ("failed to marshal empty private state" )
1180+ }
1181+ return ps .SetKey (ctx , LastVersionsKey , pvBytes )
1182+ }
0 commit comments