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
+111
View File
@@ -7394,6 +7394,69 @@ const docTemplate = `{
]
}
},
"/api/v2/templatebuilder/compose/template": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"TemplateBuilder"
],
"summary": "Compose and create a template",
"operationId": "compose-and-create-a-template",
"parameters": [
{
"description": "Create template request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.TemplateBuilderCreateTemplateRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/codersdk.TemplateBuilderCreateTemplateResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
},
"504": {
"description": "Gateway Timeout",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
}
},
"security": [
{
"CoderSessionToken": []
}
]
}
},
"/api/v2/templatebuilder/modules": {
"get": {
"produces": [
@@ -23761,6 +23824,54 @@ const docTemplate = `{
}
}
},
"codersdk.TemplateBuilderCreateTemplateRequest": {
"type": "object",
"required": [
"name",
"organization_id"
],
"properties": {
"base_template_id": {
"type": "string"
},
"description": {
"type": "string"
},
"display_name": {
"type": "string"
},
"icon": {
"type": "string"
},
"modules": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateBuilderComposeModule"
}
},
"name": {
"type": "string"
},
"organization_id": {
"type": "string",
"format": "uuid"
},
"provisioner_tags": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"codersdk.TemplateBuilderCreateTemplateResponse": {
"type": "object",
"properties": {
"template": {
"$ref": "#/definitions/codersdk.Template"
}
}
},
"codersdk.TemplateBuilderModule": {
"type": "object",
"properties": {
+102
View File
@@ -6557,6 +6557,63 @@
]
}
},
"/api/v2/templatebuilder/compose/template": {
"post": {
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["TemplateBuilder"],
"summary": "Compose and create a template",
"operationId": "compose-and-create-a-template",
"parameters": [
{
"description": "Create template request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.TemplateBuilderCreateTemplateRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/codersdk.TemplateBuilderCreateTemplateResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
},
"504": {
"description": "Gateway Timeout",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
}
},
"security": [
{
"CoderSessionToken": []
}
]
}
},
"/api/v2/templatebuilder/modules": {
"get": {
"produces": ["application/json"],
@@ -21817,6 +21874,51 @@
}
}
},
"codersdk.TemplateBuilderCreateTemplateRequest": {
"type": "object",
"required": ["name", "organization_id"],
"properties": {
"base_template_id": {
"type": "string"
},
"description": {
"type": "string"
},
"display_name": {
"type": "string"
},
"icon": {
"type": "string"
},
"modules": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateBuilderComposeModule"
}
},
"name": {
"type": "string"
},
"organization_id": {
"type": "string",
"format": "uuid"
},
"provisioner_tags": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"codersdk.TemplateBuilderCreateTemplateResponse": {
"type": "object",
"properties": {
"template": {
"$ref": "#/definitions/codersdk.Template"
}
}
},
"codersdk.TemplateBuilderModule": {
"type": "object",
"properties": {