Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/jera/.nornir/container-spec.md

jera container — the ContainerSpec & its run seam

Class: design · strict "what it IS". Sibling: jobs records that a container ran; this module (jera::container) is how one is described and routed THROUGH draupnir's ONE OCI engine.

f0 — the whole

A ContainerSpec is the plain-serde descriptor a caller (korp, a server RPC, Skidbladnir's container route) hands jera to bring a container up. jera keeps only the policy + descriptor; the single bollard/podman REST engine for the whole constellation lives in draupnir (draupnir::container). Two shapes both delegate to draupnir: a long-lived boot ([DraupnirContainerBootRunner]) and a run-to- completion job ([run_container]). There is no second engine here.

f1 — the descriptor (the run overrides)

ContainerSpec{ image, name, cmd, env, ports, net, mounts }. image is the OCI ref; name empty ⇒ a fresh jera-boot-<uuid> (a kill addresses the exact container); cmd/env/ports are the entrypoint / environment / published-port overrides. All fields are #[serde(default)] + skip-when-empty, so a spec built by struct-literal (ContainerSpec { image, ..Default::default() }) or from an older wire form is byte-identical — every knob is additive.

f2 — net mode (the airgap knob) — wired to the live run

net: NetMode — Default | None | Host | Bridge. NetMode::None renders --network none: no network namespace, no egress — the airgap isolation a routed job must not lose. [ContainerSpec::network_arg] returns the --network <mode> arg (None for the engine Default, so an existing NAT spec emits no flag and stays byte-identical); [NetMode::oci_value] is the value draupnir's engine sets on the container HostConfig.network_mode.

Live-engine wire (landed). to_draupnir now threads the net choice onto the draupnir::BootSpec both jera legs consume: d.net = draupnir::NetMode::from_oci_value(self.net.oci_value()). draupnir reads spec.net in both boot paths (Boot::boot and ContainerBoot::run_to_completion_with_binds), setting HostConfig.network_mode, so a routed NetMode::None spec produces a real --network none airgap container at the live run. NetMode::Default ⇒ from_oci_value(None) ⇒ draupnir Default ⇒ no --network flag ⇒ byte-identical create body (an existing NAT spec is unchanged). The airgap route is now un-gated: Skidbladnir may route --network none through jera→draupnir instead of a direct podman subprocess.

f3 — bind mounts (-v)

mounts: Vec<BindMount{ host, container, ro }>. [BindMount::bind_string] renders host:container[:ro]; [ContainerSpec::bind_strings] is the exact &[String] draupnir's [ContainerBoot::run_to_completion_with_binds] consumes. This one is load-bearing today: run_container/run_container_with_reports route through draupnir's mount-aware run, so a routed job keeps its mounts. Empty mounts ⇒ byte-identical to the mount-less run (draupnir guarantees an identical HostConfig).

f4 — the run seam (one ledger, two engine calls)

run_recorded is the single ledger wrap (open running row → drive the engine → terminal done/failed + Health). Two callers reuse it (L5):

caller engine call binds
run_container_with_reports (production) ContainerBoot::run_to_completion_with_binds carries bind_strings()
run_container_on<B> (mock test seam) generic run_to_completion (draupnir Boot) none — the Boot trait has no mount-aware boot

The mock seam proves the ledger/logs/exit wiring daemon-free; the production wrapper adds the mounts. to_draupnir maps image + per-boot name + cmd/env/ports and net (via d.net = NetMode::from_oci_value(self.net.oci_value())) onto a valid draupnir::BootSpec, so the net choice flows through both legs automatically; mounts are carried alongside via bind_strings (fed to the mount-aware run, since draupnir's generic Boot trait has no mount-aware boot).

f5 — the detached + exec handle (knob 3 — landed)

Skidbladnir's MachineController is start()->Handle / exec(Handle,cmd) / stop(Handle) (run detached, exec into it, rm -f). The detached handle + the stop half already existed as jera's stateful [BootProc] (poll/kill); the exec op is now landed, completing the airgap→lifecycle-swap chain (draupnir's ContainerControl::exec half landed in draupnir 0.1.6).

Skidbladnir consume (next): spawn detached → get the proc → proc.exec(&["…"]) now expresses the full start/stop/exec live-swap over jera→draupnir, unblocking the Skidbladnir lifecycle swap.