use std::collections::BTreeMap;
use std::path::Path;
use jera::container::ContainerSpec;
use serde_json::{json, Value};
pub const DEMO_REGISTRY_VERSION: &str = "2.8.3";
pub const DEMO_IMAGE_DEFAULT: &str = "docker.io/library/registry:2.8.3";
pub const DEMO_PORT: u16 = 5000;
pub const DEMO_NAME: &str = "holger-demo-registry";
pub const DEMO_WORKSPACE: &str = "holger-ui";
pub fn demo_image() -> String {
std::env::var("HOLGER_DEMO_IMAGE").unwrap_or_else(|_| DEMO_IMAGE_DEFAULT.to_string())
}
pub fn demo_url() -> String {
format!("http://127.0.0.1:{DEMO_PORT}")
}
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()
}
}
#[derive(Debug, Clone, Default)]
pub struct DemoDispatch {
pub job_id: String,
pub image: String,
pub name: String,
pub ports: Vec<u16>,
pub url: String,
pub status: String,
pub booted: bool,
pub error: Option<String>,
}
impl DemoDispatch {
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(),
})
}
}
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(),
})
}
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()
};
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();
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
}
#[cfg(feature = "containers")]
fn boot_live(spec: &ContainerSpec) -> Result<bool, String> {
start_via_jera(spec)?;
Ok(true)
}
#[cfg(not(feature = "containers"))]
fn boot_live(_spec: &ContainerSpec) -> Result<bool, String> {
Ok(false)
}
#[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(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "testmatrix")]
fn fstatus(component: &str, check: &str, ok: bool, detail: &str) {
nornir_testmatrix::functional_status(component, check, ok, detail);
}
#[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}"
);
let d = spec.to_draupnir();
d.validate().expect("mapped demo container spec is valid");
assert_eq!(state_json()["version"], DEMO_REGISTRY_VERSION);
#[cfg(feature = "testmatrix")]
fstatus(
"holger-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",
);
}
#[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());
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]);
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");
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-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",
);
}
}