From 12e7b6bc08d4f4ed6dedb7f9449b08a67ee6ae96 Mon Sep 17 00:00:00 2001 From: Fabian Wiesel Date: Fri, 10 Jul 2026 13:37:27 +0200 Subject: [PATCH] HypervisorController: also detect termination via Node deletion timestamp Gardener is moving away from setting a Terminating node condition and will instead simply delete the Node. Add node.deletionTimestamp as a second trigger for the Terminating condition on the Hypervisor, alongside the existing legacy node condition check. Both inputs produce the same result: ConditionTypeTerminating=True with ConditionReasonTerminating. The same dual-check is applied to the Spec.Maintenance seeding on first Hypervisor creation, so a node that is already being deleted when its Hypervisor CR is first created correctly enters MaintenanceTermination. --- api/v1/hypervisor_types.go | 4 +- internal/controller/hypervisor_controller.go | 16 ++++---- .../controller/hypervisor_controller_test.go | 41 +++++++++++++++++-- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/api/v1/hypervisor_types.go b/api/v1/hypervisor_types.go index baf8301..4af9043 100644 --- a/api/v1/hypervisor_types.go +++ b/api/v1/hypervisor_types.go @@ -60,7 +60,9 @@ const ( // ConditionTypeReady is the type of condition for ready status of a hypervisor ConditionTypeReady = "Ready" - // ConditionTypeTerminating is the type of condition for terminating status of a hypervisor + // ConditionTypeTerminating is set when the underlying Node is terminating — + // either because the legacy gardener node condition "Terminating" is present, + // or because the Node's deletionTimestamp has been set (gardener's future path). ConditionTypeTerminating = "Terminating" // ConditionTypeTainted is the type of condition for tainted status of a hypervisor diff --git a/internal/controller/hypervisor_controller.go b/internal/controller/hypervisor_controller.go index 0ee5730..d174eb6 100644 --- a/internal/controller/hypervisor_controller.go +++ b/internal/controller/hypervisor_controller.go @@ -130,15 +130,16 @@ func (hv *HypervisorController) Reconcile(ctx context.Context, req ctrl.Request) } } - // update terminating condition + // update terminating condition — fired by either the legacy gardener + // node condition or the node's deletion timestamp (gardener's future path) nodeTerminationCondition := FindNodeStatusCondition(node.Status.Conditions, "Terminating") - if nodeTerminationCondition != nil && nodeTerminationCondition.Status == corev1.ConditionTrue { - // Node might be terminating, propagate condition to hypervisor + if (nodeTerminationCondition != nil && nodeTerminationCondition.Status == corev1.ConditionTrue) || + !node.DeletionTimestamp.IsZero() { meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{ Type: kvmv1.ConditionTypeTerminating, - Status: metav1.ConditionStatus(nodeTerminationCondition.Status), - Reason: nodeTerminationCondition.Reason, - Message: nodeTerminationCondition.Message, + Status: metav1.ConditionTrue, + Reason: kvmv1.ConditionReasonTerminating, + Message: "Node is terminating", }) } @@ -188,7 +189,8 @@ func (hv *HypervisorController) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, fmt.Errorf("failed setting controller reference: %w", err) } - if IsNodeConditionPresentAndEqual(node.Status.Conditions, "Terminating", corev1.ConditionTrue) { + if IsNodeConditionPresentAndEqual(node.Status.Conditions, "Terminating", corev1.ConditionTrue) || + !node.DeletionTimestamp.IsZero() { hypervisor.Spec.Maintenance = kvmv1.MaintenanceTermination } diff --git a/internal/controller/hypervisor_controller_test.go b/internal/controller/hypervisor_controller_test.go index 92313ad..7c89e97 100644 --- a/internal/controller/hypervisor_controller_test.go +++ b/internal/controller/hypervisor_controller_test.go @@ -318,13 +318,12 @@ var _ = Describe("Hypervisor Controller", func() { } By("should have set the terminating condition on the Hypervisor resource") - // Get the Hypervisor resource updatedHypervisor := &kvmv1.Hypervisor{} Expect(hypervisorController.Get(ctx, hypervisorName, updatedHypervisor)).To(Succeed()) Expect(updatedHypervisor.Status.Conditions).To(ContainElement( SatisfyAll( HaveField("Type", kvmv1.ConditionTypeTerminating), - HaveField("Reason", terminatingReason), + HaveField("Reason", kvmv1.ConditionReasonTerminating), HaveField("Status", metav1.ConditionTrue), ), )) @@ -366,8 +365,42 @@ var _ = Describe("Hypervisor Controller", func() { Expect(updatedHypervisor.Status.Conditions).To(ContainElement( SatisfyAll( HaveField("Type", kvmv1.ConditionTypeTerminating), - HaveField("Reason", terminatingReason), + HaveField("Reason", kvmv1.ConditionReasonTerminating), + HaveField("Status", metav1.ConditionTrue), + ), + )) + }) + }) + Context("when only the deletion timestamp is set (no Terminating node condition)", func() { + BeforeEach(func(ctx SpecContext) { + // Use a different node that has no Terminating condition — only + // a deletion timestamp — to verify the new gardener signal path. + resource.Status.Conditions = nil + Expect(k8sClient.Status().Update(ctx, resource)).To(Succeed()) + + By("creating a Hypervisor so the existing-hypervisor path is exercised") + hypervisor := &kvmv1.Hypervisor{ + ObjectMeta: metav1.ObjectMeta{Name: resource.Name}, + } + Expect(k8sClient.Create(ctx, hypervisor)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, hypervisor))).To(Succeed()) + }) + }) + + It("should set the Terminating condition from the deletion timestamp", func(ctx SpecContext) { + _, err := hypervisorController.Reconcile(ctx, ctrl.Request{ + NamespacedName: types.NamespacedName{Name: resource.Name}, + }) + Expect(err).NotTo(HaveOccurred()) + + hypervisor := &kvmv1.Hypervisor{} + Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed()) + Expect(hypervisor.Status.Conditions).To(ContainElement( + SatisfyAll( + HaveField("Type", kvmv1.ConditionTypeTerminating), HaveField("Status", metav1.ConditionTrue), + HaveField("Reason", kvmv1.ConditionReasonTerminating), ), )) }) @@ -602,7 +635,7 @@ var _ = Describe("Hypervisor Controller", func() { SatisfyAll( HaveField("Type", kvmv1.ConditionTypeTerminating), HaveField("Status", metav1.ConditionTrue), - HaveField("Reason", terminatingReason), + HaveField("Reason", kvmv1.ConditionReasonTerminating), ), )) })