Accept intra-cluster NOTIFY on secondaries via allow-notify #14
@@ -22,6 +22,15 @@ type RenderInput struct {
|
|||||||
Forwards []bindv1alpha1.BindZone
|
Forwards []bindv1alpha1.BindZone
|
||||||
// PrimaryAddress is the in-cluster address secondaries transfer from.
|
// PrimaryAddress is the in-cluster address secondaries transfer from.
|
||||||
PrimaryAddress string
|
PrimaryAddress string
|
||||||
|
// PrimaryPodAddresses are the primary pod's own IP(s). Secondaries transfer
|
||||||
|
// from PrimaryAddress (the stable primary Service ClusterIP) but the primary
|
||||||
|
// pod's NOTIFYs egress with its *pod* IP as the source — k8s Services only
|
||||||
|
// NAT the inbound direction — so BIND, whose implicit allow-notify is the
|
||||||
|
// zone's primaries list (the ClusterIP), REFUSES them as "non-primary" and
|
||||||
|
// replication falls back to the SOA refresh timer. Secondaries render these
|
||||||
|
// into an options-scope allow-notify so intra-cluster NOTIFYs are accepted
|
||||||
|
// immediately. Empty leaves BIND's default behaviour unchanged.
|
||||||
|
PrimaryPodAddresses []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderNamedConf returns the primary and secondary named.conf contents for a
|
// RenderNamedConf returns the primary and secondary named.conf contents for a
|
||||||
@@ -86,6 +95,12 @@ func render(in RenderInput, isPrimary bool) string {
|
|||||||
b.WriteString(" allow-new-zones yes;\n")
|
b.WriteString(" allow-new-zones yes;\n")
|
||||||
}
|
}
|
||||||
b.WriteString(" dnssec-validation auto;\n")
|
b.WriteString(" dnssec-validation auto;\n")
|
||||||
|
// Secondaries accept NOTIFY from the primary's pod IP(s). Catalog member and
|
||||||
|
// plain secondary zones take their implicit allow-notify from their primaries
|
||||||
|
// (the primary Service ClusterIP), but the primary's NOTIFYs are sourced from
|
||||||
|
// its pod IP, so an options-scope allow-notify covering the pod IP(s) is
|
||||||
|
// needed or every NOTIFY is refused and replication waits for the SOA refresh.
|
||||||
|
b.WriteString(allowNotifyClause(in, isPrimary, " "))
|
||||||
for _, o := range c.Spec.ExtraOptions {
|
for _, o := range c.Spec.ExtraOptions {
|
||||||
b.WriteString(" " + strings.TrimRight(o, ";") + ";\n")
|
b.WriteString(" " + strings.TrimRight(o, ";") + ";\n")
|
||||||
}
|
}
|
||||||
@@ -277,6 +292,39 @@ func transferPrimaries(in RenderInput) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allowNotifyClause renders an options-scope allow-notify on secondaries that
|
||||||
|
// permits the primary pod IP(s). Zones (catalog members and plain secondaries)
|
||||||
|
// point their primaries at the primary Service ClusterIP for stable AXFR, which
|
||||||
|
// also becomes their implicit allow-notify — but NOTIFYs leave the primary pod
|
||||||
|
// with its pod IP as source, so without this they are refused as "non-primary".
|
||||||
|
// Emitted only on secondaries and only when the primary pod IP(s) are known.
|
||||||
|
func allowNotifyClause(in RenderInput, isPrimary bool, indent string) string {
|
||||||
|
if isPrimary {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
addrs := make([]string, 0, len(in.PrimaryPodAddresses)+1)
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, a := range in.PrimaryPodAddresses {
|
||||||
|
a = strings.TrimSpace(strings.TrimRight(a, ";"))
|
||||||
|
if a == "" || seen[a] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[a] = true
|
||||||
|
addrs = append(addrs, a)
|
||||||
|
}
|
||||||
|
// Keep the transfer address (the Service ClusterIP, or the pod IP when no
|
||||||
|
// primary Service exists) in the set: an explicit allow-notify replaces the
|
||||||
|
// implicit primaries-derived default, so it must still cover that source.
|
||||||
|
if a := strings.TrimSpace(strings.TrimRight(in.PrimaryAddress, ";")); a != "" && !seen[a] {
|
||||||
|
addrs = append(addrs, a)
|
||||||
|
}
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sort.Strings(addrs)
|
||||||
|
return fmt.Sprintf("%sallow-notify { %s };\n", indent, terminate(addrs))
|
||||||
|
}
|
||||||
|
|
||||||
func catalogZonesClause(in RenderInput, isPrimary bool, indent string) string {
|
func catalogZonesClause(in RenderInput, isPrimary bool, indent string) string {
|
||||||
// Only secondaries consume the catalog to auto-provision member zones.
|
// Only secondaries consume the catalog to auto-provision member zones.
|
||||||
if in.Catalog == nil || isPrimary {
|
if in.Catalog == nil || isPrimary {
|
||||||
|
|||||||
@@ -98,6 +98,34 @@ func TestRenderCatalogPrimariesCarryTransferKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderSecondaryAllowNotifyPrimaryPodIP(t *testing.T) {
|
||||||
|
// Secondaries transfer from the primary Service ClusterIP but the primary's
|
||||||
|
// NOTIFYs arrive from its pod IP, so an options allow-notify must cover the
|
||||||
|
// pod IP (and keep the transfer address) or BIND refuses them as non-primary.
|
||||||
|
in := RenderInput{
|
||||||
|
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
|
||||||
|
PrimaryAddress: "10.43.5.5",
|
||||||
|
PrimaryPodAddresses: []string{"10.42.3.197"},
|
||||||
|
}
|
||||||
|
primary, secondary := RenderNamedConf(in)
|
||||||
|
if !strings.Contains(secondary, "allow-notify { 10.42.3.197; 10.43.5.5; };") {
|
||||||
|
t.Fatalf("secondary allow-notify must cover the primary pod IP and transfer address:\n%s", secondary)
|
||||||
|
}
|
||||||
|
if strings.Contains(primary, "allow-notify") {
|
||||||
|
t.Fatalf("primary must not render allow-notify (it is the notifier, not a secondary):\n%s", primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderSecondaryAllowNotifyOmittedWhenPodIPUnknown(t *testing.T) {
|
||||||
|
// With no primary pod IP known there is nothing to add beyond BIND's implicit
|
||||||
|
// primaries-derived default; emit nothing rather than a bare/duplicate clause.
|
||||||
|
in := RenderInput{Cluster: newCluster(bindv1alpha1.ModeAuthoritative)}
|
||||||
|
_, secondary := RenderNamedConf(in)
|
||||||
|
if strings.Contains(secondary, "allow-notify") {
|
||||||
|
t.Fatalf("no allow-notify should be emitted when no primary addresses are known:\n%s", secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderForwardZoneInView(t *testing.T) {
|
func TestRenderForwardZoneInView(t *testing.T) {
|
||||||
rec := true
|
rec := true
|
||||||
in := RenderInput{
|
in := RenderInput{
|
||||||
|
|||||||
@@ -176,6 +176,14 @@ func (r *BindClusterReconciler) reconcileConfigMap(ctx context.Context, c *bindv
|
|||||||
// across primary pod restarts (falls back to the pod IP when no primary
|
// across primary pod restarts (falls back to the pod IP when no primary
|
||||||
// Service exists; the Pod/Service watches re-render when it changes).
|
// Service exists; the Pod/Service watches re-render when it changes).
|
||||||
in := bind.RenderInput{Cluster: c, PrimaryAddress: primaryTransferAddress(ctx, r.Client, c)}
|
in := bind.RenderInput{Cluster: c, PrimaryAddress: primaryTransferAddress(ctx, r.Client, c)}
|
||||||
|
// Secondaries transfer from the stable primary Service ClusterIP, but the
|
||||||
|
// primary pod's NOTIFYs are sourced from its pod IP, which BIND refuses as
|
||||||
|
// "non-primary" unless it appears in allow-notify. Render the primary pod IP
|
||||||
|
// so intra-cluster NOTIFYs are accepted immediately (the Pod watch re-renders
|
||||||
|
// the ConfigMap when the pod IP changes across restarts).
|
||||||
|
if ip := primaryPodIP(ctx, r.Client, c); ip != "" {
|
||||||
|
in.PrimaryPodAddresses = []string{ip}
|
||||||
|
}
|
||||||
|
|
||||||
var acls bindv1alpha1.BindACLList
|
var acls bindv1alpha1.BindACLList
|
||||||
if err := r.List(ctx, &acls, client.InNamespace(c.Namespace)); err == nil {
|
if err := r.List(ctx, &acls, client.InNamespace(c.Namespace)); err == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user