Template build jobs

Coppice still supports the blocking POST /templates OCI import path, but hosted-builder parity needs a non-blocking shape. POST /templates/build enqueues the same coppice-import-oci.sh pipeline, returns immediately with a job id, and exposes live stdout/stderr through pollable logs plus an SSE stream.

API

Start a build:

POST /templates/build
{
"name": "fbsd14-min",
"from": "oci:quay.io/dougrabson/freebsd14-minimal"
}

Response:

202 ACCEPTED
{
"jobID": "8b5c...",
"name": "fbsd14-min",
"from": "oci:quay.io/dougrabson/freebsd14-minimal",
"status": "queued",
"createdAt": "2026-04-25T...",
"logsURL": "/templates/builds/8b5c.../logs",
"eventsURL": "/templates/builds/8b5c.../events",
"templateURL": "/templates/fbsd14-min"
}

Inspect jobs:

GET /templates/builds
GET /templates/builds/:job_id
GET /templates/builds/:job_id/logs?limit=200&since=2026-04-25T12:00:00Z
GET /templates/builds/:job_id/events

The log envelope matches the sandbox log shape:

{
"logs": [
  {
    "ts": "2026-04-25T12:00:01Z",
    "source": "template-build",
    "stream": "stderr",
    "text": "buildah pull quay.io/..."
  }
],
"truncated": false
}

The SSE endpoint emits event: log records containing one JSON-encoded log line, then a terminal event: done record containing the final job object. Final statuses are succeeded or failed; success includes snapshot and best-effort sizeBytes.

Semantics

The async path deliberately reuses the blocking import runner: buildah pull, buildah mount, rsync into zroot/jails/<name>-template, zfs snapshot @base, then a registry reload. That keeps template correctness identical between POST /templates and POST /templates/build.

Conflict handling is conservative:

The registry is in-memory today. That is sufficient for live progress and does not affect the durable artifact: the template dataset and snapshot still live in ZFS. A future build-history pass can persist the job envelope if we want records across gateway restarts.

Receipt

Cross-refs