Work on Nerthus.Config¶
Nerthus.Config is nerthus-config: the
tool that compares what the estate declares with what exists and what was last observed, and
converges hosts. It is a Python package with a Rust core, downstream of
Nerthus.Platform, and it holds no declarations of its own: those are
Nerthus.Infra. This guide is how to build and
test it on a laptop.
Set up¶
You need rustup and a credential for the group's package registry, because nerthus is
published only there.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
uv sync --frozen --group dev # with UV_INDEX_NERTHUS_USERNAME and _PASSWORD set
uv run nerthus-config doctor
The credential is a read-only deploy token, handed to uv as UV_INDEX_NERTHUS_USERNAME and
UV_INDEX_NERTHUS_PASSWORD from a file you source, never typed where a shell history keeps it.
Without rustup, maturin's build backend bootstraps an unpinned toolchain of its own; install rustup
so rust-toolchain.toml decides. On macOS, nerthus has no wheel and uv builds it from the
registry's source distribution with the same local toolchain.
Without a registry credential the failure reads as «nerthus does not exist» rather than «you hold
no token», because the registry redirects an unauthenticated read to pypi.org.
nerthus-config doctor answers three questions: is this machine's clock right, does the package
registry answer, and is the installed Platform the one this package pins. It never prints the value
it holds.
Run it against the fixture estate¶
uv run nerthus-config validate tests/fixtures/estate
uv run nerthus-config state hash --dir tests/fixtures/estate
tests/fixtures/estate is a synthetic estate with the shape of the dev one and none of its names;
every test runs against it. validate judges a declaration repository and the state/ tree beside
it. Read a plan explains what plan prints and its exit codes.
Tests¶
uv run pytest tests/test_plan.py # focused; the full suite is CI's
cargo test --no-default-features # the Rust core
cargo test on macOS needs the interpreter's library directory linked in. The test binary links
libpython3.13.dylib, and macOS strips DYLD_* from a shell's children, so without an rpath the
run dies with Reason: tried: … (no such file) and SIGABRT rather than a compile error:
LIBDIR=$(uv run python -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))")
RUSTFLAGS="-C link-arg=-Wl,-rpath,$LIBDIR" cargo test --no-default-features
Linux keeps LD_LIBRARY_PATH across the same boundary, so CI needs none of this.
Two coverage numbers are reported and they do not add up. cargo test --no-default-features drops
the extension module, so the FFI layer reads as uncovered in the crate figure; the Python suite
measures those lines, because every call it makes into the core goes through them.
The rules a change meets¶
state/has three writers, and they are branch names:apply/,import/anddrift/in Nerthus.Infra.nerthus-config state guardrefuses a human branch that changed a state file, and a task that needs a state tree seeds it throughimport/*.- The resource model is one table in
crates/nerthus-config-core/src/model.rs. The JSON Schema, the validator, the state writer and the generateddocs/resources.mdall read it, so a kind gains an attribute in one place;contract:diffregeneratescontracts/and that page and fails on a difference. - A cookbook step may not fetch anything unpinned, write a secret outside
/run/nerthusor/etc/nerthus, publish a container port, touch a path another role owns, or run acommandwithoutcreates:orunless:. Each rule has a control in the suite. - Every step has an arm for all four families it claims:
debian,ubuntu,alpine,almalinux. A step with an arm missing is not done. - Never touch a real host a change does not name, and run every command against one with
--dry-runfirst.
Where things are¶
crates/nerthus-config-core/ Rust: resource model, canonical state, plan engine, providers,
step types with family arms; the nerthus-config-agent binary
packages/nerthus_config/ Python: common vault providers state plan cookbooks hardening
minters alerts agent cli
contracts/ config-cli.yaml, inventory.schema.json: generated, committed, diffed
docs/ generated reference, pulled by this site
tests/fixtures/estate/ the synthetic estate every test runs against
Imports run downward: common imports Platform's nerthus.common and nerthus.secrets and nothing
else here; providers, state and plan import common and the Rust core; cookbooks and the
modules beside it import those; cli and agent import everything below. No adapter imports
another.