Adds `POST /api/v2/templatebuilder/compose/template`, a synchronous
endpoint that composes a template from a base and modules, validates it
via a provisioner import job, and creates the template in a single
request.
The handler composes terraform files, bundles them as a tar, inserts the
file with hash-based dedup, creates a template version with an import
job, waits up to 2 minutes for the job to complete, classifies errors
for known failure modes (network-unreachable registry, DNS failures),
then creates the template on success. Canceled and failed jobs return
appropriate error responses.
Also adds `hclwrite.Format` to composed terraform output for canonical
HCL formatting.
Closes https://linear.app/codercom/issue/DEVEX-279
<details>
<summary>Implementation notes</summary>
- SDK types and client method in `codersdk/templatebuilder.go` with
validation tags matching the standard template creation path
(`template_display_name`, `lt=128`)
- `ClassifyProvisionerError` in `coderd/templatebuilder/errors.go`
detects DNS, connection refused, i/o timeout, and TLS handshake failures
and returns actionable messages
- `waitForProvisionerJob` polls with a ramp-up interval schedule (100ms,
200ms, 500ms, then 1s steady) and accepts an `onUpdate` callback for
future SSE streaming
- Audit logging for both template and template version creation
- TOCTOU name uniqueness: early check for fast feedback, DB unique
constraint catch for the race window (returns 409, not 500)
- Swagger annotations for all error responses (400, 404, 409, 504)
</details>
> 🤖 Generated by Coder Agents
Validates caller-supplied module variable keys and values in the
template builder compose endpoint before template rendering. Previously,
`mergeModuleVariables` accepted any caller-supplied key and value
without validation, allowing unknown keys, computed/sensitive variable
overrides, and malformed HCL literals (including injection payloads) to
pass through to rendered output.
Now `mergeModuleVariables` rejects unknown keys (those not in the
manifest's non-computed, non-sensitive variables) and type-checks
values: strings must be quoted HCL literals without interpolation
markers or unescaped newlines, numbers must be strict numeric literals,
and bools must be exactly `true` or `false`. The literal `null` is
accepted for any type.
Closes https://linear.app/codercom/issue/DEVEX-278
<details>
<summary>Implementation details</summary>
- Changed `mergeModuleVariables` signature from `map[string]string` to
`(map[string]string, error)` to surface validation failures
- Added `validateVariableValue`, `validateStringValue`,
`validateNumberValue`, `validateBoolValue` in `compose.go`
- String validation rejects: unquoted values, HCL interpolation (`${`,
`%{`), unescaped newlines/quotes, trailing backslashes (which would
escape the closing delimiter), and values exceeding 4096 bytes
- Errors wrap the module ID and variable name for clear diagnostics
(e.g. `module "code-server": variable "port": invalid number value`)
- Tests cover key validation, type validation, injection attempts, and
full Compose flow integration
> Generated with the help of [Coder Agents](https://coder.com) by
@jeremyruppel
</details>
> [!NOTE]
> This PR was authored by Coder Agents on behalf of @jeremyruppel.
Part 2 of DEVEX-277 (POST /api/v2/templatebuilder/compose).
Adds the core composition and bundling logic for the template builder.
`Compose` renders a base template and selected modules into Terraform source files. It validates modules before rendering (rejects duplicates, ConflictsWith violations, unknown IDs, OS incompatibility), then for each module merges manifest defaults with caller-supplied variable overrides and renders the module template.
`mergeModuleVariables` fills in defaults for non-computed, non-sensitive variables from the manifest (with basic JSON type validation via `isSimpleJSONValue`), uses `null` for non-required variables without defaults, and leaves required variables absent so `missingkey=error` catches omissions at render time.
`BundleTar` packages the result into a tar archive with reproducible timestamps. Writes `main.tf` always, `modules.tf` only when modules are present.
Conflict detection is bidirectional so module ordering in the request does not affect validation.