Gunnar

mikronova/draupnir

.nornir/container-consolidation-plan.md

<!-- 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.