Files
bootapi/docs/template-authoring.md
T
unkin-agent a414918350
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Fetch templates over HTTP instead of shelling out to git
The runtime image is distroless and has no git binary, so every sync
failed and bootapi silently served the stale embedded templates.

- fetch the branch tarball (<repo>/archive/<branch>.tar.gz) and extract
  it into an in-memory FS; no checkout, no writable volume
- digest the extracted tree, not the archive bytes, so a recompressed
  identical archive is not a change
- skip entries that would escape the tree
- log the source commit from Gitea's immutable Link header
2026-09-26 18:59:07 +10:00

5.6 KiB

Authoring templates

bootapi ships an embedded default set and lets you override or extend it.

Where templates live

  • Embedded defaults: templates/kickstart/*.ks.tmpl, templates/ipxe/*.ipxe.tmpl and templates/catalog/*.yaml, compiled into the binary (templates/embed.go). These are the always-available startup fallback.
  • Template git repo (preferred in prod): BOOTAPI_TEMPLATE_GIT_URL. bootapi fetches the branch tarball (<repo>/archive/<branch>.tar.gz) over HTTP at startup and re-fetches every BOOTAPI_TEMPLATE_GIT_INTERVAL (default 3m, like argocd), atomically swapping the loaded set on change. A parse failure keeps the last-good set and is only logged + counted (bootapi_template_sync_failures_total), so a bad push can't take bootapi down. If the repo is unreachable at startup, bootapi runs on the embedded defaults. The repo is unkin/bootapi-templates (seeded from these embedded files) and has its own CI validating templates + catalog.
  • Override directory: BOOTAPI_TEMPLATE_DIR (a ConfigMap mount), used only when no git URL is set. Files there override embedded ones by base name.

In all cases the embedded defaults are the base layer; the git repo / override dir is layered on top, replacing files of the same base name and adding new ones.

Naming

  • Kickstart: <name>.ks.tmpl → registered as template <name>.
  • iPXE: <name>.ipxe.tmpl → registered as template <name>.

<name> is what template selection matches against (platform slug, OS family, provision_template, or the configured default — see data-model.md).

Reserved iPXE names bootapi renders directly:

  • boot — the per-host boot script (/ipxe/{mac} for a known host).
  • fallback-local, fallback-shell — unknown-MAC fallbacks.

The distro catalog

catalog/*.yaml describes each bootable OS, so adding a distro is a YAML + template change (and, if needed, a new artifactapi remote) — no bootapi code change. One file per distro:

name: almalinux9              # catalog key; also what provision_template matches
match:
  platforms: [almalinux9]     # exact NetBox platform slugs
  family: almalinux           # OR an OS family (matches almalinux8/9/...)
kickstart: almalinux9         # kickstart template name to render
version_default: "9"          # used when the platform slug carries no version
kernel_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/vmlinuz"
initrd_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/initrd.img"
kernel_args: [inst.text, net.ifnames=0]
vars:                         # arbitrary templated strings -> .DistroVars.<key>
  mirror: "{{.ArtifactBase}}/almalinux/{{.Version}}"

kernel_url, initrd_url and each vars value are Go templates rendered with {{.ArtifactBase}} (BOOTAPI_ARTIFACT_BASE_URL), {{.Version}}, {{.Arch}}, {{.Hostname}}, {{.Platform}}, {{.OSFamily}}. The kickstart template reads .DistroVars.mirror to build its url/repo lines, so the install-tree layout lives entirely in the catalog. bootapi derives inst.repo for iPXE by trimming /images/pxeboot/vmlinuz off kernel_url.

Shipped entries: almalinux9 (artifactapi almalinux remote) and fedora (fedora remote). debian / talos are documented but not implemented — their artifact shapes differ (Debian netboot linux+initrd.gz under dists/<rel>/main/installer-<arch>/current/images/netboot/; Talos ships factory vmlinuz+initramfs.xz images) so they need their own catalog fields/template and possibly a new artifactapi remote. See the catalog README in the templates repo for the intended path.

Engine and functions

Standard Go text/template. Available funcs: join, upper, lower, default ({{ default "x" .Maybe }} → .Maybe unless empty). The data model is in data-model.md.

Example network stanza (iterate interfaces, skip those without an IP, set the hostname on the primary):

{{- $primary := .PrimaryInterface }}
{{- range .Interfaces }}
{{- if .IP }}
network --bootproto=static --device={{ .MAC }} --ip={{ .IP }} --netmask={{ .Netmask }}{{ if .Gateway }} --gateway={{ .Gateway }}{{ end }}{{ range $.Nameservers }} --nameserver={{ . }}{{ end }}{{ if and $primary (eq .MAC $primary.MAC) }} --hostname={{ $.FQDN }}{{ end }} --activate
{{- end }}
{{- end }}

What the default AlmaLinux template does (ported from Cobbler)

The literal .ks bodies from the old Cobbler server are not in version control (they lived in /var/lib/cobbler/{templates,snippets} on the Cobbler host). The embedded almalinux9.ks.tmpl reproduces the contract that estate relied on:

  • static per-interface networking from NetBox, hostname on the primary NIC;
  • rootpw --iscrypted from the render-time hash (Cobbler's default_password_crypted), or --lock when unset;
  • a minimal package set + openssh-server, chrony;
  • a %post that installs the Puppet agent, points it at puppet.query.consul / puppetca.query.consul, and enables it — handing off to the existing profiles::firstrun Puppet bootstrap and autosign, exactly as the Cobbler kickstart did.

Adjust partitioning, package sets and repos to taste; keep the puppet %post handoff so a freshly-installed host still checks in and converges.

Testing a template locally

BOOTAPI_NETBOX_URL=... BOOTAPI_NETBOX_TOKEN=... \
BOOTAPI_BASE_URL=http://localhost:8000 \
BOOTAPI_BOOT_BASE_URL=http://mirror/almalinux/9 \
BOOTAPI_TEMPLATE_DIR=./mytemplates ./bin/bootapi &
curl -s localhost:8000/ks/web01        # rendered kickstart
curl -s localhost:8000/ipxe/aa:bb:cc:00:11:22