8f356346eb
Implements the six review comments on PR #1: - Per-host PXE-enable gate: read NetBox pxe_enabled custom field; a known host with it false gets the safe local-boot script (Cobbler netboot_enabled). Add a token-guarded POST /provisioned/{ident} callback that clears pxe_enabled in NetBox, plus a %post snippet in the default kickstarts that calls it. - Templates from a git repo: bootapi clones a templates repo and re-pulls every BOOTAPI_TEMPLATE_GIT_INTERVAL (default 3m), atomically swapping the template set (last-good kept on parse failure; embedded defaults are the startup fallback). Metrics for syncs/failures/generation. - Distro catalog (catalog/*.yaml): NetBox host -> boot images/kickstart, so adding an OS is a YAML + template change. Ships almalinux + fedora entries (artifactapi remotes); debian/talos path documented. - Boot images from the artifactapi almalinux/fedora remotes via the catalog. - Bind resolvers, puppet server/CA and PUPPETCA_URL env file now target the k8s services (198.18.200.7; puppet(ca).k8s.syd1.au.unkin.net). - Boot path served over plain HTTP (installers lack CA trust) with an optional parallel HTTPS listener; docs say do not 301 the boot endpoints. New packages: internal/catalog, internal/gitsync. NetBox client gains a pxe_enabled write (token needs that scope - noted in docs). `bootapi validate` subcommand validates a template/catalog set for the templates-repo CI. go build/vet clean, go test -race green, golangci-lint v2 clean, pre-commit clean. Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
108 lines
5.9 KiB
Markdown
108 lines
5.9 KiB
Markdown
# Deploying bootapi
|
||
|
||
> The actual argocd-apps deployment is a **follow-up task** and is intentionally
|
||
> not part of this repo. This document is the spec for that follow-up plus the
|
||
> DHCP change the estate needs.
|
||
|
||
bootapi is a stateless HTTP service. It mirrors encapi's deployment shape: a Go
|
||
binary in a distroless image, config from env, secrets from Vault via the Vault
|
||
Secrets Operator (VSO).
|
||
|
||
## Container image
|
||
|
||
`git.unkin.net/unkin/bootapi:<tag>` (built + pushed by `.woodpecker/docker.yaml`
|
||
on a `v*` tag). Also mirror to the artifactapi local docker registry if desired.
|
||
|
||
## Kubernetes wiring (argocd-apps follow-up)
|
||
|
||
Create `apps/base/bootapi/` following the argocd-apps `AGENTS.md` pattern:
|
||
|
||
1. **namespace** `bootapi`.
|
||
2. **VaultAuth** (`default`) — kubernetes method, mount `k8s/au/syd1`, role
|
||
`default`, SA `default` (copy netbox's `vaultauth.yaml`).
|
||
3. **VaultStaticSecret** → k8s Secret `bootapi-secrets`, from Vault kv path
|
||
`kubernetes/namespace/bootapi/default/bootapi-secrets` with keys:
|
||
- `netbox_token` — a **dedicated** NetBox API token for bootapi. It needs
|
||
**read on devices/interfaces/ip-addresses AND write on the device
|
||
`pxe_enabled` custom field** (the provisioned callback PATCHes it — see
|
||
[security.md](security.md#netbox-write-scope)). Create a `bootapi` NetBox
|
||
user/token via terraform-netbox rather than reusing the seeded superuser
|
||
token at `kv/kubernetes/namespace/netbox/default/netbox-superuser`.
|
||
- `provision_token` — the shared bearer token guarding `POST /provisioned`
|
||
(also embedded in rendered kickstarts). Generate a random value.
|
||
- `root_password_hash` — crypt(3) hash for the installed root account
|
||
(the successor to Cobbler's eyaml `default_password_crypted`).
|
||
- `ssh_authorized_keys` — optional, newline-separated.
|
||
4. **Templates**: prefer git-sync — set `BOOTAPI_TEMPLATE_GIT_URL` to
|
||
`https://git.unkin.net/unkin/bootapi-templates.git` (public; no token needed)
|
||
and bootapi pulls it every `BOOTAPI_TEMPLATE_GIT_INTERVAL` (default 3m). No
|
||
ConfigMap or pod restart is needed to change templates — merge to the repo's
|
||
`main` and bootapi reloads within the interval (last-good kept on a bad push).
|
||
The embedded defaults remain the fallback if the repo is unreachable. (A
|
||
`BOOTAPI_TEMPLATE_DIR` ConfigMap is still supported for air-gapped installs.)
|
||
5. **Deployment** — image above, env from `config.example.env`, secret keys wired
|
||
as `BOOTAPI_NETBOX_TOKEN_FILE` / `BOOTAPI_PROVISION_TOKEN_FILE` /
|
||
`BOOTAPI_ROOT_PASSWORD_HASH_FILE` (mount the Secret). Least-privilege
|
||
securityContext (`runAsNonRoot`, `drop: [all]`). Baseline resources: requests
|
||
`512Mi`/`1`, limits `2Gi`/`2` cpu. The pod needs `git` on PATH for template
|
||
sync (the distroless image includes only the static binary — either add a git
|
||
layer, use an initContainer that seeds the checkout, or fall back to a
|
||
ConfigMap; simplest is a small alpine+git base for this service).
|
||
6. **Service + exposure**: see the Gateway section below.
|
||
7. Register in `argocd/applicationsets/platform.yaml` (`apps/overlays/*/bootapi`)
|
||
and the platform AppProject destinations.
|
||
|
||
### Gateway: HTTP and HTTPS
|
||
|
||
PXE installers do **not** trust the internal CA, so the boot path must be served
|
||
over **plain HTTP**. Unlike the estate default, the bootapi HTTPRoute must **not
|
||
blanket-301 HTTP→HTTPS**:
|
||
|
||
- A **plain-HTTP** listener/HTTPRoute (or a LoadBalancer Service on port 80→8000)
|
||
reachable by PXE clients at a stable address/hostname on the PXE VLAN — this is
|
||
the `BOOTAPI_BASE_URL` DHCP/iPXE points at. No redirect.
|
||
- Optionally an **HTTPS** HTTPRoute for humans/tooling that do trust the CA
|
||
(bootapi can serve TLS directly via `BOOTAPI_TLS_*`, or terminate at the
|
||
gateway). This is additive; it must not replace or redirect the HTTP boot path.
|
||
|
||
The end-of-kickstart callback (`POST /provisioned`) runs over the same plain-HTTP
|
||
base by default (the token authenticates it; the install has no CA trust yet). If
|
||
you install the internal CA early in `%post`, you may set
|
||
`BOOTAPI_CALLBACK_BASE_URL` to the HTTPS URL instead.
|
||
|
||
### Cross-repo dependencies (per estate conventions)
|
||
|
||
- **terraform-git**: `unkin/bootapi-templates` repo (this PR's sibling) holds the
|
||
live template set + distro catalog + validation CI.
|
||
- **argocd-apps**: add a `serviceaccount_*` under `apps/base/woodpecker/` if the
|
||
bootapi pipelines need a dedicated SA (they use `default` today).
|
||
- **terraform-vault**: add the k8s auth role + kv policy granting the `bootapi`
|
||
namespace read on `kv/kubernetes/namespace/bootapi/default/*`.
|
||
- **terraform-netbox**: create the `bootapi` NetBox token (read + write on the
|
||
`pxe_enabled` device custom field) and seed it, `provision_token` and
|
||
`root_password_hash` into the Vault kv path above. Also define the `pxe_enabled`
|
||
boolean custom field on the Device model.
|
||
|
||
## DHCP change (the cutover)
|
||
|
||
Cobbler advertised itself at anycast `198.18.19.19` as the DHCP `next-server`,
|
||
with `filename "/ipxe.efi"` (UEFI arch 7/9) or `/undionly.kpxe` (BIOS arch 0).
|
||
Today those are set in `puppet-prod` hieradata
|
||
`hieradata/roles/infra/dhcp/server.yaml` (`pools.*.pxeserver` and the
|
||
`UEFI-64`/`Legacy` dhcp classes).
|
||
|
||
To cut a subnet over to bootapi, repoint DHCP for that pool:
|
||
|
||
- `next-server` → bootapi's LB IP (or keep the `198.18.19.19` anycast and move
|
||
the anycast advertisement to bootapi's node/LB).
|
||
- `filename` → the iPXE binary as before (`/ipxe.efi` / `/undionly.kpxe`); bootapi
|
||
does not serve the NBP itself. The chained iPXE must then be told to fetch
|
||
bootapi's script — either bake `chain http://<bootapi>/ipxe/${net0/mac}` into
|
||
the site iPXE binary/embedded script, or set DHCP option 67 to that URL for
|
||
iPXE user-class requests. This replaces Cobbler's
|
||
`chain http://${next-server}/cblr/svc/op/gpxe/mac/${net0/mac}`.
|
||
|
||
Roll one pool at a time (the PXE subnets are `198.18.13.0/24`–`198.18.17.0/24`);
|
||
Puppet autosign already trusts those subnets and `*.main.unkin.net`, so a host
|
||
installed via bootapi checks in exactly as before.
|