From e86224c36d2024dff48c4bceaf287494dd1cae44 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 9 Jul 2025 10:56:06 -0600 Subject: [PATCH 01/14] feat: include template variables in dynamic parameter rendering --- coderd/dynamicparameters/render.go | 31 ++++++++++-- coderd/dynamicparameters/variablevalues.go | 58 ++++++++++++++++++++++ coderd/wsbuilder/wsbuilder.go | 6 +++ go.mod | 2 +- go.sum | 4 +- 5 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 coderd/dynamicparameters/variablevalues.go diff --git a/coderd/dynamicparameters/render.go b/coderd/dynamicparameters/render.go index 8a5a80cd25d22..7f0a98f18ce55 100644 --- a/coderd/dynamicparameters/render.go +++ b/coderd/dynamicparameters/render.go @@ -9,6 +9,7 @@ import ( "time" "github.com/google/uuid" + "github.com/zclconf/go-cty/cty" "golang.org/x/xerrors" "github.com/coder/coder/v2/apiversion" @@ -41,9 +42,10 @@ type loader struct { templateVersionID uuid.UUID // cache of objects - templateVersion *database.TemplateVersion - job *database.ProvisionerJob - terraformValues *database.TemplateVersionTerraformValue + templateVersion *database.TemplateVersion + job *database.ProvisionerJob + terraformValues *database.TemplateVersionTerraformValue + templateVariableValues *[]database.TemplateVersionVariable } // Prepare is the entrypoint for this package. It loads the necessary objects & @@ -61,6 +63,12 @@ func Prepare(ctx context.Context, db database.Store, cache files.FileAcquirer, v return l.Renderer(ctx, db, cache) } +func WithTemplateVariableValues(vals []database.TemplateVersionVariable) func(r *loader) { + return func(r *loader) { + r.templateVariableValues = &vals + } +} + func WithTemplateVersion(tv database.TemplateVersion) func(r *loader) { return func(r *loader) { if tv.ID == r.templateVersionID { @@ -127,6 +135,14 @@ func (r *loader) loadData(ctx context.Context, db database.Store) error { r.terraformValues = &values } + if r.templateVariableValues == nil { + vals, err := db.GetTemplateVersionVariables(ctx, r.templateVersion.ID) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + return xerrors.Errorf("template version variables: %w", err) + } + r.templateVariableValues = &vals + } + return nil } @@ -160,13 +176,17 @@ func (r *loader) dynamicRenderer(ctx context.Context, db database.Store, cache * } }() + tfVarValues, err := VariableValues(*r.templateVariableValues) + if err != nil { + return nil, xerrors.Errorf("parse variable values: %w", err) + } + // If they can read the template version, then they can read the file for // parameter loading purposes. //nolint:gocritic fileCtx := dbauthz.AsFileReader(ctx) var templateFS fs.FS - var err error templateFS, err = cache.Acquire(fileCtx, db, r.job.FileID) if err != nil { @@ -189,6 +209,7 @@ func (r *loader) dynamicRenderer(ctx context.Context, db database.Store, cache * db: db, ownerErrors: make(map[uuid.UUID]error), close: cache.Close, + tfvarValues: tfVarValues, }, nil } @@ -199,6 +220,7 @@ type dynamicRenderer struct { ownerErrors map[uuid.UUID]error currentOwner *previewtypes.WorkspaceOwner + tfvarValues map[string]cty.Value once sync.Once close func() @@ -229,6 +251,7 @@ func (r *dynamicRenderer) Render(ctx context.Context, ownerID uuid.UUID, values PlanJSON: r.data.terraformValues.CachedPlan, ParameterValues: values, Owner: *r.currentOwner, + TFVars: r.tfvarValues, // Do not emit parser logs to coderd output logs. // TODO: Returning this logs in the output would benefit the caller. // Unsure how large the logs can be, so for now we just discard them. diff --git a/coderd/dynamicparameters/variablevalues.go b/coderd/dynamicparameters/variablevalues.go new file mode 100644 index 0000000000000..1bb72cc2b0510 --- /dev/null +++ b/coderd/dynamicparameters/variablevalues.go @@ -0,0 +1,58 @@ +package dynamicparameters + +import ( + "strconv" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/json" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" +) + +func VariableValues(vals []database.TemplateVersionVariable) (map[string]cty.Value, error) { + ctyVals := make(map[string]cty.Value, len(vals)) + for _, v := range vals { + value := v.Value + if value == "" && v.DefaultValue != "" { + value = v.DefaultValue + } + + if value == "" { + // Empty strings are unsupported I guess? + continue // omit non-set vals + } + + var err error + switch v.Type { + case "string": + ctyVals[v.Name] = cty.StringVal(value) + case "number": + ctyVals[v.Name], err = cty.ParseNumberVal(value) + if err != nil { + return nil, xerrors.Errorf("parse variable %q: %w", v.Name, err) + } + case "bool": + parsed, err := strconv.ParseBool(value) + if err != nil { + return nil, xerrors.Errorf("parse variable %q: %w", v.Name, err) + } + ctyVals[v.Name] = cty.BoolVal(parsed) + default: + // If it is a complex type, let the cty json code give it a try. + // TODO: Ideally we parse `list` & `map` and build the type ourselves. + ty, err := json.ImpliedType([]byte(value)) + if err != nil { + return nil, xerrors.Errorf("implied type for variable %q: %w", v.Name, err) + } + + jv, err := json.Unmarshal([]byte(value), ty) + if err != nil { + return nil, xerrors.Errorf("unmarshal variable %q: %w", v.Name, err) + } + ctyVals[v.Name] = jv + } + } + + return ctyVals, nil +} diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 90ea02e966a09..d608682c58eee 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -633,10 +633,16 @@ func (b *Builder) getDynamicParameterRenderer() (dynamicparameters.Renderer, err return nil, xerrors.Errorf("get template version terraform values: %w", err) } + variableValues, err := b.getTemplateVersionVariables() + if err != nil { + return nil, xerrors.Errorf("get template version variables: %w", err) + } + renderer, err := dynamicparameters.Prepare(b.ctx, b.store, b.fileCache, tv.ID, dynamicparameters.WithTemplateVersion(*tv), dynamicparameters.WithProvisionerJob(*job), dynamicparameters.WithTerraformValues(*tfVals), + dynamicparameters.WithTemplateVariableValues(variableValues), ) if err != nil { return nil, xerrors.Errorf("get template version renderer: %w", err) diff --git a/go.mod b/go.mod index fa91932ceaecf..4771818015991 100644 --- a/go.mod +++ b/go.mod @@ -483,7 +483,7 @@ require ( require ( github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 github.com/coder/aisdk-go v0.0.9 - github.com/coder/preview v1.0.3-0.20250701142654-c3d6e86b9393 + github.com/coder/preview v1.0.3-0.20250709160236-8ddde200cd69 github.com/fsnotify/fsnotify v1.9.0 github.com/mark3labs/mcp-go v0.33.0 ) diff --git a/go.sum b/go.sum index e46a4eb61a477..a0950bf08fc64 100644 --- a/go.sum +++ b/go.sum @@ -916,8 +916,8 @@ github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102 h1:ahTJlTRmTogsubgRVGO github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= -github.com/coder/preview v1.0.3-0.20250701142654-c3d6e86b9393 h1:l+m2liikn8JoEv6C22QIV4qseolUfvNsyUNA6JJsD6Y= -github.com/coder/preview v1.0.3-0.20250701142654-c3d6e86b9393/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM= +github.com/coder/preview v1.0.3-0.20250709160236-8ddde200cd69 h1:bQ3r5Y22V1heD6Ah4kN/wMJ8gflyGPhzNtiFefytBVs= +github.com/coder/preview v1.0.3-0.20250709160236-8ddde200cd69/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM= github.com/coder/quartz v0.2.1 h1:QgQ2Vc1+mvzewg2uD/nj8MJ9p9gE+QhGJm+Z+NGnrSE= github.com/coder/quartz v0.2.1/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA= github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc= From 40ee3fd63e0122855da46985ed524a8ba2779716 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 9 Jul 2025 11:25:31 -0600 Subject: [PATCH 02/14] test: add unit test for template vars in dynamic parameter --- coderd/coderdtest/dynamicparameters.go | 30 +++++++++++++++++- coderd/parameters_test.go | 32 ++++++++++++++++++++ coderd/testdata/parameters/variables/main.tf | 30 ++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 coderd/testdata/parameters/variables/main.tf diff --git a/coderd/coderdtest/dynamicparameters.go b/coderd/coderdtest/dynamicparameters.go index 28e01885560ca..c6adb6c97e786 100644 --- a/coderd/coderdtest/dynamicparameters.go +++ b/coderd/coderdtest/dynamicparameters.go @@ -29,7 +29,8 @@ type DynamicParameterTemplateParams struct { // TemplateID is used to update an existing template instead of creating a new one. TemplateID uuid.UUID - Version func(request *codersdk.CreateTemplateVersionRequest) + Version func(request *codersdk.CreateTemplateVersionRequest) + Variables []codersdk.TemplateVersionVariable } func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UUID, args DynamicParameterTemplateParams) (codersdk.Template, codersdk.TemplateVersion) { @@ -48,6 +49,32 @@ func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UU }, }} + userVars := make([]codersdk.VariableValue, 0, len(args.Variables)) + parseVars := make([]*proto.TemplateVariable, 0, len(args.Variables)) + for _, argv := range args.Variables { + parseVars = append(parseVars, &proto.TemplateVariable{ + Name: argv.Name, + Description: argv.Description, + Type: argv.Type, + DefaultValue: argv.DefaultValue, + Required: argv.Required, + Sensitive: argv.Sensitive, + }) + + userVars = append(userVars, codersdk.VariableValue{ + Name: argv.Name, + Value: argv.Value, + }) + } + + files.Parse = []*proto.Response{{ + Type: &proto.Response_Parse{ + Parse: &proto.ParseComplete{ + TemplateVariables: parseVars, + }, + }, + }} + mime := codersdk.ContentTypeTar if args.Zip { mime = codersdk.ContentTypeZip @@ -59,6 +86,7 @@ func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UU if args.Version != nil { args.Version(request) } + request.UserVariableValues = userVars }) AwaitTemplateVersionJobCompleted(t, client, version.ID) diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go index 855d95eb1de59..19251ff2090d0 100644 --- a/coderd/parameters_test.go +++ b/coderd/parameters_test.go @@ -343,6 +343,36 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) { require.Len(t, preview.Diagnostics, 1) require.Equal(t, preview.Diagnostics[0].Extra.Code, "owner_not_found") }) + + t.Run("TemplateVariables", func(t *testing.T) { + t.Parallel() + + dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/variables/main.tf") + require.NoError(t, err) + + setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{ + provisionerDaemonVersion: provProto.CurrentVersion.String(), + mainTF: dynamicParametersTerraformSource, + variables: []codersdk.TemplateVersionVariable{ + {Name: "one", Value: "austin", DefaultValue: "alice", Type: "string"}, + }, + plan: nil, + static: nil, + }) + + ctx := testutil.Context(t, testutil.WaitShort*100000) + stream := setup.stream + previews := stream.Chan() + + // Should see the output of the module represented + preview := testutil.RequireReceive(ctx, t, previews) + require.Equal(t, -1, preview.ID) + require.Empty(t, preview.Diagnostics) + + require.Len(t, preview.Parameters, 1) + coderdtest.AssertParameter(t, "variable_values", preview.Parameters). + Exists().Value("austin") + }) } type setupDynamicParamsTestParams struct { @@ -355,6 +385,7 @@ type setupDynamicParamsTestParams struct { static []*proto.RichParameter expectWebsocketError bool + variables []codersdk.TemplateVersionVariable } type dynamicParamsTest struct { @@ -380,6 +411,7 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn Plan: args.plan, ModulesArchive: args.modulesArchive, StaticParams: args.static, + Variables: args.variables, }) ctx := testutil.Context(t, testutil.WaitShort) diff --git a/coderd/testdata/parameters/variables/main.tf b/coderd/testdata/parameters/variables/main.tf new file mode 100644 index 0000000000000..79e927f63685a --- /dev/null +++ b/coderd/testdata/parameters/variables/main.tf @@ -0,0 +1,30 @@ +// Base case for workspace tags + parameters. +terraform { + required_providers { + coder = { + source = "coder/coder" + } + docker = { + source = "kreuzwerker/docker" + version = "3.0.2" + } + } +} + +variable "one" { + default = "alice" + type = string +} + + +data "coder_parameter" "variable_values" { + name = "variable_values" + description = "Just to show the variable values" + type = "string" + default = var.one + + option { + name = "one" + value = var.one + } +} From 520fff71c273d2c7def28f69676c40716bb004fe Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 14 Jul 2025 10:14:18 -0600 Subject: [PATCH 03/14] update preview to main --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4771818015991..f2b12cbd6387c 100644 --- a/go.mod +++ b/go.mod @@ -483,7 +483,7 @@ require ( require ( github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 github.com/coder/aisdk-go v0.0.9 - github.com/coder/preview v1.0.3-0.20250709160236-8ddde200cd69 + github.com/coder/preview v1.0.3-0.20250714153828-a737d4750448 github.com/fsnotify/fsnotify v1.9.0 github.com/mark3labs/mcp-go v0.33.0 ) diff --git a/go.sum b/go.sum index a0950bf08fc64..1d6ae833a5dbe 100644 --- a/go.sum +++ b/go.sum @@ -916,8 +916,8 @@ github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102 h1:ahTJlTRmTogsubgRVGO github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= -github.com/coder/preview v1.0.3-0.20250709160236-8ddde200cd69 h1:bQ3r5Y22V1heD6Ah4kN/wMJ8gflyGPhzNtiFefytBVs= -github.com/coder/preview v1.0.3-0.20250709160236-8ddde200cd69/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM= +github.com/coder/preview v1.0.3-0.20250714153828-a737d4750448 h1:S86sFp4Dr4dUn++fXOMOTu6ClnEZ/NrGCYv7bxZjYYc= +github.com/coder/preview v1.0.3-0.20250714153828-a737d4750448/go.mod h1:hQtBEqOFMJ3SHl9Q9pVvDA9CpeCEXBwbONNK29+3MLk= github.com/coder/quartz v0.2.1 h1:QgQ2Vc1+mvzewg2uD/nj8MJ9p9gE+QhGJm+Z+NGnrSE= github.com/coder/quartz v0.2.1/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA= github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc= From a8bdd04324f6799bd2d2890c4eb5d2dfd67e0d31 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 14 Jul 2025 10:16:59 -0600 Subject: [PATCH 04/14] fmt --- coderd/testdata/parameters/variables/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/testdata/parameters/variables/main.tf b/coderd/testdata/parameters/variables/main.tf index 79e927f63685a..684ee4505abe3 100644 --- a/coderd/testdata/parameters/variables/main.tf +++ b/coderd/testdata/parameters/variables/main.tf @@ -13,7 +13,7 @@ terraform { variable "one" { default = "alice" - type = string + type = string } From a2f2de6694835cb3f439e6b091173cef0e4c1a14 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 14 Jul 2025 10:32:33 -0600 Subject: [PATCH 05/14] template version tags to respect tf vars --- coderd/templateversions.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/coderd/templateversions.go b/coderd/templateversions.go index fa5a7ed1fe757..9642b362d40f5 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -18,6 +18,7 @@ import ( "github.com/google/uuid" "github.com/moby/moby/pkg/namesgenerator" "github.com/sqlc-dev/pqtype" + "github.com/zclconf/go-cty/cty" "golang.org/x/xerrors" "cdr.dev/slog" @@ -1585,7 +1586,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht var parsedTags map[string]string var ok bool if dynamicTemplate { - parsedTags, ok = api.dynamicTemplateVersionTags(ctx, rw, organization.ID, apiKey.UserID, file) + parsedTags, ok = api.dynamicTemplateVersionTags(ctx, rw, organization.ID, apiKey.UserID, file, req.UserVariableValues) if !ok { return } @@ -1762,7 +1763,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht warnings)) } -func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.ResponseWriter, orgID uuid.UUID, owner uuid.UUID, file database.File) (map[string]string, bool) { +func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.ResponseWriter, orgID uuid.UUID, owner uuid.UUID, file database.File, templateVariables []codersdk.VariableValue) (map[string]string, bool) { ownerData, err := dynamicparameters.WorkspaceOwner(ctx, api.Database, orgID, owner) if err != nil { if httpapi.Is404Error(err) { @@ -1800,11 +1801,18 @@ func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.Response return nil, false } + // Pass in any manually specified template variables as TFVars. + tfVarValues := make(map[string]cty.Value) + for _, variable := range templateVariables { + tfVarValues[variable.Name] = cty.StringVal(variable.Value) + } + output, diags := preview.Preview(ctx, preview.Input{ PlanJSON: nil, // Template versions are before `terraform plan` ParameterValues: nil, // No user-specified parameters Owner: *ownerData, Logger: stdslog.New(stdslog.DiscardHandler), + TFVars: tfVarValues, }, files) tagErr := dynamicparameters.CheckTags(output, diags) if tagErr != nil { From b45cbf2ada267de491593c987d3edfefbb1e4138 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 14 Jul 2025 11:51:40 -0600 Subject: [PATCH 06/14] change workspace tags test to new flow --- enterprise/coderd/workspaces_test.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 1030536f2111d..8c170f7d4e207 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2644,6 +2644,7 @@ func TestWorkspaceTagsTerraform(t *testing.T) { } } } + provider "coder" {} data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} @@ -2817,11 +2818,29 @@ func TestWorkspaceTagsTerraform(t *testing.T) { templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - _ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags) + // Provisioner for the empty file + emptyProvisioner := coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, map[string]string{}) // This can take a while, so set a relatively long timeout. ctx := testutil.Context(t, 2*testutil.WaitSuperLong) + emptyTar := testutil.CreateTar(t, map[string]string{"main.tf": ""}) + emptyFi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(emptyTar)) + emptyTv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: testutil.GetRandomName(t), + FileID: emptyFi.ID, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + Provisioner: codersdk.ProvisionerTypeTerraform, + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, emptyTv.ID) + tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, emptyTv.ID, func(request *codersdk.CreateTemplateRequest) { + request.UseClassicParameterFlow = ptr.Ref(false) + }) + _ = emptyProvisioner.Close() // No longer needed + + // The provisioner for the next template version + _ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags) + // Creating a template as a template admin must succeed templateFiles := map[string]string{"main.tf": fmt.Sprintf(mainTfTemplate, tc.tfWorkspaceTags)} tarBytes := testutil.CreateTar(t, templateFiles) @@ -2834,10 +2853,10 @@ func TestWorkspaceTagsTerraform(t *testing.T) { Provisioner: codersdk.ProvisionerTypeTerraform, ProvisionerTags: tc.createTemplateVersionRequestTags, UserVariableValues: tc.templateImportUserVariableValues, + TemplateID: tpl.ID, }) require.NoError(t, err, "failed to create template version") coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID) - tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, tv.ID) if !tc.skipCreateWorkspace { // Creating a workspace as a non-privileged user must succeed From c0b54b1286efaeec6a83c1da4f71d67eee7e28f8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 14 Jul 2025 12:02:44 -0600 Subject: [PATCH 07/14] linting --- enterprise/coderd/workspaces_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 8c170f7d4e207..00546f3f82d9f 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2826,12 +2826,16 @@ func TestWorkspaceTagsTerraform(t *testing.T) { emptyTar := testutil.CreateTar(t, map[string]string{"main.tf": ""}) emptyFi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(emptyTar)) + require.NoError(t, err) + emptyTv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ Name: testutil.GetRandomName(t), FileID: emptyFi.ID, StorageMethod: codersdk.ProvisionerStorageMethodFile, Provisioner: codersdk.ProvisionerTypeTerraform, }) + require.NoError(t, err) + coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, emptyTv.ID) tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, emptyTv.ID, func(request *codersdk.CreateTemplateRequest) { request.UseClassicParameterFlow = ptr.Ref(false) From d0c594aa27d1f8bc31f23960bdbaa54c41dbf553 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 17 Jul 2025 10:43:36 -0600 Subject: [PATCH 08/14] update test to use the right template version --- coderd/coderdtest/coderdtest.go | 1 + enterprise/coderd/workspaces_test.go | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 4aa968468e146..1429a85da8b27 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1075,6 +1075,7 @@ func AwaitWorkspaceBuildJobCompleted(t testing.TB, client *codersdk.Client, buil t.Logf("waiting for workspace build job %s", build) var workspaceBuild codersdk.WorkspaceBuild require.Eventually(t, func() bool { + var err error workspaceBuild, err = client.WorkspaceBuild(ctx, build) return assert.NoError(t, err) && workspaceBuild.Job.CompletedAt != nil diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 00546f3f82d9f..04bf8e06f4082 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2807,7 +2807,7 @@ func TestWorkspaceTagsTerraform(t *testing.T) { client, owner := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ // We intentionally do not run a built-in provisioner daemon here. - IncludeProvisionerDaemon: false, + IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ @@ -2818,9 +2818,6 @@ func TestWorkspaceTagsTerraform(t *testing.T) { templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - // Provisioner for the empty file - emptyProvisioner := coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, map[string]string{}) - // This can take a while, so set a relatively long timeout. ctx := testutil.Context(t, 2*testutil.WaitSuperLong) @@ -2832,7 +2829,7 @@ func TestWorkspaceTagsTerraform(t *testing.T) { Name: testutil.GetRandomName(t), FileID: emptyFi.ID, StorageMethod: codersdk.ProvisionerStorageMethodFile, - Provisioner: codersdk.ProvisionerTypeTerraform, + Provisioner: codersdk.ProvisionerTypeEcho, }) require.NoError(t, err) @@ -2840,7 +2837,6 @@ func TestWorkspaceTagsTerraform(t *testing.T) { tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, emptyTv.ID, func(request *codersdk.CreateTemplateRequest) { request.UseClassicParameterFlow = ptr.Ref(false) }) - _ = emptyProvisioner.Close() // No longer needed // The provisioner for the next template version _ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags) @@ -2865,7 +2861,7 @@ func TestWorkspaceTagsTerraform(t *testing.T) { if !tc.skipCreateWorkspace { // Creating a workspace as a non-privileged user must succeed ws, err := member.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{ - TemplateID: tpl.ID, + TemplateVersionID: tv.ID, Name: coderdtest.RandomUsername(t), RichParameterValues: tc.workspaceBuildParameters, }) From 3cdcb38551f5cd9f1a3a78e86409b44d1b888148 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 17 Jul 2025 11:14:42 -0600 Subject: [PATCH 09/14] update test with some logs --- enterprise/coderd/workspaces_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 04bf8e06f4082..a99186da9b36f 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2858,14 +2858,21 @@ func TestWorkspaceTagsTerraform(t *testing.T) { require.NoError(t, err, "failed to create template version") coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID) + err = templateAdmin.UpdateActiveTemplateVersion(ctx, tpl.ID, codersdk.UpdateActiveTemplateVersion{ + ID: tv.ID, + }) + require.NoError(t, err, "set to active template version") + if !tc.skipCreateWorkspace { // Creating a workspace as a non-privileged user must succeed ws, err := member.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{ - TemplateVersionID: tv.ID, + TemplateID: tpl.ID, Name: coderdtest.RandomUsername(t), RichParameterValues: tc.workspaceBuildParameters, }) require.NoError(t, err, "failed to create workspace") + tagJSON, _ := json.Marshal(ws.LatestBuild.Job.Tags) + t.Logf("Created workspace build with tags: %s", tagJSON) coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, ws.LatestBuild.ID) } }) From e58c5e1b8005d30e9f437b5a2ca34309a80a2ca1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 17 Jul 2025 11:25:50 -0600 Subject: [PATCH 10/14] run dynamic and static --- enterprise/coderd/workspaces_test.go | 204 +++++++++++++++------------ 1 file changed, 113 insertions(+), 91 deletions(-) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index a99186da9b36f..5565960dfeb4e 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2627,6 +2627,21 @@ func TestWorkspaceTemplateParamsChange(t *testing.T) { require.Equal(t, codersdk.WorkspaceStatusDeleted, build.Status) } +type testWorkspaceTagsTerraformCase struct { + name string + // tags to apply to the external provisioner + provisionerTags map[string]string + // tags to apply to the create template version request + createTemplateVersionRequestTags map[string]string + // the coder_workspace_tags bit of main.tf. + // you can add more stuff here if you need + tfWorkspaceTags string + templateImportUserVariableValues []codersdk.VariableValue + // if we need to set parameters on workspace build + workspaceBuildParameters []codersdk.WorkspaceBuildParameter + skipCreateWorkspace bool +} + // TestWorkspaceTagsTerraform tests that a workspace can be created with tags. // This is an end-to-end-style test, meaning that we actually run the // real Terraform provisioner and validate that the workspace is created @@ -2636,7 +2651,7 @@ func TestWorkspaceTemplateParamsChange(t *testing.T) { // config file so that we only reference those // nolint:paralleltest // t.Setenv func TestWorkspaceTagsTerraform(t *testing.T) { - mainTfTemplate := ` + coderProviderTemplate := ` terraform { required_providers { coder = { @@ -2644,34 +2659,11 @@ func TestWorkspaceTagsTerraform(t *testing.T) { } } } - - provider "coder" {} - data "coder_workspace" "me" {} - data "coder_workspace_owner" "me" {} - data "coder_parameter" "unrelated" { - name = "unrelated" - type = "list(string)" - default = jsonencode(["a", "b"]) - } - %s ` - tfCliConfigPath := downloadProviders(t, fmt.Sprintf(mainTfTemplate, "")) + tfCliConfigPath := downloadProviders(t, coderProviderTemplate) t.Setenv("TF_CLI_CONFIG_FILE", tfCliConfigPath) - for _, tc := range []struct { - name string - // tags to apply to the external provisioner - provisionerTags map[string]string - // tags to apply to the create template version request - createTemplateVersionRequestTags map[string]string - // the coder_workspace_tags bit of main.tf. - // you can add more stuff here if you need - tfWorkspaceTags string - templateImportUserVariableValues []codersdk.VariableValue - // if we need to set parameters on workspace build - workspaceBuildParameters []codersdk.WorkspaceBuildParameter - skipCreateWorkspace bool - }{ + for _, tc := range []testWorkspaceTagsTerraformCase{ { name: "no tags", tfWorkspaceTags: ``, @@ -2804,78 +2796,108 @@ func TestWorkspaceTagsTerraform(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - client, owner := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - // We intentionally do not run a built-in provisioner daemon here. - IncludeProvisionerDaemon: true, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureExternalProvisionerDaemons: 1, - }, - }, + t.Run("dynamic", func(t *testing.T) { + testWorkspaceTagsTerraform(t, tc, true) }) - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) - member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + t.Run("static", func(t *testing.T) { + testWorkspaceTagsTerraform(t, tc, false) + }) + }) + } +} - // This can take a while, so set a relatively long timeout. - ctx := testutil.Context(t, 2*testutil.WaitSuperLong) +func testWorkspaceTagsTerraform(t *testing.T, tc testWorkspaceTagsTerraformCase, dynamic bool) { + mainTfTemplate := ` + terraform { + required_providers { + coder = { + source = "coder/coder" + } + } + } - emptyTar := testutil.CreateTar(t, map[string]string{"main.tf": ""}) - emptyFi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(emptyTar)) - require.NoError(t, err) + provider "coder" {} + data "coder_workspace" "me" {} + data "coder_workspace_owner" "me" {} + data "coder_parameter" "unrelated" { + name = "unrelated" + type = "list(string)" + default = jsonencode(["a", "b"]) + } + %s + ` - emptyTv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ - Name: testutil.GetRandomName(t), - FileID: emptyFi.ID, - StorageMethod: codersdk.ProvisionerStorageMethodFile, - Provisioner: codersdk.ProvisionerTypeEcho, - }) - require.NoError(t, err) + client, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + // We intentionally do not run a built-in provisioner daemon here. + IncludeProvisionerDaemon: false, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, emptyTv.ID) - tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, emptyTv.ID, func(request *codersdk.CreateTemplateRequest) { - request.UseClassicParameterFlow = ptr.Ref(false) - }) + // This can take a while, so set a relatively long timeout. + ctx := testutil.Context(t, 2*testutil.WaitSuperLong) - // The provisioner for the next template version - _ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags) - - // Creating a template as a template admin must succeed - templateFiles := map[string]string{"main.tf": fmt.Sprintf(mainTfTemplate, tc.tfWorkspaceTags)} - tarBytes := testutil.CreateTar(t, templateFiles) - fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarBytes)) - require.NoError(t, err, "failed to upload file") - tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ - Name: testutil.GetRandomName(t), - FileID: fi.ID, - StorageMethod: codersdk.ProvisionerStorageMethodFile, - Provisioner: codersdk.ProvisionerTypeTerraform, - ProvisionerTags: tc.createTemplateVersionRequestTags, - UserVariableValues: tc.templateImportUserVariableValues, - TemplateID: tpl.ID, - }) - require.NoError(t, err, "failed to create template version") - coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID) + emptyTar := testutil.CreateTar(t, map[string]string{"main.tf": ""}) + emptyFi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(emptyTar)) + require.NoError(t, err) - err = templateAdmin.UpdateActiveTemplateVersion(ctx, tpl.ID, codersdk.UpdateActiveTemplateVersion{ - ID: tv.ID, - }) - require.NoError(t, err, "set to active template version") - - if !tc.skipCreateWorkspace { - // Creating a workspace as a non-privileged user must succeed - ws, err := member.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{ - TemplateID: tpl.ID, - Name: coderdtest.RandomUsername(t), - RichParameterValues: tc.workspaceBuildParameters, - }) - require.NoError(t, err, "failed to create workspace") - tagJSON, _ := json.Marshal(ws.LatestBuild.Job.Tags) - t.Logf("Created workspace build with tags: %s", tagJSON) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, ws.LatestBuild.ID) - } - }) + // This template version does not need to succeed in being created. + // It will be in pending forever. We just need it to create a template. + emptyTv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: testutil.GetRandomName(t), + FileID: emptyFi.ID, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + Provisioner: codersdk.ProvisionerTypeTerraform, + }) + require.NoError(t, err) + + tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, emptyTv.ID, func(request *codersdk.CreateTemplateRequest) { + request.UseClassicParameterFlow = ptr.Ref(!dynamic) + }) + + // The provisioner for the next template version + _ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags) + + // Creating a template as a template admin must succeed + templateFiles := map[string]string{"main.tf": fmt.Sprintf(mainTfTemplate, tc.tfWorkspaceTags)} + tarBytes := testutil.CreateTar(t, templateFiles) + fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarBytes)) + require.NoError(t, err, "failed to upload file") + tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: testutil.GetRandomName(t), + FileID: fi.ID, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + Provisioner: codersdk.ProvisionerTypeTerraform, + ProvisionerTags: tc.createTemplateVersionRequestTags, + UserVariableValues: tc.templateImportUserVariableValues, + TemplateID: tpl.ID, + }) + require.NoError(t, err, "failed to create template version") + coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID) + + err = templateAdmin.UpdateActiveTemplateVersion(ctx, tpl.ID, codersdk.UpdateActiveTemplateVersion{ + ID: tv.ID, + }) + require.NoError(t, err, "set to active template version") + + if !tc.skipCreateWorkspace { + // Creating a workspace as a non-privileged user must succeed + ws, err := member.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{ + TemplateID: tpl.ID, + Name: coderdtest.RandomUsername(t), + RichParameterValues: tc.workspaceBuildParameters, + }) + require.NoError(t, err, "failed to create workspace") + tagJSON, _ := json.Marshal(ws.LatestBuild.Job.Tags) + t.Logf("Created workspace build [%s] with tags: %s", ws.LatestBuild.Job.Type, tagJSON) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, ws.LatestBuild.ID) } } From ac9fccb5700ce913809d6b6fec0b6f55841f0966 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 17 Jul 2025 11:54:04 -0600 Subject: [PATCH 11/14] assume default variable type of string --- coderd/coderdtest/coderdtest.go | 1 - coderd/dynamicparameters/variablevalues.go | 7 ++++++- coderd/parameters_test.go | 2 +- enterprise/coderd/workspaces_test.go | 7 +++++-- provisioner/terraform/parse.go | 4 ++++ 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 1429a85da8b27..4aa968468e146 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1075,7 +1075,6 @@ func AwaitWorkspaceBuildJobCompleted(t testing.TB, client *codersdk.Client, buil t.Logf("waiting for workspace build job %s", build) var workspaceBuild codersdk.WorkspaceBuild require.Eventually(t, func() bool { - var err error workspaceBuild, err = client.WorkspaceBuild(ctx, build) return assert.NoError(t, err) && workspaceBuild.Job.CompletedAt != nil diff --git a/coderd/dynamicparameters/variablevalues.go b/coderd/dynamicparameters/variablevalues.go index 1bb72cc2b0510..d63c7be2523e8 100644 --- a/coderd/dynamicparameters/variablevalues.go +++ b/coderd/dynamicparameters/variablevalues.go @@ -25,7 +25,12 @@ func VariableValues(vals []database.TemplateVersionVariable) (map[string]cty.Val var err error switch v.Type { - case "string": + // Defaulting the empty type to "string" + // TODO: This does not match the terraform behavior, however it is too late + // at this point in the code to determine this, as the database type stores all values + // as strings. The code needs to be fixed in the `Parse` step of the provisioner. + // That step should determine the type of the variable correctly and store it in the database. + case "string", "": ctyVals[v.Name] = cty.StringVal(value) case "number": ctyVals[v.Name], err = cty.ParseNumberVal(value) diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go index 19251ff2090d0..c00d6f9224bfb 100644 --- a/coderd/parameters_test.go +++ b/coderd/parameters_test.go @@ -360,7 +360,7 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) { static: nil, }) - ctx := testutil.Context(t, testutil.WaitShort*100000) + ctx := testutil.Context(t, testutil.WaitShort) stream := setup.stream previews := stream.Chan() diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 5565960dfeb4e..effebca2cb5e5 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2799,7 +2799,10 @@ func TestWorkspaceTagsTerraform(t *testing.T) { t.Run("dynamic", func(t *testing.T) { testWorkspaceTagsTerraform(t, tc, true) }) - t.Run("static", func(t *testing.T) { + + // classic uses tfparse for tags. This sub test can be + // removed when tf parse is removed. + t.Run("classic", func(t *testing.T) { testWorkspaceTagsTerraform(t, tc, false) }) }) @@ -3172,7 +3175,7 @@ func TestWorkspaceLock(t *testing.T) { require.NotNil(t, workspace.DeletingAt) require.NotNil(t, workspace.DormantAt) require.Equal(t, workspace.DormantAt.Add(dormantTTL), *workspace.DeletingAt) - require.WithinRange(t, *workspace.DormantAt, time.Now().Add(-time.Second*10), time.Now()) + require.WithinRange(t, *workspace.DormantAt, time.Now().Add(-time.Second), time.Now()) // Locking a workspace shouldn't update the last_used_at. require.Equal(t, lastUsedAt, workspace.LastUsedAt) diff --git a/provisioner/terraform/parse.go b/provisioner/terraform/parse.go index 7aa78e401c503..d5b59df327f65 100644 --- a/provisioner/terraform/parse.go +++ b/provisioner/terraform/parse.go @@ -15,6 +15,10 @@ import ( ) // Parse extracts Terraform variables from source-code. +// TODO: This Parse is incomplete. It uses tfparse instead of terraform. +// The inputs are incomplete, as values such as the user context, parameters, +// etc are all important to the parsing process. This should be replaced with +// preview and have all inputs. func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete { ctx := sess.Context() _, span := s.startTrace(ctx, tracing.FuncName()) From 432021d15c71e3ac8b227c453e59df917ea3ad56 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 17 Jul 2025 11:58:39 -0600 Subject: [PATCH 12/14] rename unit test --- enterprise/coderd/workspaces_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index effebca2cb5e5..e5fe7372912c8 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2803,13 +2803,13 @@ func TestWorkspaceTagsTerraform(t *testing.T) { // classic uses tfparse for tags. This sub test can be // removed when tf parse is removed. t.Run("classic", func(t *testing.T) { - testWorkspaceTagsTerraform(t, tc, false) + workspaceTagsTerraform(t, tc, false) }) }) } } -func testWorkspaceTagsTerraform(t *testing.T, tc testWorkspaceTagsTerraformCase, dynamic bool) { +func workspaceTagsTerraform(t *testing.T, tc testWorkspaceTagsTerraformCase, dynamic bool) { mainTfTemplate := ` terraform { required_providers { From 2347119da01acd0bb5fec29ec799eec4e4fb3b72 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 17 Jul 2025 12:02:19 -0600 Subject: [PATCH 13/14] fixup! rename unit test --- enterprise/coderd/workspaces_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index e5fe7372912c8..d622748899aa0 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2797,7 +2797,7 @@ func TestWorkspaceTagsTerraform(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Run("dynamic", func(t *testing.T) { - testWorkspaceTagsTerraform(t, tc, true) + workspaceTagsTerraform(t, tc, true) }) // classic uses tfparse for tags. This sub test can be From 7b2213cde7d7c2c679fef0b050895d3aad7ccb03 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 21 Jul 2025 09:59:49 -0500 Subject: [PATCH 14/14] add comments --- coderd/dynamicparameters/variablevalues.go | 2 ++ coderd/templateversions.go | 1 + 2 files changed, 3 insertions(+) diff --git a/coderd/dynamicparameters/variablevalues.go b/coderd/dynamicparameters/variablevalues.go index d63c7be2523e8..574039119c786 100644 --- a/coderd/dynamicparameters/variablevalues.go +++ b/coderd/dynamicparameters/variablevalues.go @@ -10,6 +10,8 @@ import ( "github.com/coder/coder/v2/coderd/database" ) +// VariableValues is a helper function that converts a slice of TemplateVersionVariable +// into a map of cty.Value for use in coder/preview. func VariableValues(vals []database.TemplateVersionVariable) (map[string]cty.Value, error) { ctyVals := make(map[string]cty.Value, len(vals)) for _, v := range vals { diff --git a/coderd/templateversions.go b/coderd/templateversions.go index 9642b362d40f5..72b18a2c47e92 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -1802,6 +1802,7 @@ func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.Response } // Pass in any manually specified template variables as TFVars. + // TODO: Does this break if the type is not a string? tfVarValues := make(map[string]cty.Value) for _, variable := range templateVariables { tfVarValues[variable.Name] = cty.StringVal(variable.Value)