feat(nix): first-class container-runner support — services.lab.container.* #219

Merged
dominik.polakovics merged 1 commit from afk/218 into main 2026-07-23 19:45:08 +02:00

Closes #218

A services.lab.container submodule giving container-runner deployments (ADR-0052/ADR-0053) first-class NixOS options instead of hand-wired extraFlags / extraPackages / raw serviceConfig overrides. Implemented to the grilled contract in the issue body; DoD is every acceptance criterion except the maintainer's landing gate.

Option surface

container.enable (explicit switch gating all host mutations — never option non-emptiness), container.toolsImages (attrsOf nonEmptyStr keyed by provider ID), container.defaultImage (nullable; per-repo image_ref alone is valid, ADR-0053), container.subIdRange (default { start = 100000; count = 65536; }, nullable opt-out, overridable on collision).

What enabling does

  • Renders --container-tools-image provider=ref[,provider=ref…] (name-sorted comma-join) and --container-image into ExecStart before extraFlags, so a hand-rolled migration flag still wins (Go flag parser takes the last occurrence).
  • virtualisation.podman.enable = true (crun + policy.json/registries.conf via virtualisation.containers), podman + passt on the unit PATH (what preflight probes and the pane argv resolves against).
  • Delegate = true; RuntimeDirectory=lab + RuntimeDirectoryPreserve=yes (+ mode 0700) + XDG_RUNTIME_DIR=/run/lab — Preserve is load-bearing next to KillMode=process: containers that survive a lab restart must not have the runtime dir wiped underneath them.
  • sub{Uid,Gid}Ranges on users.users.<user> via lib.mkMerge, so the ranges land on operator-brought users too.
  • Eval assertion: container.enable with empty toolsImages fails at nixos-rebuild eval (seedUser precedent). Deliberately no defaultImage assertion, no provider-ID key validation, no digest-pin enforcement.

