1
0
mirror of https://github.com/kubernetes-sigs/descheduler.git synced 2026-01-26 05:14:13 +01:00

Merge pull request #1790 from ingvagabund/podlifetime-unit-tests

refactor(TestPodLifeTime): split the unit tests into smaller semantically close groups
This commit is contained in:
Kubernetes Prow Robot
2025-12-14 02:19:43 -08:00
committed by GitHub

View File

@@ -35,167 +35,145 @@ import (
"sigs.k8s.io/descheduler/test"
)
const nodeName1 = "n1"
var (
olderPodCreationTime = metav1.NewTime(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))
newerPodCreationTime = metav1.NewTime(time.Now())
)
func buildTestNode1() *v1.Node {
return test.BuildTestNode(nodeName1, 2000, 3000, 10, nil)
}
func buildTestPodForNode1(name string, creationTime metav1.Time, apply func(*v1.Pod)) *v1.Pod {
return test.BuildTestPod(name, 100, 0, nodeName1, func(pod *v1.Pod) {
pod.ObjectMeta.CreationTimestamp = creationTime
if apply != nil {
apply(pod)
}
})
}
func buildTestPodWithRSOwnerRefForNode1(name string, creationTime metav1.Time, apply func(*v1.Pod)) *v1.Pod {
return buildTestPodForNode1(name, creationTime, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
if apply != nil {
apply(pod)
}
})
}
func buildTestPodWithRSOwnerRefWithPendingPhaseForNode1(name string, creationTime metav1.Time, apply func(*v1.Pod)) *v1.Pod {
return buildTestPodWithRSOwnerRefForNode1(name, creationTime, func(pod *v1.Pod) {
pod.Status.Phase = "Pending"
if apply != nil {
apply(pod)
}
})
}
type podLifeTimeTestCase struct {
description string
args *PodLifeTimeArgs
pods []*v1.Pod
nodes []*v1.Node
expectedEvictedPods []string // if specified, will assert specific pods were evicted
expectedEvictedPodCount uint
ignorePvcPods bool
maxPodsToEvictPerNode *uint
maxPodsToEvictPerNamespace *uint
maxPodsToEvictTotal *uint
}
func runPodLifeTimeTest(t *testing.T, tc podLifeTimeTestCase) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var objs []runtime.Object
for _, node := range tc.nodes {
objs = append(objs, node)
}
for _, pod := range tc.pods {
objs = append(objs, pod)
}
fakeClient := fake.NewSimpleClientset(objs...)
handle, podEvictor, err := frameworktesting.InitFrameworkHandle(
ctx,
fakeClient,
evictions.NewOptions().
WithMaxPodsToEvictPerNode(tc.maxPodsToEvictPerNode).
WithMaxPodsToEvictPerNamespace(tc.maxPodsToEvictPerNamespace).
WithMaxPodsToEvictTotal(tc.maxPodsToEvictTotal),
defaultevictor.DefaultEvictorArgs{IgnorePvcPods: tc.ignorePvcPods},
nil,
)
if err != nil {
t.Fatalf("Unable to initialize a framework handle: %v", err)
}
var evictedPods []string
test.RegisterEvictedPodsCollector(fakeClient, &evictedPods)
plugin, err := New(ctx, tc.args, handle)
if err != nil {
t.Fatalf("Unable to initialize the plugin: %v", err)
}
plugin.(frameworktypes.DeschedulePlugin).Deschedule(ctx, tc.nodes)
podsEvicted := podEvictor.TotalEvicted()
if podsEvicted != tc.expectedEvictedPodCount {
t.Errorf("Test error for description: %s. Expected evicted pods count %v, got %v", tc.description, tc.expectedEvictedPodCount, podsEvicted)
}
if tc.expectedEvictedPods != nil {
diff := sets.New(tc.expectedEvictedPods...).Difference(sets.New(evictedPods...))
if diff.Len() > 0 {
t.Errorf(
"Expected pods %v to be evicted but %v were not evicted. Actual pods evicted: %v",
tc.expectedEvictedPods,
diff.UnsortedList(),
evictedPods,
)
}
}
}
func TestPodLifeTime(t *testing.T) {
const nodeName1 = "n1"
buildTestNode1 := func() *v1.Node {
return test.BuildTestNode(nodeName1, 2000, 3000, 10, nil)
}
olderPodCreationTime := metav1.NewTime(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))
newerPodCreationTime := metav1.NewTime(time.Now())
buildTestPodForNode1 := func(name string, creationTime metav1.Time, apply func(*v1.Pod)) *v1.Pod {
return test.BuildTestPod(name, 100, 0, nodeName1, func(pod *v1.Pod) {
pod.ObjectMeta.CreationTimestamp = creationTime
if apply != nil {
apply(pod)
}
})
}
buildTestPodWithRSOwnerRefForNode1 := func(name string, creationTime metav1.Time, apply func(*v1.Pod)) *v1.Pod {
return buildTestPodForNode1(name, creationTime, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
if apply != nil {
apply(pod)
}
})
}
buildTestPodWithRSOwnerRefWithPendingPhaseForNode1 := func(name string, creationTime metav1.Time, apply func(*v1.Pod)) *v1.Pod {
return buildTestPodWithRSOwnerRefForNode1(name, creationTime, func(pod *v1.Pod) {
pod.Status.Phase = "Pending"
if apply != nil {
apply(pod)
}
})
}
var maxLifeTime uint = 600
testCases := []struct {
description string
args *PodLifeTimeArgs
pods []*v1.Pod
nodes []*v1.Node
expectedEvictedPods []string // if specified, will assert specific pods were evicted
expectedEvictedPodCount uint
ignorePvcPods bool
maxPodsToEvictPerNode *uint
maxPodsToEvictPerNamespace *uint
maxPodsToEvictTotal *uint
}{
testCases := []podLifeTimeTestCase{
{
description: "Two pods in the default namespace, 1 is new and 1 very is old. 1 should be evicted.",
description: "No pod should be evicted since pod terminating",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefForNode1("p1", newerPodCreationTime, nil),
buildTestPodWithRSOwnerRefForNode1("p2", olderPodCreationTime, nil),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
expectedEvictedPods: []string{"p2"},
},
{
description: "Two pods in the default namespace, 2 are new and 0 are old. 0 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefForNode1("p3", newerPodCreationTime, nil),
buildTestPodWithRSOwnerRefForNode1("p4", newerPodCreationTime, nil),
buildTestPodWithRSOwnerRefForNode1("p14", olderPodCreationTime, func(pod *v1.Pod) {
pod.DeletionTimestamp = &metav1.Time{}
}),
buildTestPodWithRSOwnerRefForNode1("p15", olderPodCreationTime, func(pod *v1.Pod) {
pod.DeletionTimestamp = &metav1.Time{}
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 0,
},
{
description: "Two pods in the default namespace, 1 created 605 seconds ago. 1 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefForNode1("p5", newerPodCreationTime, nil),
buildTestPodWithRSOwnerRefForNode1("p6", metav1.NewTime(time.Now().Add(-time.Second*605)), nil),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
expectedEvictedPods: []string{"p6"},
},
{
description: "Two pods in the default namespace, 1 created 595 seconds ago. 0 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
},
pods: []*v1.Pod{
buildTestPodForNode1("p7", newerPodCreationTime, nil),
buildTestPodForNode1("p8", metav1.NewTime(time.Now().Add(-time.Second*595)), nil),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 0,
},
{
description: "Two pods, one with ContainerCreating state. 1 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
States: []string{"ContainerCreating"},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithPendingPhaseForNode1("p9", olderPodCreationTime, nil),
test.BuildTestPod("container-creating-stuck", 0, 0, nodeName1, func(pod *v1.Pod) {
pod.Status.ContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "ContainerCreating"},
},
},
}
test.SetRSOwnerRef(pod)
pod.ObjectMeta.CreationTimestamp = olderPodCreationTime
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
},
{
description: "Two pods, one with PodInitializing state. 1 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
States: []string{"PodInitializing"},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithPendingPhaseForNode1("p9", olderPodCreationTime, nil),
test.BuildTestPod("pod-initializing-stuck", 0, 0, nodeName1, func(pod *v1.Pod) {
pod.Status.ContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "PodInitializing"},
},
},
}
test.SetRSOwnerRef(pod)
pod.ObjectMeta.CreationTimestamp = olderPodCreationTime
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
},
{
description: "Two old pods with different states. 1 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
States: []string{"Pending"},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithPendingPhaseForNode1("p9", olderPodCreationTime, nil),
buildTestPodWithRSOwnerRefForNode1("p10", olderPodCreationTime, func(pod *v1.Pod) {
pod.Status.Phase = "Running"
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
expectedEvictedPods: []string{"p9"},
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
runPodLifeTimeTest(t, tc)
})
}
}
func TestPodLifeTime_EvictorConfiguration(t *testing.T) {
var maxLifeTime uint = 600
testCases := []podLifeTimeTestCase{
{
description: "Does not evict pvc pods with ignorePvcPods set to true",
args: &PodLifeTimeArgs{
@@ -235,6 +213,18 @@ func TestPodLifeTime(t *testing.T) {
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
runPodLifeTimeTest(t, tc)
})
}
}
func TestPodLifeTime_GenericFiltering(t *testing.T) {
var maxLifeTime uint = 600
testCases := []podLifeTimeTestCase{
{
description: "1 pod matching label selector should be evicted",
args: &PodLifeTimeArgs{
@@ -255,25 +245,18 @@ func TestPodLifeTime(t *testing.T) {
expectedEvictedPodCount: 1,
expectedEvictedPods: []string{"p12"},
},
{
description: "No pod should be evicted since pod terminating",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefForNode1("p14", olderPodCreationTime, func(pod *v1.Pod) {
pod.DeletionTimestamp = &metav1.Time{}
}),
buildTestPodWithRSOwnerRefForNode1("p15", olderPodCreationTime, func(pod *v1.Pod) {
pod.DeletionTimestamp = &metav1.Time{}
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 0,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
runPodLifeTimeTest(t, tc)
})
}
}
func TestPodLifeTime_EvictionLimits(t *testing.T) {
var maxLifeTime uint = 600
testCases := []podLifeTimeTestCase{
{
description: "2 Oldest pods should be evicted when maxPodsToEvictPerNode and maxPodsToEvictPerNamespace are not set",
args: &PodLifeTimeArgs{
@@ -333,6 +316,64 @@ func TestPodLifeTime(t *testing.T) {
expectedEvictedPodCount: 1,
expectedEvictedPods: []string{"p2"},
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
runPodLifeTimeTest(t, tc)
})
}
}
func TestPodLifeTime_ContainerWaitingReasons(t *testing.T) {
var maxLifeTime uint = 600
testCases := []podLifeTimeTestCase{
{
description: "Two pods, one with ContainerCreating state. 1 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
States: []string{"ContainerCreating"},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithPendingPhaseForNode1("p9", olderPodCreationTime, nil),
test.BuildTestPod("container-creating-stuck", 0, 0, nodeName1, func(pod *v1.Pod) {
pod.Status.ContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "ContainerCreating"},
},
},
}
test.SetRSOwnerRef(pod)
pod.ObjectMeta.CreationTimestamp = olderPodCreationTime
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
},
{
description: "Two pods, one with PodInitializing state. 1 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
States: []string{"PodInitializing"},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithPendingPhaseForNode1("p9", olderPodCreationTime, nil),
test.BuildTestPod("pod-initializing-stuck", 0, 0, nodeName1, func(pod *v1.Pod) {
pod.Status.ContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "PodInitializing"},
},
},
}
test.SetRSOwnerRef(pod)
pod.ObjectMeta.CreationTimestamp = olderPodCreationTime
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
},
{
description: "1 pod with container status ImagePullBackOff should be evicted",
args: &PodLifeTimeArgs{
@@ -535,6 +576,38 @@ func TestPodLifeTime(t *testing.T) {
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
},
{
description: "1 pod with ImagePullBackOff status should be ignored when States filter is ContainerCreating",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
States: []string{"ContainerCreating"},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithPendingPhaseForNode1("p9", olderPodCreationTime, func(pod *v1.Pod) {
pod.Status.ContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "ImagePullBackOff"},
},
},
}
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 0,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
runPodLifeTimeTest(t, tc)
})
}
}
func TestPodLifeTime_PodStatusReasons(t *testing.T) {
var maxLifeTime uint = 600
testCases := []podLifeTimeTestCase{
{
description: "1 pod with pod status reason NodeLost should be evicted",
args: &PodLifeTimeArgs{
@@ -591,6 +664,96 @@ func TestPodLifeTime(t *testing.T) {
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
runPodLifeTimeTest(t, tc)
})
}
}
func TestPodLifeTime_AgeThreshold(t *testing.T) {
var maxLifeTime uint = 600
testCases := []podLifeTimeTestCase{
{
description: "Two pods in the default namespace, 1 is new and 1 very is old. 1 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefForNode1("p1", newerPodCreationTime, nil),
buildTestPodWithRSOwnerRefForNode1("p2", olderPodCreationTime, nil),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
expectedEvictedPods: []string{"p2"},
},
{
description: "Two pods in the default namespace, 2 are new and 0 are old. 0 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefForNode1("p3", newerPodCreationTime, nil),
buildTestPodWithRSOwnerRefForNode1("p4", newerPodCreationTime, nil),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 0,
},
{
description: "Two pods in the default namespace, 1 created 605 seconds ago. 1 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefForNode1("p5", newerPodCreationTime, nil),
buildTestPodWithRSOwnerRefForNode1("p6", metav1.NewTime(time.Now().Add(-time.Second*605)), nil),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
expectedEvictedPods: []string{"p6"},
},
{
description: "Two pods in the default namespace, 1 created 595 seconds ago. 0 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
},
pods: []*v1.Pod{
buildTestPodForNode1("p7", newerPodCreationTime, nil),
buildTestPodForNode1("p8", metav1.NewTime(time.Now().Add(-time.Second*595)), nil),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 0,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
runPodLifeTimeTest(t, tc)
})
}
}
func TestPodLifeTime_PodPhaseStates(t *testing.T) {
var maxLifeTime uint = 600
testCases := []podLifeTimeTestCase{
{
description: "Two old pods with different states. 1 should be evicted.",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
States: []string{"Pending"},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithPendingPhaseForNode1("p9", olderPodCreationTime, nil),
buildTestPodWithRSOwnerRefForNode1("p10", olderPodCreationTime, func(pod *v1.Pod) {
pod.Status.Phase = "Running"
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
expectedEvictedPods: []string{"p9"},
},
{
description: "1 pod with pod status phase v1.PodSucceeded should be evicted",
args: &PodLifeTimeArgs{
@@ -633,81 +796,11 @@ func TestPodLifeTime(t *testing.T) {
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 1,
},
{
description: "1 pod with ImagePullBackOff status should be ignored when States filter is ContainerCreating",
args: &PodLifeTimeArgs{
MaxPodLifeTimeSeconds: &maxLifeTime,
States: []string{"ContainerCreating"},
},
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithPendingPhaseForNode1("p9", olderPodCreationTime, func(pod *v1.Pod) {
pod.Status.ContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "ImagePullBackOff"},
},
},
}
}),
},
nodes: []*v1.Node{buildTestNode1()},
expectedEvictedPodCount: 0,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var objs []runtime.Object
for _, node := range tc.nodes {
objs = append(objs, node)
}
for _, pod := range tc.pods {
objs = append(objs, pod)
}
fakeClient := fake.NewSimpleClientset(objs...)
handle, podEvictor, err := frameworktesting.InitFrameworkHandle(
ctx,
fakeClient,
evictions.NewOptions().
WithMaxPodsToEvictPerNode(tc.maxPodsToEvictPerNode).
WithMaxPodsToEvictPerNamespace(tc.maxPodsToEvictPerNamespace).
WithMaxPodsToEvictTotal(tc.maxPodsToEvictTotal),
defaultevictor.DefaultEvictorArgs{IgnorePvcPods: tc.ignorePvcPods},
nil,
)
if err != nil {
t.Fatalf("Unable to initialize a framework handle: %v", err)
}
var evictedPods []string
test.RegisterEvictedPodsCollector(fakeClient, &evictedPods)
plugin, err := New(ctx, tc.args, handle)
if err != nil {
t.Fatalf("Unable to initialize the plugin: %v", err)
}
plugin.(frameworktypes.DeschedulePlugin).Deschedule(ctx, tc.nodes)
podsEvicted := podEvictor.TotalEvicted()
if podsEvicted != tc.expectedEvictedPodCount {
t.Errorf("Test error for description: %s. Expected evicted pods count %v, got %v", tc.description, tc.expectedEvictedPodCount, podsEvicted)
}
if tc.expectedEvictedPods != nil {
diff := sets.New(tc.expectedEvictedPods...).Difference(sets.New(evictedPods...))
if diff.Len() > 0 {
t.Errorf(
"Expected pods %v to be evicted but %v were not evicted. Actual pods evicted: %v",
tc.expectedEvictedPods,
diff.UnsortedList(),
evictedPods,
)
}
}
runPodLifeTimeTest(t, tc)
})
}
}