chore(defaultevictor): add matchlabels compatibility to the namespaceselector (#1853)

This commit is contained in:
Linus Fankhauser
2026-05-18 13:15:53 +02:00
committed by GitHub
parent 3ea8051e1e
commit 6b5f6b450c
2 changed files with 150 additions and 7 deletions

View File

@@ -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()

View File

@@ -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)
}
}
})
}
}