feat(guests): let a guest nest OCI containers (task 0009) #35

Merged
alexion merged 1 commits from task-0009-guest-nesting into main 2026-07-25 22:31:53 -04:00
4 changed files with 90 additions and 0 deletions
Showing only changes of commit 969737b6b5 - Show all commits

View File

@@ -0,0 +1,37 @@
---
spec: guests
blocked-by: 0002-guest-walking-skeleton
---
## What to build
The Host-side placement that makes the OCI fallback a plain Guest, so image-only software has a declarative home without a separate mechanism.
A Host sets `nesting` to grant the nested-container prerequisites so the Guest's interior can run Podman and other OCI containers.
The Skeleton emits the nested-container cgroup-delegation and capability prerequisites once, so the operator flips one boolean and the interior's `oci-containers` runtime works, with Podman as the default runtime.
## Acceptance criteria
- [x] A Host setting `guests.<path>.nesting` grants the nested-container cgroup-delegation and capability prerequisites, off by default.
- [x] With `nesting` on, the Guest's interior can define `oci-containers` running Podman as the default runtime.
- [x] With `nesting` off, those prerequisites are absent and the Guest is unaffected.
- [x] A Host with a nesting Guest that defines an OCI container builds via `nix flake check`.
## Implementation Notes
- `nesting` is a Host-side placement field on the guest, a bool defaulting off, alongside the other placement fields in `lib.nix`.
On, it grants the guest's container `CAP_NET_ADMIN` and the `/dev/net/tun` and `/dev/fuse` device nodes, the capability and devices an OCI runtime reaches for to network its containers and back their overlay storage.
Off, both `additionalCapabilities` and `allowedDevices` are empty, matching the NixOS defaults, so a non-nesting guest is untouched.
- The capability prerequisite is `CAP_NET_ADMIN` alone.
A container-backend guest runs privileged (`privateUsers = "no"`), so it already retains the broad nspawn capability set including `CAP_SYS_ADMIN`; the one addition an OCI runtime needs is network administration for its bridges and firewall rules.
- cgroup delegation is not toggled by `nesting`, a deliberate deviation from the criterion's wording that the flag "grants" it and that it is "absent" when off.
The NixOS container backend sets `Delegate = true` on every container's unit unconditionally, so the delegated cgroup subtree an OCI runtime manages is always present.
Re-emitting it under `nesting` would be redundant, and forcing it off for non-nesting guests to make it literally "absent" would remove a harmless, useful default for no gain.
The Skeleton records the prerequisite as satisfied-elsewhere with an absence pointer comment, so a reader does not think delegation was forgotten.
- A new `guests/nesting-sample.nix` carries an interior that defines an `oci-containers` workload, the payload the criteria exercise, and `neogaia` enables it with `nesting = true`.
This follows the walking-skeleton's precedent of proving a guest path through the one Host's `nix flake check`: the flake check builds the nested `nixos-system-nesting-sample` in full, pulling in `podman` and the generated `podman-hello.service` unit, which is how criteria two and four are verified on the build seam.
The sample carries the same modest caps as the walking-skeleton guest so an interior container cannot starve the laptop.
- The two behaviors the build seam cannot prove — that the interior Podman actually starts a container and that its networking works — are left to manual verification on the target Host and to the VM integration test of task 0010, per the spec's testing decisions.

11
guests/nesting-sample.nix Normal file
View File

@@ -0,0 +1,11 @@
args@{ my, ... }:
# A sample guest whose interior runs an OCI container on Podman.
# The image is pulled at runtime, so the guest builds with no build-time fetch.
my.guest {
name = "nesting-sample";
interior = {
virtualisation.oci-containers.containers.hello = {
image = "docker.io/library/hello-world";
};
};
} args

View File

@@ -54,6 +54,16 @@
tasksMax = 512; tasksMax = 512;
}; };
# The nesting guest, run with `nesting` on: proves an interior OCI container
# on Podman builds end to end through this host's `nix flake check`.
guests.nesting-sample.enable = true;
guests.nesting-sample.nesting = true;
guests.nesting-sample.limits = {
memory = "1G";
cpu = "100%";
tasksMax = 512;
};
modules.agents.claude-code.enable = true; modules.agents.claude-code.enable = true;
modules.agents.tools.gitea-axi.enable = true; modules.agents.tools.gitea-axi.enable = true;
modules.agents.pi.enable = true; modules.agents.pi.enable = true;

32
lib.nix
View File

@@ -279,6 +279,19 @@ let
''; '';
}; };
}; };
nesting = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Grant the guest's interior the prerequisites to run Podman or other
OCI containers of its own. Off by default, so a guest cannot nest
containers. On, the guest's container gains the network-administration
capability its container runtime uses to build bridges and firewall
rules, along with the tun and fuse device nodes such a runtime reaches
for, so the interior's `virtualisation.oci-containers` works with
Podman as its default runtime.
'';
};
autoStart = lib.mkOption { autoStart = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
@@ -335,6 +348,25 @@ let
# A private-user mapping would shift the ids and reintroduce those errors, so it stays off. # A private-user mapping would shift the ids and reintroduce those errors, so it stays off.
privateUsers = lib.mkDefault "no"; privateUsers = lib.mkDefault "no";
# A nesting guest runs Podman or other OCI containers in its interior.
# The network-administration capability lets that runtime build its
# bridges and firewall rules.
# The tun and fuse device nodes are what it reaches for to network
# those containers and back their overlay storage.
# The remaining prerequisite, a delegated cgroup subtree for the
# runtime to manage, the container backend already grants every guest.
additionalCapabilities = lib.optionals cfg.nesting [ "CAP_NET_ADMIN" ];
allowedDevices = lib.optionals cfg.nesting [
{
node = "/dev/net/tun";
modifier = "rwm";
}
{
node = "/dev/fuse";
modifier = "rwm";
}
];
bindMounts = userMounts // secretMounts; bindMounts = userMounts // secretMounts;
inherit specialArgs; inherit specialArgs;