@@ -3,6 +3,7 @@ package provider
33import (
44"bufio"
55"context"
6+ "encoding/json"
67"fmt"
78"io"
89
@@ -339,7 +340,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
339340Computed :true ,
340341},
341342"name" : schema.StringAttribute {
342- MarkdownDescription :"The name of the template version. Automatically generated if not provided." ,
343+ 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. " ,
343344Optional :true ,
344345Computed :true ,
345346},
@@ -495,6 +496,11 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
495496data .ID = UUIDValue (templateResp .ID )
496497data .DisplayName = types .StringValue (templateResp .DisplayName )
497498
499+ resp .Diagnostics .Append (data .Versions .writePrivateState (ctx ,resp .Private )... )
500+ if resp .Diagnostics .HasError () {
501+ return
502+ }
503+
498504// Save data into Terraform sutate
499505resp .Diagnostics .Append (resp .State .Set (ctx ,& data )... )
500506}
@@ -562,11 +568,11 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
562568}
563569
564570func (r * TemplateResource )Update (ctx context.Context ,req resource.UpdateRequest ,resp * resource.UpdateResponse ) {
565- var planState TemplateResourceModel
571+ var newState TemplateResourceModel
566572var curState TemplateResourceModel
567573
568574// Read Terraform plan data into the model
569- resp .Diagnostics .Append (req .Plan .Get (ctx ,& planState )... )
575+ resp .Diagnostics .Append (req .Plan .Get (ctx ,& newState )... )
570576
571577if resp .Diagnostics .HasError () {
572578return
@@ -578,25 +584,25 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
578584return
579585}
580586
581- if planState .OrganizationID .IsUnknown () {
582- planState .OrganizationID = UUIDValue (r .data .DefaultOrganizationID )
587+ if newState .OrganizationID .IsUnknown () {
588+ newState .OrganizationID = UUIDValue (r .data .DefaultOrganizationID )
583589}
584590
585- if planState .DisplayName .IsUnknown () {
586- planState .DisplayName = planState .Name
591+ if newState .DisplayName .IsUnknown () {
592+ newState .DisplayName = newState .Name
587593}
588594
589- orgID := planState .OrganizationID .ValueUUID ()
595+ orgID := newState .OrganizationID .ValueUUID ()
590596
591- templateID := planState .ID .ValueUUID ()
597+ templateID := newState .ID .ValueUUID ()
592598
593599client := r .data .Client
594600
595- templateMetadataChanged := ! planState .EqualTemplateMetadata (curState )
601+ templateMetadataChanged := ! newState .EqualTemplateMetadata (curState )
596602// This is required, as the API will reject no-diff updates.
597603if templateMetadataChanged {
598604tflog .Trace (ctx ,"change in template metadata detected, updating." )
599- updateReq := planState .toUpdateRequest (ctx ,resp )
605+ updateReq := newState .toUpdateRequest (ctx ,resp )
600606if resp .Diagnostics .HasError () {
601607return
602608}
@@ -611,9 +617,9 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
611617
612618// Since the everyone group always gets deleted by `DisableEveryoneGroupAccess`, we need to run this even if there
613619// were no ACL changes but the template metadata was updated.
614- if ! planState .ACL .IsNull ()&& (! curState .ACL .Equal (planState .ACL )|| templateMetadataChanged ) {
620+ if ! newState .ACL .IsNull ()&& (! curState .ACL .Equal (newState .ACL )|| templateMetadataChanged ) {
615621var acl ACL
616- resp .Diagnostics .Append (planState .ACL .As (ctx ,& acl , basetypes.ObjectAsOptions {})... )
622+ resp .Diagnostics .Append (newState .ACL .As (ctx ,& acl , basetypes.ObjectAsOptions {})... )
617623if resp .Diagnostics .HasError () {
618624return
619625}
@@ -625,51 +631,62 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
625631tflog .Trace (ctx ,"successfully updated template ACL" )
626632}
627633
628- for idx ,plannedVersion := range planState .Versions {
629- var curVersionID uuid.UUID
630- // All versions in the state are guaranteed to have known IDs
631- foundVersion := curState .Versions .ByID (plannedVersion .ID )
632- // If the version is new, or if the directory hash has changed, create a new version
633- if foundVersion == nil || foundVersion .DirectoryHash != plannedVersion .DirectoryHash {
634+ // Populate version IDs, based off previously created template versions stored in private state.
635+ resp .Diagnostics .Append (readPrivateState (ctx ,newState .Versions ,req .Private )... )
636+ if resp .Diagnostics .HasError () {
637+ return
638+ }
639+ for idx := range newState .Versions {
640+ if newState .Versions [idx ].ID .IsUnknown () {
634641tflog .Trace (ctx ,"discovered a new or modified template version" )
635- versionResp ,err := newVersion (ctx ,client ,newVersionRequest {
636- Version :& plannedVersion ,
642+ uploadResp ,err := newVersion (ctx ,client ,newVersionRequest {
643+ Version :& newState . Versions [ idx ] ,
637644OrganizationID :orgID ,
638645TemplateID :& templateID ,
639646})
640647if err != nil {
641648resp .Diagnostics .AddError ("Client Error" ,err .Error ())
642649return
643650}
644- curVersionID = versionResp .ID
651+ versionResp ,err := client .TemplateVersion (ctx ,uploadResp .ID )
652+ if err != nil {
653+ resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to get template version: %s" ,err ))
654+ return
655+ }
656+ newState .Versions [idx ].ID = UUIDValue (versionResp .ID )
645657}else {
646- // Or if it's an existing version, get the ID
647- curVersionID = plannedVersion .ID .ValueUUID ()
648- }
649- versionResp ,err := client .TemplateVersion (ctx ,curVersionID )
650- if err != nil {
651- resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to get template version: %s" ,err ))
652- return
658+ _ ,err := client .UpdateTemplateVersion (ctx ,newState .Versions [idx ].ID .ValueUUID (), codersdk.PatchTemplateVersionRequest {
659+ Name :newState .Versions [idx ].Name .ValueString (),
660+ Message :newState .Versions [idx ].Message .ValueStringPointer (),
661+ })
662+ if err != nil {
663+ resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to update template version metadata: %s" ,err ))
664+ return
665+ }
653666}
654- if plannedVersion .Active .ValueBool () {
667+ if newState . Versions [ idx ] .Active .ValueBool () {
655668tflog .Trace (ctx ,"marking template version as active" ,map [string ]any {
656- "version_id" :versionResp . ID ,
657- "template_id" :templateID ,
669+ "version_id" :newState . Versions [ idx ]. ID . ValueString () ,
670+ "template_id" :templateID . String () ,
658671})
659672err := client .UpdateActiveTemplateVersion (ctx ,templateID , codersdk.UpdateActiveTemplateVersion {
660- ID :versionResp . ID ,
673+ ID :newState . Versions [ idx ]. ID . ValueUUID () ,
661674})
662675if err != nil {
663676resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to update active template version: %s" ,err ))
664677return
665678}
666679tflog .Trace (ctx ,"marked template version as active" )
667680}
668- planState .Versions [idx ].ID = UUIDValue (versionResp .ID )
681+ }
682+
683+ resp .Diagnostics .Append (newState .Versions .writePrivateState (ctx ,resp .Private )... )
684+ if resp .Diagnostics .HasError () {
685+ return
669686}
670687
671688// Save updated data into Terraform state
672- resp .Diagnostics .Append (resp .State .Set (ctx ,& planState )... )
689+ resp .Diagnostics .Append (resp .State .Set (ctx ,& newState )... )
673690}
674691
675692func (r * TemplateResource )Delete (ctx context.Context ,req resource.DeleteRequest ,resp * resource.DeleteResponse ) {
@@ -1053,3 +1070,58 @@ func (r *TemplateResourceModel) toCreateRequest(ctx context.Context, resp *resou
10531070DisableEveryoneGroupAccess :! r .ACL .IsNull (),
10541071}
10551072}
1073+
1074+ type PreviousTemplateVersion struct {
1075+ ID uuid.UUID `json:"id"`
1076+ Name string `json:"name"`
1077+ }
1078+
1079+ type privateState interface {
1080+ GetKey (ctx context.Context ,key string ) ([]byte , diag.Diagnostics )
1081+ SetKey (ctx context.Context ,key string ,value []byte ) diag.Diagnostics
1082+ }
1083+
1084+ func (v Versions )writePrivateState (ctx context.Context ,ps privateState ) (diags diag.Diagnostics ) {
1085+ for _ ,version := range v {
1086+ prevBytes ,err := json .Marshal (PreviousTemplateVersion {ID :version .ID .ValueUUID (),Name :version .Name .ValueString ()})
1087+ if err != nil {
1088+ diags .AddError ("Client Error" ,fmt .Sprintf ("Failed to marshal name to json bytes: %s" ,err ))
1089+ return diags
1090+ }
1091+ diag := ps .SetKey (ctx ,version .DirectoryHash .ValueString (),prevBytes )
1092+ if diag .HasError () {
1093+ return diag
1094+ }
1095+ }
1096+ return diags
1097+ }
1098+
1099+ func readPrivateState (ctx context.Context ,v Versions ,ps privateState ) (diags diag.Diagnostics ) {
1100+ for idx ,version := range v {
1101+ jsonBytes ,diag := ps .GetKey (ctx ,version .DirectoryHash .ValueString ())
1102+ if diag .HasError () {
1103+ return diag
1104+ }
1105+ // If not in state, create it
1106+ if jsonBytes == nil {
1107+ continue
1108+ }
1109+ var prev PreviousTemplateVersion
1110+ err := json .Unmarshal (jsonBytes ,& prev )
1111+ if err != nil {
1112+ diags .AddError ("Client Error" ,fmt .Sprintf ("Failed to unmarshal name from json bytes: %s" ,err ))
1113+ return diags
1114+ }
1115+ // If in the state, but with a different name, create it
1116+ if prev .Name != version .Name .ValueString () {
1117+ continue
1118+ }
1119+ // If in the state, but with no name, create it
1120+ if prev .Name == "" {
1121+ continue
1122+ }
1123+ // Otherwise, use the ID from last time
1124+ v [idx ].ID = UUIDValue (prev .ID )
1125+ }
1126+ return
1127+ }