From 29d48eac9817c0b9c0091be936697376864ad20b Mon Sep 17 00:00:00 2001 From: Davide Salerno Date: Wed, 6 May 2026 21:22:56 +0200 Subject: [PATCH] Add preStop lifecycle hook to DNS pods to reduce upgrade disruption During cluster upgrades, DNS pods are terminated as nodes reboot. There is a race between the kubelet sending SIGTERM to CoreDNS and kube-proxy/OVN removing the pod from the DNS service endpoints. If SIGTERM arrives first, CoreDNS begins its lameduck shutdown while clients are still being routed to the pod, resulting in DNS lookup timeouts. Add a preStop lifecycle hook with a 5-second sleep to the dns container. This gives the endpoints controller and kube-proxy/OVN time to remove the pod from the ClusterIP service before CoreDNS receives SIGTERM and begins its 20-second lameduck period. Set terminationGracePeriodSeconds to 40 to accommodate the full shutdown sequence: 5s preStop + 20s CoreDNS lameduck + 15s buffer. Add Lifecycle to the daemonset change detection so the hook is applied on upgrade without requiring other container field changes. Signed-off-by: Davide Salerno --- pkg/manifests/assets/dns/daemonset.yaml | 5 ++++ .../controller/controller_dns_daemonset.go | 5 ++++ .../controller_dns_daemonset_test.go | 23 +++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/pkg/manifests/assets/dns/daemonset.yaml b/pkg/manifests/assets/dns/daemonset.yaml index 5187b192a..c6f0b99c9 100644 --- a/pkg/manifests/assets/dns/daemonset.yaml +++ b/pkg/manifests/assets/dns/daemonset.yaml @@ -56,6 +56,10 @@ spec: memory: 70Mi securityContext: readOnlyRootFilesystem: true + lifecycle: + preStop: + exec: + command: ["sleep", "5"] - name: kube-rbac-proxy # image and args are set at runtime by the operator based on the # centralized TLS security profile from apiservers.config.openshift.io/cluster @@ -75,6 +79,7 @@ spec: name: tmp-dir securityContext: readOnlyRootFilesystem: true + terminationGracePeriodSeconds: 40 dnsPolicy: Default # nodeSelector is set at runtime. volumes: diff --git a/pkg/operator/controller/controller_dns_daemonset.go b/pkg/operator/controller/controller_dns_daemonset.go index cc20f9088..a9cf12ee7 100644 --- a/pkg/operator/controller/controller_dns_daemonset.go +++ b/pkg/operator/controller/controller_dns_daemonset.go @@ -527,6 +527,11 @@ func daemonsetConfigChanged(current, expected *appsv1.DaemonSet) (bool, *appsv1. changed = true break } + if !cmp.Equal(a.Lifecycle, b.Lifecycle, cmpopts.EquateEmpty()) { + updated.Spec.Template.Spec.Containers = expected.Spec.Template.Spec.Containers + changed = true + break + } } } diff --git a/pkg/operator/controller/controller_dns_daemonset_test.go b/pkg/operator/controller/controller_dns_daemonset_test.go index 39454f987..fae09f2e9 100644 --- a/pkg/operator/controller/controller_dns_daemonset_test.go +++ b/pkg/operator/controller/controller_dns_daemonset_test.go @@ -78,6 +78,11 @@ func TestDesiredDNSDaemonset(t *testing.T) { if e, a := coreDNSImage, c.Image; e != a { t.Errorf("expected daemonset dns image %q, got %q", e, a) } + if c.Lifecycle == nil || c.Lifecycle.PreStop == nil { + t.Error("expected dns container to have a preStop lifecycle hook") + } else if !reflect.DeepEqual(c.Lifecycle.PreStop.Exec.Command, []string{"sleep", "5"}) { + t.Errorf("unexpected preStop command: %v", c.Lifecycle.PreStop.Exec.Command) + } case "kube-rbac-proxy": if e, a := kubeRBACProxyImage, c.Image; e != a { t.Errorf("expected daemonset kube rbac proxy image %q, got %q", e, a) @@ -86,6 +91,11 @@ func TestDesiredDNSDaemonset(t *testing.T) { t.Errorf("unexpected daemonset container %q", c.Name) } } + if ds.Spec.Template.Spec.TerminationGracePeriodSeconds == nil { + t.Error("expected terminationGracePeriodSeconds to be set") + } else if *ds.Spec.Template.Spec.TerminationGracePeriodSeconds != 40 { + t.Errorf("expected terminationGracePeriodSeconds=40, got %d", *ds.Spec.Template.Spec.TerminationGracePeriodSeconds) + } } } @@ -426,6 +436,19 @@ func TestDaemonsetConfigChanged(t *testing.T) { }, expect: true, }, + { + description: "if a container lifecycle is added", + mutate: func(daemonset *appsv1.DaemonSet) { + daemonset.Spec.Template.Spec.Containers[0].Lifecycle = &corev1.Lifecycle{ + PreStop: &corev1.LifecycleHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"sleep", "5"}, + }, + }, + } + }, + expect: true, + }, { description: "if an unexpected additional container is added", mutate: func(daemonset *appsv1.DaemonSet) {