diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 59a3c563193..02813d83c63 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -65,6 +65,7 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/trace" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" @@ -2707,8 +2708,7 @@ func (c *HeadlampConfig) drainNode(clientset kubernetes.Interface, nodeName stri var deleteErrors []string for _, pod := range pods.Items { - // ignore daemonsets - if pod.Labels["kubernetes.io/created-by"] == "daemonset-controller" { + if isDaemonSetPod(pod) { continue } @@ -2731,6 +2731,22 @@ func (c *HeadlampConfig) drainNode(clientset kubernetes.Interface, nodeName stri }() } +// isDaemonSetPod reports whether pod should be treated as managed by a DaemonSet. +// It supports both the legacy daemonset-controller label and modern owner references. +func isDaemonSetPod(pod corev1.Pod) bool { + if pod.Labels["kubernetes.io/created-by"] == "daemonset-controller" { + return true + } + + for _, owner := range pod.OwnerReferences { + if owner.Kind == "DaemonSet" && (owner.APIVersion == "" || strings.HasPrefix(owner.APIVersion, "apps/")) { + return true + } + } + + return false +} + /* * Handle node drain status Since node drain is an async operation, we need to poll for the status of the drain operation diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index c13401721e9..23c6296f966 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -655,7 +655,13 @@ func TestDrainNodePodDeletionFailure(t *testing.T) { //nolint:funlen ObjectMeta: metav1.ObjectMeta{ Name: "pod-daemonset", Namespace: "default", - Labels: map[string]string{"kubernetes.io/created-by": "daemonset-controller"}, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "apps/v1", + Kind: "DaemonSet", + Name: "node-agent", + }, + }, }, Spec: corev1.PodSpec{ NodeName: "test-node", @@ -721,6 +727,7 @@ func TestDrainNodePodDeletionFailure(t *testing.T) { //nolint:funlen assert.True(t, strings.HasPrefix(status, "error:"), "expected error status, got: %s", status) assert.Contains(t, status, "failed to delete") + assert.ElementsMatch(t, []string{"pod-ok", "pod-fail"}, deletedPodNames(fakeClient.Actions())) } func TestDrainNodeAllPodsDeletedSuccessfully(t *testing.T) { @@ -782,6 +789,24 @@ func TestDrainNodeAllPodsDeletedSuccessfully(t *testing.T) { require.True(t, ok) assert.Equal(t, "success", status) + + assert.ElementsMatch(t, []string{"pod-1"}, deletedPodNames(fakeClient.Actions())) +} + +// deletedPodNames returns the pod names from delete actions recorded by the fake client. +func deletedPodNames(actions []k8stesting.Action) []string { + deletedPods := []string{} + + for _, action := range actions { + if action.GetVerb() != "delete" || action.GetResource().Resource != "pods" { + continue + } + + deleteAction := action.(k8stesting.DeleteAction) + deletedPods = append(deletedPods, deleteAction.GetName()) + } + + return deletedPods } func TestDeletePlugin(t *testing.T) {