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>
113 lines
3.9 KiB
Go
113 lines
3.9 KiB
Go
package localzone
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.k8s-git-k6n.net/mats/fancy-dns/internal/zonefile"
|
|
)
|
|
|
|
// RenderZone renders a complete zone from declared records, for sources that
|
|
// own a zone outright (a Kubernetes DNSZone) rather than editing a file in
|
|
// place. It runs the same checks the console writer does: every record goes
|
|
// through newRecord, a delegation may not shadow data, and the rendering is
|
|
// re-parsed and compared with the records before it is returned, so a renderer
|
|
// bug surfaces as an error here and not as a zone that silently stopped
|
|
// answering.
|
|
//
|
|
// serial is the SOA serial; 0 means the writer's default. A declarative source
|
|
// should pass something that changes exactly when the spec does, so two
|
|
// replicas rendering the same object produce the same bytes.
|
|
func RenderZone(origin string, def ZoneDefaults, recs []Record, serial uint32) (string, error) {
|
|
origin, err := NormalizeOrigin(origin)
|
|
if err != nil {
|
|
return "", fmt.Errorf("zone name: %w", err)
|
|
}
|
|
f, err := newFileState(origin, def)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, in := range recs {
|
|
rec, err := newRecord(origin, in.Name, in.Type, ttlOr(in.TTL, f.ttl), in.Value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, have := range f.recs {
|
|
if have.Key() == rec.Key() && have.Value == rec.Value {
|
|
return "", fmt.Errorf("%s %s is declared twice", rec.Name, rec.Type)
|
|
}
|
|
}
|
|
f.recs = append(f.recs, rec)
|
|
}
|
|
if err := f.finish(); err != nil {
|
|
return "", err
|
|
}
|
|
if serial != 0 {
|
|
f.soa.Serial = serial
|
|
}
|
|
text := f.render()
|
|
check, err := parseFileState(origin, text)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the zone file for %s did not render correctly: %w", origin, err)
|
|
}
|
|
if err := sameRecords(check.recs, f.recs); err != nil {
|
|
return "", fmt.Errorf("the zone file for %s did not render correctly: %w", origin, err)
|
|
}
|
|
return text, nil
|
|
}
|
|
|
|
// ZoneSourceHeader tops a rendered zone that an external object owns.
|
|
const ZoneSourceHeader = `; fancy-dns zone rendered from a Kubernetes DNSZone object.
|
|
;
|
|
; Regenerated whenever the object changes; edit the DNSZone, not this file.
|
|
`
|
|
|
|
// CanonicalRecord returns a record in the form the zone store holds it: the
|
|
// owner fully qualified, the type uppercased, the RDATA as the master-file
|
|
// parser reads it. An editor compares records in this form, so "www" and
|
|
// "www.example.com." are the same record.
|
|
func CanonicalRecord(origin string, in Record) (Record, error) {
|
|
origin, err := NormalizeOrigin(origin)
|
|
if err != nil {
|
|
return Record{}, fmt.Errorf("zone name: %w", err)
|
|
}
|
|
rec, err := newRecord(origin, in.Name, in.Type, ttlOr(in.TTL, DefaultZoneTTL), in.Value)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
rec.TTL = in.TTL // 0 still means "the zone's default"
|
|
return rec, nil
|
|
}
|
|
|
|
// ZoneFile is a master file read back into the form a DNSZone holds it in.
|
|
type ZoneFile struct {
|
|
// TTL is the file's $TTL, or the default when it has none.
|
|
TTL uint32
|
|
// Soa is the SOA rdata ("ns mbox serial refresh retry expire minimum"), or
|
|
// "" when the file has none.
|
|
Soa string
|
|
Records []Record
|
|
}
|
|
|
|
// ParseZoneFile reads an RFC 1035 master file for the zone origin. Records come
|
|
// back canonical (absolute owner, uppercase type, rdata as the parser prints
|
|
// it) and without the SOA, which is returned separately. It is the reading half
|
|
// of a migration from master files to DNSZone objects.
|
|
func ParseZoneFile(origin, text string) (ZoneFile, error) {
|
|
origin, err := NormalizeOrigin(origin)
|
|
if err != nil {
|
|
return ZoneFile{}, fmt.Errorf("zone name: %w", err)
|
|
}
|
|
z, err := zonefile.Parse(strings.NewReader(text), zonefile.Options{Origin: origin, File: origin + "zone"})
|
|
if err != nil {
|
|
return ZoneFile{}, err
|
|
}
|
|
f := fileStateFromZone(z, origin)
|
|
out := ZoneFile{TTL: f.ttl, Records: f.recs}
|
|
if f.soa != nil {
|
|
out.Soa = fmt.Sprintf("%s %s %d %d %d %d %d", f.soa.Ns, f.soa.Mbox,
|
|
f.soa.Serial, f.soa.Refresh, f.soa.Retry, f.soa.Expire, f.soa.Minttl)
|
|
}
|
|
return out, nil
|
|
}
|