From b704ee680e879ed054ba667dc97e2f69e8020c0a Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 12 Sep 2024 12:00:31 +0000 Subject: [PATCH] fix: create new template version when tfvars change --- docs/resources/template.md | 6 +- internal/provider/template_resource.go | 78 ++++- internal/provider/template_resource_test.go | 312 ++++++++++++++++---- 3 files changed, 319 insertions(+), 77 deletions(-) diff --git a/docs/resources/template.md b/docs/resources/template.md index 51f72e8..d070206 100644 --- a/docs/resources/template.md +++ b/docs/resources/template.md @@ -4,7 +4,7 @@ page_title: "coderd_template Resource - terraform-provider-coderd" subcategory: "" description: |- A Coder template. - Logs from building template versions are streamed from the provisioner when the TF_LOG environment variable is INFO or higher. + Logs from building template versions can be optionally streamed from the provisioner by setting the TF_LOG environment variable to INFO or higher. When importing, the ID supplied can be either a template UUID retrieved via the API or /. --- @@ -12,7 +12,7 @@ description: |- A Coder template. -Logs from building template versions are streamed from the provisioner when the `TF_LOG` environment variable is `INFO` or higher. +Logs from building template versions can be optionally streamed from the provisioner by setting the `TF_LOG` environment variable to `INFO` or higher. When importing, the ID supplied can be either a template UUID retrieved via the API or `/`. @@ -101,7 +101,7 @@ Optional: - `active` (Boolean) Whether this version is the active version of the template. Only one version can be active at a time. - `message` (String) A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated. -- `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. +- `name` (String) The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents, or the `tf_vars` attribute are updated. - `provisioner_tags` (Attributes Set) Provisioner tags for the template version. (see [below for nested schema](#nestedatt--versions--provisioner_tags)) - `tf_vars` (Attributes Set) Terraform variables for the template version. (see [below for nested schema](#nestedatt--versions--tf_vars)) diff --git a/internal/provider/template_resource.go b/internal/provider/template_resource.go index 807b026..533a087 100644 --- a/internal/provider/template_resource.go +++ b/internal/provider/template_resource.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "slices" "strings" "cdr.dev/slog" @@ -230,8 +231,8 @@ func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRe func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "A Coder template.\n\nLogs from building template versions are streamed from the provisioner " + - "when the `TF_LOG` environment variable is `INFO` or higher.\n\n" + + MarkdownDescription: "A Coder template.\n\nLogs from building template versions can be optionally streamed from the provisioner " + + "by setting the `TF_LOG` environment variable to `INFO` or higher.\n\n" + "When importing, the ID supplied can be either a template UUID retrieved via the API or `/`.", Attributes: map[string]schema.Attribute{ @@ -395,7 +396,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques Computed: true, }, "name": schema.StringAttribute{ - 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.", + MarkdownDescription: "The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents, or the `tf_vars` attribute are updated.", Optional: true, Computed: true, Validators: []validator.String{ @@ -1053,7 +1054,7 @@ func markActive(ctx context.Context, client *codersdk.Client, templateID uuid.UU ID: versionID, }) if err != nil { - return fmt.Errorf("Failed to update active template version: %s", err) + return fmt.Errorf("failed to update active template version: %s", err) } tflog.Info(ctx, "marked template version as active") return nil @@ -1231,8 +1232,9 @@ type LastVersionsByHash = map[string][]PreviousTemplateVersion var LastVersionsKey = "last_versions" type PreviousTemplateVersion struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + TFVars map[string]string `json:"tf_vars"` } type privateState interface { @@ -1244,18 +1246,24 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d lv := make(LastVersionsByHash) for _, version := range v { vbh, ok := lv[version.DirectoryHash.ValueString()] + tfVars := make(map[string]string, len(version.TerraformVariables)) + for _, tfVar := range version.TerraformVariables { + tfVars[tfVar.Name.ValueString()] = tfVar.Value.ValueString() + } // Store the IDs and names of all versions with the same directory hash, // in the order they appear if ok { lv[version.DirectoryHash.ValueString()] = append(vbh, PreviousTemplateVersion{ - ID: version.ID.ValueUUID(), - Name: version.Name.ValueString(), + ID: version.ID.ValueUUID(), + Name: version.Name.ValueString(), + TFVars: tfVars, }) } else { lv[version.DirectoryHash.ValueString()] = []PreviousTemplateVersion{ { - ID: version.ID.ValueUUID(), - Name: version.Name.ValueString(), + ID: version.ID.ValueUUID(), + Name: version.Name.ValueString(), + TFVars: tfVars, }, } } @@ -1269,6 +1277,13 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d } func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVersions Versions) { + // We remove versions that we've matched from `lv`, so make a copy for + // resolving tfvar changes at the end. + fullLv := make(LastVersionsByHash) + for k, v := range lv { + fullLv[k] = slices.Clone(v) + } + for i := range planVersions { prevList, ok := lv[planVersions[i].DirectoryHash.ValueString()] // If not in state, mark as known after apply since we'll create a new version. @@ -1308,4 +1323,47 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe lv[planVersions[i].DirectoryHash.ValueString()] = prevList[1:] } } + + // If only the Terraform variables have changed, + // we need to create a new version with the new variables. + for i := range planVersions { + if !planVersions[i].ID.IsUnknown() { + prevs, ok := fullLv[planVersions[i].DirectoryHash.ValueString()] + if !ok { + continue + } + if tfVariablesChanged(prevs, &planVersions[i]) { + planVersions[i].ID = NewUUIDUnknown() + // We could always set the name to unknown here, to generate a + // random one (this is what the Web UI currently does when + // only updating tfvars). + // However, I think it'd be weird if the provider just started + // ignoring the name you set in the config, we'll instead + // require that users update the name if they update the tfvars. + if configVersions[i].Name.IsNull() { + planVersions[i].Name = types.StringUnknown() + } + } + } + } +} + +func tfVariablesChanged(prevs []PreviousTemplateVersion, planned *TemplateVersion) bool { + for _, prev := range prevs { + if prev.ID == planned.ID.ValueUUID() { + // If the previous version has no TFVars, then it was created using + // an older provider version. + if prev.TFVars == nil { + return true + } + for _, tfVar := range planned.TerraformVariables { + if prev.TFVars[tfVar.Name.ValueString()] != tfVar.Value.ValueString() { + return true + } + } + return false + } + } + return true + } diff --git a/internal/provider/template_resource_test.go b/internal/provider/template_resource_test.go index de258f3..9c54edd 100644 --- a/internal/provider/template_resource_test.go +++ b/internal/provider/template_resource_test.go @@ -286,6 +286,15 @@ func TestAccTemplateResource(t *testing.T) { cfg5.Versions = slices.Clone(cfg5.Versions) cfg5.Versions[1].Directory = PtrTo("../../integration/template-test/example-template/") + cfg6 := cfg5 + cfg6.Versions = slices.Clone(cfg6.Versions) + cfg6.Versions[0].TerraformVariables = []testAccTemplateKeyValueConfig{ + { + Key: PtrTo("name"), + Value: PtrTo("world2"), + }, + } + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, IsUnitTest: true, @@ -343,6 +352,66 @@ func TestAccTemplateResource(t *testing.T) { testAccCheckNumTemplateVersions(ctx, client, 4), ), }, + // Update the Terraform variables of the first version + { + Config: cfg6.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckNumTemplateVersions(ctx, client, 5), + ), + }, + }, + }) + }) + + t.Run("AutoGenNameUpdateTFVars", func(t *testing.T) { + cfg1 := testAccTemplateResourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + Name: PtrTo("example-template3"), + Versions: []testAccTemplateVersionConfig{ + { + // Auto-generated version name + Directory: PtrTo("../../integration/template-test/example-template-2/"), + TerraformVariables: []testAccTemplateKeyValueConfig{ + { + Key: PtrTo("name"), + Value: PtrTo("world"), + }, + }, + Active: PtrTo(true), + }, + }, + ACL: testAccTemplateACLConfig{ + null: true, + }, + } + + cfg2 := cfg1 + cfg2.Versions = slices.Clone(cfg2.Versions) + cfg2.Versions[0].TerraformVariables = []testAccTemplateKeyValueConfig{ + { + Key: PtrTo("name"), + Value: PtrTo("world2"), + }, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: true, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg1.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckNumTemplateVersions(ctx, client, 1), + ), + }, + { + Config: cfg2.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckNumTemplateVersions(ctx, client, 2), + ), + }, }, }) }) @@ -779,14 +848,16 @@ func TestReconcileVersionIDs(t *testing.T) { Name: "IdenticalDontRename", planVersions: []TemplateVersion{ { - Name: types.StringValue("foo"), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, { - Name: types.StringValue("bar"), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringValue("bar"), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, }, configVersions: []TemplateVersion{ @@ -800,21 +871,24 @@ func TestReconcileVersionIDs(t *testing.T) { inputState: map[string][]PreviousTemplateVersion{ "aaa": { { - ID: aUUID, - Name: "bar", + ID: aUUID, + Name: "bar", + TFVars: map[string]string{}, }, }, }, expectedVersions: []TemplateVersion{ { - Name: types.StringValue("foo"), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, { - Name: types.StringValue("bar"), - DirectoryHash: types.StringValue("aaa"), - ID: UUIDValue(aUUID), + Name: types.StringValue("bar"), + DirectoryHash: types.StringValue("aaa"), + ID: UUIDValue(aUUID), + TerraformVariables: []Variable{}, }, }, }, @@ -822,14 +896,16 @@ func TestReconcileVersionIDs(t *testing.T) { Name: "IdenticalRenameFirst", planVersions: []TemplateVersion{ { - Name: types.StringValue("foo"), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, { - Name: types.StringValue("bar"), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringValue("bar"), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, }, configVersions: []TemplateVersion{ @@ -843,21 +919,24 @@ func TestReconcileVersionIDs(t *testing.T) { inputState: map[string][]PreviousTemplateVersion{ "aaa": { { - ID: aUUID, - Name: "baz", + ID: aUUID, + Name: "baz", + TFVars: map[string]string{}, }, }, }, expectedVersions: []TemplateVersion{ { - Name: types.StringValue("foo"), - DirectoryHash: types.StringValue("aaa"), - ID: UUIDValue(aUUID), + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), + ID: UUIDValue(aUUID), + TerraformVariables: []Variable{}, }, { - Name: types.StringValue("bar"), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringValue("bar"), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, }, }, @@ -865,14 +944,16 @@ func TestReconcileVersionIDs(t *testing.T) { Name: "IdenticalHashesInState", planVersions: []TemplateVersion{ { - Name: types.StringValue("foo"), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, { - Name: types.StringValue("bar"), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringValue("bar"), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, }, configVersions: []TemplateVersion{ @@ -886,25 +967,29 @@ func TestReconcileVersionIDs(t *testing.T) { inputState: map[string][]PreviousTemplateVersion{ "aaa": { { - ID: aUUID, - Name: "qux", + ID: aUUID, + Name: "qux", + TFVars: map[string]string{}, }, { - ID: bUUID, - Name: "baz", + ID: bUUID, + Name: "baz", + TFVars: map[string]string{}, }, }, }, expectedVersions: []TemplateVersion{ { - Name: types.StringValue("foo"), - DirectoryHash: types.StringValue("aaa"), - ID: UUIDValue(aUUID), + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), + ID: UUIDValue(aUUID), + TerraformVariables: []Variable{}, }, { - Name: types.StringValue("bar"), - DirectoryHash: types.StringValue("aaa"), - ID: UUIDValue(bUUID), + Name: types.StringValue("bar"), + DirectoryHash: types.StringValue("aaa"), + ID: UUIDValue(bUUID), + TerraformVariables: []Variable{}, }, }, }, @@ -912,14 +997,16 @@ func TestReconcileVersionIDs(t *testing.T) { Name: "UnknownUsesStateInOrder", planVersions: []TemplateVersion{ { - Name: types.StringValue("foo"), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, { - Name: types.StringUnknown(), - DirectoryHash: types.StringValue("aaa"), - ID: NewUUIDUnknown(), + Name: types.StringUnknown(), + DirectoryHash: types.StringValue("aaa"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, }, }, configVersions: []TemplateVersion{ @@ -933,55 +1020,152 @@ func TestReconcileVersionIDs(t *testing.T) { inputState: map[string][]PreviousTemplateVersion{ "aaa": { { - ID: aUUID, - Name: "qux", + ID: aUUID, + Name: "qux", + TFVars: map[string]string{}, }, { - ID: bUUID, - Name: "baz", + ID: bUUID, + Name: "baz", + TFVars: map[string]string{}, }, }, }, expectedVersions: []TemplateVersion{ + { + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), + ID: UUIDValue(aUUID), + TerraformVariables: []Variable{}, + }, + { + Name: types.StringValue("baz"), + DirectoryHash: types.StringValue("aaa"), + ID: UUIDValue(bUUID), + TerraformVariables: []Variable{}, + }, + }, + }, + { + Name: "NewVersionNewRandomName", + planVersions: []TemplateVersion{ + { + Name: types.StringValue("weird_draught12"), + DirectoryHash: types.StringValue("bbb"), + ID: UUIDValue(aUUID), + TerraformVariables: []Variable{}, + }, + }, + configVersions: []TemplateVersion{ + { + Name: types.StringNull(), + }, + }, + inputState: map[string][]PreviousTemplateVersion{ + "aaa": { + { + ID: aUUID, + Name: "weird_draught12", + TFVars: map[string]string{}, + }, + }, + }, + expectedVersions: []TemplateVersion{ + { + Name: types.StringUnknown(), + DirectoryHash: types.StringValue("bbb"), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{}, + }, + }, + }, + { + Name: "IdenticalNewVars", + planVersions: []TemplateVersion{ { Name: types.StringValue("foo"), DirectoryHash: types.StringValue("aaa"), ID: UUIDValue(aUUID), + TerraformVariables: []Variable{ + { + Name: types.StringValue("foo"), + Value: types.StringValue("bar"), + }, + }, }, + }, + configVersions: []TemplateVersion{ { - Name: types.StringValue("baz"), + Name: types.StringValue("foo"), + }, + }, + inputState: map[string][]PreviousTemplateVersion{ + "aaa": { + { + ID: aUUID, + Name: "foo", + TFVars: map[string]string{ + "foo": "foo", + }, + }, + }, + }, + expectedVersions: []TemplateVersion{ + { + Name: types.StringValue("foo"), DirectoryHash: types.StringValue("aaa"), - ID: UUIDValue(bUUID), + ID: NewUUIDUnknown(), + TerraformVariables: []Variable{ + { + Name: types.StringValue("foo"), + Value: types.StringValue("bar"), + }, + }, }, }, }, { - Name: "NewVersionNewRandomName", + Name: "IdenticalSameVars", planVersions: []TemplateVersion{ { - Name: types.StringValue("weird_draught12"), - DirectoryHash: types.StringValue("bbb"), + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), ID: UUIDValue(aUUID), + TerraformVariables: []Variable{ + { + Name: types.StringValue("foo"), + Value: types.StringValue("bar"), + }, + }, }, }, configVersions: []TemplateVersion{ { - Name: types.StringNull(), + Name: types.StringValue("foo"), }, }, inputState: map[string][]PreviousTemplateVersion{ "aaa": { { ID: aUUID, - Name: "weird_draught12", + Name: "foo", + TFVars: map[string]string{ + "foo": "bar", + }, }, }, }, expectedVersions: []TemplateVersion{ { - Name: types.StringUnknown(), - DirectoryHash: types.StringValue("bbb"), - ID: NewUUIDUnknown(), + Name: types.StringValue("foo"), + DirectoryHash: types.StringValue("aaa"), + ID: UUIDValue(aUUID), + TerraformVariables: []Variable{ + { + Name: types.StringValue("foo"), + Value: types.StringValue("bar"), + }, + }, }, }, }, 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