- Daily database backups: jarl-backup.timer runs 'jarld backup', which writes jarl-<UTC time>.db into backup.dir, checks it before naming it, and prunes by the time in the name while always keeping the newest keep_at_least. New backup: section in jarl.yaml; the dashboard and 'jarl status' show the newest backup and warn when it is stale or failed. The Debian package enables the timer. - 'jarld restore [FILE | latest]': checks the backup, shows it next to the current database, keeps the current one aside and records the restore. 'jarld serve' holds a database lock, so a restore can't run under a live jarld and two jarlds can't share a database. - Token expiry warnings from 14 days ahead: dashboard, token lists, a daily log line, and a CLI warning via the Jarl-Token-Expires header; 'jarl token list --expiring'. - 'jarl zone serials' and the zone page's Name servers tab: compare the serial on a primary zone's secondaries (NS records, also-notify) or a secondary zone's primaries with this server's. - 'jarl zone delegation': for zones on the internet, check the parent's delegation and glue against the zone; an undelegated zone gets a note. - Server: Jarl/<version> header; CLI exit code 2 for failed checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
161 lines
8.2 KiB
Markdown
161 lines
8.2 KiB
Markdown
# Developing Jarl
|
|
|
|
## Layout
|
|
|
|
```
|
|
cmd/jarld/ daemon: serve, init, import, adopt, check, render, resync, backup, restore, user
|
|
cmd/jarl/ CLI
|
|
internal/
|
|
model/ configuration objects (State: views, ACLs, keys, zones, records, options) and changesets
|
|
dnsname/ DNS name handling: FQDNs, relative names, check-names rules, reverse zones
|
|
rr/ record data parsing/canonicalization (miekg/dns), TXT quoting, the record type catalog
|
|
namedconf/ named.conf lexer, parser and printer; address match lists
|
|
catalog/ every BIND 9.20 statement: grammar (from named.conf(5)) + ARM descriptions, embedded
|
|
validate/ whole-configuration validation and security lints
|
|
render/ State → named.conf, keys.conf and zone files, with a source map for error messages
|
|
bind/ named-checkconf, named-checkzone and rndc wrappers
|
|
engine/ changesets: expand requests into ops, replay, validate, render, check, deploy, reload, verify, revert;
|
|
import and adopt
|
|
importer/ reads an existing named.conf tree and its zone files
|
|
store/ SQLite (modernc.org/sqlite, no cgo): state, changesets, users, tokens, sessions, audit
|
|
auth/ argon2id passwords, tokens, sessions, grants and roles
|
|
server/ HTTP API, authentication, the web UI
|
|
stats/ query statistics: statistics-channel poller, response-log tailer, collector, separate SQLite store
|
|
web/static/ the web UI: plain ES modules, no build step, embedded with go:embed
|
|
api/, client/ JSON types and the Go client shared by server and CLI
|
|
acme/ ACME DNS-01 challenge records, shared by jarl acme and the httpreq endpoint
|
|
backup/ database backups: write (VACUUM INTO) and check, list, prune by the time in the name
|
|
nscheck/ DNS queries to other name servers: zone serials and the parent's delegation
|
|
config/ jarld's YAML configuration
|
|
deploy/ systemd units (jarld, the daily backup service and timer), sysusers.d and tmpfiles.d files
|
|
deploy/debian/ the Debian package's control template and postinst/prerm/postrm
|
|
contrib/acme.sh/ acme.sh DNS API hook
|
|
contrib/lego/ program for lego's exec DNS provider
|
|
scripts/e2e.sh end-to-end test against a real named
|
|
scripts/mkdeb.sh builds the Debian package (make deb)
|
|
scripts/logo.py traces artwork/logo.png into the web UI's logo (logo.svg, logo.js)
|
|
scripts/banner.py builds the README's banner from the web UI's logo and wordmark
|
|
artwork/ the logo's master image and the README's banner
|
|
```
|
|
|
|
The main flow is in `internal/engine/engine.go` (`applyOps`):
|
|
expand → replay → validate → render → check → install → reload → verify →
|
|
record. [concepts.md](concepts.md) describes it from the outside.
|
|
|
|
## Building and testing
|
|
|
|
```sh
|
|
make # bin/jarld, bin/jarl (CGO_ENABLED=0, version from ./VERSION),
|
|
# and bin/completion/{jarl,jarld} (bash completion)
|
|
make test # go vet, gofmt check, go test ./...
|
|
make e2e # scripts/e2e.sh
|
|
make deb # dist/jarl_<version>_<arch>.deb (DEB_ARCH=arm64 etc. cross-builds)
|
|
```
|
|
|
|
### The Debian package
|
|
|
|
`make deb` runs `scripts/mkdeb.sh`: it builds the binaries for
|
|
`DEB_ARCH`, installs them with `make install-files` into a staging tree
|
|
with Debian's paths (`PREFIX=/usr`, the unit in `/usr/lib/systemd/system`),
|
|
adds `deploy/debian/{postinst,prerm,postrm}` and a `control` file filled in
|
|
from `deploy/debian/control.in`, and packs it with `dpkg-deb
|
|
--root-owner-group`, so no root is needed. The version comes from
|
|
`./VERSION` (a `-` becomes `~`, so `0.2.0-rc1` sorts before `0.2.0`;
|
|
override with `DEB_VERSION`), and the maintainer from your git name and
|
|
email (override with `DEB_MAINTAINER`). The package ships no files in
|
|
`/etc`: postinst writes `/etc/jarl/jarl.yaml` once, so upgrades never touch
|
|
it.
|
|
|
|
To try the package without installing it on your machine:
|
|
|
|
```sh
|
|
docker run --rm -it -v "$PWD/dist:/pkg:ro" debian:trixie \
|
|
sh -c 'apt-get update && apt-get install -y /pkg/jarl_*_amd64.deb && bash'
|
|
```
|
|
|
|
### Shell completion
|
|
|
|
The completion scripts are generated by cobra (`jarl completion bash`).
|
|
`cmd/jarl/complete.go` adds value completion: it reads each command's usage
|
|
line (`add ZONE NAME TYPE DATA...`) and completes the placeholders it knows
|
|
(`ZONE`, `VIEW`, `ACL`, `USER`, TSIG `KEY`, `TYPE`, server and group
|
|
names), so a new command whose usage line uses these names gets completion
|
|
without extra code. Try it without a shell:
|
|
`jarl __complete record add ''`.
|
|
|
|
### Tests against real BIND
|
|
|
|
The render, engine and adopt tests run the real `named-checkconf`,
|
|
`named-checkzone`, `named`, `rndc` and `dig` when they can find them, and
|
|
skip otherwise. They are found through `JARL_BIND_ROOT` (an extracted
|
|
package tree) or on the system. To get them without root or AppArmor in the
|
|
way, extract the packages:
|
|
|
|
```sh
|
|
mkdir -p ~/bind-root/debs && cd ~/bind-root/debs
|
|
apt-get download bind9 bind9-utils bind9-libs bind9-dnsutils \
|
|
liblmdb0 libmaxminddb0 liburcu8t64 libuv1t64 libjemalloc2 libjson-c5 libnghttp2-14
|
|
for f in *.deb; do dpkg -x "$f" ~/bind-root; done
|
|
cd -
|
|
JARL_BIND_ROOT=~/bind-root make test
|
|
JARL_BIND_ROOT=~/bind-root make e2e
|
|
```
|
|
|
|
The tests start named unprivileged on random high ports in temporary
|
|
directories. On Debian/Ubuntu, AppArmor confines the installed
|
|
`/usr/sbin/named` to BIND's own directories, so the tests that *start*
|
|
named (engine integration tests, `make e2e`) need the extracted copy. The
|
|
checker tests work with the installed `bind9-utils` too.
|
|
|
|
`scripts/e2e.sh` builds a legacy BIND setup, starts named, runs
|
|
`jarld init`, `import`, `adopt` and `serve`, then drives every CLI command
|
|
(records, batches, changesets, views, ACLs, TSIG, options, users, tokens,
|
|
ACME hooks, audit) and checks the answers with `dig`. `KEEP=1` leaves named
|
|
and jarld running afterwards. The output says where; set `JARL_CLI_CONFIG`
|
|
to that directory's `cli.yaml` to keep using the CLI against it.
|
|
|
|
### The web UI
|
|
|
|
The UI is plain JavaScript modules under `internal/web/static/`, with no
|
|
framework and no build step. The logo's master is `artwork/logo.png` (blue
|
|
`#1d5287` and white on a transparent background). `scripts/logo.py` traces it
|
|
with potrace (needs `potrace` and ImageMagick) into `assets/logo.svg` (the
|
|
favicon) and `assets/logo.js` (the same paths); change the PNG and rerun the
|
|
script rather than editing those files. The header and
|
|
login page draw the logo inline from `logo.js` (`logo()` in `brand.js`), so
|
|
it appears with the page instead of popping in after a separate download.
|
|
Static files are served with `Cache-Control: no-cache` and a content ETag,
|
|
so a reload revalidates them with cheap 304 answers. The
|
|
"JARL" wordmark is drawn as SVG paths by `wordmark()` in `assets/brand.js`
|
|
(over a rule on the login page, the letters alone in the header), so the UI
|
|
needs no web font and the wordmark takes the theme's text and accent colors.
|
|
The README's banner, `artwork/banner.svg`, is the login page's logo and
|
|
wordmark, generated by `scripts/banner.py` from `logo.js` and `brand.js`
|
|
(`logo.py` runs it; run it yourself after editing the wordmark).
|
|
All DOM is built with the `h()` helper in
|
|
`ui.js`, which never uses `innerHTML`, so record data and comments can't
|
|
inject markup. The Content-Security-Policy only allows the app's own
|
|
scripts and styles. To work on it, run jarld and reload the browser: the
|
|
files are embedded at build time, so rebuild jarld after changes.
|
|
|
|
## Conventions
|
|
|
|
* **Every user-visible change goes into `CHANGELOG.md`** (under
|
|
"Unreleased"), and the docs in `docs/` are updated in the same change.
|
|
* Errors that reach users 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).
|
|
* New API operations go through the engine (`internal/engine/expand.go`,
|
|
`ops.go`) so they get permissions, validation, history and revert for
|
|
free, and are documented in [api.md](api.md).
|
|
* `gofmt` and `go vet` clean (`make vet`).
|
|
|
|
## Releasing
|
|
|
|
1. Move the "Unreleased" section of `CHANGELOG.md` under the new version
|
|
and date.
|
|
2. Put the version in `VERSION`.
|
|
3. `make test && JARL_BIND_ROOT=... make e2e && make dist`.
|