Files
jarl/docs/development.md
T
abrown38andClaude Opus 5.5 a7d389be27 Web UI: Jarl logo and wordmark
Add the logo, a Viking jarl drawn like a woodcut whose spear-pointed crown
holds a DNS tree. It is generated by scripts/logo.py, which mirrors the
left half so the figure is exactly symmetrical, as logo.svg (white on a
Nordic-blue disc) and logo-white.svg. It links home in the header, sits on
the login page and is the favicon.

The login page shows an angular "JARL" wordmark drawn as SVG paths over a
rule with a "DNS" node, coloured from the theme. The header's "Jarl" uses
a self-hosted Cinzel Bold subset (SIL OFL); jarld serves .woff2 as
font/woff2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:19:51 +00:00

5.4 KiB

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
  config/             jarld's YAML configuration
deploy/               systemd unit, sysusers.d and tmpfiles.d files
contrib/acme.sh/      acme.sh DNS API hook
scripts/e2e.sh        end-to-end test against a real named

The main flow is in internal/engine/engine.go (applyOps): expand → replay → validate → render → check → install → reload → verify → record. concepts.md describes it from the outside.

Building and testing

make            # bin/jarld, bin/jarl (CGO_ENABLED=0, version from ./VERSION)
make test       # go vet, gofmt check, go test ./...
make e2e        # scripts/e2e.sh

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:

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.
  • 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.
  • 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.