hyperhive/swarmctl
Repository files (latest commit first)
Filename Latest commit message Latest commit date
iris 7fc426b4dd swarmctl: add CLI reference docs, same pattern as hivectl
Adds swarmctl markdown-docs (a hidden Verb, same clap-markdown +
hide=true shape as hivectl markdown-docs) and generates
docs/tools/swarmctl-cli.md from it. Wires a swarmctl-docs freshness
check into nix/checks.nix, same shape as hivectl-docs, diffing against
packages.swarmctl.

One real gotcha: PathArgs::resolve() reads required
SWARMCTL_AUTHELIA_* deployment env vars and errors if unset -
swarmctl markdown-docs must not go through that path (it needs none of
those vars, and the docs build runs it outside any real deployment).
Restructured main() so resolve() only runs for the User arm, not
unconditionally before the match.

Also links the new doc from docs/tools/README.md (new 'for the swarm
operator' section), CLAUDE.md's swarmctl bullet, and
docs/conventions.md's flake-check list.

Verified: cargo check/clippy -D warnings/test/fmt -p swarmctl all
clean; swarmctl markdown-docs diffs clean against the committed doc
(checked against both a plain cargo build and the actual nix build.
#swarmctl output); scripts/check-issue-refs.sh clean.
2026-08-11 21:55:56 +02:00
..
src swarmctl: add CLI reference docs, same pattern as hivectl 2026-08-11 21:55:56 +02:00
Cargo.toml swarmctl: add CLI reference docs, same pattern as hivectl 2026-08-11 21:55:56 +02:00
README.md feat(#3089): add swarmctl and a user-add verb for the swarm's SSO 2026-08-10 21:48:45 +02:00

swarmctl

Swarm-level operator CLI. Runs as root on the host running swarm-controller, and acts on that host directly.

Distinct from hivectl, which drives one hive's hive-c0re over its admin socket. This crate does not link swarm-controller, for the same reason hivectl does not link hive-c0re.

Why root, and why no socket

The first verb writes authelia's users database. Making that write rootless was examined and rejected — relocating the file only turns a write problem into a read problem. Move users.yml into a directory the controller owns and the controller can write it, but authelia then has to read it across the same boundary in the other direction. Making that work needs either a hand-pinned gid (the container's uids are allocated inside it, at activation — see the uid-assignment issue) or world-readable password hashes. Both are worse than root.

So there is no socket, no HTTP route and no privileged helper here. When a verb has to run as a non-root user or from another host, the answer is a group-gated admin socket, separate from the controller's 0666 gateway-facing one — not a widening of what root does here.

Two files, one of them authoritative

  • users.json — canonical, ours, JSON.
  • users.yml — a rendered artifact for authelia. Written, never read back.

The split is what lets this crate work without a YAML parser: the workspace has none, and adding one costs a crates.io fetch, a lock update and a vendor hash for a schema we fully control and only ever emit.

The shortcut of writing JSON into the .yml (JSON being a subset of YAML) is deliberately not taken: authelia refuses to start on a users file it cannot parse, so that file fronts the whole SSO provider's boot, and "almost certainly parses" is not a claim worth betting a boot on without running it.

Configuration

Every path comes from the nix module that installs the binary, because every one is derived from an option that module owns. They are required rather than defaulted — a default would be an address we hope points at something, and one that resolves cleanly to the wrong place is worse than an error.

variable what
SWARMCTL_AUTHELIA_BIN the configured authelia; argon2 params must match the verifier's
SWARMCTL_AUTHELIA_USERS_FILE host-side path of the users database
SWARMCTL_AUTHELIA_MACHINE container name, for systemctl -M
SWARMCTL_AUTHELIA_UNIT authelia's unit inside that container
SWARMCTL_STORE canonical store (defaults to the controller's state dir)

Usage

# swarmctl user add mara --display-name "Mara" --group admins

The password is generated by authelia (crypto hash generate argon2 --random) and printed once. It is never passed on a command line: /proc/<pid>/cmdline is world-readable, so a password in argv is readable by any local process for the lifetime of the call.