diff --git a/README.md b/README.md
index b9561d27a..afd1a661f 100644
--- a/README.md
+++ b/README.md
@@ -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]**
Sets whether PVC pods should be evicted or ignored. |
| `evictFailedBarePods` | `bool` | `false` | **[Deprecated: Use `podProtections` with `"FailedBarePods"` instead]**
Allows eviction of pods without owner references and in a failed phase. |
| `ignorePodsWithoutPDB` | `bool` | `false` | **[Deprecated: Use `podProtections` with `"PodsWithoutPDB"` instead]**
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).
\ No newline at end of file
diff --git a/pkg/framework/plugins/defaultevictor/defaultevictor.go b/pkg/framework/plugins/defaultevictor/defaultevictor.go
index f5f91863b..e1700a3e9 100644
--- a/pkg/framework/plugins/defaultevictor/defaultevictor.go
+++ b/pkg/framework/plugins/defaultevictor/defaultevictor.go
@@ -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
}
diff --git a/pkg/framework/plugins/defaultevictor/defaultevictor_test.go b/pkg/framework/plugins/defaultevictor/defaultevictor_test.go
index b8ef9a473..deddb608b 100644
--- a/pkg/framework/plugins/defaultevictor/defaultevictor_test.go
+++ b/pkg/framework/plugins/defaultevictor/defaultevictor_test.go
@@ -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")
+ }
+ })
+}
diff --git a/pkg/framework/plugins/defaultevictor/defaults.go b/pkg/framework/plugins/defaultevictor/defaults.go
index cca10adc4..773a27a86 100644
--- a/pkg/framework/plugins/defaultevictor/defaults.go
+++ b/pkg/framework/plugins/defaultevictor/defaults.go
@@ -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) {}
diff --git a/pkg/framework/plugins/defaultevictor/types.go b/pkg/framework/plugins/defaultevictor/types.go
index 2403c939a..278b88854 100644
--- a/pkg/framework/plugins/defaultevictor/types.go
+++ b/pkg/framework/plugins/defaultevictor/types.go
@@ -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.
diff --git a/pkg/framework/plugins/defaultevictor/zz_generated.deepcopy.go b/pkg/framework/plugins/defaultevictor/zz_generated.deepcopy.go
index 579b18f19..2a3fa9cf5 100644
--- a/pkg/framework/plugins/defaultevictor/zz_generated.deepcopy.go
+++ b/pkg/framework/plugins/defaultevictor/zz_generated.deepcopy.go
@@ -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)
diff --git a/test/test_utils.go b/test/test_utils.go
index d953b4a9b..56efc3c17 100644
--- a/test/test_utils.go
+++ b/test/test_utils.go
@@ -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{