Skip to content

Commit 419fe74

Browse files
authored
Merge pull request #1723 from ingvagabund/default-evictor-no-eviction-policy
Default evictor no eviction policy
2 parents b84b262 + 7380aa6 commit 419fe74

File tree

9 files changed

+219
-6
lines changed

9 files changed

+219
-6
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ The Default Evictor Plugin is used by default for filtering pods before processi
162162
| `minReplicas` |`uint`|`0`| ignore eviction of pods where owner (e.g. `ReplicaSet`) replicas is below this threshold |
163163
| `minPodAge` |`metav1.Duration`|`0`| ignore eviction of pods with a creation time within this threshold |
164164
| `ignorePodsWithoutPDB` |`bool`|`false`| set whether pods without PodDisruptionBudget should be evicted or ignored |
165+
| `noEvictionPolicy` |`enum`|``| sets whether a `descheduler.alpha.kubernetes.io/prefer-no-eviction` pod annotation is considered preferred or mandatory. Accepted values: "", "Preferred", "Mandatory". Defaults to "Preferred". |
165166

166167
### Example policy
167168

@@ -1013,12 +1014,16 @@ never evicted because these pods won't be recreated. (Standalone pods in failed
10131014
* Pods with PVCs are evicted (unless `ignorePvcPods: true` is set).
10141015
* In `LowNodeUtilization` and `RemovePodsViolatingInterPodAntiAffinity`, pods are evicted by their priority from low to high, and if they have same priority,
10151016
best effort pods are evicted before burstable and guaranteed pods.
1016-
* All types of pods with the annotation `descheduler.alpha.kubernetes.io/evict` are eligible for eviction. This
1017+
* All types of pods with the `descheduler.alpha.kubernetes.io/evict` annotation are eligible for eviction. This
10171018
annotation is used to override checks which prevent eviction and users can select which pod is evicted.
10181019
Users should know how and if the pod will be recreated.
10191020
The annotation only affects internal descheduler checks.
10201021
The anti-disruption protection provided by the [/eviction](https://kubernetes.io/docs/concepts/scheduling-eviction/api-eviction/)
10211022
subresource is still respected.
1023+
* Pods with the `descheduler.alpha.kubernetes.io/prefer-no-eviction` annotation voice their preference not to be evicted.
1024+
Each plugin decides whether the annotation gets respected or not. When the `DefaultEvictor` plugin sets `noEvictionPolicy`
1025+
to `Mandatory` all such pods are excluded from eviction. Needs to be used with caution as some plugins may enfore
1026+
various policies that are expected to be always met.
10221027
* Pods with a non-nil DeletionTimestamp are not evicted by default.
10231028

10241029
Setting `--v=4` or greater on the Descheduler will log all reasons why any pod is not evictable.

pkg/descheduler/evictions/utils/utils.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ limitations under the License.
1717
package utils
1818

1919
import (
20+
corev1 "k8s.io/api/core/v1"
2021
clientset "k8s.io/client-go/kubernetes"
2122
)
2223

2324
const (
2425
EvictionKind = "Eviction"
2526
EvictionSubresource = "pods/eviction"
27+
// A new experimental feature for soft no-eviction preference.
28+
// Each plugin will decide whether the soft preference will be respected.
29+
// If configured the soft preference turns into a mandatory no-eviction policy for the DefaultEvictor plugin.
30+
SoftNoEvictionAnnotationKey = "descheduler.alpha.kubernetes.io/prefer-no-eviction"
2631
)
2732

2833
// SupportEviction uses Discovery API to find out if the server support eviction subresource
@@ -56,3 +61,9 @@ func SupportEviction(client clientset.Interface) (string, error) {
5661
}
5762
return "", nil
5863
}
64+
65+
// HaveEvictAnnotation checks if the pod have evict annotation
66+
func HaveNoEvictionAnnotation(pod *corev1.Pod) bool {
67+
_, found := pod.ObjectMeta.Annotations[SoftNoEvictionAnnotationKey]
68+
return found
69+
}

pkg/descheduler/pod/pods.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"k8s.io/apimachinery/pkg/util/sets"
2626
"k8s.io/client-go/tools/cache"
2727

28+
evictionutils "sigs.k8s.io/descheduler/pkg/descheduler/evictions/utils"
2829
"sigs.k8s.io/descheduler/pkg/utils"
2930
)
3031

