Skip to content

Commit ef8d298

Browse files
committed
fix: template version replacement & metadata updates
1 parent bf81000 commit ef8d298

File tree

3 files changed

+127
-39
lines changed

3 files changed

+127
-39
lines changed

docs/resources/template.md

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

5555
- `active` (Boolean) Whether this version is the active version of the template. Only one version can be active at a time.
5656
- `message` (String) A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated.
57-
- `name` (String) The name of the template version. Automatically generated if not provided.
57+
- `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.
5858
- `provisioner_tags` (Attributes Set) Provisioner tags for the template version. (see [below for nested schema](#nestedatt--versions--provisioner_tags))
5959
- `tf_vars` (Attributes Set) Terraform variables for the template version. (see [below for nested schema](#nestedatt--versions--tf_vars))
6060

internal/provider/template_resource.go

Lines changed: 107 additions & 35 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

@@ -339,7 +340,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
339340
Computed: 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.",
343344
Optional: true,
344345
Computed: true,
345346
},
@@ -495,6 +496,11 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
495496
data.ID = UUIDValue(templateResp.ID)
496497
data.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
499505
resp.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

564570
func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
565-
var planState TemplateResourceModel
571+
var newState TemplateResourceModel
566572
var 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

571577
if resp.Diagnostics.HasError() {
572578
return
@@ -578,25 +584,25 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
578584
return
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

593599
client := 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.
597603
if templateMetadataChanged {
598604
tflog.Trace(ctx, "change in template metadata detected, updating.")
599-
updateReq := planState.toUpdateRequest(ctx, resp)
605+
updateReq := newState.toUpdateRequest(ctx, resp)
600606
if resp.Diagnostics.HasError() {
601607
return
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) {
615621
var acl ACL
616-
resp.Diagnostics.Append(planState.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{})...)
622+
resp.Diagnostics.Append(newState.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{})...)
617623
if resp.Diagnostics.HasError() {
618624
return
619625
}
@@ -625,51 +631,62 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
625631
tflog.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() {
634641
tflog.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],
637644
OrganizationID: orgID,
638645
TemplateID: &templateID,
639646
})
640647
if err != nil {
641648
resp.Diagnostics.AddError("Client Error", err.Error())
642649
return
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() {
655668
tflog.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
})
659672
err := client.UpdateActiveTemplateVersion(ctx, templateID, codersdk.UpdateActiveTemplateVersion{
660-
ID: versionResp.ID,
673+
ID: newState.Versions[idx].ID.ValueUUID(),
661674
})
662675
if err != nil {
663676
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to update active template version: %s", err))
664677
return
665678
}
666679
tflog.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

675692
func (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
10531070
DisableEveryoneGroupAccess: !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+
}

internal/provider/template_resource_test.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestAccTemplateResource(t *testing.T) {
2929
Name: PtrTo("example-template"),
3030
Versions: []testAccTemplateVersionConfig{
3131
{
32-
Name: PtrTo("main"),
32+
// Auto-generated version name
3333
Directory: PtrTo("../../integration/template-test/example-template/"),
3434
Active: PtrTo(true),
3535
// TODO(ethanndickson): Remove this when we add in `*.tfvars` parsing
@@ -123,8 +123,8 @@ func TestAccTemplateResource(t *testing.T) {
123123
resource.TestCheckResourceAttr("coderd_template.test", "time_til_dormant_autodelete", "0"),
124124
resource.TestCheckResourceAttr("coderd_template.test", "require_active_version", "false"),
125125
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
126-
"name": regexp.MustCompile("main"),
127-
"id": regexp.MustCompile(".*"),
126+
"name": regexp.MustCompile(".+"),
127+
"id": regexp.MustCompile(".+"),
128128
"directory_hash": regexp.MustCompile(".+"),
129129
"message": regexp.MustCompile(""),
130130
}),
@@ -137,6 +137,7 @@ func TestAccTemplateResource(t *testing.T) {
137137
ImportState: true,
138138
ImportStateVerify: true,
139139
// In the real world, `versions` needs to be added to the configuration after importing
140+
// We can't import ACL as we can't currently differentiate between managed and unmanaged ACL
140141
ImportStateVerifyIgnore: []string{"versions", "acl"},
141142
},
142143
// Update existing version & metadata
@@ -214,6 +215,21 @@ func TestAccTemplateResource(t *testing.T) {
214215
resource.TestCheckNoResourceAttr("coderd_template.test", "acl"),
215216
),
216217
},
218+
// Check orphaned versions
219+
{
220+
Config: cfg7.String(t),
221+
PreConfig: func() {
222+
templates, err := client.Templates(ctx)
223+
require.NoError(t, err)
224+
require.Len(t, templates, 1)
225+
versions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
226+
TemplateID: templates[0].ID,
227+
})
228+
require.NoError(t, err)
229+
require.Len(t, versions, 3)
230+
},
231+
},
232+
// Resource deleted
217233
},
218234
})
219235
}

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