Skip to content

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/ and drift/ in Nerthus.Infra. nerthus-config state guard refuses a human branch that changed a state file, and a task that needs a state tree seeds it through import/*.
  • The resource model is one table in crates/nerthus-config-core/src/model.rs. The JSON Schema, the validator, the state writer and the generated docs/resources.md all read it, so a kind gains an attribute in one place; contract:diff regenerates contracts/ and that page and fails on a difference.
  • A cookbook step may not fetch anything unpinned, write a secret outside /run/nerthus or /etc/nerthus, publish a container port, touch a path another role owns, or run a command without creates: or unless:. 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-run first.

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.