Merge pull request #1786 from W1seKappa/add-namespace-label-selector

Add namespace label selector
This commit is contained in:
Kubernetes Prow Robot
2026-02-02 18:24:29 +05:30
committed by GitHub
7 changed files with 304 additions and 22 deletions

View File

@@ -158,6 +158,7 @@ The Default Evictor Plugin is used by default for filtering pods before processi
| `ignorePvcPods` | `bool` | `false` | **[Deprecated: Use `podProtections` with `"PodsWithPVC"` instead]**<br>Sets whether PVC pods should be evicted or ignored. |
| `evictFailedBarePods` | `bool` | `false` | **[Deprecated: Use `podProtections` with `"FailedBarePods"` instead]**<br>Allows eviction of pods without owner references and in a failed phase. |
| `ignorePodsWithoutPDB` | `bool` | `false` | **[Deprecated: Use `podProtections` with `"PodsWithoutPDB"` instead]**<br>Sets whether pods without PodDisruptionBudget should be evicted or ignored. |
| `namespaceLabelSelector` | `metav1.LabelSelector` | | limiting the pods which are processed by namespace (see [label filtering](#label-filtering)) |
| `labelSelector` | `metav1.LabelSelector` | | (See [label filtering](#label-filtering)) |
| `priorityThreshold` | `priorityThreshold` | | (See [priority filtering](#priority-filtering)) |
| `nodeFit` | `bool` | `false` | (See [node fit filtering](#node-fit-filtering)) |
@@ -1202,4 +1203,4 @@ This roadmap is not in any particular order.
### Code of conduct
Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md).
Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md).

View File

@@ -3,7 +3,7 @@ Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -21,6 +21,8 @@ import (
"slices"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/informers"
@@ -28,6 +30,7 @@ import (
"k8s.io/klog/v2"
evictionutils "sigs.k8s.io/descheduler/pkg/descheduler/evictions/utils"
nodeutil "sigs.k8s.io/descheduler/pkg/descheduler/node"
podutil "sigs.k8s.io/descheduler/pkg/descheduler/pod"
frameworktypes "sigs.k8s.io/descheduler/pkg/framework/types"
@@ -35,8 +38,9 @@ import (
)
const (
PluginName = "DefaultEvictor"
evictPodAnnotationKey = "descheduler.alpha.kubernetes.io/evict"
PluginName = "DefaultEvictor"
evictPodAnnotationKey = "descheduler.alpha.kubernetes.io/evict"
namespaceWithLabelSelector = "namespaceWithLabelSelector-"
)
var _ frameworktypes.EvictorPlugin = &DefaultEvictor{}
@@ -85,9 +89,43 @@ func New(ctx context.Context, args runtime.Object, handle frameworktypes.Handle)
if err != nil {
return nil, err
}
if ev.args.NamespaceLabelSelector != nil && len(ev.args.NamespaceLabelSelector.MatchLabels) > 0 {
selector, nslErr := metav1.LabelSelectorAsSelector(ev.args.NamespaceLabelSelector)
if nslErr != nil {
return nil, fmt.Errorf("unable to convert namespaceLabelSelector to label selector: %w", nslErr)
}
indexName := namespaceWithLabelSelector + ev.handle.PluginInstanceID()
if nslErr := addNamespaceLabelSelectorIndexer(ev.handle.SharedInformerFactory().Core().V1().Namespaces().Informer(), indexName, selector); nslErr != nil {
return nil, fmt.Errorf("failed to add namespace label selector indexer: %w", nslErr)
}
}
return ev, nil
}
func addNamespaceLabelSelectorIndexer(informer cache.SharedIndexInformer, indexName string, selector labels.Selector) error {
indexer := informer.GetIndexer()
for name := range indexer.GetIndexers() {
if name == indexName {
return nil
}
}
return informer.AddIndexers(cache.Indexers{
indexName: func(obj interface{}) ([]string, error) {
ns, ok := obj.(*v1.Namespace)
if !ok {
return []string{}, errors.New("unexpected object")
}
if !selector.Empty() {
if !selector.Matches(labels.Set(ns.Labels)) {
return []string{}, nil
}
}
return []string{ns.GetName()}, nil
},
})
}
func (d *DefaultEvictor) addAllConstraints(logger klog.Logger, handle frameworktypes.Handle) error {
args := d.args
// Determine effective protected policies based on the provided arguments.
@@ -407,8 +445,21 @@ func (d *DefaultEvictor) PreEvictionFilter(pod *v1.Pod) bool {
logger.Info("pod does not fit on any other node because of nodeSelector(s), Taint(s), or nodes marked as unschedulable", "pod", klog.KObj(pod))
return false
}
}
if d.args.NamespaceLabelSelector == nil || len(d.args.NamespaceLabelSelector.MatchLabels) == 0 {
return true
}
indexName := namespaceWithLabelSelector + d.handle.PluginInstanceID()
objs, err := d.handle.SharedInformerFactory().Core().V1().Namespaces().Informer().GetIndexer().ByIndex(indexName, pod.Namespace)
if err != nil {
logger.Error(err, "unable to list namespaces for namespaceLabelSelector filter in the policy parameter", "pod", klog.KObj(pod))
return false
}
if len(objs) == 0 {
logger.Info("pod namespace do not match the namespaceLabelSelector filter in the policy parameter", "pod", klog.KObj(pod))
return false
}
return true
}
@@ -473,6 +524,5 @@ func getPodIndexerByOwnerRefs(indexName string, handle frameworktypes.Handle) (c
}); err != nil {
return nil, err
}
return indexer, nil
}

View File

@@ -39,10 +39,20 @@ import (
"sigs.k8s.io/descheduler/test"
)
var (
namespace = "test"
namespaceSelector = &metav1.LabelSelector{
MatchLabels: map[string]string{
"kubernetes.io/metadata.name": namespace,
},
}
)
type testCase struct {
description string
pods []*v1.Pod
nodes []*v1.Node
namespaces []*v1.Namespace
pdbs []*policyv1.PodDisruptionBudget
evictFailedBarePods bool
evictLocalStoragePods bool
@@ -50,6 +60,7 @@ type testCase struct {
ignorePvcPods bool
priorityThreshold *int32
nodeFit bool
useNamespaceSelector bool
minReplicas uint
minPodAge *metav1.Duration
result bool
@@ -121,7 +132,8 @@ func TestDefaultEvictorPreEvictionFilter(t *testing.T) {
buildTestNode("node3", setNodeTaint),
},
nodeFit: true,
}, {
},
{
description: "Pod with correct tolerations running on normal node, all other nodes tainted",
pods: []*v1.Pod{
buildTestPod("p1", n1.Name, func(pod *v1.Pod) {
@@ -141,7 +153,8 @@ func TestDefaultEvictorPreEvictionFilter(t *testing.T) {
},
nodeFit: true,
result: true,
}, {
},
{
description: "Pod with incorrect node selector",
pods: []*v1.Pod{
buildTestPod("p1", n1.Name, func(pod *v1.Pod) {
@@ -156,7 +169,8 @@ func TestDefaultEvictorPreEvictionFilter(t *testing.T) {
buildTestNode("node3", setNodeLabel),
},
nodeFit: true,
}, {
},
{
description: "Pod with correct node selector",
pods: []*v1.Pod{
buildTestPod("p1", n1.Name, func(pod *v1.Pod) {
@@ -170,7 +184,8 @@ func TestDefaultEvictorPreEvictionFilter(t *testing.T) {
},
nodeFit: true,
result: true,
}, {
},
{
description: "Pod with correct node selector, but only available node doesn't have enough CPU",
pods: []*v1.Pod{
test.BuildTestPod("p1", 12, 8, n1.Name, func(pod *v1.Pod) {
@@ -183,7 +198,8 @@ func TestDefaultEvictorPreEvictionFilter(t *testing.T) {
test.BuildTestNode("node3-TEST", 10, 16, 10, setNodeLabel),
},
nodeFit: true,
}, {
},
{
description: "Pod with correct node selector, and one node has enough memory",
pods: []*v1.Pod{
test.BuildTestPod("p1", 12, 8, n1.Name, func(pod *v1.Pod) {
@@ -207,7 +223,8 @@ func TestDefaultEvictorPreEvictionFilter(t *testing.T) {
},
nodeFit: true,
result: true,
}, {
},
{
description: "Pod with correct node selector, but both nodes don't have enough memory",
pods: []*v1.Pod{
test.BuildTestPod("p1", 12, 8, n1.Name, func(pod *v1.Pod) {
@@ -230,7 +247,8 @@ func TestDefaultEvictorPreEvictionFilter(t *testing.T) {
test.BuildTestNode("node3", 100, 16, 10, setNodeLabel),
},
nodeFit: true,
}, {
},
{
description: "Pod with incorrect node selector, but nodefit false, should still be evicted",
pods: []*v1.Pod{
buildTestPod("p1", n1.Name, func(pod *v1.Pod) {
@@ -246,6 +264,71 @@ func TestDefaultEvictorPreEvictionFilter(t *testing.T) {
},
result: true,
},
{
description: "Pod with namespace matched namespace selector, should be evicted",
pods: []*v1.Pod{
test.BuildTestPod("p1", 400, 0, n1.Name, func(pod *v1.Pod) {
pod.ObjectMeta.Namespace = namespace
pod.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
pod.Spec.NodeSelector = map[string]string{
nodeLabelKey: nodeLabelValue,
}
}),
},
nodes: []*v1.Node{
test.BuildTestNode("node2", 1000, 2000, 13, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
nodeLabelKey: nodeLabelValue,
}
}),
test.BuildTestNode("node3", 1000, 2000, 13, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
nodeLabelKey: nodeLabelValue,
}
}),
},
namespaces: []*v1.Namespace{
test.BuildTestNamespace("default"),
test.BuildTestNamespace(namespace),
},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
nodeFit: true,
useNamespaceSelector: true,
result: true,
},
{
description: "Pod with namespace does not matched namespace selector, should not be evicted",
pods: []*v1.Pod{
test.BuildTestPod("p1", 400, 0, n1.Name, func(pod *v1.Pod) {
pod.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
pod.Spec.NodeSelector = map[string]string{
nodeLabelKey: "fail",
}
}),
},
nodes: []*v1.Node{
test.BuildTestNode("node2", 1000, 2000, 13, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
nodeLabelKey: nodeLabelValue,
}
}),
test.BuildTestNode("node3", 1000, 2000, 13, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
nodeLabelKey: nodeLabelValue,
}
}),
},
namespaces: []*v1.Namespace{
test.BuildTestNamespace("default"),
test.BuildTestNamespace(namespace),
},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
nodeFit: true,
useNamespaceSelector: true,
result: false,
},
}
for _, test := range testCases {
@@ -928,9 +1011,11 @@ func TestReinitialization(t *testing.T) {
func initializePlugin(ctx context.Context, test testCase) (frameworktypes.Plugin, error) {
var objs []runtime.Object
for _, node := range test.nodes {
objs = append(objs, node)
}
for _, pod := range test.pods {
objs = append(objs, pod)
}
@@ -941,13 +1026,17 @@ func initializePlugin(ctx context.Context, test testCase) (frameworktypes.Plugin
objs = append(objs, pvc)
}
for _, ns := range test.namespaces {
objs = append(objs, ns)
}
fakeClient := fake.NewSimpleClientset(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()
getPodsAssignedToNode, err := podutil.BuildGetPodsAssignedToNodeFunc(podInformer)
if err != nil {
return nil, fmt.Errorf("build get pods assigned to node function error: %v", err)
@@ -972,6 +1061,10 @@ func initializePlugin(ctx context.Context, test testCase) (frameworktypes.Plugin
PodProtections: test.podProtections,
}
if test.useNamespaceSelector {
defaultEvictorArgs.NamespaceLabelSelector = namespaceSelector
}
evictorPlugin, err := New(
ctx,
defaultEvictorArgs,
@@ -1153,3 +1246,119 @@ func Test_protectedPVCStorageClasses(t *testing.T) {
})
}
}
func TestMultipleProfilesWithDifferentNamespaceLabelSelectors(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
node := test.BuildTestNode("node1", 1000, 2000, 10, nil)
const (
nsProdName = "ns-prod"
nsTestName = "ns-test"
nsBackendName = "ns-backend"
)
nsProd := test.BuildTestNamespace(nsProdName)
nsProd.Labels["env"] = "prod"
nsTest := test.BuildTestNamespace(nsTestName)
nsTest.Labels["env"] = "test"
nsBackend := test.BuildTestNamespace(nsBackendName)
nsBackend.Labels["team"] = "backend"
podInProd := test.BuildTestPod("pod-in-prod", 100, 100, node.Name, func(pod *v1.Pod) {
pod.Namespace = nsProdName
test.SetNormalOwnerRef(pod)
})
podInTest := test.BuildTestPod("pod-in-test", 100, 100, node.Name, func(pod *v1.Pod) {
pod.Namespace = nsTestName
test.SetNormalOwnerRef(pod)
})
podInBackend := test.BuildTestPod("pod-in-backend", 100, 100, node.Name, func(pod *v1.Pod) {
pod.Namespace = nsBackendName
test.SetNormalOwnerRef(pod)
})
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())
// Create Profile 1: targets namespaces with env=prod
profile1Args := &DefaultEvictorArgs{
NamespaceLabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"env": "prod",
},
},
}
getPodAssignedToNode, err := podutil.BuildGetPodsAssignedToNodeFunc(sharedInformerFactory.Core().V1().Pods().Informer())
if err != nil {
t.Fatalf("build get pods assigned to node function error: %v", err)
}
profile1Plugin, err := New(ctx, profile1Args, &frameworkfake.HandleImpl{
ClientsetImpl: fakeClient,
GetPodsAssignedToNodeFuncImpl: getPodAssignedToNode,
SharedInformerFactoryImpl: sharedInformerFactory,
PluginInstanceIDImpl: nsProdName,
})
if err != nil {
t.Fatalf("unable to initialize profile1 plugin: %v", err)
}
profile2Args := &DefaultEvictorArgs{
NamespaceLabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"team": "backend",
},
},
}
profile2Plugin, err := New(ctx, profile2Args, &frameworkfake.HandleImpl{
ClientsetImpl: fakeClient,
GetPodsAssignedToNodeFuncImpl: getPodAssignedToNode,
SharedInformerFactoryImpl: sharedInformerFactory,
PluginInstanceIDImpl: nsBackendName,
})
if err != nil {
t.Fatalf("unable to initialize profile2 plugin: %v", err)
}
// Test Profile 1: evicts pods in ns-prod, reject others
t.Run("profile1", func(t *testing.T) {
profile1 := profile1Plugin.(*DefaultEvictor)
if profile1.PreEvictionFilter(podInBackend) {
t.Errorf("podInBackend should be rejected by profile1")
}
if profile1.PreEvictionFilter(podInTest) {
t.Errorf("podInTest should be rejected by profile1")
}
if !profile1.PreEvictionFilter(podInProd) {
t.Errorf("podInProd should not be rejected by profile1")
}
})
// Test Profile 2: evicts pods in ns-backend, reject others
t.Run("profile2", func(t *testing.T) {
profile2 := profile2Plugin.(*DefaultEvictor)
if profile2.PreEvictionFilter(podInProd) {
t.Errorf("podInProd should be rejected by profile2")
}
if profile2.PreEvictionFilter(podInTest) {
t.Errorf("podInTest should be rejected by profile2")
}
if !profile2.PreEvictionFilter(podInBackend) {
t.Errorf("podInBackend should not be rejected by profile2")
}
})
}

View File

@@ -23,5 +23,4 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error {
// SetDefaults_DefaultEvictorArgs sets the default values for the
// DefaultEvictorArgs configuration.
func SetDefaults_DefaultEvictorArgs(obj runtime.Object) {
}
func SetDefaults_DefaultEvictorArgs(obj runtime.Object) {}

View File

@@ -15,6 +15,7 @@ package defaultevictor
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/descheduler/pkg/api"
)
@@ -25,13 +26,14 @@ import (
type DefaultEvictorArgs struct {
metav1.TypeMeta `json:",inline"`
NodeSelector string `json:"nodeSelector,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
PriorityThreshold *api.PriorityThreshold `json:"priorityThreshold,omitempty"`
NodeFit bool `json:"nodeFit,omitempty"`
MinReplicas uint `json:"minReplicas,omitempty"`
MinPodAge *metav1.Duration `json:"minPodAge,omitempty"`
NoEvictionPolicy NoEvictionPolicy `json:"noEvictionPolicy,omitempty"`
NodeSelector string `json:"nodeSelector,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
NamespaceLabelSelector *metav1.LabelSelector `json:"namespaceLabelSelector,omitempty"`
PriorityThreshold *api.PriorityThreshold `json:"priorityThreshold,omitempty"`
NodeFit bool `json:"nodeFit,omitempty"`
MinReplicas uint `json:"minReplicas,omitempty"`
MinPodAge *metav1.Duration `json:"minPodAge,omitempty"`
NoEvictionPolicy NoEvictionPolicy `json:"noEvictionPolicy,omitempty"`
// PodProtections holds the list of enabled and disabled protection policies.
// Users can selectively disable certain default protection rules or enable extra ones.

View File

@@ -36,6 +36,11 @@ func (in *DefaultEvictorArgs) DeepCopyInto(out *DefaultEvictorArgs) {
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.NamespaceLabelSelector != nil {
in, out := &in.NamespaceLabelSelector, &out.NamespaceLabelSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.PriorityThreshold != nil {
in, out := &in.PriorityThreshold, &out.PriorityThreshold
*out = new(api.PriorityThreshold)

View File

@@ -40,6 +40,22 @@ import (
utilptr "k8s.io/utils/ptr"
)
// BuildTestNamespace creates a test namespace with given parameters.
func BuildTestNamespace(name string) *v1.Namespace {
namespace := &v1.Namespace{
TypeMeta: metav1.TypeMeta{
Kind: "Namespace",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
UID: uuid.NewUUID(),
Labels: map[string]string{"kubernetes.io/metadata.name": name},
},
}
return namespace
}
// BuildTestPod creates a test pod with given parameters.
func BuildTestPod(name string, cpu, memory int64, nodeName string, apply func(*v1.Pod)) *v1.Pod {
pod := &v1.Pod{