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

nordisk/Skidbladnir

.nornir/design.md

Skíðblaðnir — design (the whole system)

Class: design · Function axis: f0 (the pipeline) + the ten AIRGAP functions. Strict: this file states only the shape we ship. Every why, every alternative, lives in reasoning.md. Each function has its own *-design.md chapter (linked below).

f0 — the pipeline (the whole)

Skidbladnir folds a whole deployment — the infrastructure and the software on it — into one sealed, content-addressed bundle, carries it across an airgap, and on the far side provisions the machine, sets up PKI + a service, and starts every component in dependency order — with no network and no shell on the target. One pipeline (lib.rs:11):

pack → archive → fetch ║airgap║ → unfold → install → start(ordered)

The crate is lean by default (Cargo.toml:20): the offline core (pack · archive · fetch · platform · layout · product · install · orchestrate · events · identity · systemd) needs only always-on pure-Rust deps and always builds offline. PKI keygen, the znippy engine, and the Redfish/Terraform/KVM backends sit behind cargo features so the core never pulls a network client it does not use (Cargo.toml:91).

Two laws hold the whole system together:

law statement where
content-addressed, verify-on-receipt every entry carries a SHA-256; the far side verifies the manifest root before unpacking archive::BundleManifest (archive.rs:52), fetch::verify_on_receipt (fetch.rs:42)
verbs as data, no shell installs render files + argv as data; a thin driver runs them; systemd is driven over D-Bus, never systemctl service::RenderedService (service.rs:258), systemd::Systemd (systemd.rs:472), install::apply (install.rs:241)

The ten functions and how they wire (the reuse graph):

# function module(s) uses chapter
f1 pack pack archive pack-design.md
f2 archive archive — archive-design.md
f3 move fetch, platform archive move-design.md
f4 pki pki — pki-design.md
f5 layout layout — layout-design.md
f6 install service, systemd, product, install, identity layout · archive · pki · identity install-design.md
f7 unfold unfold — unfold-design.md
f8 orchestrate orchestrate, events product orchestrate-design.md
f9 matrix machine, matrix, probe/ archive · service · events matrix-design.md
f10 msi msi — msi-design.md

Optional integrations (off the f-axis, feature-gated): vault/redfish (HTTP seams), and openbao — provision an OpenBao secret store's structure (k8s auth method + per-env read policy/role); the platform half of Njord's secret split, with tillsynia-admin writing the values. See openbao-design.md.

The declarative top of the whole system is config::LakeSpec (lib.rs:129): a version, a set of product::ComponentSpecs, and one pki::PkiSource. LakeSpec::start_waves() (lib.rs:137) derives the dependency-ordered bring-up (f8) from the specs — one declaration drives pack → install → ordered start.

f1 — pack

Gather a product's artifacts + target-identical deps into a versioned staging tree (pack::Staging, pack.rs:31), with the VERSION written beside the bundle, not inside it (pack::write_version_file, pack.rs:84). See pack-design.md.

f2 — archive

Seal the staging tree into one bundle behind the Archive trait (archive.rs:86). The always-on default is TarZstd (archive.rs:192); the znippy feature swaps in the REAL znippy-core engine (seal via znippy-compress, selective read/verify/unpack via znippy-common — one canonical .znippy format, ZnippyArchive). Every bundle carries a per-entry SHA-256 manifest (BundleManifest, archive.rs:52). See archive-design.md.

f3 — move

Transfer is out-of-band (the bundle is just bytes). Receipt is a local, zero-network gate: fetch::verify_on_receipt (fetch.rs:42) recomputes the manifest root, an optional out-of-band whole-file SHA, and every entry hash before fetch_and_verify (fetch.rs:74) hands back a Bundle. platform::detect (platform.rs:23) reads /etc/os-release and platform::unpack (platform.rs:75) extracts into the software dir. See move-design.md.

f4 — pki

pki::PkiSource (pki.rs:28) is SelfGenerated (a real ed25519 X.509 CA + leaf via MachineCa, pki.rs:72, default-on, offline) | ExistingVault | TempVault (both HTTP seams behind the vault feature). Certs verify by signature, not clock — airgap targets have no NTP. See pki-design.md.

f5 — layout

layout::Layout (layout.rs:18) partitions software by date-stamped version (software/<YYYYMMDD>/<product>) beside per-product data/ and log/ dirs (prepare_product, layout.rs:96). Upgrade is an atomic symlink swap of current via temp+rename (activate, layout.rs:115); old generations are retained as rollback targets. See layout-design.md.

f6 — install

