Jarl is now an ACME client (lego's core, no DNS providers). It answers DNS-01 challenges in a zone it serves, or through Porkbun's DNS API while Porkbun serves the domain; "auto" decides from the domain's public name servers at every renewal, and a CNAME on _acme-challenge is followed. Certificates are used for jarld's HTTPS (served without a restart; each port speaks HTTPS and plain HTTP, plain HTTP from loopback and trusted proxies still served) and/or written for named's DNS over TLS/HTTPS, with rndc reconfig on renewal. Renewal follows the CA's ARI window or two thirds of the lifetime, with back-off. Certificates you renew yourself can be used too. Certificates page and dashboard card, jarl cert, /api/v1/certificates, Prometheus gauges, docs/certificates.md, and an e2e section against Pebble. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
4.1 KiB
Go
119 lines
4.1 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestExampleParses(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "jarl.yaml")
|
|
os.WriteFile(path, []byte(Example), 0o644)
|
|
c, err := Load(path, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := Default()
|
|
if c.BIND.ZoneDir != d.BIND.ZoneDir || time.Duration(c.Session.IdleTimeout) != 2*time.Hour || c.Listen[0] != "127.0.0.1:8053" {
|
|
t.Errorf("example differs from defaults: %+v", c)
|
|
}
|
|
if !reflect.DeepEqual(c.ACME, d.ACME) || !reflect.DeepEqual(c.Certificates, d.Certificates) || c.ACME.HTTPReq.Enabled || !c.ACME.HTTPReq.RequireHTTPS {
|
|
t.Errorf("acme = %+v, want the defaults %+v", c.ACME, d.ACME)
|
|
}
|
|
}
|
|
|
|
func TestHTTPReqDefaults(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "jarl.yaml")
|
|
// Enabling the endpoint keeps require_https on unless it is turned off.
|
|
os.WriteFile(path, []byte("acme: {httpreq: {enabled: true}}\n"), 0o644)
|
|
c, err := Load(path, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if h := c.ACME.HTTPReq; !h.Enabled || !h.RequireHTTPS || h.AllowLoopbackHTTP {
|
|
t.Errorf("httpreq = %+v", h)
|
|
}
|
|
}
|
|
|
|
func TestLoad(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "jarl.yaml")
|
|
os.WriteFile(path, []byte("listen: [\"0.0.0.0:8443\"]\ntls: {cert_file: /a, key_file: /b}\ntrusted_proxies: [10.0.0.1, 192.168.0.0/16]\nsession: {lifetime: 1d}\n"), 0o644)
|
|
c, err := Load(path, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if time.Duration(c.Session.Lifetime) != 24*time.Hour || len(c.Proxies()) != 2 || c.BIND.FileGroup != "bind" {
|
|
t.Errorf("config = %+v", c)
|
|
}
|
|
os.WriteFile(path, []byte("lisen: []\n"), 0o644)
|
|
if _, err := Load(path, false); err == nil {
|
|
t.Error("unknown field accepted")
|
|
}
|
|
os.WriteFile(path, []byte("tls: {cert_file: /a}\n"), 0o644)
|
|
if _, err := Load(path, false); err == nil {
|
|
t.Error("half TLS config accepted")
|
|
}
|
|
if _, err := Load(filepath.Join(dir, "missing.yaml"), true); err != nil {
|
|
t.Errorf("missing file with allowMissing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStatsConfig(t *testing.T) {
|
|
d := Default()
|
|
if d.Stats.Enabled || !d.Stats.QueryLog || d.Stats.TopN != 100 || time.Duration(d.Stats.Retention.Hour) != 90*24*time.Hour {
|
|
t.Errorf("defaults = %+v", d.Stats)
|
|
}
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "jarl.yaml")
|
|
for _, tc := range []struct {
|
|
yaml string
|
|
ok bool
|
|
}{
|
|
{"stats: {enabled: true}", true},
|
|
{"stats: {enabled: true, query_log: false, retention: {minute: 2d}}", true},
|
|
{"stats: {enabled: true, statistics_channel: 0.0.0.0:8058}", false}, // must be loopback
|
|
{"stats: {enabled: true, statistics_channel: localhost}", false},
|
|
{"stats: {enabled: true, log_size: lots}", false},
|
|
{"stats: {enabled: true, retention: {minute: 10m}}", false},
|
|
{"stats: {enabled: true, exclude_clients: [not-an-ip]}", false},
|
|
{"stats: {enabled: false, statistics_channel: nonsense}", true}, // only checked when enabled
|
|
} {
|
|
os.WriteFile(path, []byte(tc.yaml+"\n"), 0o644)
|
|
_, err := Load(path, false)
|
|
if (err == nil) != tc.ok {
|
|
t.Errorf("%s: err = %v, want ok=%v", tc.yaml, err, tc.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMetricsConfig(t *testing.T) {
|
|
d := Default()
|
|
if d.Metrics.Enabled || len(d.Metrics.AllowPrefixes()) != 2 || d.Metrics.Token != "" {
|
|
t.Errorf("defaults = %+v", d.Metrics)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "jarl.yaml")
|
|
for _, tc := range []struct {
|
|
yaml string
|
|
ok bool
|
|
}{
|
|
{"metrics: {enabled: true}", true},
|
|
{"metrics: {enabled: true, listen: [\"0.0.0.0:9450\", \"[::]:9450\"], allow: [10.0.0.5, 192.168.1.0/24]}", true},
|
|
{"metrics: {enabled: true, allow: [], token: 0123456789abcdef}", true},
|
|
{"metrics: {enabled: true, allow: []}", false}, // nobody could scrape
|
|
{"metrics: {enabled: true, token: short}", false},
|
|
{"metrics: {enabled: true, token: \"has space 0123456789\"}", false},
|
|
{"metrics: {enabled: true, listen: [\"9450\"]}", false},
|
|
{"metrics: {enabled: true, allow: [prometheus.lan]}", false},
|
|
{"metrics: {enabled: false, listen: [nonsense]}", true}, // only checked when enabled
|
|
} {
|
|
os.WriteFile(path, []byte(tc.yaml+"\n"), 0o644)
|
|
_, err := Load(path, false)
|
|
if (err == nil) != tc.ok {
|
|
t.Errorf("%s: err = %v, want ok=%v", tc.yaml, err, tc.ok)
|
|
}
|
|
}
|
|
}
|