Skip to content

Commit 6f8b09f

Browse files
committed
fix: template version replacement & metadata updates
1 parent 5c29d3c commit 6f8b09f

File tree

3 files changed

+123
-39
lines changed

3 files changed

+123
-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: 103 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,10 @@ 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(func(key string, value []byte) diag.Diagnostics {
500+
return resp.Private.SetKey(ctx, key, value)
501+
})...)
502+
498503
// Save data into Terraform sutate
499504
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
500505
}
@@ -562,11 +567,11 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
562567
}
563568

564569
func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
565-
var planState TemplateResourceModel
570+
var newState TemplateResourceModel
566571
var curState TemplateResourceModel
567572

568573
// Read Terraform plan data into the model
569-
resp.Diagnostics.Append(req.Plan.Get(ctx, &planState)...)
574+
resp.Diagnostics.Append(req.Plan.Get(ctx, &newState)...)
570575

571576
if resp.Diagnostics.HasError() {
572577
return
@@ -578,25 +583,25 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
578583
return
579584
}
580585

581-
if planState.OrganizationID.IsUnknown() {
582-
planState.OrganizationID = UUIDValue(r.data.DefaultOrganizationID)
586+
if newState.OrganizationID.IsUnknown() {
587+
newState.OrganizationID = UUIDValue(r.data.DefaultOrganizationID)
583588
}
584589

585-
if planState.DisplayName.IsUnknown() {
586-
planState.DisplayName = planState.Name
590+
if newState.DisplayName.IsUnknown() {
591+
newState.DisplayName = newState.Name
587592
}
588593

589-
orgID := planState.OrganizationID.ValueUUID()
594+
orgID := newState.OrganizationID.ValueUUID()
590595

591-
templateID := planState.ID.ValueUUID()
596+
templateID := newState.ID.ValueUUID()
592597

593598
client := r.data.Client
594599

595-
templateMetadataChanged := !planState.EqualTemplateMetadata(curState)
600+
templateMetadataChanged := !newState.EqualTemplateMetadata(curState)
596601
// This is required, as the API will reject no-diff updates.
597602
if templateMetadataChanged {
598603
tflog.Trace(ctx, "change in template metadata detected, updating.")
599-
updateReq := planState.toUpdateRequest(ctx, resp)
604+
updateReq := newState.toUpdateRequest(ctx, resp)
600605
if resp.Diagnostics.HasError() {
601606
return
602607
}
@@ -611,9 +616,9 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
611616

612617
// Since the everyone group always gets deleted by `DisableEveryoneGroupAccess`, we need to run this even if there
613618
// were no ACL changes but the template metadata was updated.
614-
if !planState.ACL.IsNull() && (!curState.ACL.Equal(planState.ACL) || templateMetadataChanged) {
619+
if !newState.ACL.IsNull() && (!curState.ACL.Equal(newState.ACL) || templateMetadataChanged) {
615620
var acl ACL
616-
resp.Diagnostics.Append(planState.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{})...)
621+
resp.Diagnostics.Append(newState.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{})...)
617622
if resp.Diagnostics.HasError() {
618623
return
619624
}
@@ -625,51 +630,64 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
625630
tflog.Trace(ctx, "successfully updated template ACL")
626631
}
627632

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 {
633+
// Populate version IDs, based off what we created last `apply`
634+
diags := readPrivateState(newState.Versions, func(key string) ([]byte, diag.Diagnostics) {
635+
return req.Private.GetKey(ctx, key)
636+
})
637+
if diags.HasError() {
638+
resp.Diagnostics.Append(diags...)
639+
return
640+
}
641+
for idx := range newState.Versions {
642+
if newState.Versions[idx].ID.IsUnknown() {
634643
tflog.Trace(ctx, "discovered a new or modified template version")
635-
versionResp, err := newVersion(ctx, client, newVersionRequest{
636-
Version: &plannedVersion,
644+
uploadResp, err := newVersion(ctx, client, newVersionRequest{
645+
Version: &newState.Versions[idx],
637646
OrganizationID: orgID,
638647
TemplateID: &templateID,
639648
})
640649
if err != nil {
641650
resp.Diagnostics.AddError("Client Error", err.Error())
642651
return
643652
}
644-
curVersionID = versionResp.ID
653+
versionResp, err := client.TemplateVersion(ctx, uploadResp.ID)
654+
if err != nil {
655+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template version: %s", err))
656+
return
657+
}
658+
newState.Versions[idx].ID = UUIDValue(versionResp.ID)
645659
} 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
660+
_, err := client.UpdateTemplateVersion(ctx, newState.Versions[idx].ID.ValueUUID(), codersdk.PatchTemplateVersionRequest{
661+
Name: newState.Versions[idx].Name.ValueString(),
662+
Message: newState.Versions[idx].Message.ValueStringPointer(),
663+
})
664+
if err != nil {
665+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to update template version metadata: %s", err))
666+
return
667+
}
653668
}
654-
if plannedVersion.Active.ValueBool() {
669+
if newState.Versions[idx].Active.ValueBool() {
655670
tflog.Trace(ctx, "marking template version as active", map[string]any{
656-
"version_id": versionResp.ID,
657-
"template_id": templateID,
671+
"version_id": newState.Versions[idx].ID.ValueString(),
672+
"template_id": templateID.String(),
658673
})
659674
err := client.UpdateActiveTemplateVersion(ctx, templateID, codersdk.UpdateActiveTemplateVersion{
660-
ID: versionResp.ID,
675+
ID: newState.Versions[idx].ID.ValueUUID(),
661676
})
662677
if err != nil {
663678
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to update active template version: %s", err))
664679
return
665680
}
666681
tflog.Trace(ctx, "marked template version as active")
667682
}
668-
planState.Versions[idx].ID = UUIDValue(versionResp.ID)
669683
}
670684

685+
resp.Diagnostics.Append(newState.Versions.writePrivateState(func(key string, value []byte) diag.Diagnostics {
686+
return resp.Private.SetKey(ctx, key, value)
687+
})...)
688+
671689
// Save updated data into Terraform state
672-
resp.Diagnostics.Append(resp.State.Set(ctx, &planState)...)
690+
resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
673691
}
674692

675693
func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
@@ -1053,3 +1071,53 @@ func (r *TemplateResourceModel) toCreateRequest(ctx context.Context, resp *resou
10531071
DisableEveryoneGroupAccess: !r.ACL.IsNull(),
10541072
}
10551073
}
1074+
1075+
type PreviousTemplateVersion struct {
1076+
ID uuid.UUID `json:"id"`
1077+
Name string `json:"name"`
1078+
}
1079+
1080+
func (v Versions) writePrivateState(privateStateWriter func(key string, value []byte) diag.Diagnostics) (diags diag.Diagnostics) {
1081+
for _, version := range v {
1082+
prevBytes, err := json.Marshal(PreviousTemplateVersion{ID: version.ID.ValueUUID(), Name: version.Name.ValueString()})
1083+
if err != nil {
1084+
diags.AddError("Client Error", fmt.Sprintf("Failed to marshal name to json bytes: %s", err))
1085+
return diags
1086+
}
1087+
diag := privateStateWriter(version.DirectoryHash.ValueString(), prevBytes)
1088+
if diag.HasError() {
1089+
return diag
1090+
}
1091+
}
1092+
return diags
1093+
}
1094+
1095+
func readPrivateState(v Versions, privateStateReader func(key string) ([]byte, diag.Diagnostics)) (diags diag.Diagnostics) {
1096+
for idx, version := range v {
1097+
jsonBytes, diag := privateStateReader(version.DirectoryHash.ValueString())
1098+
if diag.HasError() {
1099+
return diag
1100+
}
1101+
// If not in state, create it
1102+
if jsonBytes == nil {
1103+
continue
1104+
}
1105+
var prev PreviousTemplateVersion
1106+
err := json.Unmarshal(jsonBytes, &prev)
1107+
if err != nil {
1108+
diags.AddError("Client Error", fmt.Sprintf("Failed to unmarshal name from json bytes: %s", err))
1109+
return diags
1110+
}
1111+
// If in the state, but with a different name, create it
1112+
if prev.Name != version.Name.ValueString() {
1113+
continue
1114+
}
1115+
// If in the state, but with no name, create it
1116+
if prev.Name == "" {
1117+
continue
1118+
}
1119+
// Otherwise, use the ID from last time
1120+
v[idx].ID = UUIDValue(prev.ID)
1121+
}
1122+
return
1123+
}

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