@@ -26,11 +26,14 @@ import (
2626 corev1 "k8s.io/api/core/v1"
2727 "k8s.io/apimachinery/pkg/api/meta"
2828 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+ "k8s.io/apimachinery/pkg/fields"
2930 "k8s.io/apimachinery/pkg/types"
31+ k8sscheme "k8s.io/client-go/kubernetes/scheme"
3032 ctrl "sigs.k8s.io/controller-runtime"
3133 "sigs.k8s.io/controller-runtime/pkg/client"
3234
3335 kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
36+ "github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/global"
3437)
3538
3639// envtest does not actually GC pods when their namespace is deleted, so
@@ -422,6 +425,10 @@ var _ = Describe("Hypervisor Controller", func() {
422425 Expect (client .IgnoreNotFound (k8sClient .Delete (ctx , ns ))).To (Succeed ())
423426 })
424427
428+ // Restrict pod listing to the test namespace.
429+ global .AgentNamespaces = []string {agentNamespace }
430+ DeferCleanup (func () { global .AgentNamespaces = nil })
431+
425432 // The condition is only computed during termination.
426433 _ , err := hypervisorController .Reconcile (ctx , ctrl.Request {
427434 NamespacedName : types.NamespacedName {Name : resource .Name },
@@ -444,6 +451,30 @@ var _ = Describe("Hypervisor Controller", func() {
444451 Message : "All VMs evicted" ,
445452 })
446453 Expect (k8sClient .Status ().Update (ctx , hypervisor )).To (Succeed ())
454+
455+ // The pod list is only issued once the offboarding taint is on the node.
456+ node := & corev1.Node {}
457+ Expect (k8sClient .Get (ctx , types.NamespacedName {Name : resource .Name }, node )).To (Succeed ())
458+ base := node .DeepCopy ()
459+ node .Spec .Taints = append (node .Spec .Taints , corev1.Taint {
460+ Key : taintKeyOffboarding ,
461+ Effect : corev1 .TaintEffectNoExecute ,
462+ })
463+ Expect (k8sClient .Patch (ctx , node , client .MergeFrom (base ))).To (Succeed ())
464+ DeferCleanup (func (ctx SpecContext ) {
465+ fresh := & corev1.Node {}
466+ if err := k8sClient .Get (ctx , types.NamespacedName {Name : resource .Name }, fresh ); err == nil {
467+ base := fresh .DeepCopy ()
468+ taints := fresh .Spec .Taints [:0 ]
469+ for _ , t := range fresh .Spec .Taints {
470+ if t .Key != taintKeyOffboarding {
471+ taints = append (taints , t )
472+ }
473+ }
474+ fresh .Spec .Taints = taints
475+ Expect (client .IgnoreNotFound (k8sClient .Patch (ctx , fresh , client .MergeFrom (base )))).To (Succeed ())
476+ }
477+ })
447478 })
448479
449480 createPod := func (ctx SpecContext , name , namespace string , phase corev1.PodPhase , tolerations ... corev1.Toleration ) * corev1.Pod {
@@ -552,22 +583,86 @@ var _ = Describe("Hypervisor Controller", func() {
552583 Expect (fresh .DeletionTimestamp ).NotTo (BeNil ())
553584 })
554585
555- It ("should set AgentPodsEvicted=True (deletion-pending pod is treated as gone )" , func (ctx SpecContext ) {
556- _ , err := hypervisorController .Reconcile (ctx , ctrl.Request {
586+ It ("should set AgentPodsEvicted=False (deletion-pending pod still counts as running )" , func (ctx SpecContext ) {
587+ result , err := hypervisorController .Reconcile (ctx , ctrl.Request {
557588 NamespacedName : types.NamespacedName {Name : resource .Name },
558589 })
559590 Expect (err ).NotTo (HaveOccurred ())
591+ Expect (result .RequeueAfter ).To (Equal (defaultPollTime ))
560592
561593 hypervisor := & kvmv1.Hypervisor {}
562594 Expect (k8sClient .Get (ctx , hypervisorName , hypervisor )).To (Succeed ())
563595 Expect (hypervisor .Status .Conditions ).To (ContainElement (
564596 SatisfyAll (
565597 HaveField ("Type" , kvmv1 .ConditionTypeAgentPodsEvicted ),
566- HaveField ("Status" , metav1 .ConditionTrue ),
567- HaveField ("Reason" , "NoAgentPods" ),
598+ HaveField ("Status" , metav1 .ConditionFalse ),
599+ HaveField ("Reason" , "AgentPodsRunning" ),
600+ HaveField ("Message" , ContainSubstring ("nova-compute-deleting" )),
568601 ),
569602 ))
570603 })
571604 })
572605 })
573606})
607+
608+ var _ = Describe ("computeAgentPodsEvictedCondition field selector" , func () {
609+ // This test verifies that the pod list issued by computeAgentPodsEvictedCondition
610+ // uses the spec.nodeName field selector so that only pods on the target node
611+ // are returned, not all pods in the cluster.
612+ It ("should only list pods scheduled on the target node" , func (ctx SpecContext ) {
613+ // Use a client with DisableFor pods, mirroring production config.
614+ uncachedClient , err := client .New (cfg , client.Options {
615+ Scheme : k8sscheme .Scheme ,
616+ Cache : & client.CacheOptions {
617+ DisableFor : []client.Object {& corev1.Pod {}},
618+ },
619+ })
620+ Expect (err ).NotTo (HaveOccurred ())
621+
622+ ns := & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "field-selector-test" }}
623+ Expect (client .IgnoreAlreadyExists (k8sClient .Create (ctx , ns ))).To (Succeed ())
624+ DeferCleanup (func (ctx SpecContext ) {
625+ Expect (client .IgnoreNotFound (k8sClient .Delete (ctx , ns ))).To (Succeed ())
626+ })
627+
628+ // Create a pod on "target-node".
629+ onTarget := & corev1.Pod {
630+ ObjectMeta : metav1.ObjectMeta {Name : "on-target" , Namespace : ns .Name },
631+ Spec : corev1.PodSpec {
632+ NodeName : "target-node" ,
633+ Containers : []corev1.Container {{Name : "c" , Image : "registry.example.com/img:latest" }},
634+ },
635+ }
636+ Expect (k8sClient .Create (ctx , onTarget )).To (Succeed ())
637+ DeferCleanup (func (ctx SpecContext ) {
638+ Expect (client .IgnoreNotFound (k8sClient .Delete (ctx , onTarget ))).To (Succeed ())
639+ })
640+
641+ // Create a pod on a different node — must not appear in results.
642+ onOther := & corev1.Pod {
643+ ObjectMeta : metav1.ObjectMeta {Name : "on-other" , Namespace : ns .Name },
644+ Spec : corev1.PodSpec {
645+ NodeName : "other-node" ,
646+ Containers : []corev1.Container {{Name : "c" , Image : "registry.example.com/img:latest" }},
647+ },
648+ }
649+ Expect (k8sClient .Create (ctx , onOther )).To (Succeed ())
650+ DeferCleanup (func (ctx SpecContext ) {
651+ Expect (client .IgnoreNotFound (k8sClient .Delete (ctx , onOther ))).To (Succeed ())
652+ })
653+
654+ pods := & corev1.PodList {}
655+ Expect (uncachedClient .List (ctx , pods ,
656+ client.MatchingFieldsSelector {
657+ Selector : fields .OneTermEqualSelector ("spec.nodeName" , "target-node" ),
658+ },
659+ )).To (Succeed ())
660+
661+ names := make ([]string , len (pods .Items ))
662+ for i , p := range pods .Items {
663+ names [i ] = p .Name
664+ }
665+ Expect (names ).To (ConsistOf ("on-target" ),
666+ "field selector spec.nodeName must filter server-side; got pods: %v" , names )
667+ })
668+ })
0 commit comments