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

129 lines
5.6 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
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](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`), 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](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`).