Skip to content

Commit d930a29

Browse files
committed
fix: template version replacement & metadata updates
1 parent 8af42aa commit d930a29

File tree

3 files changed

+196
-52
lines changed

3 files changed

+196
-52
lines changed

docs/resources/template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Optional:
5555

5656
- `active` (Boolean) Whether this version is the active version of the template. Only one version can be active at a time.
5757
- `message` (String) A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated.
58-
- `name` (String) The name of the template version. Automatically generated if not provided.
58+
- `name` (String) The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents are updated.
5959
- `provisioner_tags` (Attributes Set) Provisioner tags for the template version. (see [below for nested schema](#nestedatt--versions--provisioner_tags))
6060
- `tf_vars` (Attributes Set) Terraform variables for the template version. (see [below for nested schema](#nestedatt--versions--tf_vars))
6161

internal/provider/template_resource.go

Lines changed: 149 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package provider
33
import (
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

571583
func (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

682707
func (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.
768793
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() {
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

790816
func NewDirectoryHashPlanModifier() planmodifier.Object {
@@ -1062,3 +1088,81 @@ 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+
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

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy