Files
steward/docs/development.md
T
abrown38andClaude Opus 5.5 3c0808591f Steward 0.1.0 (in development): a DHCPv4 server with a web UI, API and CLI
Subnets served per interface, pools, reservations, options at three
levels, leases in SQLite saved before the ACK, ping and decline conflict
checks, validated changes with history and undo, users and tokens, an
audit log, a Kea importer, a sandboxed systemd unit and Debian packages.
Tested with real udhcpc and dhcpcd clients in Docker (scripts/e2e.sh).

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

125 lines
5.2 KiB
Markdown

# 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_<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`). 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 `## [<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 `.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`).