diff --git a/README.md b/README.md index 1ffce0ad56..7a50ffc6de 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ The Default Evictor Plugin is used by default for filtering pods before processi | `evictSystemCriticalPods` |`bool`| `false` | [Warning: Will evict Kubernetes system pods] allows eviction of pods with any priority, including system pods like kube-dns | | `ignorePvcPods` |`bool`| `false` | set whether PVC pods should be evicted or ignored | | `evictFailedBarePods` |`bool`| `false` | allow eviction of pods without owner references and in failed phase | +| `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)) | diff --git a/pkg/framework/plugins/defaultevictor/defaultevictor.go b/pkg/framework/plugins/defaultevictor/defaultevictor.go index f23bf13a12..b982334f8d 100644 --- a/pkg/framework/plugins/defaultevictor/defaultevictor.go +++ b/pkg/framework/plugins/defaultevictor/defaultevictor.go @@ -27,6 +27,7 @@ import ( utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" + nodeutil "sigs.k8s.io/descheduler/pkg/descheduler/node" podutil "sigs.k8s.io/descheduler/pkg/descheduler/pod" frameworktypes "sigs.k8s.io/descheduler/pkg/framework/types" @@ -228,8 +229,28 @@ func (d *DefaultEvictor) PreEvictionFilter(pod *v1.Pod) bool { klog.InfoS("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 { return true } + + // check pod by namespace label filter + indexName := "namespaceWithLabelSelector" + indexer, err := getNamespacesListByLabelSelector(indexName, d.args.NamespaceLabelSelector, d.handle) + if err != nil { + klog.ErrorS(err, "unable to list namespaces", "pod", klog.KObj(pod)) + return false + } + objs, err := indexer.ByIndex(indexName, pod.Namespace) + if err != nil { + klog.ErrorS(err, "unable to list namespaces for namespaceLabelSelector filter in the policy parameter", "pod", klog.KObj(pod)) + return false + } + if len(objs) == 0 { + klog.InfoS("pod namespace do not match the namespaceLabelSelector filter in the policy parameter", "pod", klog.KObj(pod)) + return false + } return true } @@ -292,3 +313,39 @@ func getPodIndexerByOwnerRefs(indexName string, handle frameworktypes.Handle) (c return indexer, nil } + +func getNamespacesListByLabelSelector(indexName string, labelSelector *metav1.LabelSelector, handle frameworktypes.Handle) (cache.Indexer, error) { + nsInformer := handle.SharedInformerFactory().Core().V1().Namespaces().Informer() + indexer := nsInformer.GetIndexer() + + // do not reinitialize the indexer, if it's been defined already + for name := range indexer.GetIndexers() { + if name == indexName { + return indexer, nil + } + } + + if err := nsInformer.AddIndexers(cache.Indexers{ + indexName: func(obj interface{}) ([]string, error) { + ns, ok := obj.(*v1.Namespace) + if !ok { + return []string{}, errors.New("unexpected object") + } + + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return []string{}, errors.New("could not get selector from label selector") + } + if labelSelector != nil && !selector.Empty() { + if !selector.Matches(labels.Set(ns.Labels)) { + return []string{}, nil + } + } + return []string{ns.GetName()}, nil + }, + }); 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 dd07d570f0..3fc19bc815 100644 --- a/pkg/framework/plugins/defaultevictor/defaultevictor_test.go +++ b/pkg/framework/plugins/defaultevictor/defaultevictor_test.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" + "sigs.k8s.io/descheduler/pkg/api" podutil "sigs.k8s.io/descheduler/pkg/descheduler/pod" frameworkfake "sigs.k8s.io/descheduler/pkg/framework/fake" @@ -40,18 +41,27 @@ type testCase struct { description string pods []*v1.Pod nodes []*v1.Node + namespaces []*v1.Namespace pdbs []*policyv1.PodDisruptionBudget evictFailedBarePods bool evictLocalStoragePods bool evictSystemCriticalPods bool priorityThreshold *int32 nodeFit bool + useNamespaceSelector bool minReplicas uint minPodAge *metav1.Duration result bool ignorePodsWithoutPDB bool } +var namespace = "test" +var namespaceSelector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": namespace, + }, +} + func TestDefaultEvictorPreEvictionFilter(t *testing.T) { n1 := test.BuildTestNode("node1", 1000, 2000, 13, nil) @@ -308,6 +318,71 @@ func TestDefaultEvictorPreEvictionFilter(t *testing.T) { nodeFit: false, 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 { @@ -835,9 +910,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) } @@ -845,12 +922,16 @@ func initializePlugin(ctx context.Context, test testCase) (frameworktypes.Plugin objs = append(objs, pdb) } + 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().Namespaces().Lister() getPodsAssignedToNode, err := podutil.BuildGetPodsAssignedToNodeFunc(podInformer) if err != nil { return nil, fmt.Errorf("build get pods assigned to node function error: %v", err) @@ -873,6 +954,10 @@ func initializePlugin(ctx context.Context, test testCase) (frameworktypes.Plugin IgnorePodsWithoutPDB: test.ignorePodsWithoutPDB, } + if test.useNamespaceSelector { + defaultEvictorArgs.NamespaceLabelSelector = namespaceSelector + } + evictorPlugin, err := New( defaultEvictorArgs, &frameworkfake.HandleImpl{ diff --git a/pkg/framework/plugins/defaultevictor/defaults.go b/pkg/framework/plugins/defaultevictor/defaults.go index 463ddd5185..cc99225ad0 100644 --- a/pkg/framework/plugins/defaultevictor/defaults.go +++ b/pkg/framework/plugins/defaultevictor/defaults.go @@ -43,6 +43,9 @@ func SetDefaults_DefaultEvictorArgs(obj runtime.Object) { if !args.EvictFailedBarePods { args.EvictFailedBarePods = false } + if args.NamespaceLabelSelector == nil { + args.NamespaceLabelSelector = nil + } if args.LabelSelector == nil { args.LabelSelector = nil } diff --git a/pkg/framework/plugins/defaultevictor/types.go b/pkg/framework/plugins/defaultevictor/types.go index 3a39cbc911..f9d3c4990b 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" ) @@ -31,6 +32,7 @@ type DefaultEvictorArgs struct { EvictSystemCriticalPods bool `json:"evictSystemCriticalPods,omitempty"` IgnorePvcPods bool `json:"ignorePvcPods,omitempty"` EvictFailedBarePods bool `json:"evictFailedBarePods,omitempty"` + NamespaceLabelSelector *metav1.LabelSelector `json:"namespaceLabelSelector,omitempty"` LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` PriorityThreshold *api.PriorityThreshold `json:"priorityThreshold,omitempty"` NodeFit bool `json:"nodeFit,omitempty"` diff --git a/pkg/framework/plugins/defaultevictor/zz_generated.deepcopy.go b/pkg/framework/plugins/defaultevictor/zz_generated.deepcopy.go index 9d1746e853..1cc9891d07 100644 --- a/pkg/framework/plugins/defaultevictor/zz_generated.deepcopy.go +++ b/pkg/framework/plugins/defaultevictor/zz_generated.deepcopy.go @@ -46,6 +46,12 @@ func (in *DefaultEvictorArgs) DeepCopyInto(out *DefaultEvictorArgs) { *out = new(v1.Duration) **out = **in } + + if in.NamespaceLabelSelector != nil { + in, out := &in.NamespaceLabelSelector, &out.NamespaceLabelSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } return } diff --git a/test/test_utils.go b/test/test_utils.go index 21c85a07b2..78d257243e 100644 --- a/test/test_utils.go +++ b/test/test_utils.go @@ -37,6 +37,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{