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):
BootSpecgainedcmd: Vec<String>+ports: Vec<u16>(builderswith_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::Enginenow applies cmd + ports via a purecreate_body(image, env, cmd, ports)builder (byte-for-byte the shape jera'sBollardEngine::create_bodyemitted), streams container logs into a per-container buffer via a follow task, and exposes an exit-code-awarecontainer_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 powerLifecyclecollapses every non-running state toOffand so cannot tell a clean exit from a crash;ContainerControlpreserves 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 adraupnir::BootSpec(container mirror ofvm::BootSpec::to_draupnir).DraupnirContainerBootRunner<B>(generic over the draupnir backend for daemon-free mock testing; productionB = draupnir::container::ContainerBoot) boots viadraupnir::Bootand drives the lifecycle overContainerControl, mappingContainerState→BootStatusexactly asBollardProcdid.StubContainerBootRunner— honest not-wired failure whencontainer-ociis off (mirror ofStubVmBootRunner).connect_default_runner()returnsDraupnirContainerBootRunnerwhencontainer-ociis on; otherwise the pre-existing in-crate bollard path is unchanged. Delegation proven by a mock-Boot/ContainerControltest (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.
-
jera's
run_containerrepointed +BollardEngineDELETED (eddae7c02cc, jera 0.1.4).run_container/run_container_with_reportsnow route throughdraupnir::container::run_to_completion(mapRunOutcome→ContainerOutcome, forward theHealthreporter). With the last reference gone, jera'sBollardEngine,BollardBootRunner,BollardProc, theContainerEnginetrait,EngineState, andContainerRunSpecwere all deleted.DraupnirContainerBootRunnerStubContainerBootRunnerare the only container runners left. A genericrun_container_on<B: Boot + ContainerControl>keeps it mock-testable (no daemon).cargo test -p jeraGREEN on default /--features container-oci/--no-default-features(39 each).
-
Default feature flipped OFF bollard. jera now ships
default = [](lean std build, no engine linked); the live OCI engine is opt-in behindcontainer-oci = ["draupnir/backend-oci"]. Thebollard/futuresdeps and thebollardfeature were dropped. A single build has ONE engine (draupnir's), never two.nornir-jobsforwardscontainer-ocito jera. -
nornir's THIRD copy repointed (nornir repo).
nornir/src/viz/test_boot.rsdropped its own independentBollardEngine/BollardBootRunner/ContainerEngine/EngineState/ContainerRunSpecfor aDraupnirContainerBootRunner<B>that maps aContainerBuild→draupnir::BootSpecand drives the lifecycle over draupnir'sBoot+ContainerControl. nornir's directdep:bollardis gone; the featuretest-boot-container-bollardnow forwards todraupnir/backend-oci(name kept for back-compat). The podman-subprocessContainerBootRunnerstays 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_imageover 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 ontojera/draupnirtoo (korp already consumes knut's now-draupnir-routedSparkServer). Logged for the korp owner.
Behavior notes (parity preserved)
- Container name. jera mints
jera-boot-<uuid>for an unnamed spec;to_draupnirpasses that as the draupnir spec name, and draupnir prefixesdraupnir-, so the delegated container isdraupnir-jera-boot-<uuid>. Unique and kill-addressable (poll/kill useMachine.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→Failedunless killed;HealthStarting→Alive→Gone/Failed). - No mounts/healthcheck/labels were set by either engine, so nothing to reconcile there.