kubernetes/pkg/kubelet/eviction/helpers_test.go
2023-11-01 14:46:33 -04:00

3252 lines
107 KiB
Go

/*
Copyright 2016 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
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.
See the License for the specific language governing permissions and
limitations under the License.
*/
package eviction
import (
"context"
"fmt"
"reflect"
"sort"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
statsapi "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
"k8s.io/kubernetes/pkg/features"
evictionapi "k8s.io/kubernetes/pkg/kubelet/eviction/api"
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
)
func quantityMustParse(value string) *resource.Quantity {
q := resource.MustParse(value)
return &q
}
func TestGetReclaimableThreshold(t *testing.T) {
testCases := map[string]struct {
thresholds []evictionapi.Threshold
}{
"": {
thresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalAllocatableMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
},
},
}
for testName, testCase := range testCases {
sort.Sort(byEvictionPriority(testCase.thresholds))
_, _, ok := getReclaimableThreshold(testCase.thresholds)
if !ok {
t.Errorf("Didn't find reclaimable threshold, test: %v", testName)
}
}
}
func TestParseThresholdConfig(t *testing.T) {
gracePeriod, _ := time.ParseDuration("30s")
testCases := map[string]struct {
allocatableConfig []string
evictionHard map[string]string
evictionSoft map[string]string
evictionSoftGracePeriod map[string]string
evictionMinReclaim map[string]string
expectErr bool
expectThresholds []evictionapi.Threshold
}{
"no values": {
allocatableConfig: []string{},
evictionHard: map[string]string{},
evictionSoft: map[string]string{},
evictionSoftGracePeriod: map[string]string{},
evictionMinReclaim: map[string]string{},
expectErr: false,
expectThresholds: []evictionapi.Threshold{},
},
"all memory eviction values": {
allocatableConfig: []string{kubetypes.NodeAllocatableEnforcementKey},
evictionHard: map[string]string{"memory.available": "150Mi"},
evictionSoft: map[string]string{"memory.available": "300Mi"},
evictionSoftGracePeriod: map[string]string{"memory.available": "30s"},
evictionMinReclaim: map[string]string{"memory.available": "0"},
expectErr: false,
expectThresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalAllocatableMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
},
},
"all memory eviction values in percentages": {
allocatableConfig: []string{},
evictionHard: map[string]string{"memory.available": "10%"},
evictionSoft: map[string]string{"memory.available": "30%"},
evictionSoftGracePeriod: map[string]string{"memory.available": "30s"},
evictionMinReclaim: map[string]string{"memory.available": "5%"},
expectErr: false,
expectThresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Percentage: 0.1,
},
MinReclaim: &evictionapi.ThresholdValue{
Percentage: 0.05,
},
},
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Percentage: 0.3,
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Percentage: 0.05,
},
},
},
},
"disk eviction values": {
allocatableConfig: []string{},
evictionHard: map[string]string{"imagefs.available": "150Mi", "nodefs.available": "100Mi"},
evictionSoft: map[string]string{"imagefs.available": "300Mi", "nodefs.available": "200Mi"},
evictionSoftGracePeriod: map[string]string{"imagefs.available": "30s", "nodefs.available": "30s"},
evictionMinReclaim: map[string]string{"imagefs.available": "2Gi", "nodefs.available": "1Gi"},
expectErr: false,
expectThresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
},
},
"disk eviction values in percentages": {
allocatableConfig: []string{},
evictionHard: map[string]string{"imagefs.available": "15%", "nodefs.available": "10.5%"},
evictionSoft: map[string]string{"imagefs.available": "30%", "nodefs.available": "20.5%"},
evictionSoftGracePeriod: map[string]string{"imagefs.available": "30s", "nodefs.available": "30s"},
evictionMinReclaim: map[string]string{"imagefs.available": "10%", "nodefs.available": "5%"},
expectErr: false,
expectThresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Percentage: 0.15,
},
MinReclaim: &evictionapi.ThresholdValue{
Percentage: 0.1,
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Percentage: 0.105,
},
MinReclaim: &evictionapi.ThresholdValue{
Percentage: 0.05,
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Percentage: 0.3,
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Percentage: 0.1,
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Percentage: 0.205,
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Percentage: 0.05,
},
},
},
},
"inode eviction values": {
allocatableConfig: []string{},
evictionHard: map[string]string{"imagefs.inodesFree": "150Mi", "nodefs.inodesFree": "100Mi"},
evictionSoft: map[string]string{"imagefs.inodesFree": "300Mi", "nodefs.inodesFree": "200Mi"},
evictionSoftGracePeriod: map[string]string{"imagefs.inodesFree": "30s", "nodefs.inodesFree": "30s"},
evictionMinReclaim: map[string]string{"imagefs.inodesFree": "2Gi", "nodefs.inodesFree": "1Gi"},
expectErr: false,
expectThresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
},
},
"disable via 0%": {
allocatableConfig: []string{},
evictionHard: map[string]string{"memory.available": "0%"},
evictionSoft: map[string]string{"memory.available": "0%"},
expectErr: false,
expectThresholds: []evictionapi.Threshold{},
},
"disable via 100%": {
allocatableConfig: []string{},
evictionHard: map[string]string{"memory.available": "100%"},
evictionSoft: map[string]string{"memory.available": "100%"},
expectErr: false,
expectThresholds: []evictionapi.Threshold{},
},
"invalid-signal": {
allocatableConfig: []string{},
evictionHard: map[string]string{"mem.available": "150Mi"},
evictionSoft: map[string]string{},
evictionSoftGracePeriod: map[string]string{},
evictionMinReclaim: map[string]string{},
expectErr: true,
expectThresholds: []evictionapi.Threshold{},
},
"hard-signal-negative": {
allocatableConfig: []string{},
evictionHard: map[string]string{"memory.available": "-150Mi"},
evictionSoft: map[string]string{},
evictionSoftGracePeriod: map[string]string{},
evictionMinReclaim: map[string]string{},
expectErr: true,
expectThresholds: []evictionapi.Threshold{},
},
"hard-signal-negative-percentage": {
allocatableConfig: []string{},
evictionHard: map[string]string{"memory.available": "-15%"},
evictionSoft: map[string]string{},
evictionSoftGracePeriod: map[string]string{},
evictionMinReclaim: map[string]string{},
expectErr: true,
expectThresholds: []evictionapi.Threshold{},
},
"hard-signal-percentage-greater-than-100%": {
allocatableConfig: []string{},
evictionHard: map[string]string{"memory.available": "150%"},
evictionSoft: map[string]string{},
evictionSoftGracePeriod: map[string]string{},
evictionMinReclaim: map[string]string{},
expectErr: true,
expectThresholds: []evictionapi.Threshold{},
},
"soft-signal-negative": {
allocatableConfig: []string{},
evictionHard: map[string]string{},
evictionSoft: map[string]string{"memory.available": "-150Mi"},
evictionSoftGracePeriod: map[string]string{},
evictionMinReclaim: map[string]string{},
expectErr: true,
expectThresholds: []evictionapi.Threshold{},
},
"valid-and-invalid-signal": {
allocatableConfig: []string{},
evictionHard: map[string]string{"memory.available": "150Mi", "invalid.foo": "150Mi"},
evictionSoft: map[string]string{},
evictionSoftGracePeriod: map[string]string{},
evictionMinReclaim: map[string]string{},
expectErr: true,
expectThresholds: []evictionapi.Threshold{},
},
"soft-no-grace-period": {
allocatableConfig: []string{},
evictionHard: map[string]string{},
evictionSoft: map[string]string{"memory.available": "150Mi"},
evictionSoftGracePeriod: map[string]string{},
evictionMinReclaim: map[string]string{},
expectErr: true,
expectThresholds: []evictionapi.Threshold{},
},
"soft-negative-grace-period": {
allocatableConfig: []string{},
evictionHard: map[string]string{},
evictionSoft: map[string]string{"memory.available": "150Mi"},
evictionSoftGracePeriod: map[string]string{"memory.available": "-30s"},
evictionMinReclaim: map[string]string{},
expectErr: true,
expectThresholds: []evictionapi.Threshold{},
},
"negative-reclaim": {
allocatableConfig: []string{},
evictionHard: map[string]string{},
evictionSoft: map[string]string{},
evictionSoftGracePeriod: map[string]string{},
evictionMinReclaim: map[string]string{"memory.available": "-300Mi"},
expectErr: true,
expectThresholds: []evictionapi.Threshold{},
},
}
for testName, testCase := range testCases {
thresholds, err := ParseThresholdConfig(testCase.allocatableConfig, testCase.evictionHard, testCase.evictionSoft, testCase.evictionSoftGracePeriod, testCase.evictionMinReclaim)
if testCase.expectErr != (err != nil) {
t.Errorf("Err not as expected, test: %v, error expected: %v, actual: %v", testName, testCase.expectErr, err)
}
if !thresholdsEqual(testCase.expectThresholds, thresholds) {
t.Errorf("thresholds not as expected, test: %v, expected: %v, actual: %v", testName, testCase.expectThresholds, thresholds)
}
}
}
func TestAddAllocatableThresholds(t *testing.T) {
// About func addAllocatableThresholds, only someone threshold that "Signal" is "memory.available" and "GracePeriod" is 0,
// append this threshold(changed "Signal" to "allocatableMemory.available") to thresholds
testCases := map[string]struct {
thresholds []evictionapi.Threshold
expected []evictionapi.Threshold
}{
"non-memory-signal": {
thresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
GracePeriod: 0,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
},
expected: []evictionapi.Threshold{
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
GracePeriod: 0,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
},
},
"memory-signal-with-grace": {
thresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
GracePeriod: 10,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
},
expected: []evictionapi.Threshold{
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
GracePeriod: 10,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
},
},
"memory-signal-without-grace": {
thresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
GracePeriod: 0,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
},
expected: []evictionapi.Threshold{
{
Signal: evictionapi.SignalAllocatableMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
GracePeriod: 0,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
},
},
"memory-signal-without-grace-two-thresholds": {
thresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
GracePeriod: 0,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: 0,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
},
expected: []evictionapi.Threshold{
{
Signal: evictionapi.SignalAllocatableMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
GracePeriod: 0,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("0"),
},
},
{
Signal: evictionapi.SignalAllocatableMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: 0,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
},
},
}
for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
if !thresholdsEqual(testCase.expected, addAllocatableThresholds(testCase.thresholds)) {
t.Errorf("Err not as expected, test: %v, Unexpected data: %s", testName, cmp.Diff(testCase.expected, addAllocatableThresholds(testCase.thresholds)))
}
})
}
}
func thresholdsEqual(expected []evictionapi.Threshold, actual []evictionapi.Threshold) bool {
if len(expected) != len(actual) {
return false
}
for _, aThreshold := range expected {
equal := false
for _, bThreshold := range actual {
if thresholdEqual(aThreshold, bThreshold) {
equal = true
}
}
if !equal {
return false
}
}
for _, aThreshold := range actual {
equal := false
for _, bThreshold := range expected {
if thresholdEqual(aThreshold, bThreshold) {
equal = true
}
}
if !equal {
return false
}
}
return true
}
func thresholdEqual(a evictionapi.Threshold, b evictionapi.Threshold) bool {
return a.GracePeriod == b.GracePeriod &&
a.Operator == b.Operator &&
a.Signal == b.Signal &&
compareThresholdValue(*a.MinReclaim, *b.MinReclaim) &&
compareThresholdValue(a.Value, b.Value)
}
func TestOrderedByExceedsRequestMemory(t *testing.T) {
below := newPod("below-requests", -1, []v1.Container{
newContainer("below-requests", newResourceList("", "200Mi", ""), newResourceList("", "", "")),
}, nil)
exceeds := newPod("exceeds-requests", 1, []v1.Container{
newContainer("exceeds-requests", newResourceList("", "100Mi", ""), newResourceList("", "", "")),
}, nil)
stats := map[*v1.Pod]statsapi.PodStats{
below: newPodMemoryStats(below, resource.MustParse("199Mi")), // -1 relative to request
exceeds: newPodMemoryStats(exceeds, resource.MustParse("101Mi")), // 1 relative to request
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
pods := []*v1.Pod{below, exceeds}
orderedBy(exceedMemoryRequests(statsFn)).Sort(pods)
expected := []*v1.Pod{exceeds, below}
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod: %s, but got: %s", expected[i].Name, pods[i].Name)
}
}
}
func TestOrderedByExceedsRequestDisk(t *testing.T) {
below := newPod("below-requests", -1, []v1.Container{
newContainer("below-requests", v1.ResourceList{v1.ResourceEphemeralStorage: resource.MustParse("200Mi")}, newResourceList("", "", "")),
}, nil)
exceeds := newPod("exceeds-requests", 1, []v1.Container{
newContainer("exceeds-requests", v1.ResourceList{v1.ResourceEphemeralStorage: resource.MustParse("100Mi")}, newResourceList("", "", "")),
}, nil)
stats := map[*v1.Pod]statsapi.PodStats{
below: newPodDiskStats(below, resource.MustParse("100Mi"), resource.MustParse("99Mi"), resource.MustParse("0Mi")), // -1 relative to request
exceeds: newPodDiskStats(exceeds, resource.MustParse("90Mi"), resource.MustParse("11Mi"), resource.MustParse("0Mi")), // 1 relative to request
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
pods := []*v1.Pod{below, exceeds}
orderedBy(exceedDiskRequests(statsFn, []fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, v1.ResourceEphemeralStorage)).Sort(pods)
expected := []*v1.Pod{exceeds, below}
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod: %s, but got: %s", expected[i].Name, pods[i].Name)
}
}
}
func TestOrderedByPriority(t *testing.T) {
low := newPod("low-priority", -134, []v1.Container{
newContainer("low-priority", newResourceList("", "", ""), newResourceList("", "", "")),
}, nil)
medium := newPod("medium-priority", 1, []v1.Container{
newContainer("medium-priority", newResourceList("100m", "100Mi", ""), newResourceList("200m", "200Mi", "")),
}, nil)
high := newPod("high-priority", 12534, []v1.Container{
newContainer("high-priority", newResourceList("200m", "200Mi", ""), newResourceList("200m", "200Mi", "")),
}, nil)
pods := []*v1.Pod{high, medium, low}
orderedBy(priority).Sort(pods)
expected := []*v1.Pod{low, medium, high}
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod: %s, but got: %s", expected[i].Name, pods[i].Name)
}
}
}
func TestOrderedbyDisk(t *testing.T) {
pod1 := newPod("best-effort-high", defaultPriority, []v1.Container{
newContainer("best-effort-high", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod2 := newPod("best-effort-low", defaultPriority, []v1.Container{
newContainer("best-effort-low", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod3 := newPod("burstable-high", defaultPriority, []v1.Container{
newContainer("burstable-high", newResourceList("", "", "100Mi"), newResourceList("", "", "400Mi")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod4 := newPod("burstable-low", defaultPriority, []v1.Container{
newContainer("burstable-low", newResourceList("", "", "100Mi"), newResourceList("", "", "400Mi")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod5 := newPod("guaranteed-high", defaultPriority, []v1.Container{
newContainer("guaranteed-high", newResourceList("", "", "400Mi"), newResourceList("", "", "400Mi")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod6 := newPod("guaranteed-low", defaultPriority, []v1.Container{
newContainer("guaranteed-low", newResourceList("", "", "400Mi"), newResourceList("", "", "400Mi")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
stats := map[*v1.Pod]statsapi.PodStats{
pod1: newPodDiskStats(pod1, resource.MustParse("50Mi"), resource.MustParse("100Mi"), resource.MustParse("150Mi")), // 300Mi - 0 = 300Mi
pod2: newPodDiskStats(pod2, resource.MustParse("25Mi"), resource.MustParse("25Mi"), resource.MustParse("50Mi")), // 100Mi - 0 = 100Mi
pod3: newPodDiskStats(pod3, resource.MustParse("150Mi"), resource.MustParse("150Mi"), resource.MustParse("50Mi")), // 350Mi - 100Mi = 250Mi
pod4: newPodDiskStats(pod4, resource.MustParse("25Mi"), resource.MustParse("35Mi"), resource.MustParse("50Mi")), // 110Mi - 100Mi = 10Mi
pod5: newPodDiskStats(pod5, resource.MustParse("225Mi"), resource.MustParse("100Mi"), resource.MustParse("50Mi")), // 375Mi - 400Mi = -25Mi
pod6: newPodDiskStats(pod6, resource.MustParse("25Mi"), resource.MustParse("45Mi"), resource.MustParse("50Mi")), // 120Mi - 400Mi = -280Mi
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
pods := []*v1.Pod{pod1, pod2, pod3, pod4, pod5, pod6}
orderedBy(disk(statsFn, []fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, v1.ResourceEphemeralStorage)).Sort(pods)
expected := []*v1.Pod{pod1, pod3, pod2, pod4, pod5, pod6}
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod[%d]: %s, but got: %s", i, expected[i].Name, pods[i].Name)
}
}
}
func TestOrderedbyInodes(t *testing.T) {
low := newPod("low", defaultPriority, []v1.Container{
newContainer("low", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
medium := newPod("medium", defaultPriority, []v1.Container{
newContainer("medium", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
high := newPod("high", defaultPriority, []v1.Container{
newContainer("high", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
stats := map[*v1.Pod]statsapi.PodStats{
low: newPodInodeStats(low, resource.MustParse("50000"), resource.MustParse("100000"), resource.MustParse("50000")), // 200000
medium: newPodInodeStats(medium, resource.MustParse("100000"), resource.MustParse("150000"), resource.MustParse("50000")), // 300000
high: newPodInodeStats(high, resource.MustParse("200000"), resource.MustParse("150000"), resource.MustParse("50000")), // 400000
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
pods := []*v1.Pod{low, medium, high}
orderedBy(disk(statsFn, []fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, resourceInodes)).Sort(pods)
expected := []*v1.Pod{high, medium, low}
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod[%d]: %s, but got: %s", i, expected[i].Name, pods[i].Name)
}
}
}
// TestOrderedByPriorityDisk ensures we order pods by priority and then greediest resource consumer
func TestOrderedByPriorityDisk(t *testing.T) {
pod1 := newPod("above-requests-low-priority-high-usage", lowPriority, []v1.Container{
newContainer("above-requests-low-priority-high-usage", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod2 := newPod("above-requests-low-priority-low-usage", lowPriority, []v1.Container{
newContainer("above-requests-low-priority-low-usage", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod3 := newPod("above-requests-high-priority-high-usage", highPriority, []v1.Container{
newContainer("above-requests-high-priority-high-usage", newResourceList("", "", "100Mi"), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod4 := newPod("above-requests-high-priority-low-usage", highPriority, []v1.Container{
newContainer("above-requests-high-priority-low-usage", newResourceList("", "", "100Mi"), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod5 := newPod("below-requests-low-priority-high-usage", lowPriority, []v1.Container{
newContainer("below-requests-low-priority-high-usage", newResourceList("", "", "1Gi"), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod6 := newPod("below-requests-low-priority-low-usage", lowPriority, []v1.Container{
newContainer("below-requests-low-priority-low-usage", newResourceList("", "", "1Gi"), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod7 := newPod("below-requests-high-priority-high-usage", highPriority, []v1.Container{
newContainer("below-requests-high-priority-high-usage", newResourceList("", "", "1Gi"), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod8 := newPod("below-requests-high-priority-low-usage", highPriority, []v1.Container{
newContainer("below-requests-high-priority-low-usage", newResourceList("", "", "1Gi"), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
stats := map[*v1.Pod]statsapi.PodStats{
pod1: newPodDiskStats(pod1, resource.MustParse("200Mi"), resource.MustParse("100Mi"), resource.MustParse("200Mi")), // 500 relative to request
pod2: newPodDiskStats(pod2, resource.MustParse("10Mi"), resource.MustParse("10Mi"), resource.MustParse("30Mi")), // 50 relative to request
pod3: newPodDiskStats(pod3, resource.MustParse("200Mi"), resource.MustParse("150Mi"), resource.MustParse("250Mi")), // 500 relative to request
pod4: newPodDiskStats(pod4, resource.MustParse("90Mi"), resource.MustParse("50Mi"), resource.MustParse("10Mi")), // 50 relative to request
pod5: newPodDiskStats(pod5, resource.MustParse("500Mi"), resource.MustParse("200Mi"), resource.MustParse("100Mi")), // -200 relative to request
pod6: newPodDiskStats(pod6, resource.MustParse("50Mi"), resource.MustParse("100Mi"), resource.MustParse("50Mi")), // -800 relative to request
pod7: newPodDiskStats(pod7, resource.MustParse("250Mi"), resource.MustParse("500Mi"), resource.MustParse("50Mi")), // -200 relative to request
pod8: newPodDiskStats(pod8, resource.MustParse("100Mi"), resource.MustParse("60Mi"), resource.MustParse("40Mi")), // -800 relative to request
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
pods := []*v1.Pod{pod8, pod7, pod6, pod5, pod4, pod3, pod2, pod1}
expected := []*v1.Pod{pod1, pod2, pod3, pod4, pod5, pod6, pod7, pod8}
fsStatsToMeasure := []fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}
orderedBy(exceedDiskRequests(statsFn, fsStatsToMeasure, v1.ResourceEphemeralStorage), priority, disk(statsFn, fsStatsToMeasure, v1.ResourceEphemeralStorage)).Sort(pods)
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod[%d]: %s, but got: %s", i, expected[i].Name, pods[i].Name)
}
}
}
// TestOrderedByPriorityInodes ensures we order pods by priority and then greediest resource consumer
func TestOrderedByPriorityInodes(t *testing.T) {
pod1 := newPod("low-priority-high-usage", lowPriority, []v1.Container{
newContainer("low-priority-high-usage", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod2 := newPod("low-priority-low-usage", lowPriority, []v1.Container{
newContainer("low-priority-low-usage", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod3 := newPod("high-priority-high-usage", highPriority, []v1.Container{
newContainer("high-priority-high-usage", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
pod4 := newPod("high-priority-low-usage", highPriority, []v1.Container{
newContainer("high-priority-low-usage", newResourceList("", "", ""), newResourceList("", "", "")),
}, []v1.Volume{
newVolume("local-volume", v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
}),
})
stats := map[*v1.Pod]statsapi.PodStats{
pod1: newPodInodeStats(pod1, resource.MustParse("50000"), resource.MustParse("100000"), resource.MustParse("250000")), // 400000
pod2: newPodInodeStats(pod2, resource.MustParse("60000"), resource.MustParse("30000"), resource.MustParse("10000")), // 100000
pod3: newPodInodeStats(pod3, resource.MustParse("150000"), resource.MustParse("150000"), resource.MustParse("50000")), // 350000
pod4: newPodInodeStats(pod4, resource.MustParse("10000"), resource.MustParse("40000"), resource.MustParse("100000")), // 150000
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
pods := []*v1.Pod{pod4, pod3, pod2, pod1}
orderedBy(priority, disk(statsFn, []fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, resourceInodes)).Sort(pods)
expected := []*v1.Pod{pod1, pod2, pod3, pod4}
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod[%d]: %s, but got: %s", i, expected[i].Name, pods[i].Name)
}
}
}
// TestOrderedByMemory ensures we order pods by greediest memory consumer relative to request.
func TestOrderedByMemory(t *testing.T) {
pod1 := newPod("best-effort-high", defaultPriority, []v1.Container{
newContainer("best-effort-high", newResourceList("", "", ""), newResourceList("", "", "")),
}, nil)
pod2 := newPod("best-effort-low", defaultPriority, []v1.Container{
newContainer("best-effort-low", newResourceList("", "", ""), newResourceList("", "", "")),
}, nil)
pod3 := newPod("burstable-high", defaultPriority, []v1.Container{
newContainer("burstable-high", newResourceList("", "100Mi", ""), newResourceList("", "1Gi", "")),
}, nil)
pod4 := newPod("burstable-low", defaultPriority, []v1.Container{
newContainer("burstable-low", newResourceList("", "100Mi", ""), newResourceList("", "1Gi", "")),
}, nil)
pod5 := newPod("guaranteed-high", defaultPriority, []v1.Container{
newContainer("guaranteed-high", newResourceList("", "1Gi", ""), newResourceList("", "1Gi", "")),
}, nil)
pod6 := newPod("guaranteed-low", defaultPriority, []v1.Container{
newContainer("guaranteed-low", newResourceList("", "1Gi", ""), newResourceList("", "1Gi", "")),
}, nil)
stats := map[*v1.Pod]statsapi.PodStats{
pod1: newPodMemoryStats(pod1, resource.MustParse("500Mi")), // 500 relative to request
pod2: newPodMemoryStats(pod2, resource.MustParse("300Mi")), // 300 relative to request
pod3: newPodMemoryStats(pod3, resource.MustParse("800Mi")), // 700 relative to request
pod4: newPodMemoryStats(pod4, resource.MustParse("300Mi")), // 200 relative to request
pod5: newPodMemoryStats(pod5, resource.MustParse("800Mi")), // -200 relative to request
pod6: newPodMemoryStats(pod6, resource.MustParse("200Mi")), // -800 relative to request
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
pods := []*v1.Pod{pod1, pod2, pod3, pod4, pod5, pod6}
orderedBy(memory(statsFn)).Sort(pods)
expected := []*v1.Pod{pod3, pod1, pod2, pod4, pod5, pod6}
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod[%d]: %s, but got: %s", i, expected[i].Name, pods[i].Name)
}
}
}
// TestOrderedByPriorityMemory ensures we order by priority and then memory consumption relative to request.
func TestOrderedByPriorityMemory(t *testing.T) {
pod1 := newPod("above-requests-low-priority-high-usage", lowPriority, []v1.Container{
newContainer("above-requests-low-priority-high-usage", newResourceList("", "", ""), newResourceList("", "", "")),
}, nil)
pod2 := newPod("above-requests-low-priority-low-usage", lowPriority, []v1.Container{
newContainer("above-requests-low-priority-low-usage", newResourceList("", "", ""), newResourceList("", "", "")),
}, nil)
pod3 := newPod("above-requests-high-priority-high-usage", highPriority, []v1.Container{
newContainer("above-requests-high-priority-high-usage", newResourceList("", "100Mi", ""), newResourceList("", "", "")),
}, nil)
pod4 := newPod("above-requests-high-priority-low-usage", highPriority, []v1.Container{
newContainer("above-requests-high-priority-low-usage", newResourceList("", "100Mi", ""), newResourceList("", "", "")),
}, nil)
pod5 := newPod("below-requests-low-priority-high-usage", lowPriority, []v1.Container{
newContainer("below-requests-low-priority-high-usage", newResourceList("", "1Gi", ""), newResourceList("", "", "")),
}, nil)
pod6 := newPod("below-requests-low-priority-low-usage", lowPriority, []v1.Container{
newContainer("below-requests-low-priority-low-usage", newResourceList("", "1Gi", ""), newResourceList("", "", "")),
}, nil)
pod7 := newPod("below-requests-high-priority-high-usage", highPriority, []v1.Container{
newContainer("below-requests-high-priority-high-usage", newResourceList("", "1Gi", ""), newResourceList("", "", "")),
}, nil)
pod8 := newPod("below-requests-high-priority-low-usage", highPriority, []v1.Container{
newContainer("below-requests-high-priority-low-usage", newResourceList("", "1Gi", ""), newResourceList("", "", "")),
}, nil)
stats := map[*v1.Pod]statsapi.PodStats{
pod1: newPodMemoryStats(pod1, resource.MustParse("500Mi")), // 500 relative to request
pod2: newPodMemoryStats(pod2, resource.MustParse("50Mi")), // 50 relative to request
pod3: newPodMemoryStats(pod3, resource.MustParse("600Mi")), // 500 relative to request
pod4: newPodMemoryStats(pod4, resource.MustParse("150Mi")), // 50 relative to request
pod5: newPodMemoryStats(pod5, resource.MustParse("800Mi")), // -200 relative to request
pod6: newPodMemoryStats(pod6, resource.MustParse("200Mi")), // -800 relative to request
pod7: newPodMemoryStats(pod7, resource.MustParse("800Mi")), // -200 relative to request
pod8: newPodMemoryStats(pod8, resource.MustParse("200Mi")), // -800 relative to request
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
pods := []*v1.Pod{pod8, pod7, pod6, pod5, pod4, pod3, pod2, pod1}
expected := []*v1.Pod{pod1, pod2, pod3, pod4, pod5, pod6, pod7, pod8}
orderedBy(exceedMemoryRequests(statsFn), priority, memory(statsFn)).Sort(pods)
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod[%d]: %s, but got: %s", i, expected[i].Name, pods[i].Name)
}
}
}
// TestOrderedByPriorityProcess ensures we order by priority and then process consumption relative to request.
func TestOrderedByPriorityProcess(t *testing.T) {
pod1 := newPod("low-priority-high-usage", lowPriority, nil, nil)
pod2 := newPod("low-priority-low-usage", lowPriority, nil, nil)
pod3 := newPod("high-priority-high-usage", highPriority, nil, nil)
pod4 := newPod("high-priority-low-usage", highPriority, nil, nil)
stats := map[*v1.Pod]statsapi.PodStats{
pod1: newPodProcessStats(pod1, 20),
pod2: newPodProcessStats(pod2, 6),
pod3: newPodProcessStats(pod3, 20),
pod4: newPodProcessStats(pod4, 5),
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
pods := []*v1.Pod{pod4, pod3, pod2, pod1}
expected := []*v1.Pod{pod1, pod2, pod3, pod4}
orderedBy(priority, process(statsFn)).Sort(pods)
for i := range expected {
if pods[i] != expected[i] {
t.Errorf("Expected pod[%d]: %s, but got: %s", i, expected[i].Name, pods[i].Name)
}
}
}
func TestSortByEvictionPriority(t *testing.T) {
for _, tc := range []struct {
name string
thresholds []evictionapi.Threshold
expected []evictionapi.Threshold
}{
{
name: "empty threshold list",
thresholds: []evictionapi.Threshold{},
expected: []evictionapi.Threshold{},
},
{
name: "memory first",
thresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalNodeFsAvailable,
},
{
Signal: evictionapi.SignalPIDAvailable,
},
{
Signal: evictionapi.SignalMemoryAvailable,
},
},
expected: []evictionapi.Threshold{
{
Signal: evictionapi.SignalMemoryAvailable,
},
{
Signal: evictionapi.SignalNodeFsAvailable,
},
{
Signal: evictionapi.SignalPIDAvailable,
},
},
},
{
name: "allocatable memory first",
thresholds: []evictionapi.Threshold{
{
Signal: evictionapi.SignalNodeFsAvailable,
},
{
Signal: evictionapi.SignalPIDAvailable,
},
{
Signal: evictionapi.SignalAllocatableMemoryAvailable,
},
},
expected: []evictionapi.Threshold{
{
Signal: evictionapi.SignalAllocatableMemoryAvailable,
},
{
Signal: evictionapi.SignalNodeFsAvailable,
},
{
Signal: evictionapi.SignalPIDAvailable,
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
sort.Sort(byEvictionPriority(tc.thresholds))
for i := range tc.expected {
if tc.thresholds[i].Signal != tc.expected[i].Signal {
t.Errorf("At index %d, expected threshold with signal %s, but got %s", i, tc.expected[i].Signal, tc.thresholds[i].Signal)
}
}
})
}
}
type fakeSummaryProvider struct {
result *statsapi.Summary
}
func (f *fakeSummaryProvider) Get(ctx context.Context, updateStats bool) (*statsapi.Summary, error) {
return f.result, nil
}
func (f *fakeSummaryProvider) GetCPUAndMemoryStats(ctx context.Context) (*statsapi.Summary, error) {
return f.result, nil
}
// newPodStats returns a pod stat where each container is using the specified working set
// each pod must have a Name, UID, Namespace
func newPodStats(pod *v1.Pod, podWorkingSetBytes uint64) statsapi.PodStats {
return statsapi.PodStats{
PodRef: statsapi.PodReference{
Name: pod.Name,
Namespace: pod.Namespace,
UID: string(pod.UID),
},
Memory: &statsapi.MemoryStats{
WorkingSetBytes: &podWorkingSetBytes,
},
}
}
func TestMakeSignalObservations(t *testing.T) {
podMaker := func(name, namespace, uid string, numContainers int) *v1.Pod {
pod := &v1.Pod{}
pod.Name = name
pod.Namespace = namespace
pod.UID = types.UID(uid)
pod.Spec = v1.PodSpec{}
for i := 0; i < numContainers; i++ {
pod.Spec.Containers = append(pod.Spec.Containers, v1.Container{
Name: fmt.Sprintf("ctr%v", i),
})
}
return pod
}
nodeAvailableBytes := uint64(1024 * 1024 * 1024)
nodeWorkingSetBytes := uint64(1024 * 1024 * 1024)
allocatableMemoryCapacity := uint64(5 * 1024 * 1024 * 1024)
imageFsAvailableBytes := uint64(1024 * 1024)
imageFsCapacityBytes := uint64(1024 * 1024 * 2)
nodeFsAvailableBytes := uint64(1024)
nodeFsCapacityBytes := uint64(1024 * 2)
imageFsInodesFree := uint64(1024)
imageFsInodes := uint64(1024 * 1024)
nodeFsInodesFree := uint64(1024)
nodeFsInodes := uint64(1024 * 1024)
containerFsAvailableBytes := uint64(1024 * 1024 * 2)
containerFsCapacityBytes := uint64(1024 * 1024 * 8)
containerFsInodesFree := uint64(1024 * 2)
containerFsInodes := uint64(1024 * 2)
maxPID := int64(255816)
numberOfRunningProcesses := int64(1000)
fakeStats := &statsapi.Summary{
Node: statsapi.NodeStats{
Memory: &statsapi.MemoryStats{
AvailableBytes: &nodeAvailableBytes,
WorkingSetBytes: &nodeWorkingSetBytes,
},
Runtime: &statsapi.RuntimeStats{
ImageFs: &statsapi.FsStats{
AvailableBytes: &imageFsAvailableBytes,
CapacityBytes: &imageFsCapacityBytes,
InodesFree: &imageFsInodesFree,
Inodes: &imageFsInodes,
},
ContainerFs: &statsapi.FsStats{
AvailableBytes: &containerFsAvailableBytes,
CapacityBytes: &containerFsCapacityBytes,
InodesFree: &containerFsInodesFree,
Inodes: &containerFsInodes,
},
},
Rlimit: &statsapi.RlimitStats{
MaxPID: &maxPID,
NumOfRunningProcesses: &numberOfRunningProcesses,
},
Fs: &statsapi.FsStats{
AvailableBytes: &nodeFsAvailableBytes,
CapacityBytes: &nodeFsCapacityBytes,
InodesFree: &nodeFsInodesFree,
Inodes: &nodeFsInodes,
},
SystemContainers: []statsapi.ContainerStats{
{
Name: statsapi.SystemContainerPods,
Memory: &statsapi.MemoryStats{
AvailableBytes: &nodeAvailableBytes,
WorkingSetBytes: &nodeWorkingSetBytes,
},
},
},
},
Pods: []statsapi.PodStats{},
}
pods := []*v1.Pod{
podMaker("pod1", "ns1", "uuid1", 1),
podMaker("pod1", "ns2", "uuid2", 1),
podMaker("pod3", "ns3", "uuid3", 1),
}
podWorkingSetBytes := uint64(1024 * 1024 * 1024)
for _, pod := range pods {
fakeStats.Pods = append(fakeStats.Pods, newPodStats(pod, podWorkingSetBytes))
}
res := quantityMustParse("5Gi")
// Allocatable thresholds are always 100%. Verify that Threshold == Capacity.
if res.CmpInt64(int64(allocatableMemoryCapacity)) != 0 {
t.Errorf("Expected Threshold %v to be equal to value %v", res.Value(), allocatableMemoryCapacity)
}
actualObservations, statsFunc := makeSignalObservations(fakeStats)
allocatableMemQuantity, found := actualObservations[evictionapi.SignalAllocatableMemoryAvailable]
if !found {
t.Errorf("Expected allocatable memory observation, but didnt find one")
}
if expectedBytes := int64(nodeAvailableBytes); allocatableMemQuantity.available.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, allocatableMemQuantity.available.Value())
}
if expectedBytes := int64(nodeWorkingSetBytes + nodeAvailableBytes); allocatableMemQuantity.capacity.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, allocatableMemQuantity.capacity.Value())
}
memQuantity, found := actualObservations[evictionapi.SignalMemoryAvailable]
if !found {
t.Error("Expected available memory observation")
}
if expectedBytes := int64(nodeAvailableBytes); memQuantity.available.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, memQuantity.available.Value())
}
if expectedBytes := int64(nodeWorkingSetBytes + nodeAvailableBytes); memQuantity.capacity.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, memQuantity.capacity.Value())
}
nodeFsQuantity, found := actualObservations[evictionapi.SignalNodeFsAvailable]
if !found {
t.Error("Expected available nodefs observation")
}
if expectedBytes := int64(nodeFsAvailableBytes); nodeFsQuantity.available.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, nodeFsQuantity.available.Value())
}
if expectedBytes := int64(nodeFsCapacityBytes); nodeFsQuantity.capacity.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, nodeFsQuantity.capacity.Value())
}
nodeFsInodesQuantity, found := actualObservations[evictionapi.SignalNodeFsInodesFree]
if !found {
t.Error("Expected inodes free nodefs observation")
}
if expected := int64(nodeFsInodesFree); nodeFsInodesQuantity.available.Value() != expected {
t.Errorf("Expected %v, actual: %v", expected, nodeFsInodesQuantity.available.Value())
}
if expected := int64(nodeFsInodes); nodeFsInodesQuantity.capacity.Value() != expected {
t.Errorf("Expected %v, actual: %v", expected, nodeFsInodesQuantity.capacity.Value())
}
imageFsQuantity, found := actualObservations[evictionapi.SignalImageFsAvailable]
if !found {
t.Error("Expected available imagefs observation")
}
if expectedBytes := int64(imageFsAvailableBytes); imageFsQuantity.available.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, imageFsQuantity.available.Value())
}
if expectedBytes := int64(imageFsCapacityBytes); imageFsQuantity.capacity.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, imageFsQuantity.capacity.Value())
}
containerFsQuantity, found := actualObservations[evictionapi.SignalContainerFsAvailable]
if !found {
t.Error("Expected available containerfs observation")
}
if expectedBytes := int64(containerFsAvailableBytes); containerFsQuantity.available.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, containerFsQuantity.available.Value())
}
if expectedBytes := int64(containerFsCapacityBytes); containerFsQuantity.capacity.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, containerFsQuantity.capacity.Value())
}
imageFsInodesQuantity, found := actualObservations[evictionapi.SignalImageFsInodesFree]
if !found {
t.Error("Expected inodes free imagefs observation")
}
if expected := int64(imageFsInodesFree); imageFsInodesQuantity.available.Value() != expected {
t.Errorf("Expected %v, actual: %v", expected, imageFsInodesQuantity.available.Value())
}
if expected := int64(imageFsInodes); imageFsInodesQuantity.capacity.Value() != expected {
t.Errorf("Expected %v, actual: %v", expected, imageFsInodesQuantity.capacity.Value())
}
containerFsInodesQuantity, found := actualObservations[evictionapi.SignalContainerFsInodesFree]
if !found {
t.Error("Expected indoes free containerfs observation")
}
if expected := int64(containerFsInodesFree); containerFsInodesQuantity.available.Value() != expected {
t.Errorf("Expected %v, actual: %v", expected, containerFsInodesQuantity.available.Value())
}
if expected := int64(containerFsInodes); containerFsInodesQuantity.capacity.Value() != expected {
t.Errorf("Expected %v, actual: %v", expected, containerFsInodesQuantity.capacity.Value())
}
pidQuantity, found := actualObservations[evictionapi.SignalPIDAvailable]
if !found {
t.Error("Expected available memory observation")
}
if expectedBytes := int64(maxPID); pidQuantity.capacity.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, pidQuantity.capacity.Value())
}
if expectedBytes := int64(maxPID - numberOfRunningProcesses); pidQuantity.available.Value() != expectedBytes {
t.Errorf("Expected %v, actual: %v", expectedBytes, pidQuantity.available.Value())
}
for _, pod := range pods {
podStats, found := statsFunc(pod)
if !found {
t.Errorf("Pod stats were not found for pod %v", pod.UID)
}
if *podStats.Memory.WorkingSetBytes != podWorkingSetBytes {
t.Errorf("Pod working set expected %v, actual: %v", podWorkingSetBytes, *podStats.Memory.WorkingSetBytes)
}
}
}
func TestThresholdsMet(t *testing.T) {
hardThreshold := evictionapi.Threshold{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
}
testCases := map[string]struct {
enforceMinReclaim bool
thresholds []evictionapi.Threshold
observations signalObservations
result []evictionapi.Threshold
}{
"empty": {
enforceMinReclaim: false,
thresholds: []evictionapi.Threshold{},
observations: signalObservations{},
result: []evictionapi.Threshold{},
},
"threshold-met-memory": {
enforceMinReclaim: false,
thresholds: []evictionapi.Threshold{hardThreshold},
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("500Mi"),
},
},
result: []evictionapi.Threshold{hardThreshold},
},
"threshold-not-met": {
enforceMinReclaim: false,
thresholds: []evictionapi.Threshold{hardThreshold},
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("2Gi"),
},
},
result: []evictionapi.Threshold{},
},
"threshold-met-with-min-reclaim": {
enforceMinReclaim: true,
thresholds: []evictionapi.Threshold{hardThreshold},
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("1.05Gi"),
},
},
result: []evictionapi.Threshold{hardThreshold},
},
"threshold-not-met-with-min-reclaim": {
enforceMinReclaim: true,
thresholds: []evictionapi.Threshold{hardThreshold},
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("2Gi"),
},
},
result: []evictionapi.Threshold{},
},
}
for testName, testCase := range testCases {
actual := thresholdsMet(testCase.thresholds, testCase.observations, testCase.enforceMinReclaim)
if !thresholdList(actual).Equal(thresholdList(testCase.result)) {
t.Errorf("Test case: %s, expected: %v, actual: %v", testName, testCase.result, actual)
}
}
}
func TestThresholdsUpdatedStats(t *testing.T) {
updatedThreshold := evictionapi.Threshold{
Signal: evictionapi.SignalMemoryAvailable,
}
locationUTC, err := time.LoadLocation("UTC")
if err != nil {
t.Error(err)
return
}
testCases := map[string]struct {
thresholds []evictionapi.Threshold
observations signalObservations
last signalObservations
result []evictionapi.Threshold
}{
"empty": {
thresholds: []evictionapi.Threshold{},
observations: signalObservations{},
last: signalObservations{},
result: []evictionapi.Threshold{},
},
"no-time": {
thresholds: []evictionapi.Threshold{updatedThreshold},
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{},
},
last: signalObservations{},
result: []evictionapi.Threshold{updatedThreshold},
},
"no-last-observation": {
thresholds: []evictionapi.Threshold{updatedThreshold},
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
time: metav1.Date(2016, 1, 1, 0, 0, 0, 0, locationUTC),
},
},
last: signalObservations{},
result: []evictionapi.Threshold{updatedThreshold},
},
"time-machine": {
thresholds: []evictionapi.Threshold{updatedThreshold},
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
time: metav1.Date(2016, 1, 1, 0, 0, 0, 0, locationUTC),
},
},
last: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
time: metav1.Date(2016, 1, 1, 0, 1, 0, 0, locationUTC),
},
},
result: []evictionapi.Threshold{},
},
"same-observation": {
thresholds: []evictionapi.Threshold{updatedThreshold},
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
time: metav1.Date(2016, 1, 1, 0, 0, 0, 0, locationUTC),
},
},
last: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
time: metav1.Date(2016, 1, 1, 0, 0, 0, 0, locationUTC),
},
},
result: []evictionapi.Threshold{},
},
"new-observation": {
thresholds: []evictionapi.Threshold{updatedThreshold},
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
time: metav1.Date(2016, 1, 1, 0, 1, 0, 0, locationUTC),
},
},
last: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
time: metav1.Date(2016, 1, 1, 0, 0, 0, 0, locationUTC),
},
},
result: []evictionapi.Threshold{updatedThreshold},
},
}
for testName, testCase := range testCases {
actual := thresholdsUpdatedStats(testCase.thresholds, testCase.observations, testCase.last)
if !thresholdList(actual).Equal(thresholdList(testCase.result)) {
t.Errorf("Test case: %s, expected: %v, actual: %v", testName, testCase.result, actual)
}
}
}
func TestPercentageThresholdsMet(t *testing.T) {
specificThresholds := []evictionapi.Threshold{
{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Percentage: 0.2,
},
MinReclaim: &evictionapi.ThresholdValue{
Percentage: 0.05,
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Percentage: 0.3,
},
},
}
testCases := map[string]struct {
enforceMinRelaim bool
thresholds []evictionapi.Threshold
observations signalObservations
result []evictionapi.Threshold
}{
"BothMet": {
enforceMinRelaim: false,
thresholds: specificThresholds,
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("100Mi"),
capacity: quantityMustParse("1000Mi"),
},
evictionapi.SignalNodeFsAvailable: signalObservation{
available: quantityMustParse("100Gi"),
capacity: quantityMustParse("1000Gi"),
},
},
result: specificThresholds,
},
"NoneMet": {
enforceMinRelaim: false,
thresholds: specificThresholds,
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("300Mi"),
capacity: quantityMustParse("1000Mi"),
},
evictionapi.SignalNodeFsAvailable: signalObservation{
available: quantityMustParse("400Gi"),
capacity: quantityMustParse("1000Gi"),
},
},
result: []evictionapi.Threshold{},
},
"DiskMet": {
enforceMinRelaim: false,
thresholds: specificThresholds,
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("300Mi"),
capacity: quantityMustParse("1000Mi"),
},
evictionapi.SignalNodeFsAvailable: signalObservation{
available: quantityMustParse("100Gi"),
capacity: quantityMustParse("1000Gi"),
},
},
result: []evictionapi.Threshold{specificThresholds[1]},
},
"MemoryMet": {
enforceMinRelaim: false,
thresholds: specificThresholds,
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("100Mi"),
capacity: quantityMustParse("1000Mi"),
},
evictionapi.SignalNodeFsAvailable: signalObservation{
available: quantityMustParse("400Gi"),
capacity: quantityMustParse("1000Gi"),
},
},
result: []evictionapi.Threshold{specificThresholds[0]},
},
"MemoryMetWithMinReclaim": {
enforceMinRelaim: true,
thresholds: specificThresholds,
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("225Mi"),
capacity: quantityMustParse("1000Mi"),
},
},
result: []evictionapi.Threshold{specificThresholds[0]},
},
"MemoryNotMetWithMinReclaim": {
enforceMinRelaim: true,
thresholds: specificThresholds,
observations: signalObservations{
evictionapi.SignalMemoryAvailable: signalObservation{
available: quantityMustParse("300Mi"),
capacity: quantityMustParse("1000Mi"),
},
},
result: []evictionapi.Threshold{},
},
}
for testName, testCase := range testCases {
actual := thresholdsMet(testCase.thresholds, testCase.observations, testCase.enforceMinRelaim)
if !thresholdList(actual).Equal(thresholdList(testCase.result)) {
t.Errorf("Test case: %s, expected: %v, actual: %v", testName, testCase.result, actual)
}
}
}
func TestThresholdsFirstObservedAt(t *testing.T) {
hardThreshold := evictionapi.Threshold{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
}
now := metav1.Now()
oldTime := metav1.NewTime(now.Time.Add(-1 * time.Minute))
testCases := map[string]struct {
thresholds []evictionapi.Threshold
lastObservedAt thresholdsObservedAt
now time.Time
result thresholdsObservedAt
}{
"empty": {
thresholds: []evictionapi.Threshold{},
lastObservedAt: thresholdsObservedAt{},
now: now.Time,
result: thresholdsObservedAt{},
},
"no-previous-observation": {
thresholds: []evictionapi.Threshold{hardThreshold},
lastObservedAt: thresholdsObservedAt{},
now: now.Time,
result: thresholdsObservedAt{
hardThreshold: now.Time,
},
},
"previous-observation": {
thresholds: []evictionapi.Threshold{hardThreshold},
lastObservedAt: thresholdsObservedAt{
hardThreshold: oldTime.Time,
},
now: now.Time,
result: thresholdsObservedAt{
hardThreshold: oldTime.Time,
},
},
}
for testName, testCase := range testCases {
actual := thresholdsFirstObservedAt(testCase.thresholds, testCase.lastObservedAt, testCase.now)
if !reflect.DeepEqual(actual, testCase.result) {
t.Errorf("Test case: %s, expected: %v, actual: %v", testName, testCase.result, actual)
}
}
}
func TestThresholdsMetGracePeriod(t *testing.T) {
now := metav1.Now()
hardThreshold := evictionapi.Threshold{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
}
softThreshold := evictionapi.Threshold{
Signal: evictionapi.SignalMemoryAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: 1 * time.Minute,
}
oldTime := metav1.NewTime(now.Time.Add(-2 * time.Minute))
testCases := map[string]struct {
observedAt thresholdsObservedAt
now time.Time
result []evictionapi.Threshold
}{
"empty": {
observedAt: thresholdsObservedAt{},
now: now.Time,
result: []evictionapi.Threshold{},
},
"hard-threshold-met": {
observedAt: thresholdsObservedAt{
hardThreshold: now.Time,
},
now: now.Time,
result: []evictionapi.Threshold{hardThreshold},
},
"soft-threshold-not-met": {
observedAt: thresholdsObservedAt{
softThreshold: now.Time,
},
now: now.Time,
result: []evictionapi.Threshold{},
},
"soft-threshold-met": {
observedAt: thresholdsObservedAt{
softThreshold: oldTime.Time,
},
now: now.Time,
result: []evictionapi.Threshold{softThreshold},
},
}
for testName, testCase := range testCases {
actual := thresholdsMetGracePeriod(testCase.observedAt, now.Time)
if !thresholdList(actual).Equal(thresholdList(testCase.result)) {
t.Errorf("Test case: %s, expected: %v, actual: %v", testName, testCase.result, actual)
}
}
}
func TestNodeConditions(t *testing.T) {
testCases := map[string]struct {
inputs []evictionapi.Threshold
result []v1.NodeConditionType
}{
"empty-list": {
inputs: []evictionapi.Threshold{},
result: []v1.NodeConditionType{},
},
"memory.available": {
inputs: []evictionapi.Threshold{
{Signal: evictionapi.SignalMemoryAvailable},
},
result: []v1.NodeConditionType{v1.NodeMemoryPressure},
},
}
for testName, testCase := range testCases {
actual := nodeConditions(testCase.inputs)
if !nodeConditionList(actual).Equal(nodeConditionList(testCase.result)) {
t.Errorf("Test case: %s, expected: %v, actual: %v", testName, testCase.result, actual)
}
}
}
func TestNodeConditionsLastObservedAt(t *testing.T) {
now := metav1.Now()
oldTime := metav1.NewTime(now.Time.Add(-1 * time.Minute))
testCases := map[string]struct {
nodeConditions []v1.NodeConditionType
lastObservedAt nodeConditionsObservedAt
now time.Time
result nodeConditionsObservedAt
}{
"no-previous-observation": {
nodeConditions: []v1.NodeConditionType{v1.NodeMemoryPressure},
lastObservedAt: nodeConditionsObservedAt{},
now: now.Time,
result: nodeConditionsObservedAt{
v1.NodeMemoryPressure: now.Time,
},
},
"previous-observation": {
nodeConditions: []v1.NodeConditionType{v1.NodeMemoryPressure},
lastObservedAt: nodeConditionsObservedAt{
v1.NodeMemoryPressure: oldTime.Time,
},
now: now.Time,
result: nodeConditionsObservedAt{
v1.NodeMemoryPressure: now.Time,
},
},
"old-observation": {
nodeConditions: []v1.NodeConditionType{},
lastObservedAt: nodeConditionsObservedAt{
v1.NodeMemoryPressure: oldTime.Time,
},
now: now.Time,
result: nodeConditionsObservedAt{
v1.NodeMemoryPressure: oldTime.Time,
},
},
}
for testName, testCase := range testCases {
actual := nodeConditionsLastObservedAt(testCase.nodeConditions, testCase.lastObservedAt, testCase.now)
if !reflect.DeepEqual(actual, testCase.result) {
t.Errorf("Test case: %s, expected: %v, actual: %v", testName, testCase.result, actual)
}
}
}
func TestNodeConditionsObservedSince(t *testing.T) {
now := metav1.Now()
observedTime := metav1.NewTime(now.Time.Add(-1 * time.Minute))
testCases := map[string]struct {
observedAt nodeConditionsObservedAt
period time.Duration
now time.Time
result []v1.NodeConditionType
}{
"in-period": {
observedAt: nodeConditionsObservedAt{
v1.NodeMemoryPressure: observedTime.Time,
},
period: 2 * time.Minute,
now: now.Time,
result: []v1.NodeConditionType{v1.NodeMemoryPressure},
},
"out-of-period": {
observedAt: nodeConditionsObservedAt{
v1.NodeMemoryPressure: observedTime.Time,
},
period: 30 * time.Second,
now: now.Time,
result: []v1.NodeConditionType{},
},
}
for testName, testCase := range testCases {
actual := nodeConditionsObservedSince(testCase.observedAt, testCase.period, testCase.now)
if !nodeConditionList(actual).Equal(nodeConditionList(testCase.result)) {
t.Errorf("Test case: %s, expected: %v, actual: %v", testName, testCase.result, actual)
}
}
}
func TestHasNodeConditions(t *testing.T) {
testCases := map[string]struct {
inputs []v1.NodeConditionType
item v1.NodeConditionType
result bool
}{
"has-condition": {
inputs: []v1.NodeConditionType{v1.NodeReady, v1.NodeDiskPressure, v1.NodeMemoryPressure},
item: v1.NodeMemoryPressure,
result: true,
},
"does-not-have-condition": {
inputs: []v1.NodeConditionType{v1.NodeReady, v1.NodeDiskPressure},
item: v1.NodeMemoryPressure,
result: false,
},
}
for testName, testCase := range testCases {
if actual := hasNodeCondition(testCase.inputs, testCase.item); actual != testCase.result {
t.Errorf("Test case: %s, expected: %v, actual: %v", testName, testCase.result, actual)
}
}
}
func TestParsePercentage(t *testing.T) {
testCases := map[string]struct {
hasError bool
value float32
}{
"blah": {
hasError: true,
},
"25.5%": {
value: 0.255,
},
"foo%": {
hasError: true,
},
"12%345": {
hasError: true,
},
}
for input, expected := range testCases {
value, err := parsePercentage(input)
if (err != nil) != expected.hasError {
t.Errorf("Test case: %s, expected: %v, actual: %v", input, expected.hasError, err != nil)
}
if value != expected.value {
t.Errorf("Test case: %s, expected: %v, actual: %v", input, expected.value, value)
}
}
}
func TestCompareThresholdValue(t *testing.T) {
testCases := []struct {
a, b evictionapi.ThresholdValue
equal bool
}{
{
a: evictionapi.ThresholdValue{
Quantity: resource.NewQuantity(123, resource.BinarySI),
},
b: evictionapi.ThresholdValue{
Quantity: resource.NewQuantity(123, resource.BinarySI),
},
equal: true,
},
{
a: evictionapi.ThresholdValue{
Quantity: resource.NewQuantity(123, resource.BinarySI),
},
b: evictionapi.ThresholdValue{
Quantity: resource.NewQuantity(456, resource.BinarySI),
},
equal: false,
},
{
a: evictionapi.ThresholdValue{
Quantity: resource.NewQuantity(123, resource.BinarySI),
},
b: evictionapi.ThresholdValue{
Percentage: 0.1,
},
equal: false,
},
{
a: evictionapi.ThresholdValue{
Percentage: 0.1,
},
b: evictionapi.ThresholdValue{
Percentage: 0.1,
},
equal: true,
},
{
a: evictionapi.ThresholdValue{
Percentage: 0.2,
},
b: evictionapi.ThresholdValue{
Percentage: 0.1,
},
equal: false,
},
}
for i, testCase := range testCases {
if compareThresholdValue(testCase.a, testCase.b) != testCase.equal ||
compareThresholdValue(testCase.b, testCase.a) != testCase.equal {
t.Errorf("Test case: %v failed", i)
}
}
}
func TestAddContainerFsThresholds(t *testing.T) {
gracePeriod := time.Duration(1)
testCases := []struct {
description string
imageFs bool
containerFs bool
expectedContainerFsHard evictionapi.Threshold
expectedContainerFsSoft evictionapi.Threshold
expectedContainerFsINodesHard evictionapi.Threshold
expectedContainerFsINodesSoft evictionapi.Threshold
expectErr bool
thresholdList []evictionapi.Threshold
}{
{
description: "single filesystem",
imageFs: false,
containerFs: false,
expectedContainerFsHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsINodesHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsINodesSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
thresholdList: []evictionapi.Threshold{
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
},
},
{
description: "image filesystem",
imageFs: true,
containerFs: false,
expectedContainerFsHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1.5Gi"),
},
},
expectedContainerFsSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("3Gi"),
},
},
expectedContainerFsINodesHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
expectedContainerFsINodesSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
thresholdList: []evictionapi.Threshold{
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1.5Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1.5Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("3Gi"),
},
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
},
},
{
description: "container and image are separate",
imageFs: true,
containerFs: true,
expectedContainerFsHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsINodesHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsINodesSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
thresholdList: []evictionapi.Threshold{
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1.5Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("3Gi"),
},
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
},
},
{
description: "single filesystem; existing containerfsstats",
imageFs: false,
containerFs: false,
expectErr: true,
expectedContainerFsHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsINodesHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsINodesSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
thresholdList: []evictionapi.Threshold{
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
},
},
},
{
description: "image filesystem; expect error",
imageFs: true,
containerFs: false,
expectErr: true,
expectedContainerFsHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1.5Gi"),
},
},
expectedContainerFsSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("3Gi"),
},
},
expectedContainerFsINodesHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
expectedContainerFsINodesSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
thresholdList: []evictionapi.Threshold{
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1.5Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1.5Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("3Gi"),
},
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
},
},
},
{
description: "container and image are separate; expect error",
imageFs: true,
containerFs: true,
expectErr: true,
expectedContainerFsHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsINodesHard: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
expectedContainerFsINodesSoft: evictionapi.Threshold{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
thresholdList: []evictionapi.Threshold{
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("200Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalNodeFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("100Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("1.5Gi"),
},
},
{
Signal: evictionapi.SignalImageFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("3Gi"),
},
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("150Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalImageFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("300Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("2Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsAvailable,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
GracePeriod: gracePeriod,
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
},
{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
GracePeriod: gracePeriod,
},
{
Signal: evictionapi.SignalContainerFsInodesFree,
Operator: evictionapi.OpLessThan,
Value: evictionapi.ThresholdValue{
Quantity: quantityMustParse("500Mi"),
},
MinReclaim: &evictionapi.ThresholdValue{
Quantity: quantityMustParse("5Gi"),
},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.description, func(t *testing.T) {
expected, err := UpdateContainerFsThresholds(testCase.thresholdList, testCase.imageFs, testCase.containerFs)
if err != nil && !testCase.expectErr {
t.Fatalf("got error but did not expect any")
}
hardContainerFsMatch := -1
softContainerFsMatch := -1
hardContainerFsINodesMatch := -1
softContainerFsINodesMatch := -1
for idx, val := range expected {
if val.Signal == evictionapi.SignalContainerFsAvailable && isHardEvictionThreshold(val) {
if !reflect.DeepEqual(val, testCase.expectedContainerFsHard) {
t.Fatalf("want %v got %v", testCase.expectedContainerFsHard, val)
}
hardContainerFsMatch = idx
}
if val.Signal == evictionapi.SignalContainerFsAvailable && !isHardEvictionThreshold(val) {
if !reflect.DeepEqual(val, testCase.expectedContainerFsSoft) {
t.Fatalf("want %v got %v", testCase.expectedContainerFsSoft, val)
}
softContainerFsMatch = idx
}
if val.Signal == evictionapi.SignalContainerFsInodesFree && isHardEvictionThreshold(val) {
if !reflect.DeepEqual(val, testCase.expectedContainerFsINodesHard) {
t.Fatalf("want %v got %v", testCase.expectedContainerFsINodesHard, val)
}
hardContainerFsINodesMatch = idx
}
if val.Signal == evictionapi.SignalContainerFsInodesFree && !isHardEvictionThreshold(val) {
if !reflect.DeepEqual(val, testCase.expectedContainerFsINodesSoft) {
t.Fatalf("want %v got %v", testCase.expectedContainerFsINodesSoft, val)
}
softContainerFsINodesMatch = idx
}
}
if hardContainerFsMatch == -1 {
t.Fatalf("did not find hard containerfs.available")
}
if softContainerFsMatch == -1 {
t.Fatalf("did not find soft containerfs.available")
}
if hardContainerFsINodesMatch == -1 {
t.Fatalf("did not find hard containerfs.inodesfree")
}
if softContainerFsINodesMatch == -1 {
t.Fatalf("did not find soft containerfs.inodesfree")
}
})
}
}
// newPodInodeStats returns stats with specified usage amounts.
func newPodInodeStats(pod *v1.Pod, rootFsInodesUsed, logsInodesUsed, perLocalVolumeInodesUsed resource.Quantity) statsapi.PodStats {
result := statsapi.PodStats{
PodRef: statsapi.PodReference{
Name: pod.Name, Namespace: pod.Namespace, UID: string(pod.UID),
},
}
rootFsUsed := uint64(rootFsInodesUsed.Value())
logsUsed := uint64(logsInodesUsed.Value())
for range pod.Spec.Containers {
result.Containers = append(result.Containers, statsapi.ContainerStats{
Rootfs: &statsapi.FsStats{
InodesUsed: &rootFsUsed,
},
Logs: &statsapi.FsStats{
InodesUsed: &logsUsed,
},
})
}
perLocalVolumeUsed := uint64(perLocalVolumeInodesUsed.Value())
for _, volumeName := range localVolumeNames(pod) {
result.VolumeStats = append(result.VolumeStats, statsapi.VolumeStats{
Name: volumeName,
FsStats: statsapi.FsStats{
InodesUsed: &perLocalVolumeUsed,
},
})
}
return result
}
// newPodDiskStats returns stats with specified usage amounts.
func newPodDiskStats(pod *v1.Pod, rootFsUsed, logsUsed, perLocalVolumeUsed resource.Quantity) statsapi.PodStats {
result := statsapi.PodStats{
PodRef: statsapi.PodReference{
Name: pod.Name, Namespace: pod.Namespace, UID: string(pod.UID),
},
}
rootFsUsedBytes := uint64(rootFsUsed.Value())
logsUsedBytes := uint64(logsUsed.Value())
for range pod.Spec.Containers {
result.Containers = append(result.Containers, statsapi.ContainerStats{
Rootfs: &statsapi.FsStats{
UsedBytes: &rootFsUsedBytes,
},
Logs: &statsapi.FsStats{
UsedBytes: &logsUsedBytes,
},
})
}
perLocalVolumeUsedBytes := uint64(perLocalVolumeUsed.Value())
for _, volumeName := range localVolumeNames(pod) {
result.VolumeStats = append(result.VolumeStats, statsapi.VolumeStats{
Name: volumeName,
FsStats: statsapi.FsStats{
UsedBytes: &perLocalVolumeUsedBytes,
},
})
}
return result
}
func newPodMemoryStats(pod *v1.Pod, workingSet resource.Quantity) statsapi.PodStats {
workingSetBytes := uint64(workingSet.Value())
return statsapi.PodStats{
PodRef: statsapi.PodReference{
Name: pod.Name, Namespace: pod.Namespace, UID: string(pod.UID),
},
Memory: &statsapi.MemoryStats{
WorkingSetBytes: &workingSetBytes,
},
VolumeStats: []statsapi.VolumeStats{
{
FsStats: statsapi.FsStats{
UsedBytes: &workingSetBytes,
},
Name: "local-volume",
},
},
Containers: []statsapi.ContainerStats{
{
Name: pod.Name,
Logs: &statsapi.FsStats{
UsedBytes: &workingSetBytes,
},
Rootfs: &statsapi.FsStats{UsedBytes: &workingSetBytes},
},
},
EphemeralStorage: &statsapi.FsStats{UsedBytes: &workingSetBytes},
}
}
func newPodProcessStats(pod *v1.Pod, num uint64) statsapi.PodStats {
return statsapi.PodStats{
PodRef: statsapi.PodReference{
Name: pod.Name, Namespace: pod.Namespace, UID: string(pod.UID),
},
ProcessStats: &statsapi.ProcessStats{
ProcessCount: &num,
},
}
}
func newResourceList(cpu, memory, disk string) v1.ResourceList {
res := v1.ResourceList{}
if cpu != "" {
res[v1.ResourceCPU] = resource.MustParse(cpu)
}
if memory != "" {
res[v1.ResourceMemory] = resource.MustParse(memory)
}
if disk != "" {
res[v1.ResourceEphemeralStorage] = resource.MustParse(disk)
}
return res
}
func newResourceRequirements(requests, limits v1.ResourceList) v1.ResourceRequirements {
res := v1.ResourceRequirements{}
res.Requests = requests
res.Limits = limits
return res
}
func newContainer(name string, requests v1.ResourceList, limits v1.ResourceList) v1.Container {
return v1.Container{
Name: name,
Resources: newResourceRequirements(requests, limits),
}
}
func newVolume(name string, volumeSource v1.VolumeSource) v1.Volume {
return v1.Volume{
Name: name,
VolumeSource: volumeSource,
}
}
// newPod uses the name as the uid. Make names unique for testing.
func newPod(name string, priority int32, containers []v1.Container, volumes []v1.Volume) *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
UID: types.UID(name),
},
Spec: v1.PodSpec{
Containers: containers,
Volumes: volumes,
Priority: &priority,
},
}
}
// nodeConditionList is a simple alias to support equality checking independent of order
type nodeConditionList []v1.NodeConditionType
// Equal adds the ability to check equality between two lists of node conditions.
func (s1 nodeConditionList) Equal(s2 nodeConditionList) bool {
if len(s1) != len(s2) {
return false
}
for _, item := range s1 {
if !hasNodeCondition(s2, item) {
return false
}
}
return true
}
// thresholdList is a simple alias to support equality checking independent of order
type thresholdList []evictionapi.Threshold
// Equal adds the ability to check equality between two lists of node conditions.
func (s1 thresholdList) Equal(s2 thresholdList) bool {
if len(s1) != len(s2) {
return false
}
for _, item := range s1 {
if !hasThreshold(s2, item) {
return false
}
}
return true
}
func TestEvictonMessageWithResourceResize(t *testing.T) {
testpod := newPod("testpod", 1, []v1.Container{
newContainer("testcontainer", newResourceList("", "200Mi", ""), newResourceList("", "", "")),
}, nil)
testpod.Status = v1.PodStatus{
ContainerStatuses: []v1.ContainerStatus{
{
Name: "testcontainer",
AllocatedResources: newResourceList("", "100Mi", ""),
},
},
}
testpodMemory := resource.MustParse("150Mi")
testpodStats := newPodMemoryStats(testpod, testpodMemory)
testpodMemoryBytes := uint64(testpodMemory.Value())
testpodStats.Containers = []statsapi.ContainerStats{
{
Name: "testcontainer",
Memory: &statsapi.MemoryStats{
WorkingSetBytes: &testpodMemoryBytes,
},
},
}
stats := map[*v1.Pod]statsapi.PodStats{
testpod: testpodStats,
}
statsFn := func(pod *v1.Pod) (statsapi.PodStats, bool) {
result, found := stats[pod]
return result, found
}
threshold := []evictionapi.Threshold{}
observations := signalObservations{}
for _, enabled := range []bool{true, false} {
t.Run(fmt.Sprintf("InPlacePodVerticalScaling enabled=%v", enabled), func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, enabled)()
msg, _ := evictionMessage(v1.ResourceMemory, testpod, statsFn, threshold, observations)
if enabled {
if !strings.Contains(msg, "testcontainer was using 150Mi, request is 100Mi") {
t.Errorf("Expected 'exceeds memory' eviction message was not found.")
}
} else {
if strings.Contains(msg, "which exceeds its request") {
t.Errorf("Found 'exceeds memory' eviction message which was not expected.")
}
}
})
}
}