//! **Tools · Demo appliance — a jera-backed demo container for the holger UI.**
//!
//! The shared "common framework" the whole nordisk suite adopts (nornir / dwarves /
//! korp / holger) gives every app a **Tools menu whose demo entries start containers
//! THROUGH the shared job manager `jera`** — never a bespoke `thread::spawn` or a
//! hand-rolled docker call (LAW 5, reuse-first). korp's `infra.rs` is the reference:
//! a container is declared as a plain [`jera::container::ContainerSpec`] (which maps
//! onto draupnir's ONE OCI engine via `ContainerSpec::to_draupnir`) and its bring-up
//! is *recorded as a durable job* in the shared [`jera::jobs`] ledger — the exact
//! seam holger-server already routes promote/GC through.
//!
//! holger's demo stands up a small, **pinned** upstream artifact registry
//! (`docker.io/library/registry:2.8.3`) — the natural demo for an artifact server:
//! once up, holger can proxy / migrate from it. Declaring + dispatching the spec is
//! testable in the lean default build (draupnir's spec types + the jera ledger always
//! compile; the dispatch opens a real redb job row with **no daemon**); the LIVE boot
//! is opt-in behind holger-server-ui's `containers` feature (→ `jera/container-oci` →
//! `draupnir/backend-oci`).
use std::collections::BTreeMap;
use std::path::Path;
use jera::container::ContainerSpec;
use serde_json::{json, Value};
// ---------------------------------------------------------------------------
// The pinned demo image + endpoint (env-overridable).
// ---------------------------------------------------------------------------
/// The demo registry version this UI pins (mirrors korp's pinned FalkorDB/Spark
/// versions). **Never `latest`** — a re-launch must be reproducible, not silently
/// drifted by an upstream retag.
pub const DEMO_REGISTRY_VERSION: &str = "2.8.3";
/// The default demo image, **pinned** to [`DEMO_REGISTRY_VERSION`]: the CNCF
/// distribution registry — a real upstream artifact store holger can proxy /
/// migrate from. Env-overridable via `HOLGER_DEMO_IMAGE`.
pub const DEMO_IMAGE_DEFAULT: &str = "docker.io/library/registry:2.8.3";
/// The registry HTTP port — published host↔container 1:1.
pub const DEMO_PORT: u16 = 5000;
/// The stable managed-container name for the demo registry (draupnir prefixes
/// `draupnir-`). A stable name makes teardown / idempotent re-launch addressable.
pub const DEMO_NAME: &str = "holger-demo-registry";
/// The workspace label the demo job is recorded under in the jera ledger.
pub const DEMO_WORKSPACE: &str = "holger-server-ui";
/// The demo image to run — `HOLGER_DEMO_IMAGE` else [`DEMO_IMAGE_DEFAULT`].
pub fn demo_image() -> String {
std::env::var("HOLGER_DEMO_IMAGE").unwrap_or_else(|_| DEMO_IMAGE_DEFAULT.to_string())
}
/// The demo registry client URL (`http://127.0.0.1:5000`).
pub fn demo_url() -> String {
format!("http://127.0.0.1:{DEMO_PORT}")
}
// ---------------------------------------------------------------------------
// The demo container spec (jera).
// ---------------------------------------------------------------------------
/// The **demo registry** container spec as a [`jera::container::ContainerSpec`]:
/// the pinned image, the stable name, and the published `:5000`. Starting it goes
/// THROUGH jera→draupnir ([`start_via_jera`] / [`dispatch_demo`]); no bespoke docker
/// call. Engine defaults for the remaining knobs (net = default NAT, no mounts);
/// `..Default::default()` keeps this forward-compatible as jera grows the spec.
pub fn demo_registry_spec() -> ContainerSpec {
ContainerSpec {
image: demo_image(),
name: DEMO_NAME.to_string(),
cmd: Vec::new(),
env: BTreeMap::new(),
ports: vec![DEMO_PORT],
..Default::default()
}
}
// ---------------------------------------------------------------------------
// The dispatch result + the headless oracle.
// ---------------------------------------------------------------------------
/// The outcome of a demo dispatch: the **jera job id** the bring-up was recorded
/// under, the container spec's image / name / ports, and the terminal status. This
/// is what the Tools#6 pane shows AND what the robot asserts in `state_json` — proof
/// the demo went THROUGH the shared job manager (a non-empty `job_id`), not a bespoke
/// spawn.
#[derive(Debug, Clone, Default)]
pub struct DemoDispatch {
/// The jera ledger job id the demo bring-up was recorded under (empty only if
/// the ledger could not be opened).
pub job_id: String,
/// The demo container image dispatched.
pub image: String,
/// The demo container name.
pub name: String,
/// The published ports.
pub ports: Vec<u16>,
/// The client URL a caller would reach the demo registry at once up.
pub url: String,
/// Terminal status: `"booted"` (the live engine brought it up), `"dispatched"`
/// (recorded as a job; no live engine in this build — the default), or `"error"`.
pub status: String,
/// `true` only when the LIVE engine (`containers` feature) actually booted it.
pub booted: bool,
/// A failure reason, if any (a ledger-open or boot error).
pub error: Option<String>,
}
impl DemoDispatch {
/// The observable state a robot asserts — carries the job id (proof it went
/// through jera), the container spec, and the terminal status.
pub fn state_json(&self) -> Value {
json!({
"job_id": self.job_id,
"image": self.image,
"name": self.name,
"ports": self.ports,
"url": self.url,
"status": self.status,
"booted": self.booted,
"error": self.error,
"dispatched": !self.job_id.is_empty(),
})
}
}
/// A robot/headless oracle for the *declared* demo (before any dispatch): the spec's
/// image · name · ports · url · pinned version. Lets a test assert the demo container
/// is declared + pinned without a live daemon.
pub fn state_json() -> Value {
let spec = demo_registry_spec();
json!({
"version": DEMO_REGISTRY_VERSION,
"image": spec.image,
"name": spec.name,
"ports": spec.ports,
"url": demo_url(),
})
}
// ---------------------------------------------------------------------------
// Dispatch — record the bring-up as a jera job (and boot it live when wired).
// ---------------------------------------------------------------------------
/// Dispatch the demo container **through the shared job manager** `jera`: open a
/// durable [`jera::jobs`] ledger at `jobs_root`, start a
/// [`kind::BOOT_CONTAINER`](jera::jobs::kind::BOOT_CONTAINER) job carrying the
/// [`demo_registry_spec`] (image / name / ports), capture its **job id**, then bring
/// the container up THROUGH jera→draupnir when the live engine is compiled in
/// (`containers` feature) — else record it as a planned `dispatched` job (the default
/// laptop/CI build has no daemon). Either way the ledger holds a row a reader (the
/// `nornir jobs` list / a Jobs panel) can enumerate — exactly like holger-server's
/// promote/GC jobs.
///
/// This is the load-bearing "no thread::spawn, no bespoke docker" seam: the demo's
/// bring-up is a tracked job, not an ad-hoc spawn.
pub fn dispatch_demo(jobs_root: &Path) -> DemoDispatch {
use jera::jobs::{kind, JobStore};
let spec = demo_registry_spec();
let mut out = DemoDispatch {
image: spec.image.clone(),
name: spec.name.clone(),
ports: spec.ports.clone(),
url: demo_url(),
..Default::default()
};
// The shared durable ledger (redb) — the same seam server-lib routes promote/GC
// through. Open failure is honest: no job id, status `error`.
let store = match JobStore::open(jobs_root) {
Ok(s) => s,
Err(e) => {
out.status = "error".to_string();
out.error = Some(format!("jera ledger open: {e}"));
return out;
}
};
let detail = json!({ "image": spec.image, "name": spec.name, "ports": spec.ports });
let job = store.start(kind::BOOT_CONTAINER, &spec.image, DEMO_WORKSPACE, detail);
out.job_id = job.job_id().to_string();
// Bring it up THROUGH jera→draupnir when the live engine is linked; else record a
// planned dispatch (no daemon in this build).
match boot_live(&spec) {
Ok(true) => {
out.booted = true;
out.status = "booted".to_string();
job.finish(json!({ "booted": true, "image": spec.image, "name": spec.name }), "");
}
Ok(false) => {
out.status = "dispatched".to_string();
job.finish(json!({ "dispatched": true, "image": spec.image, "name": spec.name }), "");
}
Err(e) => {
out.status = "error".to_string();
out.error = Some(e.clone());
job.fail_with_detail(json!({ "error": e, "image": spec.image }));
}
}
out
}
/// Boot the demo container THROUGH jera→draupnir's ONE OCI engine (the `containers`
/// feature turns on `jera/container-oci` → `draupnir/backend-oci`). Detached: the
/// container keeps running after the boot proc is dropped, so a caller can then reach
/// `http://127.0.0.1:5000`. Returns `Ok(true)` when it came up.
#[cfg(feature = "containers")]
fn boot_live(spec: &ContainerSpec) -> Result<bool, String> {
start_via_jera(spec)?;
Ok(true)
}
/// No live engine in this build (`containers` off): nothing to boot — the dispatch is
/// recorded as a job and the spec is validated, which is what the default tests check.
#[cfg(not(feature = "containers"))]
fn boot_live(_spec: &ContainerSpec) -> Result<bool, String> {
Ok(false)
}
/// Start the demo container **through jera** (→ draupnir's ONE OCI engine), reusing
/// the shared runtime rather than any bespoke docker call (LAW 5). Detached bring-up
/// (the boot proc is dropped; draupnir's start is detached). Only compiled with
/// holger-server-ui's `containers` feature.
#[cfg(feature = "containers")]
pub fn start_via_jera(spec: &ContainerSpec) -> Result<(), String> {
use jera::boot::BootTarget;
let runner = jera::container::connect_default_runner();
let _proc = runner.spawn(&BootTarget::Container(spec.clone()))?;
Ok(())
}
// ---------------------------------------------------------------------------
// Tests — spec + dispatch asserted in the LEAN default build (no daemon).
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
/// Emit one functional-status row for a real check. Gated behind `testmatrix`
/// so release builds strip it (the dep is optional on holger-server-ui).
#[cfg(feature = "testmatrix")]
fn fstatus(component: &str, check: &str, ok: bool, detail: &str) {
nornir_testmatrix::functional_status(component, check, ok, detail);
}
/// **RED-when-broken: the demo spec resolves + validates + is PINNED.** The jera
/// spec maps onto a draupnir container spec that passes draupnir's own
/// `validate()` (the "the wiring is real" gate) and carries the registry port;
/// the default image is pinned, never `:latest` (reproducibility). A blank image,
/// a wrong mapping, or a `:latest` default drift turns this red.
#[test]
fn demo_spec_maps_validates_and_is_pinned() {
let spec = demo_registry_spec();
assert_eq!(spec.name, DEMO_NAME);
assert_eq!(spec.ports, vec![DEMO_PORT]);
assert!(spec.image.contains("registry"), "demo image is a registry: {}", spec.image);
assert!(
!DEMO_IMAGE_DEFAULT.ends_with(":latest"),
"the default demo image must be pinned, not :latest: {DEMO_IMAGE_DEFAULT}"
);
assert!(
DEMO_IMAGE_DEFAULT.ends_with(&format!(":{DEMO_REGISTRY_VERSION}")),
"the default demo image pins DEMO_REGISTRY_VERSION ({DEMO_REGISTRY_VERSION}): {DEMO_IMAGE_DEFAULT}"
);
// The mapped draupnir spec validates (the wiring is real).
let d = spec.to_draupnir();
d.validate().expect("mapped demo container spec is valid");
// The declared-demo oracle carries the pinned version.
assert_eq!(state_json()["version"], DEMO_REGISTRY_VERSION);
#[cfg(feature = "testmatrix")]
fstatus(
"holger-server-ui/tools/demo",
"demo_spec_pinned_and_validates",
!DEMO_IMAGE_DEFAULT.ends_with(":latest") && spec.ports == vec![DEMO_PORT],
"the demo container is a pinned jera ContainerSpec that maps + validates onto draupnir",
);
}
/// **RED-when-broken: the demo dispatch routes THROUGH the shared jera job
/// ledger.** Dispatching opens a real redb ledger and records a
/// [`kind::BOOT_CONTAINER`] job carrying the spec, returning its **job id** — the
/// proof the demo went through the shared job manager, not a bespoke spawn. We
/// then re-open the ledger and find the SAME job id (with the demo image) in the
/// `BOOT_CONTAINER` list. If the dispatch stopped routing through jera (no row /
/// empty id), this is red end-to-end. No daemon: the default build records a
/// terminal `dispatched` job.
#[test]
fn demo_dispatch_records_a_jera_boot_container_job() {
use jera::jobs::{kind, JobSelector, JobStore};
let dir = tempfile::tempdir().expect("tempdir");
let out = dispatch_demo(dir.path());
// The dispatch produced a job id + carried the pinned spec.
assert!(!out.job_id.is_empty(), "dispatch records a jera job id: {out:?}");
assert_eq!(out.image, DEMO_IMAGE_DEFAULT, "the dispatched image is the pinned demo image");
assert_eq!(out.ports, vec![DEMO_PORT]);
// No live engine in the default build → recorded as a planned `dispatched` job.
assert_eq!(out.status, "dispatched", "no containers feature → dispatched (not booted): {out:?}");
assert!(!out.booted, "the default build does not boot a live container");
assert!(out.error.is_none(), "a clean dispatch has no error: {out:?}");
assert!(out.state_json()["dispatched"].as_bool().unwrap_or(false), "oracle flags dispatched");
// Re-open the SAME ledger and find the row — the durable proof.
let store = JobStore::open(dir.path()).expect("reopen ledger");
let rows = store
.list(&JobSelector::Kind(kind::BOOT_CONTAINER.to_string()))
.expect("list boot_container jobs");
let found = rows.iter().find(|r| r.job_id == out.job_id);
assert!(found.is_some(), "the dispatched job id is in the jera ledger: {:?}", rows);
let row = found.unwrap();
assert_eq!(row.kind, kind::BOOT_CONTAINER, "recorded under the boot_container kind");
assert!(row.target.contains("registry"), "the row targets the demo image: {}", row.target);
assert!(row.detail_json.contains(&DEMO_NAME.to_string()), "the row carries the spec detail");
#[cfg(feature = "testmatrix")]
fstatus(
"holger-server-ui/tools/demo",
"demo_dispatch_through_jera",
!out.job_id.is_empty() && found.is_some(),
"the Tools demo starts the container as a jera BOOT_CONTAINER job (durable ledger row), not a bespoke spawn",
);
}
}