Add a read-only Kubernetes source for zones, client groups and blocklists

The runtime can now take ClientGroup, BlockList and DNSZone objects
(fancydns.k6n.net/v1alpha1) from a cluster with -kube, from a pod's service
account or a kubeconfig alike. internal/kubesource watches them with dynamic
informers, converts and validates each object on its own, and publishes an
immutable snapshot. A bad object is skipped and keeps its last good version,
so one broken edit cannot remove a running policy; an invalid merged config is
dropped whole. The last snapshot is saved so a restart without apiserver access
still serves.

config.Store gains an overlay: Base() is what the YAML says, Get() is base plus
cluster objects. Everything that writes the config back (mutateConfig,
SetAdminAddr, the settings editor) now starts from Base(), otherwise cluster
objects would be copied into the file and shadow their own objects. YAML wins
every name collision. putACL and toggleBlocklist refuse to edit cluster-owned
objects. BlockList has no path field and refuses file:// URLs so a namespaced
object cannot pick files the runtime reads or writes. DNSZone is rendered
through the zone writer's own prove-by-reparse step, with the SOA serial taken
from metadata.generation so every replica renders identical bytes.

qlog: Flush reset the pending slice to [:0], so entries logged while it was
still storing the batch were appended over the same backing array. The race
detector found it through the new Kubernetes integration test. pending is now
set to nil, with a regression test that fails without the change.

CRD manifests, RBAC and examples are in deploy/k8s/crds; tests pin the schemas
to the Go spec structs so the two cannot drift.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
matsandClaude Sonnet 5.5 committed 2026-10-16 08:35:36 +02:00
1 parent 340b4eaf80
commit 4d38945d94
27 files changed
+2250 -19

No files matched your search

+12
View File
@@ -32,6 +32,8 @@ internal/
qlog/ bbolt query-log ring + live in-memory ring + SSE fanout
metrics/ Prometheus collectors + rolling time-series rings
cluster/ Valkey pub/sub, leader lease, divergence detection
kubesource/ read-only watch of the fancydns.k6n.net CRDs (DNSZone,
ClientGroup, BlockList) -> Snapshot -> config overlay
api/ admin REST + SSE, auth, CORS, rate limiting
app/ wiring, lifecycle, API hook implementations, memory envelope
web/ go:embed of the built console
@@ -103,6 +105,16 @@ Break these and you break the design, not just the test.
a mirror N times. Followers serve their compiled copy.
- **Blocklist changes flush the cache** rather than guessing which subtrees are
affected (`invalidateBlocklistCache()` in `internal/app/hooks.go`).
- **The Kubernetes source is read-only and is an overlay, not a config.**
`config.Store` keeps `Base()` (what the YAML says) beside the effective
config (`Get()`, base + cluster objects). Anything that writes the config back
to the file — `mutateConfig`, `SetAdminAddr`, the settings editor — must start
from `Base()`, or cluster objects get copied into the YAML where they shadow
their own objects. YAML wins every collision (groups run first; a same-named
list is dropped). A cluster object is validated on its own: a bad object is
skipped and keeps its last good version, it never fails the merge. A BlockList
has no `path` and refuses `file://`, because a namespaced object must not
choose a file the runtime reads or writes.
- **API keys are compared by SHA-256 hash, constant-time**, and
`X-Forwarded-For` is honoured *only* from `trusted_proxies`. Honouring it from
anyone lets a client spoof its identity past the ACL and the rate limiter.