Add a Debian package and bash completion

make deb builds dist/jarl_<version>_<arch>.deb with dpkg-deb alone (no
root), for the host or another architecture (DEB_ARCH=arm64). The package
creates the jarl user and directories and an example jarl.yaml on the
first install, restarts a running jarld on upgrades, never touches
/etc/jarl/jarl.yaml, stops jarld on removal, and keeps Jarl's data even
on purge.

make install and the package install bash completion for jarl and jarld.
Besides commands and flags it completes server and group names from the
CLI configuration, zone, view, ACL, TSIG key and user names from the
server, and record types. make dist includes the completion scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-08 15:10:57 +00:00
1 parent 0f2c318aaf
commit 352c9cb394
13 files changed
+550 -25

No files matched your search

+11
View File
@@ -24,6 +24,17 @@ The first version, to be released as 0.1.0.
the web UI. Networks smaller than a /24 explain that they belong in the the web UI. Networks smaller than a /24 explain that they belong in the
enclosing /24's zone. enclosing /24's zone.
- docs/operations.md: setting up a secondary server with its own jarld. - docs/operations.md: setting up a secondary server with its own jarld.
- A Debian package: `make deb` builds `dist/jarl_<version>_<arch>.deb` with
only `dpkg-deb` (no root), for the host or any other architecture
(`DEB_ARCH=arm64`). Installing it creates the `jarl` user and directories
and an example `/etc/jarl/jarl.yaml`; upgrades restart a running jarld and
never touch `/etc/jarl/jarl.yaml`; removing it stops jarld, and even a
purge keeps Jarl's database and the files named uses.
- Bash completion for `jarl` and `jarld`, installed by `make install` and
the Debian package. Besides commands and flags, it completes server and
group names from the CLI configuration, zone, view, ACL, TSIG key and
user names from the server, and record types.
- `make dist` tarballs include the completion scripts (`completion/`).
- Token rotation: `jarl token rotate`, the Rotate button in the web UI and - Token rotation: `jarl token rotate`, the Rotate button in the web UI and
`POST /api/v1/tokens/{id}/rotate` give a token a new secret while it `POST /api/v1/tokens/{id}/rotate` give a token a new secret while it
keeps its ID, name, user and grants. The old secret stops at once or after keeps its ID, name, user and grants. The old secret stops at once or after
+51 -19
View File
@@ -1,11 +1,13 @@
# Jarl build and install. # Jarl build and install.
# #
# make build bin/jarld and bin/jarl # make build bin/jarld and bin/jarl (and their bash completion)
# make test unit tests (integration tests run when BIND is found; # make test unit tests (integration tests run when BIND is found;
# set JARL_BIND_ROOT, see docs/development.md) # set JARL_BIND_ROOT, see docs/development.md)
# make e2e end-to-end test against a real named (scripts/e2e.sh) # make e2e end-to-end test against a real named (scripts/e2e.sh)
# sudo make install install binaries, systemd unit, sysusers/tmpfiles # sudo make install install binaries, bash completion, systemd unit,
# files and an example /etc/jarl/jarl.yaml # sysusers/tmpfiles files and an example /etc/jarl/jarl.yaml
# make deb a Debian package in dist/ (DEB_ARCH=arm64 for another
# architecture); install it with 'sudo apt install ./dist/jarl_*.deb'
# make dist static binaries for linux/amd64 and linux/arm64 in dist/ # make dist static binaries for linux/amd64 and linux/arm64 in dist/
VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev) VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev)
@@ -16,18 +18,36 @@ export CGO_ENABLED := 0
PREFIX ?= /usr/local PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin BINDIR ?= $(PREFIX)/bin
DOCDIR ?= $(PREFIX)/share/doc/jarl DOCDIR ?= $(PREFIX)/share/doc/jarl
# bash-completion also looks in /usr/local/share (XDG_DATA_DIRS).
BASHCOMPDIR ?= $(PREFIX)/share/bash-completion/completions
UNITDIR ?= /etc/systemd/system UNITDIR ?= /etc/systemd/system
SYSUSERSDIR ?= /usr/lib/sysusers.d SYSUSERSDIR ?= /usr/lib/sysusers.d
TMPFILESDIR ?= /usr/lib/tmpfiles.d TMPFILESDIR ?= /usr/lib/tmpfiles.d
DESTDIR ?= DESTDIR ?=
.PHONY: all build test vet e2e install uninstall dist clean # The binaries install-files copies (make deb points these at other
# architectures' builds).
JARLD_BIN ?= bin/jarld
JARL_BIN ?= bin/jarl
# make deb: the package's architecture (Debian name) and version.
DEB_ARCH ?= $(shell dpkg --print-architecture 2>/dev/null || go env GOARCH)
DEB_VERSION ?= $(subst -,~,$(VERSION))
.PHONY: all build completion test vet e2e install install-files uninstall deb dist clean
all: build all: build
build: build:
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o bin/jarld ./cmd/jarld go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o bin/jarld ./cmd/jarld
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o bin/jarl ./cmd/jarl go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o bin/jarl ./cmd/jarl
$(MAKE) --no-print-directory completion
# Bash completion scripts, generated by the binaries themselves.
completion:
@mkdir -p bin/completion
bin/jarl completion bash > bin/completion/jarl
bin/jarld completion bash > bin/completion/jarld
vet: vet:
go vet ./... go vet ./...
@@ -39,19 +59,7 @@ test: vet
e2e: build e2e: build
scripts/e2e.sh scripts/e2e.sh
install: build install: build install-files
install -D -m 0755 bin/jarld $(DESTDIR)$(BINDIR)/jarld
install -D -m 0755 bin/jarl $(DESTDIR)$(BINDIR)/jarl
install -D -m 0644 deploy/systemd/jarl.service $(DESTDIR)$(UNITDIR)/jarl.service
sed -i 's#/usr/local/bin/jarld#$(BINDIR)/jarld#; s#/usr/local/share/doc/jarl#$(DOCDIR)#' $(DESTDIR)$(UNITDIR)/jarl.service
install -D -m 0644 deploy/sysusers.d/jarl.conf $(DESTDIR)$(SYSUSERSDIR)/jarl.conf
install -D -m 0644 deploy/tmpfiles.d/jarl.conf $(DESTDIR)$(TMPFILESDIR)/jarl.conf
install -D -m 0644 README.md $(DESTDIR)$(DOCDIR)/README.md
install -D -m 0644 CHANGELOG.md $(DESTDIR)$(DOCDIR)/CHANGELOG.md
install -d $(DESTDIR)$(DOCDIR)/docs $(DESTDIR)$(DOCDIR)/contrib/acme.sh $(DESTDIR)$(DOCDIR)/contrib/lego
install -m 0644 docs/*.md $(DESTDIR)$(DOCDIR)/docs/
install -m 0644 contrib/acme.sh/dns_jarl.sh $(DESTDIR)$(DOCDIR)/contrib/acme.sh/
install -m 0755 contrib/lego/jarl-lego.sh $(DESTDIR)$(DOCDIR)/contrib/lego/
@if [ ! -e $(DESTDIR)/etc/jarl/jarl.yaml ]; then \ @if [ ! -e $(DESTDIR)/etc/jarl/jarl.yaml ]; then \
install -d -m 0750 $(DESTDIR)/etc/jarl; \ install -d -m 0750 $(DESTDIR)/etc/jarl; \
bin/jarld config --example > $(DESTDIR)/etc/jarl/jarl.yaml; \ bin/jarld config --example > $(DESTDIR)/etc/jarl/jarl.yaml; \
@@ -63,17 +71,41 @@ install: build
@echo " chgrp jarl /etc/jarl /etc/jarl/jarl.yaml && systemctl daemon-reload" @echo " chgrp jarl /etc/jarl /etc/jarl/jarl.yaml && systemctl daemon-reload"
@echo "Then follow docs/install.md (init, import, adopt, start)." @echo "Then follow docs/install.md (init, import, adopt, start)."
# Everything but /etc/jarl/jarl.yaml (shared by install and deb).
install-files:
install -D -m 0755 $(JARLD_BIN) $(DESTDIR)$(BINDIR)/jarld
install -D -m 0755 $(JARL_BIN) $(DESTDIR)$(BINDIR)/jarl
install -D -m 0644 bin/completion/jarl $(DESTDIR)$(BASHCOMPDIR)/jarl
install -D -m 0644 bin/completion/jarld $(DESTDIR)$(BASHCOMPDIR)/jarld
install -D -m 0644 deploy/systemd/jarl.service $(DESTDIR)$(UNITDIR)/jarl.service
sed -i 's#/usr/local/bin/jarld#$(BINDIR)/jarld#; s#/usr/local/share/doc/jarl#$(DOCDIR)#' $(DESTDIR)$(UNITDIR)/jarl.service
install -D -m 0644 deploy/sysusers.d/jarl.conf $(DESTDIR)$(SYSUSERSDIR)/jarl.conf
install -D -m 0644 deploy/tmpfiles.d/jarl.conf $(DESTDIR)$(TMPFILESDIR)/jarl.conf
install -D -m 0644 README.md $(DESTDIR)$(DOCDIR)/README.md
install -D -m 0644 CHANGELOG.md $(DESTDIR)$(DOCDIR)/CHANGELOG.md
install -d $(DESTDIR)$(DOCDIR)/docs $(DESTDIR)$(DOCDIR)/contrib/acme.sh $(DESTDIR)$(DOCDIR)/contrib/lego
install -m 0644 docs/*.md $(DESTDIR)$(DOCDIR)/docs/
install -m 0644 contrib/acme.sh/dns_jarl.sh $(DESTDIR)$(DOCDIR)/contrib/acme.sh/
install -m 0755 contrib/lego/jarl-lego.sh $(DESTDIR)$(DOCDIR)/contrib/lego/
uninstall: uninstall:
rm -f $(DESTDIR)$(BINDIR)/jarld $(DESTDIR)$(BINDIR)/jarl $(DESTDIR)$(UNITDIR)/jarl.service \ rm -f $(DESTDIR)$(BINDIR)/jarld $(DESTDIR)$(BINDIR)/jarl $(DESTDIR)$(UNITDIR)/jarl.service \
$(DESTDIR)$(BASHCOMPDIR)/jarl $(DESTDIR)$(BASHCOMPDIR)/jarld \
$(DESTDIR)$(SYSUSERSDIR)/jarl.conf $(DESTDIR)$(TMPFILESDIR)/jarl.conf $(DESTDIR)$(SYSUSERSDIR)/jarl.conf $(DESTDIR)$(TMPFILESDIR)/jarl.conf
rm -rf $(DESTDIR)$(DOCDIR) rm -rf $(DESTDIR)$(DOCDIR)
@echo "Kept /etc/jarl, /var/lib/jarl and the files in /etc/bind/jarl and /var/lib/bind/jarl." @echo "Kept /etc/jarl, /var/lib/jarl and the files in /etc/bind/jarl and /var/lib/bind/jarl."
dist: # A Debian package, built with dpkg-deb (no root needed). The package's
# scripts are in deploy/debian; scripts/mkdeb.sh explains the layout.
deb: build
DEB_ARCH='$(DEB_ARCH)' DEB_VERSION='$(DEB_VERSION)' VERSION='$(VERSION)' \
LDFLAGS='$(LDFLAGS)' GOFLAGS='$(GOFLAGS)' scripts/mkdeb.sh
dist: build
for arch in amd64 arm64; do \ for arch in amd64 arm64; do \
GOOS=linux GOARCH=$$arch go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o dist/jarl-$(VERSION)-linux-$$arch/jarld ./cmd/jarld && \ GOOS=linux GOARCH=$$arch go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o dist/jarl-$(VERSION)-linux-$$arch/jarld ./cmd/jarld && \
GOOS=linux GOARCH=$$arch go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o dist/jarl-$(VERSION)-linux-$$arch/jarl ./cmd/jarl && \ GOOS=linux GOARCH=$$arch go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o dist/jarl-$(VERSION)-linux-$$arch/jarl ./cmd/jarl && \
cp -r README.md CHANGELOG.md docs deploy contrib dist/jarl-$(VERSION)-linux-$$arch/ && \ cp -r README.md CHANGELOG.md docs deploy contrib bin/completion dist/jarl-$(VERSION)-linux-$$arch/ && \
tar -C dist -czf dist/jarl-$(VERSION)-linux-$$arch.tar.gz jarl-$(VERSION)-linux-$$arch || exit 1; \ tar -C dist -czf dist/jarl-$(VERSION)-linux-$$arch.tar.gz jarl-$(VERSION)-linux-$$arch || exit 1; \
done done
+1 -3
View File
@@ -64,9 +64,7 @@ On a Debian/Ubuntu BIND 9.20 server ([install.md](docs/install.md) has the
details): details):
```sh ```sh
make && sudo make install make deb && sudo apt install ./dist/jarl_*.deb # or: make && sudo make install, see install.md
sudo systemd-sysusers jarl.conf && sudo systemd-tmpfiles --create jarl.conf
sudo chgrp jarl /etc/jarl/jarl.yaml
sudo -u jarl jarld init --token # first admin + a CLI token sudo -u jarl jarld init --token # first admin + a CLI token
sudo -u jarl jarld import # read the current named.conf and zones sudo -u jarl jarld import # read the current named.conf and zones
sudo jarld adopt # switch named over (keeps a backup, rolls back on failure) sudo jarld adopt # switch named over (keeps a backup, rolls back on failure)
+221
View File
@@ -0,0 +1,221 @@
package main
// Shell completion. Cobra completes commands and flags by itself
// ('jarl completion bash'); this adds the values: server and group names
// from the configuration file, and zone, view, ACL, TSIG key, user and
// record type names from the server. Lookups on the server use the
// configured token and give up quickly, completing nothing, when the
// server can't be reached.
import (
"strings"
"time"
"github.com/spf13/cobra"
"jarl/internal/api"
"jarl/internal/rr"
)
const completeTimeout = 3 * time.Second
// setupCompletion adds value completion to the command tree.
func setupCompletion(root *cobra.Command) {
root.RegisterFlagCompletionFunc("server", completeServers(true))
root.RegisterFlagCompletionFunc("output", cobra.FixedCompletions([]string{"table", "json", "yaml"}, cobra.ShellCompDirectiveNoFileComp))
var walk func(c *cobra.Command)
walk = func(c *cobra.Command) {
for _, name := range []string{"view", "user", "zone"} {
if c.LocalFlags().Lookup(name) != nil {
c.RegisterFlagCompletionFunc(name, completeFromServer(name))
}
}
if c.ValidArgsFunction == nil && c.Runnable() {
if fn := argCompletion(c); fn != nil {
c.ValidArgsFunction = fn
}
}
for _, sub := range c.Commands() {
walk(sub)
}
}
walk(root)
}
// argCompletion completes a command's arguments from the placeholders in
// its usage line ("add ZONE NAME TYPE DATA...").
func argCompletion(c *cobra.Command) cobra.CompletionFunc {
fields := strings.Fields(c.Use)
if len(fields) < 2 {
return nil
}
params := fields[1:]
parent := ""
if c.HasParent() {
parent = c.Parent().Name()
}
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) >= len(params) && !strings.HasSuffix(params[len(params)-1], "...") {
return nil, cobra.ShellCompDirectiveNoFileComp
}
p := params[min(len(args), len(params)-1)]
p = strings.Trim(strings.TrimSuffix(p, "..."), "[]")
if i := strings.Index(p, "|"); i >= 0 {
p = p[i+1:] // "[ID|NAME]": complete names
}
switch {
case p == "FILE":
return nil, cobra.ShellCompDirectiveDefault
case p == "ZONE":
return lookup(cmd, "zone")
case p == "VIEW":
return lookup(cmd, "view")
case p == "ACL":
return lookup(cmd, "acl")
case p == "USER":
return lookup(cmd, "user")
case p == "KEY" && parent == "tsig":
return lookup(cmd, "tsig")
case p == "TYPE":
var out []cobra.Completion
for _, t := range rr.Types() {
out = append(out, cobra.CompletionWithDesc(t.Type, t.Description))
}
return out, cobra.ShellCompDirectiveNoFileComp
case p == "NAME" && (parent == "server" || parent == "config" && cmd.Name() == "use"):
return completeServers(false)(cmd, args, toComplete)
case p == "NAME" && parent == "group" && len(args) == 0:
return completeGroups(cmd)
case p == "MEMBER":
all, d := completeServers(true)(cmd, args, toComplete)
var out []cobra.Completion
for _, c := range all {
name, _, _ := strings.Cut(c, "\t")
if name != "all" && name != args[0] {
out = append(out, c)
}
}
return out, d
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
// completeServers completes server names (and with groups, group names
// and "all") from the configuration file.
func completeServers(groups bool) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
cfg, err := loadCLIConfig()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
// After a comma, complete the next item of a list.
prefix := ""
if i := strings.LastIndex(toComplete, ","); i >= 0 {
prefix = toComplete[:i+1]
}
var out []cobra.Completion
for _, name := range sortedKeys(cfg.Servers) {
out = append(out, cobra.CompletionWithDesc(prefix+name, cfg.Servers[name].URL))
}
if groups {
for _, name := range sortedKeys(cfg.Groups) {
out = append(out, cobra.CompletionWithDesc(prefix+name, "group: "+strings.Join(cfg.Groups[name], ", ")))
}
if len(cfg.Servers) > 0 {
out = append(out, cobra.CompletionWithDesc(prefix+"all", "every server"))
}
}
return out, cobra.ShellCompDirectiveNoFileComp
}
}
func completeGroups(cmd *cobra.Command) ([]cobra.Completion, cobra.ShellCompDirective) {
cfg, err := loadCLIConfig()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return sortedKeys(cfg.Groups), cobra.ShellCompDirectiveNoFileComp
}
func completeFromServer(kind string) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
return lookup(cmd, kind)
}
}
// lookup asks the server for names of a kind of object.
func lookup(cmd *cobra.Command, kind string) ([]cobra.Completion, cobra.ShellCompDirective) {
none := cobra.ShellCompDirectiveNoFileComp
c, err := newClient()
if err != nil {
return nil, none
}
c.HTTP.Timeout = completeTimeout
seen := map[string]bool{}
var out []cobra.Completion
addName := func(name, desc string) {
if name != "" && !seen[name] {
seen[name] = true
out = append(out, cobra.CompletionWithDesc(name, desc))
}
}
switch kind {
case "zone":
var zones []api.Zone
if c.Get(ctx(), "/zones", nil, &zones) != nil {
return nil, none
}
// A zone in several views is one name; its description lists them.
views := map[string][]string{}
for _, z := range zones {
name := dot(z.Name)
if z.View != "" {
views[name] = append(views[name], z.View)
}
addName(name, z.Type)
}
for i, c := range out {
if vs := views[c[:strings.IndexByte(c, '\t')]]; len(vs) > 0 {
label := "view "
if len(vs) > 1 {
label = "views "
}
out[i] = c + ", " + label + strings.Join(vs, ", ")
}
}
case "view":
var views []api.View
if c.Get(ctx(), "/views", nil, &views) != nil {
return nil, none
}
for _, v := range views {
addName(v.Name, v.Comment)
}
case "acl":
var acls []api.ACL
if c.Get(ctx(), "/acls", nil, &acls) != nil {
return nil, none
}
for _, a := range acls {
addName(a.Name, a.Comment)
}
case "tsig":
var keys []api.TSIGKey
if c.Get(ctx(), "/tsig-keys", nil, &keys) != nil {
return nil, none
}
for _, k := range keys {
addName(k.Name, k.Algorithm)
}
case "user":
var users []api.User
if c.Get(ctx(), "/users", nil, &users) != nil {
return nil, none
}
for _, u := range users {
addName(u.Username, u.Comment)
}
}
return out, none
}
+1
View File
@@ -207,6 +207,7 @@ Exit codes: 0 ok, 1 error, 3 not authenticated or not allowed, 4 not found,
add("dns", zoneCmd(), recordCmd(), viewCmd(), aclCmd(), tsigCmd(), optionCmd(), settingsCmd(), acmeCmd(), searchCmd(), statsCmd()) add("dns", zoneCmd(), recordCmd(), viewCmd(), aclCmd(), tsigCmd(), optionCmd(), settingsCmd(), acmeCmd(), searchCmd(), statsCmd())
add("changes", applyCmd(), changesetCmd(), historyCmd()) add("changes", applyCmd(), changesetCmd(), historyCmd())
add("admin", statusCmd(), whoamiCmd(), userCmd(), tokenCmd(), auditCmd(), checkCmd(), resyncCmd(), configCmd()) add("admin", statusCmd(), whoamiCmd(), userCmd(), tokenCmd(), auditCmd(), checkCmd(), resyncCmd(), configCmd())
setupCompletion(root)
return root return root
} }
+20
View File
@@ -0,0 +1,20 @@
Package: jarl
Version: @VERSION@
Architecture: @ARCH@
Maintainer: @MAINTAINER@
Installed-Size: @SIZE@
Depends: systemd
Recommends: bind9 (>= 1:9.20)
Section: net
Priority: optional
Description: web UI, REST API and CLI for managing a BIND 9 server
Jarl manages a BIND 9.20 server: zones, records, views, ACLs, TSIG keys
and options, through a web UI, a REST API and the jarl command line
client. Every change is validated, checked with named-checkconf and
loaded into named as one changeset that can be reverted. Users and API
tokens have fine-grained permissions, and every change is audited.
.
The package installs jarld (the daemon, as the systemd service jarl) and
jarl (the CLI), with bash completion for both. jarld isn't started on a
first install: see /usr/share/doc/jarl/docs/install.md to create the
database and adopt the BIND configuration.
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# Jarl: runs after the package's files are in place.
set -e
if [ "$1" = configure ]; then
# The jarl system user (a member of group bind) and Jarl's directories:
# /etc/jarl, /var/lib/jarl, /etc/bind/jarl, /var/lib/bind/jarl,
# /var/cache/bind/jarl and /var/log/named.
systemd-sysusers jarl.conf
systemd-tmpfiles --create jarl.conf || true
# An example configuration on the first install; an existing one is
# never touched.
if [ ! -e /etc/jarl/jarl.yaml ]; then
/usr/bin/jarld config --example > /etc/jarl/jarl.yaml.dpkg-new
chown root:jarl /etc/jarl/jarl.yaml.dpkg-new
chmod 0640 /etc/jarl/jarl.yaml.dpkg-new
mv /etc/jarl/jarl.yaml.dpkg-new /etc/jarl/jarl.yaml
fi
if [ -d /run/systemd/system ]; then
systemctl daemon-reload || true
# An upgrade restarts a running jarld (its database is upgraded
# when it starts). A first install starts nothing: Jarl needs
# 'jarld init' and 'jarld adopt' first.
if [ -n "$2" ] && systemctl is-active --quiet jarl.service; then
systemctl restart jarl.service || echo "jarl: restarting jarld failed; see 'journalctl -u jarl'" >&2
fi
fi
if [ -z "$2" ]; then
echo "Jarl is installed. Next: create the database and adopt your BIND"
echo "configuration, as described in /usr/share/doc/jarl/docs/install.md"
echo "(from step 3), then: systemctl enable --now jarl"
fi
fi
exit 0
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
# Jarl: runs after the package's files are removed.
set -e
case "$1" in
remove|purge)
if [ -d /run/systemd/system ]; then
systemctl daemon-reload || true
fi
;;
esac
# Even a purge keeps Jarl's data: the database in /var/lib/jarl is the
# source of your DNS configuration, and named.conf may still include the
# files in /etc/bind/jarl. Remove them by hand when you no longer need them.
if [ "$1" = purge ]; then
echo "jarl: kept /etc/jarl, /var/lib/jarl, /etc/bind/jarl, /var/lib/bind/jarl and"
echo "/var/cache/bind/jarl (named may still use them); remove them by hand if unused."
fi
exit 0
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# Jarl: runs before the package's files are removed (also on upgrades).
set -e
# On removal (not on upgrade) stop jarld and remove its boot-time link.
# named keeps running with the configuration Jarl last wrote.
if [ "$1" = remove ] && [ -d /run/systemd/system ]; then
systemctl stop jarl.service 2>/dev/null || true
systemctl disable jarl.service 2>/dev/null || true
fi
exit 0
+20 -1
View File
@@ -2,7 +2,26 @@
`jarl` talks to jarld's REST API with an API token. It runs on the DNS server `jarl` talks to jarld's REST API with an API token. It runs on the DNS server
or on any machine that can reach jarld. Every command has `--help` with or on any machine that can reach jarld. Every command has `--help` with
examples, and `jarl completion bash|zsh|fish` prints shell completion. examples.
## Shell completion
`make install` and the Debian package install bash completion for `jarl`
and `jarld`; it works in new shells (it needs the `bash-completion`
package, which Debian and Ubuntu install by default). Elsewhere, or for
zsh and fish, `jarl completion bash|zsh|fish` prints the script; for
example `jarl completion bash > ~/.local/share/bash-completion/completions/jarl`.
Besides commands and flags, Tab completes:
* `-s/--server` and the `config` commands: the server and group names in
your configuration file (and `all`; after a comma, the next name);
* `ZONE`, `VIEW`, `ACL`, `USER` and TSIG `KEY` arguments, and the `--view`,
`--zone` and `--user` flags: names from the server, asked with your
token. A zone in several views is offered once, with its views shown.
If the server can't be reached in 3 seconds, nothing is offered;
* record `TYPE`s, with a short description of each;
* `-o/--output`: `table`, `json`, `yaml`.
## Setup ## Setup
+36 -1
View File
@@ -26,9 +26,11 @@ internal/
acme/ ACME DNS-01 challenge records, shared by jarl acme and the httpreq endpoint acme/ ACME DNS-01 challenge records, shared by jarl acme and the httpreq endpoint
config/ jarld's YAML configuration config/ jarld's YAML configuration
deploy/ systemd unit, sysusers.d and tmpfiles.d files 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/acme.sh/ acme.sh DNS API hook
contrib/lego/ program for lego's exec DNS provider contrib/lego/ program for lego's exec DNS provider
scripts/e2e.sh end-to-end test against a real named 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`): The main flow is in `internal/engine/engine.go` (`applyOps`):
@@ -38,11 +40,44 @@ record. [concepts.md](concepts.md) describes it from the outside.
## Building and testing ## Building and testing
```sh ```sh
make # bin/jarld, bin/jarl (CGO_ENABLED=0, version from ./VERSION) 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 test # go vet, gofmt check, go test ./...
make e2e # scripts/e2e.sh 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 ### Tests against real BIND
The render, engine and adopt tests run the real `named-checkconf`, The render, engine and adopt tests run the real `named-checkconf`,
+41 -1
View File
@@ -22,10 +22,49 @@ make dist # optional: dist/jarl-<version>-linux-{amd64,arm64}.tar.gz
``` ```
If you build on another machine, copy the binaries plus the `deploy/`, If you build on another machine, copy the binaries plus the `deploy/`,
`docs/` and `contrib/` directories to the DNS server. `docs/` and `contrib/` directories to the DNS server, or build a Debian
package (below) and copy that.
## 2. Install the files ## 2. Install the files
There are two ways: a Debian package, which is easiest to upgrade and
remove, or `make install`.
### With the Debian package
```sh
make deb # dist/jarl_0.1.0_amd64.deb
make deb DEB_ARCH=arm64 # for another architecture (also armhf, i386, ...)
sudo apt install ./dist/jarl_0.1.0_amd64.deb
```
`make deb` needs only `dpkg-deb` (no root, no debhelper) and cross-builds
for any architecture Go supports. apt also installs `bind9` (recommended)
if it's missing. Installing the package:
* puts the binaries in `/usr/bin`, bash completion in
`/usr/share/bash-completion/completions`, the systemd unit in
`/usr/lib/systemd/system/jarl.service`, the sysusers and tmpfiles files
in `/usr/lib`, and the docs and contrib files in `/usr/share/doc/jarl`;
* creates the `jarl` user (in group `bind`) and Jarl's directories, and
writes `/etc/jarl/jarl.yaml` from the example on the first install. No
file in `/etc` belongs to the package, so your configuration is never
replaced or asked about on upgrades;
* doesn't start jarld: continue with [3. Configure](#3-configure), then
`systemctl enable --now jarl` in step 8.
**Upgrading**: build and install the newer package the same way. A running
jarld is restarted, and upgrades its database as it starts.
**Removing** (`sudo apt remove jarl`) stops jarld and removes the program,
but named keeps running with the configuration Jarl last wrote. Even
`apt purge jarl` keeps `/etc/jarl`, `/var/lib/jarl` (the database) and the
files in `/etc/bind/jarl`, `/var/lib/bind/jarl` and `/var/cache/bind/jarl`,
since named.conf may still include them. Remove them by hand once named no
longer uses them.
### With make install
On the DNS server, as root: On the DNS server, as root:
```sh ```sh
@@ -37,6 +76,7 @@ This installs:
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `/usr/local/bin/jarld`, `/usr/local/bin/jarl` | daemon and CLI | | `/usr/local/bin/jarld`, `/usr/local/bin/jarl` | daemon and CLI |
| `/usr/local/share/bash-completion/completions/jarl`, `.../jarld` | bash completion (loaded automatically by the `bash-completion` package) |
| `/etc/systemd/system/jarl.service` | systemd unit (`Type=notify`, sandboxed) | | `/etc/systemd/system/jarl.service` | systemd unit (`Type=notify`, sandboxed) |
| `/usr/lib/sysusers.d/jarl.conf` | creates the `jarl` system user and adds it to group `bind` | | `/usr/lib/sysusers.d/jarl.conf` | creates the `jarl` system user and adds it to group `bind` |
| `/usr/lib/tmpfiles.d/jarl.conf` | creates `/etc/jarl`, `/var/lib/jarl`, `/etc/bind/jarl`, `/var/lib/bind/jarl`, `/var/cache/bind/jarl` | | `/usr/lib/tmpfiles.d/jarl.conf` | creates `/etc/jarl`, `/var/lib/jarl`, `/etc/bind/jarl`, `/var/lib/bind/jarl`, `/var/cache/bind/jarl` |
+77
View File
@@ -0,0 +1,77 @@
#!/bin/sh
# Builds a Debian package of Jarl: dist/jarl_<version>_<arch>.deb.
# Run through 'make deb' (which sets the variables below); needs only
# dpkg-deb, no root and no debhelper.
#
# The package installs, with Debian's paths:
# /usr/bin/jarld, /usr/bin/jarl
# /usr/share/bash-completion/completions/{jarl,jarld}
# /usr/lib/systemd/system/jarl.service
# /usr/lib/sysusers.d/jarl.conf, /usr/lib/tmpfiles.d/jarl.conf
# /usr/share/doc/jarl/ (README, CHANGELOG, docs, contrib)
# and its scripts (deploy/debian) create the jarl user and directories and
# /etc/jarl/jarl.yaml on the first install, restart a running jarld on
# upgrades, and stop it on removal. No file in /etc belongs to the package,
# so an upgrade never asks about configuration files.
#
# Variables: DEB_ARCH (Debian architecture, e.g. amd64, arm64, armhf),
# DEB_VERSION, VERSION, LDFLAGS, GOFLAGS, and DEB_MAINTAINER (default: your
# git name and email).
set -eu
: "${DEB_ARCH:?}" "${DEB_VERSION:?}" "${VERSION:?}" "${LDFLAGS:?}"
GOFLAGS=${GOFLAGS:-}
# Debian architecture -> Go architecture.
goarm=
case "$DEB_ARCH" in
amd64) goarch=amd64 ;;
arm64) goarch=arm64 ;;
armhf) goarch=arm goarm=7 ;;
i386) goarch=386 ;;
ppc64el) goarch=ppc64le ;;
riscv64) goarch=riscv64 ;;
s390x) goarch=s390x ;;
*) echo "mkdeb: unsupported architecture $DEB_ARCH" >&2; exit 1 ;;
esac
if [ -z "${DEB_MAINTAINER:-}" ]; then
name=$(git config user.name 2>/dev/null || true)
email=$(git config user.email 2>/dev/null || true)
DEB_MAINTAINER="${name:-Jarl packager} <${email:-root@localhost}>"
fi
work=dist/deb/jarl_${DEB_VERSION}_${DEB_ARCH}
root=$work/root
out=dist/jarl_${DEB_VERSION}_${DEB_ARCH}.deb
rm -rf "$work"
mkdir -p "$root/DEBIAN"
# Binaries for the package's architecture (bin/ holds the host's, which
# also generated the completion scripts).
echo "mkdeb: building for linux/$goarch"
# go reads GOFLAGS (-trimpath) from the environment.
export GOFLAGS
GOOS=linux GOARCH=$goarch GOARM=$goarm CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o "$work/jarld" ./cmd/jarld
GOOS=linux GOARCH=$goarch GOARM=$goarm CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o "$work/jarl" ./cmd/jarl
make --no-print-directory install-files DESTDIR="$root" PREFIX=/usr \
UNITDIR=/usr/lib/systemd/system JARLD_BIN="$work/jarld" JARL_BIN="$work/jarl" >/dev/null
for f in postinst prerm postrm; do
install -m 0755 "deploy/debian/$f" "$root/DEBIAN/$f"
done
# Directories 0755 and no group/other write, whatever the build's umask.
find "$root" -type d -exec chmod 0755 {} +
chmod -R go-w "$root"
size=$(du -sk --exclude=DEBIAN "$root" | cut -f1)
sed -e "s/@VERSION@/$DEB_VERSION/" -e "s/@ARCH@/$DEB_ARCH/" -e "s/@SIZE@/$size/" \
-e "s|@MAINTAINER@|$DEB_MAINTAINER|" deploy/debian/control.in > "$root/DEBIAN/control"
(cd "$root" && find . -path ./DEBIAN -prune -o -type f -print | sed 's|^\./||' | sort | xargs md5sum) > "$root/DEBIAN/md5sums"
chmod 0644 "$root/DEBIAN/control" "$root/DEBIAN/md5sums"
# Files belong to root in the package, whoever builds it.
dpkg-deb --root-owner-group -Zxz --build "$root" "$out" >/dev/null
rm -rf dist/deb
echo "mkdeb: wrote $out"
dpkg-deb --info "$out" | sed -n 's/^ \(Package\|Version\|Architecture\|Installed-Size\):/ \1:/p'