Sort render inputs so config is deterministic (stop restart loop)
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

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.
This commit is contained in:
2026-07-12 23:03:00 +10:00
parent 8fac152537
commit 59612f157a
2 changed files with 50 additions and 0 deletions
+19
View File
@@ -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