The install tree: one declarative service::ServiceSpec (service.rs:137) renders through the ServiceRenderer trait (service.rs:285) to many init systems (renderer_for, service.rs:292 — systemd-system, systemd-user, OpenRC, SysV, runit, s6-rc, dinit, Kubernetes, OCI, Quadlet and Windows SCM ALL implemented — 11 of 11, full coverage). Output is a RenderedService (service.rs:258): files + enable/start/status/stop argv as data. The product lifecycle (install::{install,update,uninstall}_product, install.rs:88) provisions dirs, unfolds the product, mints an install identity (identity::ensure, identity.rs:135), and returns an InstallOutcome (install.rs:57) a thin apply (install.rs:241) runs. See install-design.md.

The eleven backends group into THREE target families — host-init (systemd ×2 · OpenRC · SysV · runit · s6-rc · dinit · Windows SCM) · container-host (OCI · Quadlet) · cluster (Kubernetes) — one per identity + supervision + config-delivery model, and that family (not the backend) is where an honest-degrade decision belongs. The Kubernetes family ships today as a sync renderer plus an async applier (k8s-unfold.md), with the Talos node tier beneath it (talos-unfold.md). What ServiceSpec must still gain to make that third family honest — lifecycle hooks (pre_start/post_start/pre_stop/post_stop), the health split (startup vs liveness vs readiness), a neutral secret_env (today only SystemdServiceSpec has one, so a secret in ServiceSpec::env renders inline into a world-readable unit), a stop_timeout_sec (native on 10 of 11 backends), and an observable RenderedService::degraded so a dropped field is data instead of a doc comment — is derived capability-by-capability against Kubernetes and the vetra/Njord Helm+Talos deployer in kubernetes-target-design.md (rejected alternatives: kubernetes-target-reasoning.md).

f7 — unfold

One unfold::Unfold trait (unfold.rs:34) over four Targets (unfold.rs:22): KVM (unfold.rs:44, local test), libvirt (pure-Rust RPC, unfold.rs:138), Redfish (BMC, feature redfish, unfold.rs:451), Terraform (feature terraform, unfold.rs:638). Every backend's request/config builder is pure and testable without the tool; only unfold() executes. See unfold-design.md.

f8 — orchestrate

orchestrate::start_waves (orchestrate.rs:42) Kahn-topo-sorts the products into dependency waves; start_order/teardown_order (orchestrate.rs:23,:31) flatten and reverse them. Each boundary emits an events::AirgapEvent (events.rs:49) to a warehouse JsonlSink (events.rs:76). See orchestrate-design.md.

Tier-0.5 common-infra bring-up — product::common_infra() describes the app-agnostic platform substrate (ingress-nginx · cert-manager · holger-registry · OpenBao) as ComponentSpecs; product::common_infra_waves() orders them through the SAME start_waves engine ([[cert-manager, ingress-nginx], [holger-registry, openbao]]), each rendering to k8s manifests via the ONE k8s renderer. The OpenBao structure is populated by the pure openbao::PopulatePlan (openbao feature). Renderer half is pure/laptop-testable; live apply is deferred. See tier0-common-infra-design.md.

f9 — matrix

machine::MachineController (machine.rs:273) boots a real KVM guest (KvmController, machine.rs:481) or container (ContainerController, machine.rs:891) from a content-addressed ImageCache (machine.rs:312). matrix::run_matrix (matrix.rs:347) runs the (OS × backend) install cells, injects the static-musl skidbladnir-probe, and collects a typed ProbeReport (matrix.rs:63) back over virtio-serial. See matrix-design.md.

These spawns are being routed through jera (edda), the ONE job/container/VM manager, instead of raw podman/qemu (feature jera): MachineSpec::job_policy classifies into a jera::JobPolicy and to_jera_container_spec maps the container case onto a jera::container::ContainerSpec (→ draupnir::BootSpec), with an honest jera_unsupported() gap-list gating a live swap so airgap isolation is never dropped. See machine-jera-routing.md.

f10 — msi

msi::guid_v5 (msi.rs:244) mints stable UUID-v5 component GUIDs; msi::render_wxs (msi.rs:58) emits real WiX v4 sources; msi::WixBuilder (msi.rs:145) builds the MSI under Wine-in-podman. See msi-design.md.

Which recipe reader this repo uses, and why (lane AJ, 2026-08-30)

Reader: nornir-build-thing 0.1.12, PATH.

Why: in-constellation; recipe is schema 1. This edge had carried a path and no version at all — the float with nowhere left to read it: the manifest made no claim about which reader the guard runs while compiling whatever edda's tree happened to be that hour (the lock said 0.1.10).

The rule itself — [nornir-build-thing] schema = N, a refusal that names the version to upgrade to, and a loader that refuses to DROP a key it cannot read — lives once, in edda .nornir/build-design.md, "THE COMPATIBILITY RULE". The cross-version guard that would have caught the 2026-08-30 skew is edda/nornir-build-thing-skew.