diff --git a/pkg/framework/plugins/defaultevictor/defaultevictor.go b/pkg/framework/plugins/defaultevictor/defaultevictor.go index e1700a3e93..8f8bf3265b 100644 --- a/pkg/framework/plugins/defaultevictor/defaultevictor.go +++ b/pkg/framework/plugins/defaultevictor/defaultevictor.go @@ -90,7 +90,7 @@ func New(ctx context.Context, args runtime.Object, handle frameworktypes.Handle) return nil, err } - if ev.args.NamespaceLabelSelector != nil && len(ev.args.NamespaceLabelSelector.MatchLabels) > 0 { + if ev.args.NamespaceLabelSelector != nil && (len(ev.args.NamespaceLabelSelector.MatchLabels) > 0 || len(ev.args.NamespaceLabelSelector.MatchExpressions) > 0) { selector, nslErr := metav1.LabelSelectorAsSelector(ev.args.NamespaceLabelSelector) if nslErr != nil { return nil, fmt.Errorf("unable to convert namespaceLabelSelector to label selector: %w", nslErr) @@ -447,7 +447,7 @@ func (d *DefaultEvictor) PreEvictionFilter(pod *v1.Pod) bool { } } - if d.args.NamespaceLabelSelector == nil || len(d.args.NamespaceLabelSelector.MatchLabels) == 0 { + if d.args.NamespaceLabelSelector == nil || (len(d.args.NamespaceLabelSelector.MatchLabels) == 0 && len(d.args.NamespaceLabelSelector.MatchExpressions) == 0) { return true } indexName := namespaceWithLabelSelector + d.handle.PluginInstanceID() diff --git a/pkg/framework/plugins/defaultevictor/defaultevictor_test.go b/pkg/framework/plugins/defaultevictor/defaultevictor_test.go index deddb608b2..9db47452f8 100644 --- a/pkg/framework/plugins/defaultevictor/defaultevictor_test.go +++ b/pkg/framework/plugins/defaultevictor/defaultevictor_test.go @@ -1030,13 +1030,13 @@ func initializePlugin(ctx context.Context, test testCase) (frameworktypes.Plugin objs = append(objs, ns) } - fakeClient := fake.NewSimpleClientset(objs...) + fakeClient := fake.NewClientset(objs...) sharedInformerFactory := informers.NewSharedInformerFactory(fakeClient, 0) podInformer := sharedInformerFactory.Core().V1().Pods().Informer() _ = sharedInformerFactory.Policy().V1().PodDisruptionBudgets().Lister() _ = sharedInformerFactory.Core().V1().PersistentVolumeClaims().Lister() - _ = sharedInformerFactory.Core().V1().Namespaces().Lister() + _ = sharedInformerFactory.Core().V1().Namespaces().Informer() getPodsAssignedToNode, err := podutil.BuildGetPodsAssignedToNodeFunc(podInformer) if err != nil { return nil, fmt.Errorf("build get pods assigned to node function error: %v", err) @@ -1283,9 +1283,7 @@ func TestMultipleProfilesWithDifferentNamespaceLabelSelectors(t *testing.T) { fakeClient := fake.NewClientset(node, nsProd, nsBackend, nsTest, podInProd, podInBackend, podInTest) sharedInformerFactory := informers.NewSharedInformerFactory(fakeClient, 0) - _ = sharedInformerFactory.Core().V1().Namespaces().Lister() - sharedInformerFactory.Start(ctx.Done()) - sharedInformerFactory.WaitForCacheSync(ctx.Done()) + _ = sharedInformerFactory.Core().V1().Namespaces().Informer() // Create Profile 1: targets namespaces with env=prod profile1Args := &DefaultEvictorArgs{ @@ -1328,6 +1326,9 @@ func TestMultipleProfilesWithDifferentNamespaceLabelSelectors(t *testing.T) { t.Fatalf("unable to initialize profile2 plugin: %v", err) } + sharedInformerFactory.Start(ctx.Done()) + sharedInformerFactory.WaitForCacheSync(ctx.Done()) + // Test Profile 1: evicts pods in ns-prod, reject others t.Run("profile1", func(t *testing.T) { profile1 := profile1Plugin.(*DefaultEvictor) @@ -1362,3 +1363,145 @@ func TestMultipleProfilesWithDifferentNamespaceLabelSelectors(t *testing.T) { } }) } + +func TestNamespaceLabelSelectorAllOperations(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + node := test.BuildTestNode("node1", 1000, 2000, 10, nil) + + nsProd := test.BuildTestNamespace("ns-prod") + nsProd.Labels = map[string]string{"env": "prod", "tier": "frontend"} + + nsStaging := test.BuildTestNamespace("ns-staging") + nsStaging.Labels = map[string]string{"env": "staging", "tier": "backend"} + + nsTest := test.BuildTestNamespace("ns-test") + nsTest.Labels = map[string]string{"env": "test"} + + nsNoLabels := test.BuildTestNamespace("ns-nolabels") + nsNoLabels.Labels = map[string]string{} + + podInProdFrontend := test.BuildTestPod("pod-in-prod", 100, 100, node.Name, func(pod *v1.Pod) { + pod.Namespace = nsProd.Name + test.SetNormalOwnerRef(pod) + }) + + podInStagingBackend := test.BuildTestPod("pod-in-staging", 100, 100, node.Name, func(pod *v1.Pod) { + pod.Namespace = nsStaging.Name + test.SetNormalOwnerRef(pod) + }) + + podInTest := test.BuildTestPod("pod-in-test", 100, 100, node.Name, func(pod *v1.Pod) { + pod.Namespace = nsTest.Name + test.SetNormalOwnerRef(pod) + }) + + podInNoLabels := test.BuildTestPod("pod-in-nolabels", 100, 100, node.Name, func(pod *v1.Pod) { + pod.Namespace = nsNoLabels.Name + test.SetNormalOwnerRef(pod) + }) + + fakeClient := fake.NewClientset(node, nsProd, nsStaging, nsTest, nsNoLabels, podInProdFrontend, podInStagingBackend, podInTest, podInNoLabels) + sharedInformerFactory := informers.NewSharedInformerFactory(fakeClient, 0) + getPodAssignedToNode, _ := podutil.BuildGetPodsAssignedToNodeFunc(sharedInformerFactory.Core().V1().Pods().Informer()) + + testCases := []struct { + name string + args *DefaultEvictorArgs + pass []*v1.Pod + fail []*v1.Pod + }{ + { + name: "MatchLabels and MatchExpressions In", + args: &DefaultEvictorArgs{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tier": "frontend"}, + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "env", Operator: metav1.LabelSelectorOpIn, Values: []string{"prod", "staging"}}, + }, + }, + }, + pass: []*v1.Pod{podInProdFrontend}, + fail: []*v1.Pod{podInStagingBackend, podInTest}, + }, + { + name: "MatchLabels and MatchExpressions Exists", + args: &DefaultEvictorArgs{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tier": "backend"}, + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "env", Operator: metav1.LabelSelectorOpExists}, + }, + }, + }, + pass: []*v1.Pod{podInStagingBackend}, + fail: []*v1.Pod{podInProdFrontend, podInTest}, + }, + { + name: "Exists and In", + args: &DefaultEvictorArgs{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "tier", Operator: metav1.LabelSelectorOpExists}, + {Key: "env", Operator: metav1.LabelSelectorOpIn, Values: []string{"staging"}}, + }, + }, + }, + pass: []*v1.Pod{podInStagingBackend}, + fail: []*v1.Pod{podInProdFrontend, podInTest}, + }, + { + name: "DoesNotExist", + args: &DefaultEvictorArgs{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "tier", Operator: metav1.LabelSelectorOpDoesNotExist}, + }, + }, + }, + pass: []*v1.Pod{podInTest, podInNoLabels}, + fail: []*v1.Pod{podInProdFrontend, podInStagingBackend}, + }, + { + name: "NotIn", + args: &DefaultEvictorArgs{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "env", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"prod", "test"}}, + }, + }, + }, + pass: []*v1.Pod{podInStagingBackend}, + fail: []*v1.Pod{podInProdFrontend, podInTest}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + plugin, err := New(ctx, tc.args, &frameworkfake.HandleImpl{ + ClientsetImpl: fakeClient, + GetPodsAssignedToNodeFuncImpl: getPodAssignedToNode, + SharedInformerFactoryImpl: sharedInformerFactory, + PluginInstanceIDImpl: "test-" + tc.name, + }) + if err != nil { + t.Fatalf("unable to initialize plugin: %v", err) + } + + sharedInformerFactory.Start(ctx.Done()) + sharedInformerFactory.WaitForCacheSync(ctx.Done()) + + evictor := plugin.(*DefaultEvictor) + for _, pod := range tc.pass { + if result := evictor.PreEvictionFilter(pod); !result { + t.Errorf("pod %s expected to pass (true), got false", pod.Name) + } + } + for _, pod := range tc.fail { + if result := evictor.PreEvictionFilter(pod); result { + t.Errorf("pod %s expected to fail (false), got true", pod.Name) + } + } + }) + } +}