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:
1 parent
0f2c318aaf
commit
352c9cb394
13 files changed
+555
-30
No files matched your search
@@ -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
|
||||
enclosing /24's zone.
|
||||
- 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
|
||||
`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
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# 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;
|
||||
# set JARL_BIND_ROOT, see docs/development.md)
|
||||
# make e2e end-to-end test against a real named (scripts/e2e.sh)
|
||||
# sudo make install install binaries, systemd unit, sysusers/tmpfiles
|
||||
# files and an example /etc/jarl/jarl.yaml
|
||||
# sudo make install install binaries, bash completion, systemd unit,
|
||||
# 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/
|
||||
|
||||
VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev)
|
||||
@@ -13,21 +15,39 @@ LDFLAGS := -s -w -X jarl/internal/version.Version=$(VERSION)
|
||||
GOFLAGS := -trimpath
|
||||
export CGO_ENABLED := 0
|
||||
|
||||
PREFIX ?= /usr/local
|
||||
BINDIR ?= $(PREFIX)/bin
|
||||
DOCDIR ?= $(PREFIX)/share/doc/jarl
|
||||
UNITDIR ?= /etc/systemd/system
|
||||
PREFIX ?= /usr/local
|
||||
BINDIR ?= $(PREFIX)/bin
|
||||
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
|
||||
SYSUSERSDIR ?= /usr/lib/sysusers.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
|
||||
|
||||
build:
|
||||
go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o bin/jarld ./cmd/jarld
|
||||
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:
|
||||
go vet ./...
|
||||
@@ -39,19 +59,7 @@ test: vet
|
||||
e2e: build
|
||||
scripts/e2e.sh
|
||||
|
||||
install: build
|
||||
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/
|
||||
install: build install-files
|
||||
@if [ ! -e $(DESTDIR)/etc/jarl/jarl.yaml ]; then \
|
||||
install -d -m 0750 $(DESTDIR)/etc/jarl; \
|
||||
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 "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:
|
||||
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
|
||||
rm -rf $(DESTDIR)$(DOCDIR)
|
||||
@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 \
|
||||
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 && \
|
||||
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; \
|
||||
done
|
||||
|
||||
|
||||
@@ -64,9 +64,7 @@ On a Debian/Ubuntu BIND 9.20 server ([install.md](docs/install.md) has the
|
||||
details):
|
||||
|
||||
```sh
|
||||
make && sudo make install
|
||||
sudo systemd-sysusers jarl.conf && sudo systemd-tmpfiles --create jarl.conf
|
||||
sudo chgrp jarl /etc/jarl/jarl.yaml
|
||||
make deb && sudo apt install ./dist/jarl_*.deb # or: make && sudo make install, see install.md
|
||||
sudo -u jarl jarld init --token # first admin + a CLI token
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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("changes", applyCmd(), changesetCmd(), historyCmd())
|
||||
add("admin", statusCmd(), whoamiCmd(), userCmd(), tokenCmd(), auditCmd(), checkCmd(), resyncCmd(), configCmd())
|
||||
setupCompletion(root)
|
||||
return root
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
Executable
+38
@@ -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
|
||||
Executable
+21
@@ -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
|
||||
Executable
+12
@@ -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
@@ -2,7 +2,26 @@
|
||||
|
||||
`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
|
||||
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
|
||||
|
||||
|
||||
+36
-1
@@ -26,9 +26,11 @@ internal/
|
||||
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`):
|
||||
@@ -38,11 +40,44 @@ 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)
|
||||
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`,
|
||||
|
||||
+41
-1
@@ -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/`,
|
||||
`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
|
||||
|
||||
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:
|
||||
|
||||
```sh
|
||||
@@ -37,6 +76,7 @@ This installs:
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `/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) |
|
||||
| `/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` |
|
||||
|
||||
Executable
+77
@@ -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'
|
||||
Reference in new issue
Block a user