# Developing Steward ## Layout ``` cmd/stewardd/ the daemon: serve, init, user, check, backup, restore, config cmd/steward/ the CLI internal/ dhcp4/ DHCPv4 messages (RFC 2131/2132/3396) and the option catalog; no I/O model/ the configuration (State: settings, subnets, reservations), leases, users, history validate/ whole-configuration checks with stable codes (docs/validation.md) store/ SQLite (modernc.org/sqlite, no cgo): configuration, leases, history, users, tokens, sessions, audit engine/ configuration changes: edit a copy, diff, validate, save with history, undo dhcpd/ the DHCP server: sockets per interface, the protocol, address choice, leases, ping check, activity server/ the HTTP API and the web UI, authentication, the local socket api/, client/ JSON types and the Go client shared by server and CLI kea/ the Kea configuration and lease file importer auth/ passwords (argon2id), tokens, sessions, grants and roles config/ stewardd's YAML configuration version/ what a build reports web/static/ the web UI: plain ES modules, no build step, embedded with go:embed deploy/ systemd units, sysusers.d and tmpfiles.d files, Debian package scripts scripts/e2e.sh end-to-end test with real DHCP clients in Docker scripts/mkdeb.sh the Debian packages (make deb) scripts/logo.py the web UI's logo from artwork/logo.svg scripts/optionsdoc.py docs/options.md from the option catalog artwork/logo.svg the logo's master ``` [design.md](design.md) describes how the pieces fit. ## Building and testing ```sh make # bin/stewardd, bin/steward, bash completion make test # go vet, gofmt check, go test ./... make e2e # scripts/e2e.sh: needs Docker make deb # dist/steward__.deb and steward-cli_... ``` **Unit tests** cover the protocol without sockets. `dhcpd.Server.Handle` takes a parsed message and an interface name and returns the reply, with fake interfaces, a fake pinger and a fake clock (`internal/dhcpd/handle_test.go`). The API tests drive real DHCP exchanges through `Handle` too. **The e2e test** runs the real thing: * stewardd in a `debian:trixie` container on two Docker bridge networks (eth0 and eth1, like two VLANs); * busybox `udhcpc` and `dhcpcd` clients in Alpine containers. It checks: * addresses and options per interface, and reservations; * the ping check, against a container that squats on a pool address; * init-reboot, renewal and release with dhcpcd; * a client moved to another VLAN: NAK, then that VLAN's address; * a full pool and a disabled subnet; * history and undo, and leases across a restart; * the API with tokens, and backups. `KEEP=1` leaves the containers running; `DOCKER_HOST` points at another Docker daemon. Root isn't needed on the host. stewardd can't serve port 67 unprivileged, so a stewardd run by hand outside a container shows `permission denied` for its interfaces. The API and web UI still work, which is enough for UI work. The systemd sandbox is only tested by installing the package in a container that runs systemd (`--privileged /sbin/init`) and pointing a client container at it. ## The web UI Plain JavaScript modules under `internal/web/static/`, built with the same helpers as Jarl's: * All DOM goes through `h()` in `ui.js`, never `innerHTML`. * The Content-Security-Policy allows only the app's own scripts and styles, so no inline styles. Use a class in `style.css`, or set `el.style.prop`. * The files are embedded, so rebuild stewardd after changing them. The logo's master is `artwork/logo.svg`; `scripts/logo.py` writes `assets/logo.svg` (the favicon) and `assets/logo.js` (drawn inline by `brand.js`). The STEWARD wordmark is drawn by `wordmark()` in `brand.js`, in the letters of Jarl's wordmark. ## Conventions * Every user-visible change goes into `CHANGELOG.md` under "Unreleased", with the docs updated in the same change. * Errors say what is wrong and how to fix it, naming the object. * New validation rules get a stable code, a test in `internal/validate/validate_test.go` and a row in [validation.md](validation.md). * Configuration changes go through `engine.Apply`, never straight to the store, so they are validated, recorded and undoable. * Schema changes append a migration to `internal/store/store.go`; never edit a shipped one. * `gofmt` and `go vet` clean (`make test`). ## Releasing As for Jarl: 1. Move "Unreleased" in `CHANGELOG.md` under `## [] - `. A release candidate may leave it there. 2. Put the version in `VERSION`, and update the README's status line. 3. Run `make test && make e2e`. 4. Commit as "Release X", tag it `git tag -a vX -m "Steward X"`, and push. The tag starts `.gitea/workflows/release.yml`: * it checks the tag against `VERSION` and the CHANGELOG; * it runs `make test`; * it builds the tarballs, the CLI for each OS and the `.deb`s (amd64, arm64); * `scripts/gitea-release.sh` publishes the release, and uploads the `.deb`s to the registry: `stable`, or `testing` for a version with a hyphen. It needs the repository secret `RELEASE_TOKEN` (`write:repository`, `write:package`).