Files
bind-operator/internal/bind/render_test.go
T
unkinben 59612f157a
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
Sort render inputs so config is deterministic (stop restart loop)
client.List returns cache-ordered (non-deterministic) results, so the
forward zones (and DNSSEC policies) reshuffled between reconciles. Before
the config-hash change this was harmless, but now a reshuffled render
rewrites the ConfigMap, flips the pod-template config hash, and the
StatefulSet rolls forever (observed: resolver forward zones churn every
reconcile, pod endlessly recreated).

Sort every list rendered into named.conf (ACLs, views, forward zones,
policies, DNSSEC policies) before rendering, so identical inputs always
produce byte-identical config and the hash is stable.
2026-07-12 23:03:00 +10:00

180 lines
7.1 KiB
Go

package bind
import (
"strings"
"testing"
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func newCluster(mode bindv1alpha1.BindMode) *bindv1alpha1.BindCluster {
return &bindv1alpha1.BindCluster{
ObjectMeta: metav1.ObjectMeta{Name: "auth", Namespace: "dns"},
Spec: bindv1alpha1.BindClusterSpec{Mode: mode, Replicas: 3},
}
}
func TestRenderResolverEnablesRecursion(t *testing.T) {
primary, secondary := RenderNamedConf(RenderInput{Cluster: newCluster(bindv1alpha1.ModeResolver)})
if !strings.Contains(primary, "recursion yes;") {
t.Fatalf("resolver primary should enable recursion:\n%s", primary)
}
if !strings.Contains(secondary, "recursion yes;") {
t.Fatalf("resolver secondary should enable recursion")
}
}
func TestRenderAuthoritativeDisablesRecursion(t *testing.T) {
primary, _ := RenderNamedConf(RenderInput{Cluster: newCluster(bindv1alpha1.ModeAuthoritative)})
if !strings.Contains(primary, "recursion no;") {
t.Fatalf("authoritative should disable recursion:\n%s", primary)
}
if !strings.Contains(primary, "allow-new-zones yes;") {
t.Fatalf("authoritative should allow new zones for dynamic provisioning")
}
}
func TestRenderCatalogOnSecondaryOnly(t *testing.T) {
in := RenderInput{
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
Catalog: &bindv1alpha1.BindCatalogZone{Spec: bindv1alpha1.BindCatalogZoneSpec{ZoneName: "catalog.internal", DefaultPrimaries: []string{"10.0.0.1"}}},
PrimaryAddress: "auth-0.auth-headless.dns.svc.cluster.local",
}
primary, secondary := RenderNamedConf(in)
if strings.Contains(primary, "catalog-zones") {
t.Fatalf("primary must not consume the catalog it publishes:\n%s", primary)
}
if !strings.Contains(secondary, "catalog-zones") {
t.Fatalf("secondary must consume the catalog zone:\n%s", secondary)
}
if !strings.Contains(secondary, "type secondary;") {
t.Fatalf("secondary must declare the catalog zone as a secondary")
}
}
func TestRenderCatalogOmittedWhenPrimaryIPUnknown(t *testing.T) {
// Primary IP not known yet and no explicit default-primaries: the secondary
// must not emit a catalog-zones / secondary catalog zone with an empty
// primaries list (which BIND rejects at config load).
in := RenderInput{
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
Catalog: &bindv1alpha1.BindCatalogZone{Spec: bindv1alpha1.BindCatalogZoneSpec{ZoneName: "catalog.internal"}},
PrimaryAddress: "",
}
_, secondary := RenderNamedConf(in)
if strings.Contains(secondary, "catalog-zones") || strings.Contains(secondary, "primaries {") {
t.Fatalf("secondary must omit catalog primaries when the primary IP is unknown:\n%s", secondary)
}
}
func TestRenderCatalogUsesPrimaryIP(t *testing.T) {
in := RenderInput{
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
Catalog: &bindv1alpha1.BindCatalogZone{Spec: bindv1alpha1.BindCatalogZoneSpec{ZoneName: "catalog.internal"}},
PrimaryAddress: "10.42.0.7",
}
_, secondary := RenderNamedConf(in)
if !strings.Contains(secondary, "primaries { 10.42.0.7; }") {
t.Fatalf("secondary should point primaries at the primary pod IP:\n%s", secondary)
}
}
func TestRenderCatalogPrimariesCarryTransferKey(t *testing.T) {
// When the catalog declares a transfer key, secondaries must present it in
// both the catalog-zones default-primaries and the secondary catalog zone,
// or the key-authenticated primary REFUSES the AXFR.
in := RenderInput{
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
Catalog: &bindv1alpha1.BindCatalogZone{Spec: bindv1alpha1.BindCatalogZoneSpec{ZoneName: "catalog.internal", TransferKeyRef: "transfer-key"}},
PrimaryAddress: "10.43.0.5",
}
_, secondary := RenderNamedConf(in)
if !strings.Contains(secondary, `default-primaries { 10.43.0.5 key "transfer-key"; }`) {
t.Fatalf("catalog-zones default-primaries must carry the transfer key:\n%s", secondary)
}
if !strings.Contains(secondary, `primaries { 10.43.0.5 key "transfer-key"; }`) {
t.Fatalf("secondary catalog zone primaries must carry the transfer key:\n%s", secondary)
}
}
func TestRenderForwardZoneInView(t *testing.T) {
rec := true
in := RenderInput{
Cluster: newCluster(bindv1alpha1.ModeResolver),
Views: []bindv1alpha1.BindView{{
ObjectMeta: metav1.ObjectMeta{Name: "openforwarder"},
Spec: bindv1alpha1.BindViewSpec{ClusterRef: "auth", MatchClients: []string{"acl-main"}, Recursion: &rec},
}},
Forwards: []bindv1alpha1.BindZone{{
Spec: bindv1alpha1.BindZoneSpec{ClusterRef: "auth", ZoneName: "unkin.net", Type: bindv1alpha1.ZoneForward, ViewRef: "openforwarder", Forwarders: []string{"198.18.19.15"}},
}},
}
primary, secondary := RenderNamedConf(in)
for _, out := range []string{primary, secondary} {
if !strings.Contains(out, `view "openforwarder"`) {
t.Fatalf("view missing:\n%s", out)
}
if !strings.Contains(out, `zone "unkin.net" {`) || !strings.Contains(out, "type forward;") || !strings.Contains(out, "forwarders { 198.18.19.15; }") {
t.Fatalf("forward zone not rendered inside view (must be on all pods):\n%s", out)
}
}
}
func TestRenderACL(t *testing.T) {
in := RenderInput{
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
ACLs: []bindv1alpha1.BindACL{{
ObjectMeta: metav1.ObjectMeta{Name: "internal"},
Spec: bindv1alpha1.BindACLSpec{Entries: []string{"10.0.0.0/8", "192.168.0.0/16"}},
}},
}
primary, _ := RenderNamedConf(in)
if !strings.Contains(primary, `acl "internal" { 10.0.0.0/8; 192.168.0.0/16; };`) {
t.Fatalf("ACL not rendered correctly:\n%s", primary)
}
}
func TestCatalogHashStable(t *testing.T) {
// SHA-1 of the wire format of "example.com" is well-defined and stable.
h1 := catalogHash("example.com")
h2 := catalogHash("example.com.")
if h1 != h2 {
t.Fatalf("trailing dot should not change hash: %s vs %s", h1, h2)
}
if len(h1) != 40 {
t.Fatalf("expected 40-char hex sha1, got %d: %s", len(h1), h1)
}
}
func TestRenderDeterministicWithShuffledForwards(t *testing.T) {
// client.List order is non-deterministic; the render must not depend on
// input order, or the ConfigMap churns and (with the config hash) the
// StatefulSet rolls forever.
mkFwd := func(zone, fwd string) bindv1alpha1.BindZone {
return bindv1alpha1.BindZone{
ObjectMeta: metav1.ObjectMeta{Name: zone},
Spec: bindv1alpha1.BindZoneSpec{
ClusterRef: "r", Type: bindv1alpha1.ZoneForward,
ZoneName: zone, Forwarders: []string{fwd},
},
}
}
base := RenderInput{Cluster: newCluster(bindv1alpha1.ModeResolver)}
orderA := base
orderA.Forwards = []bindv1alpha1.BindZone{
mkFwd("unkin.net", "198.18.200.6"), mkFwd("consul", "198.18.19.14"),
mkFwd("k8s.syd1.au.unkin.net", "198.18.200.8"), mkFwd("13.18.198.in-addr.arpa", "198.18.200.6"),
}
orderB := base
orderB.Forwards = []bindv1alpha1.BindZone{
mkFwd("13.18.198.in-addr.arpa", "198.18.200.6"), mkFwd("k8s.syd1.au.unkin.net", "198.18.200.8"),
mkFwd("consul", "198.18.19.14"), mkFwd("unkin.net", "198.18.200.6"),
}
pa, _ := RenderNamedConf(orderA)
pb, _ := RenderNamedConf(orderB)
if pa != pb {
t.Fatalf("render must be independent of forward-zone input order:\n--- A ---\n%s\n--- B ---\n%s", pa, pb)
}
}