Files
steward/docs/development.md
T
abrown38andClaude Opus 5.5 fbad6ef3c4 README banner: the logo above the STEWARD wordmark
scripts/banner.py builds artwork/banner.svg from the web UI's logo and
wordmark, as Jarl's does; scripts/logo.py runs it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:28:20 +01:00

5.6 KiB

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
scripts/banner.py     the README's banner from the web UI's logo and wordmark
artwork/logo.svg      the logo's master; artwork/banner.svg the README's banner

design.md describes how the pieces fit.

Building and testing

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_<version>_<arch>.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), and then runs scripts/banner.py. The STEWARD wordmark is drawn by wordmark() in brand.js, in the letters of Jarl's wordmark. artwork/banner.svg, the README's banner, is the login page's logo and wordmark, generated by scripts/banner.py. It draws the rule with the same numbers as brand.js, so change both together.

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.
  • 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 ## [<version>] - <date>. 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 .debs (amd64, arm64);
  • scripts/gitea-release.sh publishes the release, and uploads the .debs to the registry: stable, or testing for a version with a hyphen.

It needs the repository secret RELEASE_TOKEN (write:repository, write:package).