From bee1c76250348cf59dea98556b7418759a0aa265 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Mon, 6 Jul 2026 18:10:04 +0000 Subject: [PATCH 1/2] fix(coderd/templatebuilder): declare project_id variable in GCP base templates GCP base templates (gcp-linux, gcp-windows) had a Terraform variable "project_id" with no default, but the base.json manifests did not declare it. The Template Builder UI never prompted for it, so the provisioner import failed with "required template variables need values: project_id". Add project_id to both GCP base.json manifests as a required variable, and convert the templates from raw Terraform variable blocks to Go template injection (matching the kubernetes pattern). Also fix DefaultBaseRenderContext to supply a placeholder for required variables without defaults, preventing in rendered output. --- coderd/templatebuilder/bases.go | 5 +++ .../templatebuilder/bases/gcp-linux/base.json | 12 ++++++- .../bases/gcp-linux/main.tf.tmpl | 6 +--- .../bases/gcp-windows/base.json | 12 ++++++- .../bases/gcp-windows/main.tf.tmpl | 6 +--- coderd/templatebuilder/compose_test.go | 32 +++++++++++-------- .../testdata/gcp-linux.tf.golden | 6 +--- .../testdata/gcp-windows.tf.golden | 6 +--- .../testdata/kubernetes.tf.golden | 4 +-- coderd/templatebuilder_handler_test.go | 3 +- 10 files changed, 54 insertions(+), 38 deletions(-) diff --git a/coderd/templatebuilder/bases.go b/coderd/templatebuilder/bases.go index 774167a619f..350182679f5 100644 --- a/coderd/templatebuilder/bases.go +++ b/coderd/templatebuilder/bases.go @@ -227,6 +227,11 @@ func DefaultBaseRenderContext(exampleID string) BaseRenderContext { } if len(v.Default) > 0 && isSimpleJSONValue(v.Default) { vars[v.Name] = string(v.Default) + } else if v.Required { + // Provide a placeholder so Go template rendering + // succeeds. The compose path validates that real + // values are supplied before rendering. + vars[v.Name] = `"REQUIRED"` } } diff --git a/coderd/templatebuilder/bases/gcp-linux/base.json b/coderd/templatebuilder/bases/gcp-linux/base.json index d03489c572d..02ad23ce0bc 100644 --- a/coderd/templatebuilder/bases/gcp-linux/base.json +++ b/coderd/templatebuilder/bases/gcp-linux/base.json @@ -2,5 +2,15 @@ "id": "gcp-linux", "display_name": "Google Compute Engine (Linux)", "os": "linux", - "default_context": {} + "default_context": {}, + "variables": [ + { + "name": "project_id", + "type": "string", + "description": "Which Google Compute Project should your workspace live in?", + "required": true, + "sensitive": false, + "computed": false + } + ] } diff --git a/coderd/templatebuilder/bases/gcp-linux/main.tf.tmpl b/coderd/templatebuilder/bases/gcp-linux/main.tf.tmpl index acb8e8fa48e..9600065cd2d 100644 --- a/coderd/templatebuilder/bases/gcp-linux/main.tf.tmpl +++ b/coderd/templatebuilder/bases/gcp-linux/main.tf.tmpl @@ -11,10 +11,6 @@ terraform { provider "coder" {} -variable "project_id" { - description = "Which Google Compute Project should your workspace live in?" -} - # See https://registry.coder.com/modules/coder/gcp-region module "gcp_region" { source = "registry.coder.com/coder/gcp-region/coder" @@ -28,7 +24,7 @@ module "gcp_region" { provider "google" { zone = module.gcp_region.value - project = var.project_id + project = {{ .Variables.project_id }} } data "google_compute_default_service_account" "default" {} diff --git a/coderd/templatebuilder/bases/gcp-windows/base.json b/coderd/templatebuilder/bases/gcp-windows/base.json index 98f8106f0ce..c9bbb73fba4 100644 --- a/coderd/templatebuilder/bases/gcp-windows/base.json +++ b/coderd/templatebuilder/bases/gcp-windows/base.json @@ -2,5 +2,15 @@ "id": "gcp-windows", "display_name": "Google Compute Engine (Windows)", "os": "windows", - "default_context": {} + "default_context": {}, + "variables": [ + { + "name": "project_id", + "type": "string", + "description": "Which Google Compute Project should your workspace live in?", + "required": true, + "sensitive": false, + "computed": false + } + ] } diff --git a/coderd/templatebuilder/bases/gcp-windows/main.tf.tmpl b/coderd/templatebuilder/bases/gcp-windows/main.tf.tmpl index aea409eee7a..3c88f087101 100644 --- a/coderd/templatebuilder/bases/gcp-windows/main.tf.tmpl +++ b/coderd/templatebuilder/bases/gcp-windows/main.tf.tmpl @@ -11,10 +11,6 @@ terraform { provider "coder" {} -variable "project_id" { - description = "Which Google Compute Project should your workspace live in?" -} - # See https://registry.coder.com/modules/coder/gcp-region module "gcp_region" { source = "registry.coder.com/coder/gcp-region/coder" @@ -28,7 +24,7 @@ module "gcp_region" { provider "google" { zone = module.gcp_region.value - project = var.project_id + project = {{ .Variables.project_id }} } data "coder_workspace" "me" {} diff --git a/coderd/templatebuilder/compose_test.go b/coderd/templatebuilder/compose_test.go index b15692036fd..59bd91f3d2c 100644 --- a/coderd/templatebuilder/compose_test.go +++ b/coderd/templatebuilder/compose_test.go @@ -78,36 +78,42 @@ func TestCompose(t *testing.T) { require.Contains(t, result.ExtraFiles, "cloud-init/userdata.sh.tftpl") }) - t.Run("GCPWindowsBase", func(t *testing.T) { + t.Run("GCPLinuxBaseWithProjectID", func(t *testing.T) { t.Parallel() result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{ - BaseTemplateID: "gcp-windows", - RegistryURL: "https://registry.coder.com", + BaseTemplateID: "gcp-linux", + BaseVariableValues: map[string]string{"project_id": "my-gcp-project"}, + RegistryURL: "https://registry.coder.com", }) require.NoError(t, err) require.NotEmpty(t, result.MainTF) require.Contains(t, string(result.MainTF), `resource "coder_agent" "main"`) + require.Contains(t, string(result.MainTF), `"my-gcp-project"`) + require.NotContains(t, string(result.MainTF), `var.project_id`) }) - t.Run("AzureLinuxExtraFiles", func(t *testing.T) { + t.Run("GCPWindowsBase", func(t *testing.T) { t.Parallel() result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{ - BaseTemplateID: "azure-linux", - RegistryURL: "https://registry.coder.com", + BaseTemplateID: "gcp-windows", + BaseVariableValues: map[string]string{"project_id": "my-gcp-project"}, + RegistryURL: "https://registry.coder.com", }) require.NoError(t, err) - require.Contains(t, result.ExtraFiles, "cloud-init/cloud-config.yaml.tftpl") + require.NotEmpty(t, result.MainTF) + require.Contains(t, string(result.MainTF), `resource "coder_agent" "main"`) + require.Contains(t, string(result.MainTF), `"my-gcp-project"`) + require.NotContains(t, string(result.MainTF), `var.project_id`) }) - t.Run("GCPWindowsBase", func(t *testing.T) { + t.Run("GCPMissingRequiredBaseVariable", func(t *testing.T) { t.Parallel() - result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{ - BaseTemplateID: "gcp-windows", + _, err := templatebuilder.Compose(templatebuilder.ComposeRequest{ + BaseTemplateID: "gcp-linux", RegistryURL: "https://registry.coder.com", }) - require.NoError(t, err) - require.NotEmpty(t, result.MainTF) - require.Contains(t, string(result.MainTF), `resource "coder_agent" "main"`) + require.Error(t, err) + require.Contains(t, err.Error(), `variable "project_id" is required`) }) t.Run("AzureLinuxExtraFiles", func(t *testing.T) { diff --git a/coderd/templatebuilder/testdata/gcp-linux.tf.golden b/coderd/templatebuilder/testdata/gcp-linux.tf.golden index acb8e8fa48e..56fd9f66e59 100644 --- a/coderd/templatebuilder/testdata/gcp-linux.tf.golden +++ b/coderd/templatebuilder/testdata/gcp-linux.tf.golden @@ -11,10 +11,6 @@ terraform { provider "coder" {} -variable "project_id" { - description = "Which Google Compute Project should your workspace live in?" -} - # See https://registry.coder.com/modules/coder/gcp-region module "gcp_region" { source = "registry.coder.com/coder/gcp-region/coder" @@ -28,7 +24,7 @@ module "gcp_region" { provider "google" { zone = module.gcp_region.value - project = var.project_id + project = "REQUIRED" } data "google_compute_default_service_account" "default" {} diff --git a/coderd/templatebuilder/testdata/gcp-windows.tf.golden b/coderd/templatebuilder/testdata/gcp-windows.tf.golden index aea409eee7a..94e07bdac74 100644 --- a/coderd/templatebuilder/testdata/gcp-windows.tf.golden +++ b/coderd/templatebuilder/testdata/gcp-windows.tf.golden @@ -11,10 +11,6 @@ terraform { provider "coder" {} -variable "project_id" { - description = "Which Google Compute Project should your workspace live in?" -} - # See https://registry.coder.com/modules/coder/gcp-region module "gcp_region" { source = "registry.coder.com/coder/gcp-region/coder" @@ -28,7 +24,7 @@ module "gcp_region" { provider "google" { zone = module.gcp_region.value - project = var.project_id + project = "REQUIRED" } data "coder_workspace" "me" {} diff --git a/coderd/templatebuilder/testdata/kubernetes.tf.golden b/coderd/templatebuilder/testdata/kubernetes.tf.golden index 876f3c1dfc2..965eb96991d 100644 --- a/coderd/templatebuilder/testdata/kubernetes.tf.golden +++ b/coderd/templatebuilder/testdata/kubernetes.tf.golden @@ -153,7 +153,7 @@ resource "coder_agent" "main" { resource "kubernetes_persistent_volume_claim_v1" "home" { metadata { name = "coder-${data.coder_workspace.me.id}-home" - namespace = + namespace = "REQUIRED" labels = { "app.kubernetes.io/name" = "coder-pvc" "app.kubernetes.io/instance" = "coder-pvc-${data.coder_workspace.me.id}" @@ -188,7 +188,7 @@ resource "kubernetes_deployment_v1" "main" { wait_for_rollout = false metadata { name = "coder-${data.coder_workspace.me.id}" - namespace = + namespace = "REQUIRED" labels = { "app.kubernetes.io/name" = "coder-workspace" "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" diff --git a/coderd/templatebuilder_handler_test.go b/coderd/templatebuilder_handler_test.go index baa89e80aeb..453555acd64 100644 --- a/coderd/templatebuilder_handler_test.go +++ b/coderd/templatebuilder_handler_test.go @@ -66,7 +66,8 @@ func TestTemplateBuilderBases(t *testing.T) { { id: "gcp-windows", expectedOS: "windows", - hasVariables: false, + hasVariables: true, + expectedVars: []string{"project_id"}, }, } From 1025c088982d619658b8efeb4fad492909c7d51c Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Mon, 6 Jul 2026 20:27:50 +0000 Subject: [PATCH 2/2] fix(coderd): address review feedback for GCP template builder bases - Remove placeholder injection from DefaultBaseRenderContext. Required variables with no default are no longer silently filled with "REQUIRED". The compose path validates via mergeBaseVariables. - Add testRenderContext helper in tests that supplies deterministic test values for required variables from base metadata. - Keep the Terraform variable block in GCP templates with a default injected from the Go template. This preserves the variable in the rendered Terraform so users can override it after template creation. - Update compose test assertions to match the new output format. --- coderd/templatebuilder/bases.go | 5 ---- .../bases/gcp-linux/main.tf.tmpl | 7 +++++- .../bases/gcp-windows/main.tf.tmpl | 7 +++++- coderd/templatebuilder/compose_test.go | 14 ++++++----- coderd/templatebuilder/render_test.go | 25 +++++++++++++++++-- .../testdata/gcp-linux.tf.golden | 7 +++++- .../testdata/gcp-windows.tf.golden | 7 +++++- .../testdata/kubernetes.tf.golden | 4 +-- 8 files changed, 57 insertions(+), 19 deletions(-) diff --git a/coderd/templatebuilder/bases.go b/coderd/templatebuilder/bases.go index 350182679f5..774167a619f 100644 --- a/coderd/templatebuilder/bases.go +++ b/coderd/templatebuilder/bases.go @@ -227,11 +227,6 @@ func DefaultBaseRenderContext(exampleID string) BaseRenderContext { } if len(v.Default) > 0 && isSimpleJSONValue(v.Default) { vars[v.Name] = string(v.Default) - } else if v.Required { - // Provide a placeholder so Go template rendering - // succeeds. The compose path validates that real - // values are supplied before rendering. - vars[v.Name] = `"REQUIRED"` } } diff --git a/coderd/templatebuilder/bases/gcp-linux/main.tf.tmpl b/coderd/templatebuilder/bases/gcp-linux/main.tf.tmpl index 9600065cd2d..bb6e3211c8c 100644 --- a/coderd/templatebuilder/bases/gcp-linux/main.tf.tmpl +++ b/coderd/templatebuilder/bases/gcp-linux/main.tf.tmpl @@ -11,6 +11,11 @@ terraform { provider "coder" {} +variable "project_id" { + description = "Which Google Compute Project should your workspace live in?" + default = {{ .Variables.project_id }} +} + # See https://registry.coder.com/modules/coder/gcp-region module "gcp_region" { source = "registry.coder.com/coder/gcp-region/coder" @@ -24,7 +29,7 @@ module "gcp_region" { provider "google" { zone = module.gcp_region.value - project = {{ .Variables.project_id }} + project = var.project_id } data "google_compute_default_service_account" "default" {} diff --git a/coderd/templatebuilder/bases/gcp-windows/main.tf.tmpl b/coderd/templatebuilder/bases/gcp-windows/main.tf.tmpl index 3c88f087101..45cc09f2409 100644 --- a/coderd/templatebuilder/bases/gcp-windows/main.tf.tmpl +++ b/coderd/templatebuilder/bases/gcp-windows/main.tf.tmpl @@ -11,6 +11,11 @@ terraform { provider "coder" {} +variable "project_id" { + description = "Which Google Compute Project should your workspace live in?" + default = {{ .Variables.project_id }} +} + # See https://registry.coder.com/modules/coder/gcp-region module "gcp_region" { source = "registry.coder.com/coder/gcp-region/coder" @@ -24,7 +29,7 @@ module "gcp_region" { provider "google" { zone = module.gcp_region.value - project = {{ .Variables.project_id }} + project = var.project_id } data "coder_workspace" "me" {} diff --git a/coderd/templatebuilder/compose_test.go b/coderd/templatebuilder/compose_test.go index 59bd91f3d2c..276741c8168 100644 --- a/coderd/templatebuilder/compose_test.go +++ b/coderd/templatebuilder/compose_test.go @@ -87,9 +87,10 @@ func TestCompose(t *testing.T) { }) require.NoError(t, err) require.NotEmpty(t, result.MainTF) - require.Contains(t, string(result.MainTF), `resource "coder_agent" "main"`) - require.Contains(t, string(result.MainTF), `"my-gcp-project"`) - require.NotContains(t, string(result.MainTF), `var.project_id`) + mainTF := string(result.MainTF) + require.Contains(t, mainTF, `resource "coder_agent" "main"`) + require.Contains(t, mainTF, `default = "my-gcp-project"`) + require.Contains(t, mainTF, `project = var.project_id`) }) t.Run("GCPWindowsBase", func(t *testing.T) { @@ -101,9 +102,10 @@ func TestCompose(t *testing.T) { }) require.NoError(t, err) require.NotEmpty(t, result.MainTF) - require.Contains(t, string(result.MainTF), `resource "coder_agent" "main"`) - require.Contains(t, string(result.MainTF), `"my-gcp-project"`) - require.NotContains(t, string(result.MainTF), `var.project_id`) + mainTF := string(result.MainTF) + require.Contains(t, mainTF, `resource "coder_agent" "main"`) + require.Contains(t, mainTF, `default = "my-gcp-project"`) + require.Contains(t, mainTF, `project = var.project_id`) }) t.Run("GCPMissingRequiredBaseVariable", func(t *testing.T) { diff --git a/coderd/templatebuilder/render_test.go b/coderd/templatebuilder/render_test.go index fd75eb1b1d6..12927737e2d 100644 --- a/coderd/templatebuilder/render_test.go +++ b/coderd/templatebuilder/render_test.go @@ -2,6 +2,7 @@ package templatebuilder_test import ( "flag" + "fmt" "os" "path/filepath" "testing" @@ -14,6 +15,26 @@ import ( var updateGolden = flag.Bool("update", false, "update golden files") +// testRenderContext builds a BaseRenderContext from defaults and fills +// in deterministic test values for any required variables that have no +// default. This keeps the all-bases test loops working without silently +// producing broken HCL. +func testRenderContext(exampleID string) templatebuilder.BaseRenderContext { + rc := templatebuilder.DefaultBaseRenderContext(exampleID) + if rc.Variables == nil { + rc.Variables = make(map[string]string) + } + for _, v := range templatebuilder.BaseVariables(exampleID) { + if v.Computed || v.Sensitive { + continue + } + if _, ok := rc.Variables[v.Name]; !ok { + rc.Variables[v.Name] = fmt.Sprintf("%q", "test-"+v.Name) + } + } + return rc +} + func TestRenderBaseTemplate(t *testing.T) { t.Parallel() @@ -297,7 +318,7 @@ func TestAllBasesRenderAndExtractAgent(t *testing.T) { for _, id := range templatebuilder.BaseTemplateIDs() { t.Run(id, func(t *testing.T) { t.Parallel() - renderCtx := templatebuilder.DefaultBaseRenderContext(id) + renderCtx := testRenderContext(id) rendered, err := templatebuilder.RenderBaseTemplate(id, "main.tf.tmpl", renderCtx) require.NoError(t, err, "base %q should render without error", id) require.NotEmpty(t, rendered) @@ -330,7 +351,7 @@ func TestBaseTemplateSnapshot(t *testing.T) { t.Run(tc.exampleID, func(t *testing.T) { t.Parallel() - renderCtx := templatebuilder.DefaultBaseRenderContext(tc.exampleID) + renderCtx := testRenderContext(tc.exampleID) rendered, err := templatebuilder.RenderBaseTemplate(tc.exampleID, "main.tf.tmpl", renderCtx) require.NoError(t, err) require.NotEmpty(t, rendered) diff --git a/coderd/templatebuilder/testdata/gcp-linux.tf.golden b/coderd/templatebuilder/testdata/gcp-linux.tf.golden index 56fd9f66e59..4d81ed0759d 100644 --- a/coderd/templatebuilder/testdata/gcp-linux.tf.golden +++ b/coderd/templatebuilder/testdata/gcp-linux.tf.golden @@ -11,6 +11,11 @@ terraform { provider "coder" {} +variable "project_id" { + description = "Which Google Compute Project should your workspace live in?" + default = "test-project_id" +} + # See https://registry.coder.com/modules/coder/gcp-region module "gcp_region" { source = "registry.coder.com/coder/gcp-region/coder" @@ -24,7 +29,7 @@ module "gcp_region" { provider "google" { zone = module.gcp_region.value - project = "REQUIRED" + project = var.project_id } data "google_compute_default_service_account" "default" {} diff --git a/coderd/templatebuilder/testdata/gcp-windows.tf.golden b/coderd/templatebuilder/testdata/gcp-windows.tf.golden index 94e07bdac74..1cc6d2b76ad 100644 --- a/coderd/templatebuilder/testdata/gcp-windows.tf.golden +++ b/coderd/templatebuilder/testdata/gcp-windows.tf.golden @@ -11,6 +11,11 @@ terraform { provider "coder" {} +variable "project_id" { + description = "Which Google Compute Project should your workspace live in?" + default = "test-project_id" +} + # See https://registry.coder.com/modules/coder/gcp-region module "gcp_region" { source = "registry.coder.com/coder/gcp-region/coder" @@ -24,7 +29,7 @@ module "gcp_region" { provider "google" { zone = module.gcp_region.value - project = "REQUIRED" + project = var.project_id } data "coder_workspace" "me" {} diff --git a/coderd/templatebuilder/testdata/kubernetes.tf.golden b/coderd/templatebuilder/testdata/kubernetes.tf.golden index 965eb96991d..6345f4d893d 100644 --- a/coderd/templatebuilder/testdata/kubernetes.tf.golden +++ b/coderd/templatebuilder/testdata/kubernetes.tf.golden @@ -153,7 +153,7 @@ resource "coder_agent" "main" { resource "kubernetes_persistent_volume_claim_v1" "home" { metadata { name = "coder-${data.coder_workspace.me.id}-home" - namespace = "REQUIRED" + namespace = "test-namespace" labels = { "app.kubernetes.io/name" = "coder-pvc" "app.kubernetes.io/instance" = "coder-pvc-${data.coder_workspace.me.id}" @@ -188,7 +188,7 @@ resource "kubernetes_deployment_v1" "main" { wait_for_rollout = false metadata { name = "coder-${data.coder_workspace.me.id}" - namespace = "REQUIRED" + namespace = "test-namespace" labels = { "app.kubernetes.io/name" = "coder-workspace" "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"