aab11457af
v0.2.5 (PR #14) added an options-scope allow-notify enumerating the primary pod IP on secondaries. Options-scope config feeds the config-hash annotation that rolls the StatefulSet, so any config change rolled the pods, the primary came back on a new pod IP, the operator re-rendered with the new IP, the hash changed, the pods rolled again — an infinite roll loop across every BindCluster. The prod deployment was reverted to v0.2.4. Replace the pod-IP allow-notify with TSIG-authenticated NOTIFY: - Secondaries render `allow-notify { key "<name>"; };` — a static key element with NO IPs. It depends only on the key name, so pod-IP churn can never change the render, the config-hash, or trigger a restart. - The primary signs its outgoing NOTIFYs: the zone-scope also-notify entries (already enumerating replica pod IPs, applied via rndc addzone/modzone with NO restart) now carry `key "<name>"`. - Key choice: reuse the cluster's catalog transfer TSIG key (TransferKeyRef). Secondaries already present it for AXFR and it is in keys.conf on every pod, so no new key plumbing is needed. Add a permanent regression guard for the loop class: - controller: reconcile the ConfigMap with the primary pod on two different IPs and assert the config-hash is byte-identical. - render: render restart-scoped input and assert no pod IP appears in allow-notify; RenderInput no longer has any pod-IP field. Zone-scope also-notify (rndc, no restart) legitimately still lists pod IPs; only restart-scoped config must be pod-IP-independent.
66 lines
2.3 KiB
Go
66 lines
2.3 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
|
)
|
|
|
|
// A primary zone with known secondaries renders notify explicit + also-notify
|
|
// so a dynamic update NOTIFYs the secondaries immediately.
|
|
func TestBuildZoneConfigPrimaryAlsoNotify(t *testing.T) {
|
|
r := &BindZoneReconciler{}
|
|
zone := &bindv1alpha1.BindZone{
|
|
Spec: bindv1alpha1.BindZoneSpec{
|
|
ZoneName: "main.unkin.net",
|
|
Type: bindv1alpha1.ZonePrimary,
|
|
},
|
|
}
|
|
|
|
cfg, err := r.buildZoneConfig(context.Background(), zone, "transfer-key", []string{"10.42.2.6", "10.42.1.5"}, "externaldns-key")
|
|
if err != nil {
|
|
t.Fatalf("buildZoneConfig: %v", err)
|
|
}
|
|
if !strings.Contains(cfg, "notify explicit") {
|
|
t.Errorf("expected notify explicit in %q", cfg)
|
|
}
|
|
// also-notify entries are keyed so secondaries can admit the NOTIFY by TSIG
|
|
// key (allow-notify { key ... }) instead of by (churning) pod IP.
|
|
if !strings.Contains(cfg, `also-notify { 10.42.2.6 key "externaldns-key"; 10.42.1.5 key "externaldns-key"; }`) {
|
|
t.Errorf("expected keyed also-notify with the secondary IPs in %q", cfg)
|
|
}
|
|
}
|
|
|
|
// also-notify entries carry no key when none is configured (unkeyed NOTIFY).
|
|
func TestBuildZoneConfigPrimaryAlsoNotifyUnkeyed(t *testing.T) {
|
|
r := &BindZoneReconciler{}
|
|
zone := &bindv1alpha1.BindZone{
|
|
Spec: bindv1alpha1.BindZoneSpec{ZoneName: "main.unkin.net", Type: bindv1alpha1.ZonePrimary},
|
|
}
|
|
cfg, err := r.buildZoneConfig(context.Background(), zone, "transfer-key", []string{"10.42.2.6"}, "")
|
|
if err != nil {
|
|
t.Fatalf("buildZoneConfig: %v", err)
|
|
}
|
|
if !strings.Contains(cfg, "also-notify { 10.42.2.6; }") {
|
|
t.Errorf("expected unkeyed also-notify in %q", cfg)
|
|
}
|
|
}
|
|
|
|
// With no secondaries, no also-notify is emitted (single-replica cluster).
|
|
func TestBuildZoneConfigPrimaryNoNotifyTargets(t *testing.T) {
|
|
r := &BindZoneReconciler{}
|
|
zone := &bindv1alpha1.BindZone{
|
|
Spec: bindv1alpha1.BindZoneSpec{ZoneName: "main.unkin.net", Type: bindv1alpha1.ZonePrimary},
|
|
}
|
|
|
|
cfg, err := r.buildZoneConfig(context.Background(), zone, "transfer-key", nil, "externaldns-key")
|
|
if err != nil {
|
|
t.Fatalf("buildZoneConfig: %v", err)
|
|
}
|
|
if strings.Contains(cfg, "also-notify") || strings.Contains(cfg, "notify explicit") {
|
|
t.Errorf("did not expect notify clauses with no targets: %q", cfg)
|
|
}
|
|
}
|