Gunnar

nordisk/Skidbladnir

.nornir/ron-schema.md

<!-- nornir:class: design -->
# Skidbladnir RON schema — the declarative provisioning object model

> **Class:** `design` · Source of truth: [`src/orchestration.rs`](../src/orchestration.rs).
> This documents the RON object model **as implemented** (nornir commit `78deafc`),
> verified field-by-field against the serde structs. The companion
> [`custom-objects.md`](custom-objects.md) covers the `SteeringModule` custom-code seam;
> the original design capture is [`ron-orchestration-design.md`](ron-orchestration-design.md).

Skidbladnir has two ways to drive its imperative airgap core (pack · archive · fetch ·
unfold · install · start):

1. **Declarative RON** — a `.ron` file describes an ordered list of steps; the
   [`run_flow`](../src/orchestration.rs) interpreter executes it. No new Rust per scenario.
2. **Custom/imperative** — call the Rust API or a `SteeringModule` directly. See
   [`custom-objects.md`](custom-objects.md).

This doc is the **RON object model**: every top-level type, its fields, how it
deserializes, and an annotated end-to-end example.

## The top-level objects

Everything lives in `src/orchestration.rs`. The RON-facing types (the ones you write in
a `.ron`) are `Flow`, `Step`, and `BootBackend`. The rest of the object model
(`RunContext`, `StepOutcome`, `Dispatch`, `Registry`, `SteeringModule`) is the **runtime**
side — produced/consumed by the interpreter, never written in RON.

