<!-- nornir:class: design -->
# Container engine consolidation — jera → draupnir (status + deferred gaps)
Decided 2026-07-12. The nordisk consolidation law (`workspace_nordisk/.nornir/
consolidation-panels-maps-design.md` §1.1): the app→jera→draupnir chain must own
provisioning in exactly ONE place. KVM was already unified (jera's
`DraupnirVmBootRunner` boots via `draupnir::Boot`). Containers were NOT: jera had
its own `BollardEngine` duplicating draupnir's `backend-oci` (both pin bollard
0.20). This doc records what landed and what is deliberately deferred.
## What landed (this pass, additive, no regression)
**draupnir (the one engine now lives here):**
- `BootSpec` gained `cmd: Vec<String>` + `ports: Vec<u16>` (builders `with_cmd` /
`with_port`) so the container backend reaches full parity with what jera's engine
produced — entrypoint override + published ports, not just image + env.
- `container::Engine` now applies cmd + ports via a pure `create_body(image, env,
cmd, ports)` builder (byte-for-byte the shape jera's `BollardEngine::create_body`
emitted), streams container logs into a per-container buffer via a follow task,
and exposes an exit-code-aware `container_state`.
- New public seam `container::ContainerControl` (`container_state` →
`ContainerState{Running|Exited(i64)|Gone}`, `drain_logs`, `stop`) — the richer
container control a job runner maps onto its own status/log model. The generic
power `Lifecycle` collapses every non-running state to `Off` and so cannot tell a
clean exit from a crash; `ContainerControl` preserves the distinction.
**jera (delegates through draupnir, mirrors `vm.rs`):**
- Feature `container-oci = ["draupnir/backend-oci"]`.
- `ContainerSpec::to_draupnir()` maps image + per-boot name + cmd/env/ports onto a
`draupnir::BootSpec` (container mirror of `vm::BootSpec::to_draupnir`).
- `DraupnirContainerBootRunner<B>` (generic over the draupnir backend for
daemon-free mock testing; production `B = draupnir::container::ContainerBoot`)
boots via `draupnir::Boot` and drives the lifecycle over `ContainerControl`,
mapping `ContainerState` → `BootStatus` exactly as `BollardProc` did.
- `StubContainerBootRunner` — honest not-wired failure when `container-oci` is off
(mirror of `StubVmBootRunner`).
- `connect_default_runner()` returns `DraupnirContainerBootRunner` when
`container-oci` is on; otherwise the pre-existing in-crate bollard path is
unchanged. Delegation proven by a mock-`Boot`/`ContainerControl` test (no daemon,
no feature needed).
## DONE — the duplicate engines are removed; one engine, in draupnir
The consolidation is complete (2026-07-12). Every duplicate bollard container
engine in the constellation has been deleted now that draupnir owns the one true
engine; the removals were gated on the shared draupnir path being green + tested.
1. **jera's `run_container` repointed + `BollardEngine` DELETED (edda `e7c02cc`,
jera 0.1.4).** `run_container` / `run_container_with_reports` now route through
`draupnir::container::run_to_completion` (map `RunOutcome` → `ContainerOutcome`,
forward the `Health` reporter). With the last reference gone, jera's
`BollardEngine`, `BollardBootRunner`, `BollardProc`, the `ContainerEngine` trait,
`EngineState`, and `ContainerRunSpec` were all deleted. `DraupnirContainerBootRunner`
+ `StubContainerBootRunner` are the only container runners left. A generic
`run_container_on<B: Boot + ContainerControl>` keeps it mock-testable (no daemon).
`cargo test -p jera` GREEN on default / `--features container-oci` /
`--no-default-features` (39 each).
2. **Default feature flipped OFF bollard.** jera now ships `default = []` (lean std
build, no engine linked); the live OCI engine is opt-in behind
`container-oci = ["draupnir/backend-oci"]`. The `bollard`/`futures` deps and the
`bollard` feature were dropped. A single build has ONE engine (draupnir's), never
two. `nornir-jobs` forwards `container-oci` to jera.
3. **nornir's THIRD copy repointed (nornir repo).** `nornir/src/viz/test_boot.rs`
dropped its own independent `BollardEngine` / `BollardBootRunner` /
`ContainerEngine` / `EngineState` / `ContainerRunSpec` for a
`DraupnirContainerBootRunner<B>` that maps a `ContainerBuild` → `draupnir::BootSpec`
and drives the lifecycle over draupnir's `Boot` + `ContainerControl`. nornir's
direct `dep:bollard` is gone; the feature `test-boot-container-bollard` now
forwards to `draupnir/backend-oci` (name kept for back-compat). The
podman-subprocess `ContainerBootRunner` stays as the no-socket fallback.
`cargo test -p nornir` (default) GREEN; the container tests are driven by a mock
draupnir backend (no daemon).
## Constellation grep — one container RUN engine (2026-07-12)
`grep -r 'bollard::'` across the constellation source now shows the container
run/lifecycle engine in exactly ONE place — draupnir. The other two bollard users
are **not** container run engines and are deliberately left:
- **`nornir/crates/nornir-build-thing` (`BollardImageBuilder`)** — an image
*builder* (`build_image` over the REST API), a different concern from running a
container. Not a duplicate of draupnir's run engine; out of scope.
- **`korp/src/demo_drive.rs`** — korp's own bollard bring-up of the demo FalkorDB
(and Spark helper) containers. This IS a run-engine duplicate, but it belongs to
**korp's own refactor slot** (main-only; not touched here). Follow-up: fold korp's
demo container bring-up onto `jera`/`draupnir` too (korp already consumes knut's
now-draupnir-routed `SparkServer`). Logged for the korp owner.
## Behavior notes (parity preserved)
- **Container name.** jera mints `jera-boot-<uuid>` for an unnamed spec;
`to_draupnir` passes that as the draupnir spec name, and draupnir prefixes
`draupnir-`, so the delegated container is `draupnir-jera-boot-<uuid>`. Unique and
kill-addressable (poll/kill use `Machine.id`); only the external name string
differs from the in-crate path. No functional change.
- **poll / logs / exit codes / kill semantics** are mapped identically to the old
`BollardProc` (`Exited(0)`→`BootedOk`, non-zero→`Failed(code)`, `Gone`→`Failed`
unless killed; `Health` Starting→Alive→Gone/Failed).
- No mounts/healthcheck/labels were set by either engine, so nothing to reconcile
there.