Gunnar

nordisk/Skidbladnir

.nornir/install-guide.md

# install — guide

> **Class:** `guide` · audience: a techie installing a product on a target. The *what* is in
> [`install-design.md`](install-design.md); the *why* in [`install-reasoning.md`](install-reasoning.md).

![The install tree](assets/install-tree.svg)

## f6 — one spec, any init, three lifecycle verbs (the whole flow)

```rust
use skidbladnir::{archive::TarZstd, layout::Layout, product::ComponentSpec,
                  service::Backend, install};

let eng    = TarZstd::new();
let layout = Layout::dated("/var/lib/nordisk", "/var/log/nordisk", "/opt/nordisk");
let spec   = ComponentSpec::holger();

// INSTALL: provision dirs, unfold the product, mint identity, render + write unit files
let outcome = install::install_product(&spec, &layout, &bundle_path, &eng,
                                       Backend::SystemdSystem, "/")?;   // install.rs:88
// nothing privileged has run yet — inspect the plan as data:
assert_eq!(outcome.verbs, vec![vec!["systemctl","enable","holger.service"],
                               vec!["systemctl","start","holger.service"]]);
install::apply(&outcome)?;   // the only step that runs the verbs (systemd → D-Bus)
```

Swap `Backend::SystemdSystem` for `Backend::OpenRc` and the *same* `ComponentSpec` installs on
Alpine — different files (`/etc/init.d/holger` + `/etc/conf.d/holger`), different verbs
(`rc-update add …`), same call.

## Update (preserve data + log)

```rust
let next = Layout::dated("/var/lib/nordisk", "/var/log/nordisk", "/opt/nordisk");   // new version
let outcome = install::update_product(&spec, &next, &new_bundle, &eng, Backend::SystemdSystem, "/")?;
// data/ and log/ are preserved; the install id is loaded (not re-minted); verbs = [stop, start]
install::apply(&outcome)?;
```

## Uninstall (optionally purge)

```rust
let outcome = install::uninstall_product(&spec, &layout, Backend::SystemdSystem, "/", /* purge */ false)?;
// unit files removed; data/log kept (purge=false) or removed (purge=true); verbs = [stop, disable]
install::apply(&outcome)?;
```

## Build a spec by hand

```rust
use skidbladnir::{product::{ComponentSpec, HealthCheck}, systemd::RestartPolicy};

let spec = ComponentSpec::new("holger", "linux-x86_64/holger", "_holger")
    .with_arg("serve").with_arg("--config").with_arg("/var/lib/nordisk/holger/config.toml")
    .with_port(7070)
    .after("nornir-server")
    .with_health(HealthCheck::Tcp(7070));
```

`bundle_binary` is the **full relative path inside the bundle** — the lifecycle unfolds the whole
product tree (`extract_all`), so keep the real path (`linux-x86_64/holger`), not a flattened name.

## Test a render with zero privilege

Because verbs are data, you can assert an install without installing anything:

```rust
let neutral = spec.to_service_spec(&layout, Backend::OpenRc);          // product.rs:174
let rendered = skidbladnir::service::renderer_for(Backend::OpenRc).unwrap().render(&neutral);
assert!(rendered.enable == vec!["rc-update","add","holger","default"]);
assert!(rendered.file_ending("/etc/init.d/holger").unwrap().contains("supervise-daemon"));
```

## Identity

Each install mints (or, on update, loads) an identity under the data dir:

```rust
let id = skidbladnir::identity::ensure(&data_dir, "holger", scope, "_holger")?;   // identity.rs:135
println!("{}", id.wire());   // nid1:<machine_fp>:<install_id>
```

## Gotchas

- **Full coverage — all eleven backends render** (`renderer_for` never returns `None` today; the
  `Option` return is kept for a future backend). systemd-system, systemd-user, OpenRC, SysV, runit,
  dinit, s6-rc, Kubernetes, OCI, Quadlet and Windows SCM — swap `Backend::OpenRc` above for
  `Backend::Runit` and the SAME `ComponentSpec` renders a `/etc/sv/holger/` runsv service directory,
  `Backend::Dinit` for a `/etc/dinit.d/holger` textual service description, `Backend::S6Rc` for a
  `/etc/s6-rc/source/holger/` compiled-source definition dir, `Backend::Oci` for a `podman run -d
  --name holger …` container invocation (+ a 0600 `--env-file`) — a container runtime is the supervisor,
  so the spec is the run argv, not a unit file — `Backend::Quadlet` for a systemd
  `/etc/containers/systemd/holger.container` unit that `podman-systemd` turns into a `holger.service`
  on `daemon-reload` (that reload IS the enable-at-boot step; no separate `systemctl enable`), or
  `Backend::WindowsService` for a WiX `C:\ProgramData\holger\holger.wxs` service fragment (via the MSI
  `WindowsServiceSpec` seam) driven by `sc.exe create/start/query/stop` — SCM is machine-scoped, env
  lives in the `.wxs`, and `restart_sec` maps to the service's crash-recovery restart delay.
- Pass `root = "/"` in production; pass a tempdir in tests to capture the exact files written.
- `to_service_spec` surfaces data/log as `DATA_DIR`/`LOG_DIR` env (there is no native systemd
  `StateDirectory=`); the dirs are provisioned explicitly by `prepare_product`.
- **Secrets go in `with_secret_env`, never `with_env`.** A `SystemdServiceSpec` non-secret env
  (`with_env`) is rendered inline as `Environment=KEY=VALUE` in the *world-readable* unit
  (`/etc/systemd/system/<svc>.service`, 0644). A token/password/join-key belongs in
  `with_secret_env(key, value)`: it is written ONLY into the mode-0600 env file and never inline.
  A secret makes `with_env_file(path)` mandatory — `spec.validate()` (and `install_systemd`) reject
  a secret-without-env_file spec. The env file and the ed25519 `node_key` are both persisted via
  `systemd::write_private_file`, created 0600 from the start (no world-readable window).