Remove the ConfigMap, RPZ and managed-directory editors the cluster objects replace
Zones, address redirects, inline rules, lists and groups are cluster objects now (see the previous commits and deploy/MIGRATION.md), so the mechanisms that held them are gone: * internal/kubezones and the resolver's service-account access to a ConfigMap, the address-overrides read/write path and its poller, the zone-file poller and the Local zones editor with its /api/local-zones routes. * localzone.Managed, the directory of console-written master files, and the blocklist.managed_zones key. The resolver now writes no zone data at all; its zone hooks only list. The shared half of that file (the record validator, the renderer and the round-trip check) stays as zonestate.go, because DNSZone rendering is built on it. Its tests are ported to RenderZone, which keeps the coverage of every editable record type, wildcards, relative owners, the SOA/NS rule and the delegation rule. * Tests of the removed write paths: the contract test now builds its zone from a DNSZone, and a new test pins that every zone write route on the resolver is refused and changes nothing. Standalone installs keep editing master files on disk and SIGHUP, and the YAML-backed domain, client, upstream and settings editors, which have not moved. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2f25463347
commit
467733a7f9
21 files changed
+505
-2312
No files matched your search
+8
-211
@@ -1,38 +1,22 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.k8s-git-k6n.net/mats/fancy-dns/internal/api"
|
||||
"git.k8s-git-k6n.net/mats/fancy-dns/internal/localzone"
|
||||
)
|
||||
|
||||
// The local-zone write path.
|
||||
//
|
||||
// Everything the console changes lives in the managed zone directory
|
||||
// (blocklist.managed_zones), one <apex>.zone per zone. The operator's own master
|
||||
// files in blocklist.local_zones are read and reported, never rewritten: a
|
||||
// deployment that never configures a managed directory gets a read-only zone
|
||||
// browser, and one that does gets a console whose edits land in a file it
|
||||
// names and owns.
|
||||
//
|
||||
// Every change follows the same four steps: write the file, recompile the
|
||||
// whole store, drop the cached answers, report. A change that does not
|
||||
// recompile is a change the query path is not serving, so the store is only
|
||||
// swapped after the new file parsed, and a failure keeps the previous store.
|
||||
// The resolver only reads zones. Master files from blocklist.local_zones and the
|
||||
// rendered DNSZone objects are compiled into one store; editing is the
|
||||
// console's job and happens on the objects.
|
||||
|
||||
// listZones renders the compiled store for the console.
|
||||
func (a *App) listZones() (any, error) {
|
||||
dir, writable := a.managedZoneDir(), a.managedZones.Writable()
|
||||
if a.kube != nil {
|
||||
// With the Kubernetes source the zones that can be edited are the
|
||||
// DNSZone objects, so those are the "managed" ones. The runtime itself
|
||||
// cannot write them; the console, which can, reports writable.
|
||||
dir, writable = a.kubeZoneDir(), false
|
||||
}
|
||||
// The zones that can be edited are the DNSZone objects, which the Kubernetes
|
||||
// source renders into a directory of its own. The resolver itself writes
|
||||
// nothing; the console, which can, reports the zones writable.
|
||||
dir := a.kubeZoneDir()
|
||||
store := a.local.Get()
|
||||
// The slices are never nil: the console reads lengths and maps without
|
||||
// a null check, and an empty list is a list.
|
||||
@@ -40,7 +24,7 @@ func (a *App) listZones() (any, error) {
|
||||
Zones: []api.Zone{},
|
||||
Files: []string{},
|
||||
ManagedDir: dir,
|
||||
Writable: writable,
|
||||
Writable: false,
|
||||
Types: localzone.EditableTypes(),
|
||||
}
|
||||
if store != nil {
|
||||
@@ -55,193 +39,6 @@ func (a *App) listZones() (any, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// addZoneRecord adds one record to a zone.
|
||||
func (a *App) addZoneRecord(_ context.Context, in api.ZoneRecordInput) (any, error) {
|
||||
origin, err := a.zoneOrigin(in.Zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serial, err := a.managedZones.Add(origin, localzone.Record{
|
||||
Name: in.Name,
|
||||
Type: in.Type,
|
||||
TTL: in.TTL,
|
||||
Value: in.Value,
|
||||
}, a.zoneDefaults(origin))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.afterZoneWrite(origin, "add", serial)
|
||||
return api.ZoneWriteResponse{OK: true, Zone: origin, Serial: serial}, nil
|
||||
}
|
||||
|
||||
// putZoneRecord replaces the whole RRset of one name and type.
|
||||
func (a *App) putZoneRecord(_ context.Context, in api.ZoneRecordInput) (any, error) {
|
||||
origin, err := a.zoneOrigin(in.Zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serial, err := a.managedZones.Put(origin, localzone.Record{
|
||||
Name: in.Name,
|
||||
Type: in.Type,
|
||||
TTL: in.TTL,
|
||||
Value: in.Value,
|
||||
}, a.zoneDefaults(origin))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.afterZoneWrite(origin, "replace", serial)
|
||||
return api.ZoneWriteResponse{OK: true, Zone: origin, Serial: serial}, nil
|
||||
}
|
||||
|
||||
// deleteZoneRecord removes a record, or its whole RRset when value is empty.
|
||||
func (a *App) deleteZoneRecord(_ context.Context, zone, name, rrtype, value string) error {
|
||||
origin, err := a.zoneOrigin(zone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serial, removed, err := a.managedZones.Delete(origin, name, rrtype, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.afterZoneWrite(origin, "delete", serial)
|
||||
if !removed {
|
||||
return fmt.Errorf("no managed record matches %s %s in %s", name, rrtype, origin)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addZoneDelegation hands a name below an apex to other servers.
|
||||
func (a *App) addZoneDelegation(_ context.Context, in api.ZoneDelegationInput) (any, error) {
|
||||
origin, err := a.zoneOrigin(in.Zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets := make([]string, 0, len(in.Targets))
|
||||
for _, t := range in.Targets {
|
||||
if t = strings.TrimSpace(t); t != "" {
|
||||
targets = append(targets, t)
|
||||
}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return nil, errors.New("a delegation needs at least one NS target")
|
||||
}
|
||||
// A brand new zone has no NS set of its own, so the delegation's targets
|
||||
// are also the zone's. An existing file ignores this.
|
||||
def := a.zoneDefaults(origin)
|
||||
def.NS = targets
|
||||
if def.TTL == 0 {
|
||||
def.TTL = in.TTL
|
||||
}
|
||||
serial, err := a.managedZones.SetDelegation(origin, in.Name, targets, in.TTL, def)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.afterZoneWrite(origin, "delegate", serial)
|
||||
return api.ZoneWriteResponse{OK: true, Zone: origin, Serial: serial}, nil
|
||||
}
|
||||
|
||||
// deleteZoneDelegation removes a cut.
|
||||
func (a *App) deleteZoneDelegation(_ context.Context, zone, name string) error {
|
||||
origin, err := a.zoneOrigin(zone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serial, removed, err := a.managedZones.RemoveDelegation(origin, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.afterZoneWrite(origin, "undelegate", serial)
|
||||
if !removed {
|
||||
return fmt.Errorf("no delegation for %s in %s", name, origin)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// zoneOrigin canonicalises the apex a write names and refuses to go near the
|
||||
// disk when the console has no managed directory.
|
||||
func (a *App) zoneOrigin(zone string) (string, error) {
|
||||
origin, err := localzone.NormalizeOrigin(zone)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := a.managedZones.Check(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return origin, nil
|
||||
}
|
||||
|
||||
// deleteZone removes a zone the console created, which is the only way a name
|
||||
// goes back to upstream once the console has made a zone authoritative for
|
||||
// it: a zone that exists answers NXDOMAIN for everything it does not hold.
|
||||
func (a *App) deleteZone(_ context.Context, zone string) error {
|
||||
origin, err := a.zoneOrigin(zone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The zone has to be wholly the console's: deleting a managed file that
|
||||
// shares its apex with an operator's master file would remove none of
|
||||
// the zone, only half of it.
|
||||
whole := false
|
||||
if store := a.local.Get(); store != nil {
|
||||
if view := store.Snapshot(origin, a.managedZoneDir()); view != nil {
|
||||
whole = view.Managed
|
||||
} else {
|
||||
return fmt.Errorf("no zone %s is loaded", origin)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("no zone %s is loaded", origin)
|
||||
}
|
||||
removed, err := a.managedZones.Remove(origin, whole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !removed {
|
||||
return fmt.Errorf("no managed zone file for %s", origin)
|
||||
}
|
||||
a.afterZoneWrite(origin, "delete-zone", 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// zoneDefaults seeds a managed file for an apex that has none from the zone
|
||||
// as it is compiled today, so the console never has to restate a zone's
|
||||
// server list and never invents one.
|
||||
func (a *App) zoneDefaults(origin string) localzone.ZoneDefaults {
|
||||
store := a.local.Get()
|
||||
if store == nil {
|
||||
return localzone.ZoneDefaults{}
|
||||
}
|
||||
zone := store.Snapshot(origin, a.managedZoneDir())
|
||||
if zone == nil {
|
||||
return localzone.ZoneDefaults{}
|
||||
}
|
||||
def := localzone.ZoneDefaults{TTL: zone.DefaultTTL, Soa: zone.Soa}
|
||||
// The apex NS set follows the zone's existing one, from the operator's
|
||||
// file if that is where it lives. A managed file that already has its own
|
||||
// is not asked for defaults at all, so this only ever happens once.
|
||||
for _, rec := range zone.Records {
|
||||
if rec.Name == origin && rec.Type == "NS" {
|
||||
def.NS = append(def.NS, rec.Value)
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// afterZoneWrite is the tail every change shares: recompile, drop the cached
|
||||
// answers, log. The flush is the blunt half and it is deliberate — a name the
|
||||
// zone now answers authoritatively may have an upstream answer cached, and
|
||||
// which names changed is not knowable from here.
|
||||
func (a *App) afterZoneWrite(origin, action string, serial uint32) {
|
||||
a.reloadLocalZones()
|
||||
if a.cache != nil {
|
||||
a.cache.Flush()
|
||||
}
|
||||
records := 0
|
||||
if store := a.local.Get(); store != nil {
|
||||
records = store.Count()
|
||||
}
|
||||
a.log.Info("local zone changed", "zone", origin, "action", action, "serial", serial, "records", records)
|
||||
}
|
||||
|
||||
// localZoneError is the reason the store is stale, or "" when it is current.
|
||||
// It comes from the degraded list, so the console and /healthz agree.
|
||||
func (a *App) localZoneError() string {
|
||||
|
||||
Reference in new issue
Block a user