install — guide
Class:
guide· audience: a techie installing a product on a target. The what is ininstall-design.md; the why ininstall-reasoning.md.
f6 — one spec, any init, three lifecycle verbs (the whole flow)
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)
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)
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
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:
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:
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_fornever returnsNonetoday; theOptionreturn is kept for a future backend). systemd-system, systemd-user, OpenRC, SysV, runit, dinit, s6-rc, Kubernetes, OCI, Quadlet and Windows SCM — swapBackend::OpenRcabove forBackend::Runitand the SAMEComponentSpecrenders a/etc/sv/holger/runsv service directory,Backend::Dinitfor a/etc/dinit.d/holgertextual service description,Backend::S6Rcfor a/etc/s6-rc/source/holger/compiled-source definition dir,Backend::Ocifor apodman 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::Quadletfor a systemd/etc/containers/systemd/holger.containerunit thatpodman-systemdturns into aholger.serviceondaemon-reload(that reload IS the enable-at-boot step; no separatesystemctl enable), orBackend::WindowsServicefor a WiXC:\ProgramData\holger\holger.wxsservice fragment (via the MSIWindowsServiceSpecseam) driven bysc.exe create/start/query/stop— SCM is machine-scoped, env lives in the.wxs, andrestart_secmaps 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_specsurfaces data/log asDATA_DIR/LOG_DIRenv (there is no native systemdStateDirectory=); the dirs are provisioned explicitly byprepare_product.- Secrets go in
with_secret_env, neverwith_env. ASystemdServiceSpecnon-secret env (with_env) is rendered inline asEnvironment=KEY=VALUEin the world-readable unit (/etc/systemd/system/<svc>.service, 0644). A token/password/join-key belongs inwith_secret_env(key, value): it is written ONLY into the mode-0600 env file and never inline. A secret makeswith_env_file(path)mandatory —spec.validate()(andinstall_systemd) reject a secret-without-env_file spec. The env file and the ed25519node_keyare both persisted viasystemd::write_private_file, created 0600 from the start (no world-readable window).