Verification (all run locally, green)

  • Zero-diff criterion: unit text for a container.enable = false config rendered from b59ae43:nix/module.nix vs this branch, identical pinned config/package — byte-identical (old == new in one nix eval). Also made permanent: the nixos-module check now greps the disabled dummy's unit for the absence of --container*, Delegate=, RuntimeDirectory*, and XDG_RUNTIME_DIR.
  • nixos-module check extended: container-enabled unit greps (comma-joined tools flag with name-sorted order, --container-image, Delegate=true, RuntimeDirectory=lab + Preserve=yes + Mode=0700, XDG_RUNTIME_DIR=/run/lab, podman + passt store paths on PATH); eval-asserts for the empty-toolsImages assertion message, defaultImage = null (tools flag renders, image flag doesn't), subIdRange default/custom/null/disabled, and virtualisation.podman.enable on/off. Mutation-tested: perturbing the dummy's defaultImage fails the check.
  • New nixos-container-closure check builds a full container-enabled NixOS system closure with dummy refs (tmpfs root, no bootloader, no real agent CLIs) — option typos and assertion regressions fail CI without booting anything. Its own attr so the eval/grep check stays cheap.
  • Full nix flake check green.

Docs

docs/ops.md: option-table rows for container.*; "What the host must provide" now leads with the one-block NixOS config (bullets kept as the cross-distro/preflight substance + by-hand guidance); RuntimeDirectoryPreserve rationale; stateDir sizing note for the rootless image store under <state>/.local/share/containers plus a matching backup-exclusion bullet (digest-pinned, re-pulled on demand); CI disk note mentions the new closure check.

Maintainer landing gate (per the issue, not in this PR's DoD): validate on a real host — preflight green after one rebuild, a container-runner repo spawns, and running containers survive a lab restart with their runtime dir intact.

🤖 Generated with Claude Code

Closes #218 A `services.lab.container` submodule giving container-runner deployments (ADR-0052/ADR-0053) first-class NixOS options instead of hand-wired `extraFlags` / `extraPackages` / raw `serviceConfig` overrides. Implemented to the grilled contract in the issue body; DoD is every acceptance criterion except the maintainer's landing gate. ## Option surface `container.enable` (explicit switch gating **all** host mutations — never option non-emptiness), `container.toolsImages` (`attrsOf nonEmptyStr` keyed by provider ID), `container.defaultImage` (nullable; per-repo `image_ref` alone is valid, ADR-0053), `container.subIdRange` (default `{ start = 100000; count = 65536; }`, nullable opt-out, overridable on collision). ## What enabling does - Renders `--container-tools-image provider=ref[,provider=ref…]` (name-sorted comma-join) and `--container-image` into ExecStart **before** `extraFlags`, so a hand-rolled migration flag still wins (Go flag parser takes the last occurrence). - `virtualisation.podman.enable = true` (crun + policy.json/registries.conf via `virtualisation.containers`), podman + passt on the unit PATH (what preflight probes and the pane argv resolves against). - `Delegate = true`; `RuntimeDirectory=lab` + `RuntimeDirectoryPreserve=yes` (+ mode 0700) + `XDG_RUNTIME_DIR=/run/lab` — Preserve is load-bearing next to `KillMode=process`: containers that survive a lab restart must not have the runtime dir wiped underneath them. - `sub{Uid,Gid}Ranges` on `users.users.<user>` via `lib.mkMerge`, so the ranges land on operator-brought users too. - Eval assertion: `container.enable` with empty `toolsImages` fails at `nixos-rebuild` eval (seedUser precedent). Deliberately **no** `defaultImage` assertion, no provider-ID key validation, no digest-pin enforcement. ## Verification (all run locally, green) - **Zero-diff criterion**: unit text for a `container.enable = false` config rendered from `b59ae43:nix/module.nix` vs this branch, identical pinned config/package — byte-identical (`old == new` in one nix eval). Also made permanent: the `nixos-module` check now greps the disabled dummy's unit for the absence of `--container*`, `Delegate=`, `RuntimeDirectory*`, and `XDG_RUNTIME_DIR`. - **`nixos-module` check extended**: container-enabled unit greps (comma-joined tools flag with name-sorted order, `--container-image`, `Delegate=true`, `RuntimeDirectory=lab` + `Preserve=yes` + `Mode=0700`, `XDG_RUNTIME_DIR=/run/lab`, podman + passt store paths on PATH); eval-asserts for the empty-`toolsImages` assertion message, `defaultImage = null` (tools flag renders, image flag doesn't), `subIdRange` default/custom/null/disabled, and `virtualisation.podman.enable` on/off. Mutation-tested: perturbing the dummy's `defaultImage` fails the check. - **New `nixos-container-closure` check** builds a full container-enabled NixOS system closure with dummy refs (tmpfs root, no bootloader, no real agent CLIs) — option typos and assertion regressions fail CI without booting anything. Its own attr so the eval/grep check stays cheap. - Full `nix flake check` green. ## Docs docs/ops.md: option-table rows for `container.*`; "What the host must provide" now leads with the one-block NixOS config (bullets kept as the cross-distro/preflight substance + by-hand guidance); `RuntimeDirectoryPreserve` rationale; stateDir sizing note for the rootless image store under `<state>/.local/share/containers` plus a matching backup-exclusion bullet (digest-pinned, re-pulled on demand); CI disk note mentions the new closure check. **Maintainer landing gate (per the issue, not in this PR's DoD):** validate on a real host — preflight green after one rebuild, a container-runner repo spawns, and running containers survive a lab restart with their runtime dir intact. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(nix): first-class container-runner support — services.lab.container.*
All checks were successful
ci / native (pull_request) Successful in 8m55s
ci-nix / flake-check (pull_request) Successful in 19m15s
b3e3ed8ded
Add a services.lab.container submodule so container-runner deployments
(ADR-0052/ADR-0053) stop hand-wiring extraFlags, Delegate=yes, unit PATH
packages, subuid/subgid ranges, and the rootless runtime dir from
docs/ops.md:

- Options: container.enable (gates ALL host mutations, never option
  non-emptiness), toolsImages (attrsOf nonEmptyStr keyed by provider ID),
  defaultImage (nullable — per-repo image_ref alone is valid), subIdRange
  (default 100000/65536, nullable opt-out, overridable on collision).
- Flags: --container-tools-image provider=ref[,provider=ref…] (name-sorted
  comma-join) and --container-image, rendered before extraFlags so a
  hand-rolled migration flag still wins.
- Provisioning under enable: virtualisation.podman.enable, podman+passt on
  the unit PATH, Delegate=true, RuntimeDirectory=lab with
  RuntimeDirectoryPreserve=yes (+0700) and XDG_RUNTIME_DIR=/run/lab —
  Preserve is load-bearing next to KillMode=process so podman-attached
  containers survive a lab restart — and sub{Uid,Gid}Ranges on the service
  user (merges onto operator-brought users too).
- Eval assertion: container.enable with empty toolsImages fails at
  nixos-rebuild eval (seedUser precedent). Deliberately no defaultImage
  assertion, no provider-ID key validation, no digest-pin enforcement.
- Checks: nixos-module gains container-enabled unit greps, subIdRange /
  defaultImage / assertion eval-asserts, and zero-diff guards pinning that
  a container-disabled unit stays byte-identical to today's output (proven
  against b59ae43 with a pinned package); new nixos-container-closure
  check builds a container-enabled NixOS system closure with dummy refs.
- docs/ops.md: NixOS guidance collapses to services.lab.container.*;
  stateDir sizing + backup-exclusion notes for the rootless image store
  under <state>/.local/share/containers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

Clean PASS. Both CI gates green (ci/native success; ci-nix/flake-check success in 19m15s — ran the full nix flake check including the extended nixos-module greps/asserts and the new nixos-container-closure full-system build). Title is Conventional Commits; Closes #218 present and matches head afk/218. Diff scope is exactly module.nix + flake.nix + docs/ops.md, each change mapping to an issue-#218 acceptance criterion with no drive-by edits. main is a direct ancestor of the head, so no conflict.

[autoland] verdict: pass Clean PASS. Both CI gates green (ci/native success; ci-nix/flake-check success in 19m15s — ran the full nix flake check including the extended nixos-module greps/asserts and the new nixos-container-closure full-system build). Title is Conventional Commits; Closes #218 present and matches head afk/218. Diff scope is exactly module.nix + flake.nix + docs/ops.md, each change mapping to an issue-#218 acceptance criterion with no drive-by edits. main is a direct ancestor of the head, so no conflict.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!219
No description provided.