@@ -254,14 +255,32 @@ func SortPodsBasedOnPriorityLowToHigh(pods []*v1.Pod) {
254255
return false
255256
}
256257
if (pods[j].Spec.Priority == nil && pods[i].Spec.Priority == nil) || (*pods[i].Spec.Priority == *pods[j].Spec.Priority) {
257-
if IsBestEffortPod(pods[i]) {
258+
iIsBestEffortPod := IsBestEffortPod(pods[i])
259+
jIsBestEffortPod := IsBestEffortPod(pods[j])
260+
iIsBurstablePod := IsBurstablePod(pods[i])
261+
jIsBurstablePod := IsBurstablePod(pods[j])
262+
iIsGuaranteedPod := IsGuaranteedPod(pods[i])
263+
jIsGuaranteedPod := IsGuaranteedPod(pods[j])
264+
if (iIsBestEffortPod && jIsBestEffortPod) || (iIsBurstablePod && jIsBurstablePod) || (iIsGuaranteedPod && jIsGuaranteedPod) {
265+
iHasNoEvictonPolicy := evictionutils.HaveNoEvictionAnnotation(pods[i])
266+
jHasNoEvictonPolicy := evictionutils.HaveNoEvictionAnnotation(pods[j])
267+
if !iHasNoEvictonPolicy {
268+
return true
269+
}
270+
if !jHasNoEvictonPolicy {
271+
return false
272+
}
258273
return true
259274
}
260-
if IsBurstablePod(pods[i]) && IsGuaranteedPod(pods[j]) {
275+
if iIsBestEffortPod {
276+
return true
277+
}
278+
if iIsBurstablePod && jIsGuaranteedPod {
261279
return true
262280
}
263281
return false
264282
}
283+
265284
return *pods[i].Spec.Priority < *pods[j].Spec.Priority
266285
})
267286
}

