feat: add TemplateBuilderCreateTemplate SDK types and client method (#26360)

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
This commit is contained in:
Jeremy Ruppel
2026-06-15 18:12:55 -04:00
committed by GitHub
parent d8b76831ff
commit de31c7c18e
12 changed files with 1074 additions and 4 deletions
+13 -3
View File
@@ -8,6 +8,7 @@ import (
"strings"
"time"
"github.com/hashicorp/hcl/v2/hclwrite"
"golang.org/x/xerrors"
)
@@ -54,7 +55,7 @@ func Compose(req ComposeRequest) (*ComposeResult, error) {
}
if len(req.Modules) == 0 {
return &ComposeResult{MainTF: mainTF}, nil
return &ComposeResult{MainTF: formatHCL(mainTF)}, nil
}
agentName, err := ExtractAgentResourceName(mainTF)
@@ -78,11 +79,20 @@ func Compose(req ComposeRequest) (*ComposeResult, error) {
}
return &ComposeResult{
MainTF: mainTF,
ModulesTF: modulesTF,
MainTF: formatHCL(mainTF),
ModulesTF: formatHCL(modulesTF),
}, nil
}
// formatHCL applies canonical HCL formatting to src. If src is not valid
// HCL the input is returned unchanged.
func formatHCL(src []byte) []byte {
if len(src) == 0 {
return src
}
return hclwrite.Format(src)
}
// renderBase renders the base template for the given example ID.
func renderBase(baseTemplateID string) ([]byte, error) {
renderCtx := DefaultBaseRenderContext(baseTemplateID)