fix(coderd): declare project_id variable in GCP template builder bases (#27015)

## Summary

GCP base templates (`gcp-linux`, `gcp-windows`) in the Template Builder
had a Terraform `variable "project_id"` with no default, but their
`base.json` manifests didn't declare it. The UI never prompted for it,
so the provisioner import always failed with:

```
required template variables need values: project_id
```

## Changes

- Add `project_id` as a required variable in both GCP `base.json`
manifests
- Convert templates from raw Terraform variable blocks to Go template
injection (`{{ .Variables.project_id }}`), matching the existing
kubernetes pattern
- Fix `DefaultBaseRenderContext` to supply a `"REQUIRED"` placeholder
for required variables without defaults (previously rendered as `<no
value>`)
- Replace duplicate test subtests with proper GCP coverage including a
missing-variable error case

<details>
<summary>Implementation plan</summary>

### Root cause

The GCP base templates contained `variable "project_id" {}` (no default
= required) in their `.tf.tmpl` files, but the `base.json` manifests had
an empty `variables` array. The Template Builder UI
(`BaseTemplateParametersStep`) is data-driven from `base.json`, so it
never showed a field for `project_id`. The composed Terraform output
still contained the required variable, causing the provisioner import to
fail.

### Fix approach

Follow the pattern established by the kubernetes base template:
1. Declare variables in `base.json` so the UI prompts for them
2. Use Go template syntax (`{{ .Variables.project_id }}`) to inject
values at compose time
3. Remove raw Terraform `variable` blocks from the template since the
value is now baked in

### Files changed

| File | Change |
|---|---|
| `bases/gcp-linux/base.json` | Added `project_id` as a required
variable |
| `bases/gcp-windows/base.json` | Added `project_id` as a required
variable |
| `bases/gcp-linux/main.tf.tmpl` | Removed Terraform variable block, use
Go template injection |
| `bases/gcp-windows/main.tf.tmpl` | Same |
| `bases.go` | `DefaultBaseRenderContext` supplies placeholder for
required vars without defaults |
| `compose_test.go` | Replaced duplicate subtests with proper GCP tests
|
| `templatebuilder_handler_test.go` | Updated `gcp-windows` spec to
expect `project_id` variable |
| Golden files | Regenerated |

</details>

> 🤖 Generated by Coder Agents on behalf of @jeremyruppel
This commit is contained in:
Jeremy Ruppel
2026-07-06 16:48:00 -04:00
committed by GitHub
parent 7b19ec3933
commit 581f906947
10 changed files with 78 additions and 24 deletions
+25 -17
View File
@@ -78,36 +78,44 @@ 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"`)
})
t.Run("AzureLinuxExtraFiles", func(t *testing.T) {
t.Parallel()
result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{
BaseTemplateID: "azure-linux",
RegistryURL: "https://registry.coder.com",
})
require.NoError(t, err)
require.Contains(t, result.ExtraFiles, "cloud-init/cloud-config.yaml.tftpl")
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) {
t.Parallel()
result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{
BaseTemplateID: "gcp-windows",
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.NotEmpty(t, result.MainTF)
require.Contains(t, string(result.MainTF), `resource "coder_agent" "main"`)
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) {
t.Parallel()
_, err := templatebuilder.Compose(templatebuilder.ComposeRequest{
BaseTemplateID: "gcp-linux",
RegistryURL: "https://registry.coder.com",
})
require.Error(t, err)
require.Contains(t, err.Error(), `variable "project_id" is required`)
})
t.Run("AzureLinuxExtraFiles", func(t *testing.T) {