An imperative scale left kubectl owning the replica count while GitOps still declared one, causing apply conflicts. Declare two replicas so DNS remains available across a node failure and GitOps can reconcile the intended count.
DO NOT SYNC THIS BEFORE MIGRATING. Removing fancy-dns-zones and fancy-dns-rpz
from the kustomization lets a pruning GitOps tool delete them, and they hold the
only copy of the zones and address redirects until the objects exist. Follow
deploy/MIGRATION.md: install the definitions, run `fancy-dns-console migrate`
against the live cluster, then roll this out.
The StatefulSet starts the resolver with -kube and no longer mounts the two
ConfigMaps; its service account loses the ConfigMap role and keeps only the
read-only Role on the fancy-dns objects (crds/rbac.yaml). The kustomization now
installs the definitions and that Role. The shipped config no longer lists the
migrated sources.
Config validation no longer refuses an enabled blocklist with no sources in the
file, because BlockList, DomainRule and AddressOverride objects can supply every
rule and the file cannot know. The existing "no source" warning says so. That
removed the only validation message the settings tests used as a fixture; they
now use an invalid acl.default_policy, and an empty settings document is refused
by the handler instead of by that check.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With traffic charts gone from the console, deploy/grafana/fancy-dns.json is
where they live: queries by outcome, transport and client group, latency
quantiles, RCODEs, cache hits/misses/stale, the upstream round trips saved by
coalescing and early refresh, upstream health and latency, blocklist and zone
sizes, and (in a Loki row) the blocked/failed-query access log. A node variable
spreads or narrows every panel, and the per-node panel shows whether load is
even across pods.
A test reads the dashboard and fails if a panel queries a metric the resolver
does not export, so a rename cannot silently empty a panel.
The ingress no longer needs the SSE buffering/timeout annotations, and the
statefulset comments no longer describe a query log on the volume.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
RPZ A records were parsed as address redirects, but the query pipeline treated the address as a CNAME target and returned the wrong result. Answer IP-literal redirects directly with the matching A or AAAA record, and return NODATA for other query types.
Add the requested EdgeRouter domain overrides as a local RPZ source so each bare owner covers its apex and all subdomains. Mount that source into the pods and document the address-record form. When the Zones page has no existing zone, start the Add Record form in New zone mode and tell the operator to enter its apex.
The published image is built for amd64 only, but the StatefulSet had no architecture constraint and could be scheduled on arm64 nodes. Restrict pods to Kubernetes amd64 nodes so every replica can run the image.
Local zones were only editable as mounted files, so operators could not manage DNS entries from the console. Add a Local zones editor backed by a shared Kubernetes ConfigMap, validate master files before saving, scope the pod service account to that ConfigMap, and reload changed zones on every replica while flushing stale cache entries.
The cache snapshot never loaded, so every restart was a cold cache. Snapshot
packed the whole key while Load decoded the record header out of the key's
remainder instead of the value, so every entry was skipped. The two sides are
now an explicit encodeRecord/decodeRecord pair with a version byte and a CRC-32C
trailer, so a future format change is detected instead of silently misparsed.
Load also drops entries that are already past ExpiresAt and rewrites the TTL to
the time actually left, so a served TTL never jumps back up across a restart.
Blocklist, all six gaps the failing tests were pointing at: a leading UTF-8 BOM
is stripped, "!allow example.com" works, SourceMeta exists for a source that
only ever contributed a rule, Pattern() reports the bare name again (the new
WireName() produces the wildcard form for the API), an RPZ record with no
explicit TTL inherits the zone default instead of whatever the reader left in
the RR, a hosts line with no address in the first field says so, and the
adblock parser handles CRLF and adblock-flavoured lines pasted into a plain
list. TestHolderSwapIsAtomic was asserting the wrong invariant — the swap was
always atomic, a name blocked by the old set is legitimately unblocked by the
new one.
Tests added for acl and resolver, both of which turned up real bugs:
- a PTR suffix matched across label boundaries, so a group scoped to
example.com. also swallowed ample.com. and notexample.com. — a client could
land in a group it does not belong to;
- the "*" catch-all was dropped from the rendered domain patterns, so the
engine's per-query recompile lost the "everything" case;
- Upstream.Success never closed an open breaker, so a recovered upstream stayed
excluded for the full cooldown;
- Exchange fanned out in parallel even under policy: failover.
Deploy set: go.mod requires Go 1.27.1 but CI pinned 1.23 and the Dockerfile
pinned golang:1.23-alpine, so neither could have built. The Dockerfile also
copied the source tree after the console bundle, which clobbered it.
The k8s set asked for hostNetwork with two replicas, but the server never sets
SO_REUSEPORT, so the second pod could not bind :53 and would crash-loop. It now
runs without hostNetwork behind a LoadBalancer Service, with a separate
ClusterIP Service for the Ingress (a headless Service cannot back one), a
ServiceAccount, an API-key Secret, per-pod PVCs, and a kustomization. The HPA
is gone: it cannot drive a StatefulSet. To let the API key come from that
Secret, config.Load now expands ${VAR} / ${VAR:-default} / ${NAME-default} and
fails to start on an unset variable with no default, so a missing secret is a
crash loop rather than an unauthenticated console. Expansion skips YAML
comments, so documenting ${VAR} in the ConfigMap does not break the file.
Verified: gofmt, go build, go vet clean; go test -race ./internal/... green
(acl, app, blocklist, cache, config, resolver, zonefile).