@@ -19,6 +19,7 @@ package controller
1919
2020import (
2121 "fmt"
22+ "time"
2223
2324 . "github.com/onsi/ginkgo/v2"
2425 . "github.com/onsi/gomega"
@@ -28,6 +29,7 @@ import (
2829 "k8s.io/apimachinery/pkg/api/meta"
2930 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031 "k8s.io/apimachinery/pkg/types"
32+ "k8s.io/utils/clock"
3133 ctrl "sigs.k8s.io/controller-runtime"
3234 k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
3335
@@ -47,6 +49,7 @@ var _ = Describe("Gardener Maintenance Controller", func() {
4749 controller = & GardenerNodeLifecycleController {
4850 Client : k8sClient ,
4951 Scheme : k8sClient .Scheme (),
52+ Clock : clock.RealClock {},
5053 }
5154
5255 By ("creating the core resource for the Kind Node" )
@@ -166,18 +169,21 @@ var _ = Describe("Gardener Maintenance Controller", func() {
166169
167170 Context ("When node is terminating and offboarded" , func () {
168171 BeforeEach (func (ctx SpecContext ) {
169- // Set node as terminating and add required labels for disableInstanceHA
172+ // Set node labels (spec/metadata update)
170173 node := & corev1.Node {}
171174 Expect (k8sClient .Get (ctx , name , node )).To (Succeed ())
172175 node .Labels = map [string ]string {
173176 corev1 .LabelHostname : nodeName ,
174177 "topology.kubernetes.io/zone" : "test-zone" ,
175178 }
179+ Expect (k8sClient .Update (ctx , node )).To (Succeed ())
180+ // Set node Terminating condition separately via the status subresource,
181+ // using a fresh Get to avoid the spec Update overwriting the status.
182+ Expect (k8sClient .Get (ctx , name , node )).To (Succeed ())
176183 node .Status .Conditions = append (node .Status .Conditions , corev1.NodeCondition {
177184 Type : "Terminating" ,
178185 Status : corev1 .ConditionTrue ,
179186 })
180- Expect (k8sClient .Update (ctx , node )).To (Succeed ())
181187 Expect (k8sClient .Status ().Update (ctx , node )).To (Succeed ())
182188
183189 // Set hypervisor as onboarded and offboarded
@@ -198,13 +204,70 @@ var _ = Describe("Gardener Maintenance Controller", func() {
198204 Expect (k8sClient .Status ().Update (ctx , hypervisor )).To (Succeed ())
199205 })
200206
201- It ("should allow pod eviction by setting the PDB to minAvailable 0" , func (ctx SpecContext ) {
202- _ , err := controller .Reconcile (ctx , reconcileReq )
203- Expect (err ).NotTo (HaveOccurred ())
207+ When ("HaEnabled is explicitly False" , func () {
208+ BeforeEach (func (ctx SpecContext ) {
209+ hypervisor := & kvmv1.Hypervisor {}
210+ Expect (k8sClient .Get (ctx , name , hypervisor )).To (Succeed ())
211+ meta .SetStatusCondition (& hypervisor .Status .Conditions , metav1.Condition {
212+ Type : kvmv1 .ConditionTypeHaEnabled ,
213+ Status : metav1 .ConditionFalse ,
214+ Reason : "Evicted" ,
215+ Message : "HA disabled due to eviction" ,
216+ })
217+ Expect (k8sClient .Status ().Update (ctx , hypervisor )).To (Succeed ())
218+ })
204219
205- pdb := & policyv1.PodDisruptionBudget {}
206- Expect (k8sClient .Get (ctx , maintenanceName , pdb )).To (Succeed ())
207- Expect (pdb .Spec .MinAvailable ).To (HaveField ("IntVal" , BeNumerically ("==" , int32 (0 ))))
220+ It ("should allow pod eviction immediately by setting the PDB to minAvailable 0" , func (ctx SpecContext ) {
221+ result , err := controller .Reconcile (ctx , reconcileReq )
222+ Expect (err ).NotTo (HaveOccurred ())
223+ Expect (result .RequeueAfter ).To (BeZero ())
224+
225+ pdb := & policyv1.PodDisruptionBudget {}
226+ Expect (k8sClient .Get (ctx , maintenanceName , pdb )).To (Succeed ())
227+ Expect (pdb .Spec .MinAvailable ).To (HaveField ("IntVal" , BeNumerically ("==" , int32 (0 ))))
228+ })
229+ })
230+
231+ When ("HaEnabled is not yet False and the timeout has not elapsed" , func () {
232+ BeforeEach (func () {
233+ // LastTransitionTime ≈ now (set by meta.SetStatusCondition above),
234+ // so deadline = now + 1h is in the future.
235+ controller .HaDisabledTimeout = time .Hour
236+ })
237+
238+ It ("should requeue and not proceed" , func (ctx SpecContext ) {
239+ result , err := controller .Reconcile (ctx , reconcileReq )
240+ Expect (err ).NotTo (HaveOccurred ())
241+ // Should requeue before the deadline rather than returning immediately.
242+ Expect (result .RequeueAfter ).To (BeNumerically (">" , 0 ))
243+ })
244+ })
245+
246+ When ("HaEnabled is not False but the timeout has elapsed" , func () {
247+ BeforeEach (func (ctx SpecContext ) {
248+ // Push LastTransitionTime 2h into the past so that
249+ // deadline = (now - 2h) + 1h = now - 1h, which has already elapsed.
250+ hypervisor := & kvmv1.Hypervisor {}
251+ Expect (k8sClient .Get (ctx , name , hypervisor )).To (Succeed ())
252+ for i := range hypervisor .Status .Conditions {
253+ if hypervisor .Status .Conditions [i ].Type == kvmv1 .ConditionTypeOffboarded {
254+ hypervisor .Status .Conditions [i ].LastTransitionTime = metav1 .NewTime (time .Now ().Add (- 2 * time .Hour ))
255+ break
256+ }
257+ }
258+ Expect (k8sClient .Status ().Update (ctx , hypervisor )).To (Succeed ())
259+ controller .HaDisabledTimeout = time .Hour
260+ })
261+
262+ It ("should allow pod eviction by setting the PDB to minAvailable 0" , func (ctx SpecContext ) {
263+ result , err := controller .Reconcile (ctx , reconcileReq )
264+ Expect (err ).NotTo (HaveOccurred ())
265+ Expect (result .RequeueAfter ).To (BeZero ())
266+
267+ pdb := & policyv1.PodDisruptionBudget {}
268+ Expect (k8sClient .Get (ctx , maintenanceName , pdb )).To (Succeed ())
269+ Expect (pdb .Spec .MinAvailable ).To (HaveField ("IntVal" , BeNumerically ("==" , int32 (0 ))))
270+ })
208271 })
209272 })
210273})
0 commit comments