retract apex NS from recorded state, not a live query
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

An unsigned localhost query silently returns nothing for a zone behind a
BindView, which would strand the placeholder. Record what was published instead.
This commit is contained in:
2026-09-26 19:06:47 +10:00
parent e4ed6c8052
commit dc57ac1b2d
9 changed files with 134 additions and 76 deletions
+65 -42
View File
@@ -125,63 +125,92 @@ func alsoNotifyList(addrs []string, key string) string {
// zone: an in-zone nameserver is spelled out in full.
func absolute(name string) string { return strings.TrimSuffix(name, ".") + "." }
// zoneNameservers resolves the names to publish in a zone's apex NS RRset and
// reports whether the zone declared them. An apex NS in spec.records counts as a
// declaration: BIND ignores an RRset-wide delete at the apex, so records alone
// can only append to what the zone was seeded with, never replace it. Undeclared
// zones fall back to the primary's stable in-cluster name, which is deliberately
// out-of-zone so no pod IP is needed as glue.
func zoneNameservers(zone *bindv1alpha1.BindZone, cluster *bindv1alpha1.BindCluster) (names []string, declared bool) {
// zoneNameservers resolves the names to publish in a zone's apex NS RRset, the
// TTL to publish them with, and whether the zone declared them. An apex NS in
// spec.records counts as a declaration: BIND ignores an RRset-wide delete at the
// apex, so records alone can only append to what the zone was seeded with, never
// replace it. Undeclared zones fall back to the primary's stable in-cluster name,
// which is deliberately out-of-zone so no pod IP is needed as glue.
func zoneNameservers(zone *bindv1alpha1.BindZone, cluster *bindv1alpha1.BindCluster) (names []string, ttl int32, declared bool) {
ttl = zone.Spec.DefaultTTL
for _, ns := range zone.Spec.Nameservers {
names = append(names, absolute(ns))
}
if len(names) > 0 {
return names, true
}
for _, rec := range zone.Spec.Records {
if strings.EqualFold(rec.Type, "NS") && fqdn(rec.Name, zone.Spec.ZoneName) == fqdn("@", zone.Spec.ZoneName) {
for _, v := range rec.Values {
names = append(names, absolute(v))
}
if len(names) > 0 {
break
}
if !strings.EqualFold(rec.Type, "NS") || fqdn(rec.Name, zone.Spec.ZoneName) != fqdn("@", zone.Spec.ZoneName) {
continue
}
for _, v := range rec.Values {
names = append(names, absolute(v))
}
if rec.TTL != nil {
ttl = *rec.TTL
}
}
if len(names) > 0 {
return names, true
}
return []string{primaryAddress(cluster.Name, cluster.Namespace) + "."}, false
}
// apexNSUpdates converges a zone's live apex NS RRset onto desired, and retires
// the seed's ns1 glue once no published nameserver needs it. Adds come first:
// BIND refuses to leave an apex with no NS record, so the replacement must exist
// before the old name goes.
func apexNSUpdates(zone *bindv1alpha1.BindZone, desired, live []string, ttl int32) []bind.RecordUpdate {
if ttl <= 0 {
ttl = 3600
}
if len(names) > 0 {
return names, ttl, true
}
return clusterNameservers(cluster), ttl, false
}
// publishedNameservers is what the operator has already put in the apex NS RRset.
// It retracts only these, never a name someone else added, and needs no query
// against the pod: reading the live RRset back would take a view-scoped lookup,
// and an unsigned one silently returns nothing for a zone behind a BindView.
func publishedNameservers(zone *bindv1alpha1.BindZone, cluster *bindv1alpha1.BindCluster) []string {
if len(zone.Status.Nameservers) > 0 {
return zone.Status.Nameservers
}
// Nothing recorded yet, so the only names in the RRset are what a seed can
// write: an in-zone ns1 glued to the primary pod's IP (older seeds) or the
// stable in-cluster name (current ones).
return append([]string{fqdn("ns1", zone.Spec.ZoneName)}, clusterNameservers(cluster)...)
}
// apexNSUpdates moves a zone's apex NS RRset from published to desired, and
// retires the glue of any in-zone name it retracts. Adds come first: BIND refuses
// to leave an apex with no NS record, so the replacement must exist before the old
// name goes, and deleting glue still referenced by an in-zone NS fails named's
// post-update nameserver sanity check.
func apexNSUpdates(zone *bindv1alpha1.BindZone, desired, published []string, ttl int32) []bind.RecordUpdate {
apex := fqdn("@", zone.Spec.ZoneName)
add := missing(desired, live)
del := missing(live, desired)
add := missing(desired, published)
del := missing(published, desired)
var updates []bind.RecordUpdate
if len(add) > 0 {
updates = append(updates, bind.RecordUpdate{FQDN: apex, Type: "NS", TTL: ttl, Values: add, PerValue: true})
}
if len(del) > 0 {
updates = append(updates, bind.RecordUpdate{FQDN: apex, Type: "NS", Values: del, PerValue: true, Delete: true})
if len(del) == 0 {
return updates
}
updates = append(updates, bind.RecordUpdate{FQDN: apex, Type: "NS", Values: del, PerValue: true, Delete: true})
// The seed glues an in-zone nameserver to the primary pod's IP, which goes
// stale on the first reschedule. Drop it once no published nameserver is that
// name, unless spec.records owns the address itself. Deleting it while an
// in-zone NS still points at it would fail named's post-update sanity check.
glue := fqdn("ns1", zone.Spec.ZoneName)
if containsName(del, glue) && !containsName(desired, glue) && !recordsOwn(zone, "ns1", "A") {
updates = append(updates, bind.RecordUpdate{FQDN: glue, Type: "A", Delete: true})
// stale on the first reschedule. Drop that address with the name, unless
// spec.records owns it (then it is real data, not the placeholder).
for _, ns := range del {
owner, in := bind.InZoneOwner(ns, zone.Spec.ZoneName)
if in && owner != "@" && !recordsOwn(zone, owner, "A") {
updates = append(updates, bind.RecordUpdate{FQDN: fqdn(owner, zone.Spec.ZoneName), Type: "A", Delete: true})
}
}
return updates
}
// missing returns the names in want that have no case-insensitive match in have.
// clusterNameservers is the apex NS for the operator's own internal zones
// (catalog, policy): the primary's stable in-cluster name, never a pod IP.
func clusterNameservers(cluster *bindv1alpha1.BindCluster) []string {
return []string{primaryAddress(cluster.Name, cluster.Namespace) + "."}
}
// missing returns the names in want with no match in have. DNS names compare
// case-insensitively.
func missing(want, have []string) (out []string) {
for _, w := range want {
if !containsName(have, w) {
@@ -210,9 +239,3 @@ func recordsOwn(zone *bindv1alpha1.BindZone, name, typ string) bool {
}
return false
}
// clusterNameservers is the apex NS for the operator's own internal zones
// (catalog, policy): the primary's stable in-cluster name, never a pod IP.
func clusterNameservers(cluster *bindv1alpha1.BindCluster) []string {
return []string{primaryAddress(cluster.Name, cluster.Namespace) + "."}
}