Salmon ops patterns
On Fri, 25 Sep 2026, by @lucasdicioccio, 580 words, 1 code snippets, 1 links, 0images.
Generated from resources/salmon-ops-patterns.md — the repository is the canonical source, and may be ahead of this page.
Salmon ops patterns
This is a companion to howto-ops.md: where that doc is a
per-primitive cookbook (how to write one node, one Op, one CLI binary),
this one collects recurring shapes worth reusing across recipes — patterns
that combine several of those primitives to solve a problem that comes up
more than once. Read howto-ops.md first if a term here (Op, Track,
seed/Spec, check) is unfamiliar.
Pattern: one-time privileged bootstrap, then unprivileged forever after
Problem. A recipe needs some action that only root can perform —
granting a Linux capability (setcap), installing an OS package, chown-ing
a path to a different user — but you don’t want every routine invocation
of the binary to require root just because one op deep in its graph does.
Requiring sudo for everything is both a security smell (broader blast
radius than needed) and an ergonomics problem (can’t run unattended as a
normal user, breaks non-interactive/CI invocations).
Shape. Split the privileged, one-time setup from the routine, repeated
work as two different seeds of the same binary, using the existing
seed → spec → ops CLI protocol (howto-ops.md §9):
sudo my-salmon config bootstrap | sudo my-salmon run up # once per machine
my-salmon config <routine-seed> | my-salmon run up # every other time, no sudo
- The
bootstrapseed’sOpgraph contains only the privileged, machine-wide setup: capability grants, package installs, ownership fixes. Nothing routine (booting a VM, writing a recipe’s actual state) belongs in it. - Every other seed’s graph assumes that setup already happened and never needs privilege itself.
- This only works if every op in the bootstrap graph is genuinely
idempotent (
howto-ops.md§4) — re-runningbootstrapundersudolater (e.g. after a package upgrade wipes a capability) must be a safe, cheap no-op viacheck, not a hazard. If you can’t make the bootstrap step idempotent, this pattern isn’t safe to recommend to users as “run it whenever” — treat it as a real migration instead. - The bootstrap seed usually needs to know which unprivileged user/group
future invocations will run as (to
chown/grant-to the right identity) — take that as an explicit seed argument rather than inferring it from$SUDO_USER/similar, sosudo my-salmon config bootstrap --for aliceis unambiguous about who it’s provisioning for.
Worked example. Salmon.Builtin.Nodes.Capabilities.grantCapabilities
(salmon-ops/src/Salmon/Builtin/Nodes/Capabilities.hs) is exactly this
kind of bootstrap-only op: it needs CAP_SETFCAP (in practice, root) to
run, but its check (getcap-based) makes every subsequent run a
no-op — see its haddock. The qemu test tier
(specs/qemu-test-vms-progress.md §0.2) combines it with
Salmon.Builtin.Nodes.User.chown into one bootstrap graph, currently
exposed as a standalone fixture binary
(salmon-ops/fixtures/QemuHostSetupFixture.hs) rather than a real
bootstrap seed on a unified CLI binary — folding it into the latter
shape (a proper Seed = Bootstrap User | BootVm VmSpec | ...) is the
natural next step if/when this tier grows a real production binary instead
of remaining test-only support code. salmon-toy-qemu-pg-ha
(salmon-apps/src/QemuPgHaToy.hs) already has the two-seed shape — a
prereqs seed run once under sudo (root filesystems, and /etc/ssh
handed to the unprivileged user), then up/client without it — though
its two capability grants are still a documented manual setcap, not a
node in prereqs.
Related, but not this pattern: if the privileged step isn’t safely
re-runnable (e.g. a real schema migration, a one-shot data backfill), don’t
reach for “bootstrap seed” — that’s ordinary migration territory
(Salmon.Builtin.Migrations reads migration files into a graph;
SreBox.PostgresMigrations.migrate runs each as a psql script). Be aware
that salmon records nothing about which migrations were applied: there is no
once-ever guarantee, and every run up runs every migration again. So write
each migration to be idempotent (CREATE ... IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, a guarded DO $$ ... $$ block, a backfill with a WHERE that
selects only unfinished rows), like any other up.