Gunnar

nordisk/korp-collectors

.nornir/runtime-substrate-design.md

<!-- nornir:class: design -->
# Runtime substrate trait — the interface (design)

`korp_collectors::runtime` (feature `daemon`) is the **object-safe interface** that decides
*WHERE* a collector daemon (or, later, a knut pipeline) runs, decoupled from its *logic*. A
collector's behaviour is substrate-agnostic (`Collector` against `CollectorCtx`); this
trait is the seam that spawns it. See the workspace design
`runtime-aortas-design-2026-07-29.md`.

## Leaf discipline

This crate depends on **neither korp nor jera nor kube**, so the trait pulls **no new
deps** — pure `serde` + `anyhow` (both already under `daemon`). The concrete
implementations (`JeraRuntime`, `KubeRuntime`) live in **korp**, which owns jera / aegir /
skidbladnir. Keeping the interface here lets both korp and facett-demo consume it without
the `facett ⇄ korp` cycle.

## The shapes

* **`RuntimeKind`** — `Jera` (metal in-process jera DAEMON job — default) · `JeraDistributed`
  (DESIGN-ONLY, a stubbed `TODO`) · `Kube` (full Kubernetes via kube-rs SSA, zero-shell).
* **`UnitSpec`** — the backend-neutral description of a runnable: `name`, `UnitKind`
  (`Daemon` → Deployment/StatefulSet · `Cron` → CronJob · `OneShot` → Job), `Entry`
  (`InProcess{collector}` = the Jera-metal path · `Image{image,argv}` = the Kube/distributed
  path, `korp collector run <name>`), `config` (a ConfigMap on Kube / the `CollectorCtx`
  config on metal), `StorageSpec` (durable dir ⇒ PVC + StatefulSet; S3 bucket ⇒ the rustfs
  Service), `Resources`, env / secret_env, image, namespace. Builder + serde round-trip.
* **`Handle`** — opaque `{runtime, name, id}` (the jera `job_id` / the k8s
  `<ns>/<kind>/<name>`) for `stop`/`status`.
* **`RunStatus`** — `{phase, healthy, logs}` — the live phase label + a health flag.

## The trait

```rust
pub trait Runtime: Send + Sync {
    fn kind(&self) -> RuntimeKind;
    fn start(&self, spec: &UnitSpec) -> Result<Handle>;   // idempotent where the substrate allows
    fn stop(&self, h: &Handle) -> Result<()>;             // idempotent
    fn status(&self, h: &Handle) -> Result<RunStatus>;
    fn reconcile(&self, desired: &[UnitSpec]) -> Result<()>; // the Settings toggle / matrix ▶Run route
}
```

Object-safe (`&self`, no generics), so a `Box<dyn Runtime>` selects the substrate at
runtime from the persisted per-collector choice. Proven by a mock in the crate tests
(`runtime_is_object_safe_via_a_mock`) plus key/serde round-trips.