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>
125 lines
5.2 KiB
Markdown
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`).
|