pkg/descheduler/pod/pods_test.go

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,66 @@ func TestSortPodsBasedOnPriorityLowToHigh(t *testing.T) {
157157
p6 := test.BuildTestPod("p6", 400, 100, n1.Name, test.MakeGuaranteedPod)
158158
p6.Spec.Priority = nil
159159

160-
podList := []*v1.Pod{p4, p3, p2, p1, p6, p5}
161-
expectedPodList := []*v1.Pod{p5, p6, p1, p2, p3, p4}
160+
p7 := test.BuildTestPod("p7", 400, 0, n1.Name, func(pod *v1.Pod) {
161+
test.SetPodPriority(pod, lowPriority)
162+
pod.Annotations = map[string]string{
163+
"descheduler.alpha.kubernetes.io/prefer-no-eviction": "",
164+
}
165+
})
166+
167+
// BestEffort
168+
p8 := test.BuildTestPod("p8", 400, 0, n1.Name, func(pod *v1.Pod) {
169+
test.SetPodPriority(pod, highPriority)
170+
test.MakeBestEffortPod(pod)
171+
pod.Annotations = map[string]string{
172+
"descheduler.alpha.kubernetes.io/prefer-no-eviction": "",
173+
}
174+
})
175+
176+
// Burstable
177+
p9 := test.BuildTestPod("p9", 400, 0, n1.Name, func(pod *v1.Pod) {
178+
test.SetPodPriority(pod, highPriority)
179+
test.MakeBurstablePod(pod)
180+
pod.Annotations = map[string]string{
181+
"descheduler.alpha.kubernetes.io/prefer-no-eviction": "",
182+
}
183+
})
184+
185+
// Guaranteed
186+
p10 := test.BuildTestPod("p10", 400, 100, n1.Name, func(pod *v1.Pod) {
187+
test.SetPodPriority(pod, highPriority)
188+
test.MakeGuaranteedPod(pod)
189+
pod.Annotations = map[string]string{
190+
"descheduler.alpha.kubernetes.io/prefer-no-eviction": "",
191+
}
192+
})
193+
194+
// Burstable
195+
p11 := test.BuildTestPod("p11", 400, 0, n1.Name, func(pod *v1.Pod) {
196+
test.MakeBurstablePod(pod)
197+
})
198+
199+
// Burstable
200+
p12 := test.BuildTestPod("p12", 400, 0, n1.Name, func(pod *v1.Pod) {
201+
test.MakeBurstablePod(pod)
202+
pod.Annotations = map[string]string{
203+
"descheduler.alpha.kubernetes.io/prefer-no-eviction": "",
204+
}
205+
})
206+
207+
podList := []*v1.Pod{p1, p8, p9, p10, p2, p3, p4, p5, p6, p7, p11, p12}
208+
// p5: no priority, best effort
209+
// p11: no priority, burstable
210+
// p6: no priority, guaranteed
211+
// p1: low priority
212+
// p7: low priority, prefer-no-eviction
213+
// p2: high priority, best effort
214+
// p8: high priority, best effort, prefer-no-eviction
215+
// p3: high priority, burstable
216+
// p9: high priority, burstable, prefer-no-eviction
217+
// p4: high priority, guaranteed
218+
// p10: high priority, guaranteed, prefer-no-eviction
219+
expectedPodList := []*v1.Pod{p5, p11, p12, p6, p1, p7, p2, p8, p3, p9, p4, p10}
162220

163221
SortPodsBasedOnPriorityLowToHigh(podList)
164222
if !reflect.DeepEqual(getPodListNames(podList), getPodListNames(expectedPodList)) {

pkg/framework/plugins/defaultevictor/defaultevictor.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"k8s.io/client-go/tools/cache"
2525
"k8s.io/klog/v2"
2626

27+
evictionutils "sigs.k8s.io/descheduler/pkg/descheduler/evictions/utils"
2728
nodeutil "sigs.k8s.io/descheduler/pkg/descheduler/node"
2829
podutil "sigs.k8s.io/descheduler/pkg/descheduler/pod"
2930
frameworktypes "sigs.k8s.io/descheduler/pkg/framework/types"
@@ -140,6 +141,10 @@ func (d *DefaultEvictor) Filter(pod *v1.Pod) bool {
140141
return true
141142
}
142143

144+
if d.args.NoEvictionPolicy == MandatoryNoEvictionPolicy && evictionutils.HaveNoEvictionAnnotation(pod) {
145+
return false
146+
}
147+
143148
if utils.IsMirrorPod(pod) {
144149
checkErrs = append(checkErrs, fmt.Errorf("pod is a mirror pod"))
145150
}

pkg/framework/plugins/defaultevictor/defaultevictor_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"k8s.io/client-go/informers"
3030
"k8s.io/client-go/kubernetes/fake"
3131
"sigs.k8s.io/descheduler/pkg/api"
32+
evictionutils "sigs.k8s.io/descheduler/pkg/descheduler/evictions/utils"
3233
podutil "sigs.k8s.io/descheduler/pkg/descheduler/pod"
3334
frameworkfake "sigs.k8s.io/descheduler/pkg/framework/fake"
3435
frameworktypes "sigs.k8s.io/descheduler/pkg/framework/types"
@@ -51,6 +52,7 @@ type testCase struct {
5152
minPodAge *metav1.Duration
5253
result bool
5354
ignorePodsWithoutPDB bool
55+
noEvictionPolicy NoEvictionPolicy
5456
}
5557

5658
func TestDefaultEvictorPreEvictionFilter(t *testing.T) {
@@ -358,6 +360,29 @@ func TestDefaultEvictorFilter(t *testing.T) {
358360
}),
359361
},
360362
result: true,
363+
}, {
364+
description: "Normal pod eviction with normal ownerRefs and " + evictionutils.SoftNoEvictionAnnotationKey + " annotation (preference)",
365+
pods: []*v1.Pod{
366+
test.BuildTestPod("p2", 400, 0, n1.Name, func(pod *v1.Pod) {
367+
pod.Annotations = map[string]string{evictionutils.SoftNoEvictionAnnotationKey: ""}
368+
pod.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
369+
}),
370+
},
371+
evictLocalStoragePods: false,
372+
evictSystemCriticalPods: false,
373+
result: true,
374+
}, {
375+
description: "Normal pod eviction with normal ownerRefs and " + evictionutils.SoftNoEvictionAnnotationKey + " annotation (mandatory)",
376+
pods: []*v1.Pod{
377+
test.BuildTestPod("p2", 400, 0, n1.Name, func(pod *v1.Pod) {
378+
pod.Annotations = map[string]string{evictionutils.SoftNoEvictionAnnotationKey: ""}
379+
pod.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
380+
}),
381+
},
382+
evictLocalStoragePods: false,
383+
evictSystemCriticalPods: false,
384+
noEvictionPolicy: MandatoryNoEvictionPolicy,
385+
result: false,
361386
}, {
362387
description: "Normal pod eviction with replicaSet ownerRefs",
363388
pods: []*v1.Pod{
@@ -831,6 +856,7 @@ func initializePlugin(ctx context.Context, test testCase) (frameworktypes.Plugin
831856
MinReplicas: test.minReplicas,
832857
MinPodAge: test.minPodAge,
833858
IgnorePodsWithoutPDB: test.ignorePodsWithoutPDB,
859+
NoEvictionPolicy: test.noEvictionPolicy,
834860
}
835861

836862
evictorPlugin, err := New(

pkg/framework/plugins/defaultevictor/types.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,23 @@ type DefaultEvictorArgs struct {
3737
MinReplicas uint `json:"minReplicas,omitempty"`
3838
MinPodAge *metav1.Duration `json:"minPodAge,omitempty"`
3939
IgnorePodsWithoutPDB bool `json:"ignorePodsWithoutPDB,omitempty"`
40+
NoEvictionPolicy NoEvictionPolicy `json:"noEvictionPolicy,omitempty"`
4041
}
42+
43+
// NoEvictionPolicy dictates whether a no-eviction policy is preferred or mandatory.
44+
// Needs to be used with caution as this will give users ability to protect their pods
45+
// from eviction. Which might work against enfored policies. E.g. plugins evicting pods
46+
// violating security policies.
47+
type NoEvictionPolicy string
48+
49+
const (
50+
// PreferredNoEvictionPolicy interprets the no-eviction policy as a preference.
51+
// Meaning the annotation will get ignored by the DefaultEvictor plugin.
52+
// Yet, plugins may optionally sort their pods based on the annotation
53+
// and focus on evicting pods that do not set the annotation.
54+
PreferredNoEvictionPolicy NoEvictionPolicy = "Preferred"
55+
56+
// MandatoryNoEvictionPolicy interprets the no-eviction policy as mandatory.
57+
// Every pod carying the annotation will get excluded from eviction.
58+
MandatoryNoEvictionPolicy NoEvictionPolicy = "Mandatory"
59+
)

pkg/framework/plugins/defaultevictor/validation.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@ func ValidateDefaultEvictorArgs(obj runtime.Object) error {
2525
args := obj.(*DefaultEvictorArgs)
2626

2727
if args.PriorityThreshold != nil && args.PriorityThreshold.Value != nil && len(args.PriorityThreshold.Name) > 0 {
28-
return fmt.Errorf("priority threshold misconfigured, only one of priorityThreshold fields can be set, got %v", args)
28+
return fmt.Errorf("priority threshold misconfigured, only one of priorityThreshold fields can be set")
2929
}
3030

3131
if args.MinReplicas == 1 {
3232
klog.V(4).Info("DefaultEvictor minReplicas must be greater than 1 to check for min pods during eviction. This check will be ignored during eviction.")
3333
}
3434

35+
if args.NoEvictionPolicy != "" {
36+
if args.NoEvictionPolicy != PreferredNoEvictionPolicy && args.NoEvictionPolicy != MandatoryNoEvictionPolicy {
37+
return fmt.Errorf("noEvictionPolicy accepts only %q values", []NoEvictionPolicy{PreferredNoEvictionPolicy, MandatoryNoEvictionPolicy})
38+
}
39+
}
40+
3541
return nil
3642
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package defaultevictor
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
"k8s.io/apimachinery/pkg/runtime"
24+
utilptr "k8s.io/utils/ptr"
25+
"sigs.k8s.io/descheduler/pkg/api"
26+
)
27+
28+
func TestValidateDefaultEvictorArgs(t *testing.T) {
29+
tests := []struct {
30+
name string
31+
args *DefaultEvictorArgs
32+
errInfo error
33+
}{
34+
{
35+
name: "passing invalid priority",
36+
args: &DefaultEvictorArgs{
37+
PriorityThreshold: &api.PriorityThreshold{
38+
Value: utilptr.To[int32](1),
39+
Name: "priority-name",
40+
},
41+
},
42+
errInfo: fmt.Errorf("priority threshold misconfigured, only one of priorityThreshold fields can be set"),
43+
}, {
44+
name: "passing invalid no eviction policy",
45+
args: &DefaultEvictorArgs{
46+
NoEvictionPolicy: "invalid-no-eviction-policy",
47+
},
48+
errInfo: fmt.Errorf("noEvictionPolicy accepts only %q values", []NoEvictionPolicy{PreferredNoEvictionPolicy, MandatoryNoEvictionPolicy}),
49+
},
50+
}
51+
52+
for _, testCase := range tests {
53+
t.Run(testCase.name, func(t *testing.T) {
54+
validateErr := ValidateDefaultEvictorArgs(runtime.Object(testCase.args))
55+
if validateErr == nil || testCase.errInfo == nil {
56+
if validateErr != testCase.errInfo {
57+
t.Errorf("expected validity of plugin config: %q but got %q instead", testCase.errInfo, validateErr)
58+
}
59+
} else if validateErr.Error() != testCase.errInfo.Error() {
60+
t.Errorf("expected validity of plugin config: %q but got %q instead", testCase.errInfo, validateErr)
61+
}
62+
})
63+
}
64+
}

0 commit comments

Comments
 (0)