# Developing Jarl ## Layout ``` cmd/jarld/ daemon: serve, init, import, adopt, check, render, resync, backup, 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 config/ jarld's YAML configuration deploy/ systemd unit, 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) ``` 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__.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 is `assets/logo.svg` (a white figure on a Nordic-blue `#2f5fb3` disc) with an all-white variant `assets/logo-white.svg`. Both are generated by `scripts/logo.py` (shapes are given for the left half and mirrored); edit the script, not the SVGs. The login page's "JARL" wordmark is drawn as SVG paths in `assets/brand.js`, so it needs no font and takes the theme's text and accent colors. The header's "Jarl" uses a Latin subset of Cinzel Bold in `assets/fonts/` (SIL OFL; keep `Cinzel-OFL.txt` next to it). 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`.