| Object | Kind | File:line | Written in RON? |
|---|---|---|---|
| `Flow` | struct `{ steps: Vec<Step> }` | `orchestration.rs:104` | yes (optional wrapper) |
| `Step` | enum (5 variants) | `orchestration.rs:68` | **yes** — the list elements |
| `BootBackend` | enum (4 variants) | `orchestration.rs:49` | yes (a `BootOS` field) |
| `RunContext` | struct (threaded state) | `orchestration.rs:139` | no (runtime) |
| `Dispatch` | struct (one trace row) | `orchestration.rs:153` | no (runtime) |
| `StepOutcome` | struct (a step's result) | `orchestration.rs:165` | no (runtime) |
| `SteeringModule` | trait (the seam) | `orchestration.rs:186` | no (Rust) |
| `Registry` | struct (name→module) | `orchestration.rs:198` | no (runtime) |

### `Flow` — the whole scenario

```rust
pub struct Flow { pub steps: Vec<Step> }          // orchestration.rs:104
```

A flow is just an ordered `Vec<Step>`. `Flow` derives `Serialize, Deserialize, Default,
Clone, PartialEq, Eq`.

### `Step` — the typed vocabulary (5 variants)

```rust
pub enum Step {                                   // orchestration.rs:68
    BootOS    { backend: BootBackend, os: String },
    Seed      { config: String },
    InstallApp{ spec: String },
    Configure { target: String, params: BTreeMap<String, String> },
    Federate  { registry: String },
}
```

| Variant | Fields | What it does | Module it routes to | REAL? |
|---|---|---|---|---|
| `BootOS` | `backend: BootBackend`, `os: String` | Boot/provision an OS on a backend | per-backend (below) | Kvm real, rest mock |
| `Seed` | `config: String` | First-boot NoCloud (cloud-init) config | `Seed` | mock |
| `InstallApp` | `spec: String` | Install a registered application | `InstallApp` | **real** |
| `Configure` | `target: String`, `params: map` | Configure a target subsystem | `Configure` | mock |
| `Federate` | `registry: String` | Federate with an upstream registry | `Federate` | mock |

The "REAL?" column is verified against the native module implementations — see
[`custom-objects.md`](custom-objects.md) for the honest-mock rationale per module.

Each `Step` knows its own module name through
[`Step::module_kind()`](../src/orchestration.rs) (`orchestration.rs:85`) — that string is
the registry key the interpreter dispatches on.

### `BootBackend` — the boot/provision target (4 variants)

```rust
pub enum BootBackend { Redfish, Kvm, Oci, Terraform }   // orchestration.rs:49
```

`module_kind()` maps each variant to a distinct boot module name:

| `BootBackend` | Module kind | Status |
|---|---|---|
| `Kvm` | `KvmBoot` | **REAL** — wraps `unfold::kvm::KvmTarget`, builds the tested qemu argv |
| `Redfish` | `RedfishBoot` | honest mock — needs the `redfish` feature + a reachable BMC |
| `Oci` | `OciDeploy` | honest mock — needs draupnir's OCI engine (`pack-oci`/`msi-oci`) |
| `Terraform` | `TerraformApply` | honest mock — needs the `terraform` feature + the tf binary |

## Deserialization — how a `.ron` becomes a `Flow`

Two entry points, both in `orchestration.rs`:

- [`Flow::from_ron(src: &str)`](../src/orchestration.rs) (`:112`)
- [`Flow::from_ron_file(path: &Path)`](../src/orchestration.rs) (`:126`)

`from_ron` accepts **two surface forms** and tries them in order:

1. **Bare list** (canonical, no-code): `[ BootOS(...), InstallApp(...) ]` —
   `ron::from_str::<Vec<Step>>(src)`.
2. **Explicit wrapper** (fallback): `Flow(steps: [ ... ])` — `ron::from_str::<Flow>(src)`.

If both fail, the error names both parse failures. Because `Step` is a serde enum with
struct variants, RON writes them as `Variant(field: value, …)` — e.g.
`BootOS(backend: Kvm, os: "ubuntu-24.04")`. `BootBackend` variants are bare identifiers
(`Kvm`, `Redfish`, …). `Configure`'s `params` is a RON map: `{"KEY": "value"}`.

> There is **no CLI runner and no auto-discovery of `.ron` files**. Flows are executed
> through the library API (`run_flow` / `run_ron`) — the example at
> [`examples/korp-provision.ron`](../examples/korp-provision.ron) is illustrative data,
> not wired to a binary. NOT YET IMPLEMENTED: a `skidbladnir run <flow.ron>` command.

## Annotated end-to-end example

The real, tested scenario (mirrors `examples/korp-provision.ron` and the
`ron_drives_infra_then_installs_the_app` test, `orchestration.rs:727`):

```ron
// A declarative provisioning flow: boot infra → seed → configure → install the app.
// Run it with skidbladnir::orchestration::run_flow(&flow, &registry, &mut ctx).
[
    // 1. INFRA — boot ubuntu on a local KVM VM. Routes to KvmBoot (REAL: builds the
    //    qemu argv from unfold::kvm::KvmTarget; the actual spawn is gated on `boot`).
    BootOS(backend: Kvm, os: "ubuntu-24.04"),

    // 2. INFRA — first-boot cloud-init config. Routes to Seed (records + validates the
    //    NoCloud config; authoring the CIDATA image needs draupnir/seed → honest mock).
    Seed(config: "#cloud-config\npackages: [curl, ca-certificates]\n"),

    // 3. INFRA — set a target param. Routes to Configure (records params → honest mock;
    //    real needs the target-specific driver).
    Configure(target: "korp", params: {"KORP_DATA": "/var/lib/korp"}),

    // 4. APP — install the application. Routes to InstallApp (REAL: reuses the
    //    install::install_product lifecycle when a bundle is registered, else renders
    //    the real ServiceSpec unit).
    InstallApp(spec: "korp"),
]
```

Executed, this produces (verified by the test at `orchestration.rs:727`):
a `RunContext.trace` of `["KvmBoot", "Seed", "Configure", "InstallApp"]`, a real qemu argv
in outcome 0's `data`, and — with a real korp bundle — a full install: software/data/log
tree, minted identity, rendered unit, and ordered enable/start verbs.

## Serialization / round-trip

`Flow` and `Step` derive `Serialize` too, so a flow round-trips through serde (RON in,
JSON out, RON out). The `ron_wrapper_form_and_roundtrip` test (`orchestration.rs:715`)
asserts the JSON round-trip. `RunContext`, `Dispatch`, and `StepOutcome` are **not**
serde types — they are inspected in-process (the inject-assert law: a test asserts exactly
what each step did from the plain-data `StepOutcome`).

## Verified against implementation

Everything above was checked field-by-field against `src/orchestration.rs` at nornir
`78deafc`. Gaps that the original design capture describes but the code does **not** yet
implement are called out explicitly here and in [`custom-objects.md`](custom-objects.md):

- **NOT YET IMPLEMENTED:** a CLI runner (`skidbladnir run flow.ron`).
- **NOT YET IMPLEMENTED:** znippy-packaged modules and the WASM module-kind (the design's
  Phase-2 — Phase 1 is native, in-tree only).
- The design lists a `TalosConfig` module kind; the code ships a **generic `Configure`**
  step/module instead (honest mock).