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 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) { 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) } }