diff --git a/internal/bind/render.go b/internal/bind/render.go index 34f5009..4fe19a2 100644 --- a/internal/bind/render.go +++ b/internal/bind/render.go @@ -28,9 +28,28 @@ type RenderInput struct { // cluster. Both variants are shipped in the ConfigMap; the entrypoint selects // one based on the pod ordinal. func RenderNamedConf(in RenderInput) (primary string, secondary string) { + // client.List returns cache-ordered (non-deterministic) results, so sort + // every input slice before rendering. Otherwise the rendered config + // reshuffles between reconciles, churning the ConfigMap — and with the + // pod-template config hash that means an endless rolling restart. + sortInput(&in) return render(in, true), render(in, false) } +// sortInput orders every list rendered into named.conf deterministically. +func sortInput(in *RenderInput) { + sort.Slice(in.ACLs, func(i, j int) bool { return in.ACLs[i].Name < in.ACLs[j].Name }) + sort.Slice(in.Views, func(i, j int) bool { + if in.Views[i].Spec.Order != in.Views[j].Spec.Order { + return in.Views[i].Spec.Order < in.Views[j].Spec.Order + } + return in.Views[i].Name < in.Views[j].Name + }) + sort.Slice(in.Forwards, func(i, j int) bool { return in.Forwards[i].Spec.ZoneName < in.Forwards[j].Spec.ZoneName }) + sort.Slice(in.Policies, func(i, j int) bool { return in.Policies[i].Spec.ZoneName < in.Policies[j].Spec.ZoneName }) + sort.Slice(in.DNSSECPolicies, func(i, j int) bool { return in.DNSSECPolicies[i].Name < in.DNSSECPolicies[j].Name }) +} + func render(in RenderInput, isPrimary bool) string { c := in.Cluster var b strings.Builder diff --git a/internal/bind/render_test.go b/internal/bind/render_test.go index 3607de6..520653f 100644 --- a/internal/bind/render_test.go +++ b/internal/bind/render_test.go @@ -146,3 +146,34 @@ func TestCatalogHashStable(t *testing.T) { 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) + } +}