Files
kubernetes/pkg/kubelet/allocation/allocation_manager_test.go
Kubernetes Prow Robot 9a5193796b Merge pull request #134825 from natasha41575/resize-events
[InPlacePodVerticalScaling] emit more events when the pod resize status changes
2025-11-05 15:56:50 -08:00

1911 lines
73 KiB
Go

/*
Copyright 2025 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 allocation
import (
"context"
"fmt"
goruntime "runtime"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
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"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/record"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/testutil"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/kubelet/allocation/state"
"k8s.io/kubernetes/pkg/kubelet/cm"
"k8s.io/kubernetes/pkg/kubelet/cm/cpumanager"
"k8s.io/kubernetes/pkg/kubelet/cm/memorymanager"
"k8s.io/kubernetes/pkg/kubelet/config"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
containertest "k8s.io/kubernetes/pkg/kubelet/container/testing"
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
"k8s.io/kubernetes/pkg/kubelet/metrics"
kubepod "k8s.io/kubernetes/pkg/kubelet/pod"
"k8s.io/kubernetes/pkg/kubelet/status"
statustest "k8s.io/kubernetes/pkg/kubelet/status/testing"
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
kubeletutil "k8s.io/kubernetes/pkg/kubelet/util"
_ "k8s.io/kubernetes/pkg/volume/hostpath"
"k8s.io/utils/ptr"
)
func TestUpdatePodFromAllocation(t *testing.T) {
containerRestartPolicyAlways := v1.ContainerRestartPolicyAlways
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
UID: "12345",
Name: "test",
Namespace: "default",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI),
v1.ResourceMemory: *resource.NewQuantity(200, resource.DecimalSI),
},
Limits: v1.ResourceList{
v1.ResourceCPU: *resource.NewMilliQuantity(300, resource.DecimalSI),
v1.ResourceMemory: *resource.NewQuantity(400, resource.DecimalSI),
},
},
},
{
Name: "c2",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI),
v1.ResourceMemory: *resource.NewQuantity(600, resource.DecimalSI),
},
Limits: v1.ResourceList{
v1.ResourceCPU: *resource.NewMilliQuantity(700, resource.DecimalSI),
v1.ResourceMemory: *resource.NewQuantity(800, resource.DecimalSI),
},
},
},
},
InitContainers: []v1.Container{
{
Name: "c1-restartable-init",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: *resource.NewMilliQuantity(200, resource.DecimalSI),
v1.ResourceMemory: *resource.NewQuantity(300, resource.DecimalSI),
},
Limits: v1.ResourceList{
v1.ResourceCPU: *resource.NewMilliQuantity(400, resource.DecimalSI),
v1.ResourceMemory: *resource.NewQuantity(500, resource.DecimalSI),
},
},
RestartPolicy: &containerRestartPolicyAlways,
},
{
Name: "c1-init",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI),
v1.ResourceMemory: *resource.NewQuantity(600, resource.DecimalSI),
},
Limits: v1.ResourceList{
v1.ResourceCPU: *resource.NewMilliQuantity(700, resource.DecimalSI),
v1.ResourceMemory: *resource.NewQuantity(800, resource.DecimalSI),
},
},
},
},
},
}
resizedPod := pod.DeepCopy()
resizedPod.Spec.Containers[0].Resources.Requests[v1.ResourceCPU] = *resource.NewMilliQuantity(200, resource.DecimalSI)
resizedPod.Spec.InitContainers[0].Resources.Requests[v1.ResourceCPU] = *resource.NewMilliQuantity(300, resource.DecimalSI)
tests := []struct {
name string
pod *v1.Pod
allocated state.PodResourceInfo
expectPod *v1.Pod
expectUpdate bool
}{{
name: "steady state",
pod: pod,
allocated: state.PodResourceInfo{
ContainerResources: map[string]v1.ResourceRequirements{
"c1": *pod.Spec.Containers[0].Resources.DeepCopy(),
"c2": *pod.Spec.Containers[1].Resources.DeepCopy(),
"c1-restartable-init": *pod.Spec.InitContainers[0].Resources.DeepCopy(),
"c1-init": *pod.Spec.InitContainers[1].Resources.DeepCopy(),
},
},
expectUpdate: false,
}, {
name: "no allocations",
pod: pod,
allocated: state.PodResourceInfo{},
expectUpdate: false,
}, {
name: "missing container allocation",
pod: pod,
allocated: state.PodResourceInfo{
ContainerResources: map[string]v1.ResourceRequirements{
"c2": *pod.Spec.Containers[1].Resources.DeepCopy(),
},
},
expectUpdate: false,
}, {
name: "resized container",
pod: pod,
allocated: state.PodResourceInfo{
ContainerResources: map[string]v1.ResourceRequirements{
"c1": *resizedPod.Spec.Containers[0].Resources.DeepCopy(),
"c2": *resizedPod.Spec.Containers[1].Resources.DeepCopy(),
"c1-restartable-init": *resizedPod.Spec.InitContainers[0].Resources.DeepCopy(),
"c1-init": *resizedPod.Spec.InitContainers[1].Resources.DeepCopy(),
},
},
expectUpdate: true,
expectPod: resizedPod,
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pod := test.pod.DeepCopy()
allocatedPod, updated := updatePodFromAllocation(pod, test.allocated)
if test.expectUpdate {
assert.True(t, updated, "updated")
assert.Equal(t, test.expectPod, allocatedPod)
assert.NotEqual(t, pod, allocatedPod)
} else {
assert.False(t, updated, "updated")
assert.Same(t, pod, allocatedPod)
}
})
}
}
func TestRetryPendingResizes(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
}
metrics.Register()
metrics.PodInfeasibleResizes.Reset()
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true)
containerRestartPolicyAlways := v1.ContainerRestartPolicyAlways
cpu2m := resource.MustParse("2m")
cpu500m := resource.MustParse("500m")
cpu1000m := resource.MustParse("1")
cpu1500m := resource.MustParse("1500m")
cpu2500m := resource.MustParse("2500m")
cpu5000m := resource.MustParse("5000m")
mem500M := resource.MustParse("500Mi")
mem1000M := resource.MustParse("1Gi")
mem1500M := resource.MustParse("1500Mi")
mem2500M := resource.MustParse("2500Mi")
mem4500M := resource.MustParse("4500Mi")
testPod1 := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
UID: "1111",
Name: "pod1",
Namespace: "ns1",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "c1",
Image: "i1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
},
},
},
},
Status: v1.PodStatus{
Phase: v1.PodRunning,
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
AllocatedResources: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
Resources: &v1.ResourceRequirements{},
},
},
},
}
testPod2 := testPod1.DeepCopy()
testPod2.UID = "2222"
testPod2.Name = "pod2"
testPod2.Namespace = "ns2"
testPod2.Spec = v1.PodSpec{
InitContainers: []v1.Container{
{
Name: "c1-init",
Image: "i1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
},
RestartPolicy: &containerRestartPolicyAlways,
},
},
}
testPod2.Status = v1.PodStatus{
Phase: v1.PodRunning,
InitContainerStatuses: []v1.ContainerStatus{
{
Name: "c1-init",
AllocatedResources: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
Resources: &v1.ResourceRequirements{},
},
},
}
testPod3 := testPod1.DeepCopy()
testPod3.UID = "3333"
testPod3.Name = "pod3"
testPod3.Namespace = "ns2"
const (
originalInProgressMsg = "original in-progress"
originalPendingMsg = "original pending"
)
tests := []struct {
name string
originalRequests v1.ResourceList
newRequests v1.ResourceList
originalLimits v1.ResourceList
newLimits v1.ResourceList
originalInProgress bool // Whether to prepopulate an in-progress condition.
originalPending bool // Whether to prepopulate a pending condition.
newResourcesAllocated bool // Whether the new requests have already been allocated (but not actuated)
expectedAllocatedReqs v1.ResourceList
expectedAllocatedLims v1.ResourceList
expectedResize []*v1.PodCondition
expectPodSyncTriggered string
annotations map[string]string
}{
{
name: "Request CPU and memory decrease - expect InProgress",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
Message: "", // Expect the original InProgress condition to be cleared.
},
},
expectPodSyncTriggered: "true",
},
{
name: "Request CPU increase, memory decrease - expect InProgress",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem500M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Request CPU decrease, memory increase - expect InProgress",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem1500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem1500M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Request CPU and memory increase beyond current capacity - expect Deferred",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu2500m, v1.ResourceMemory: mem2500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Deferred",
Message: "Node didn't have enough resource: cpu",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Request CPU decrease and memory increase beyond current capacity - expect Deferred",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem2500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Deferred",
Message: "Node didn't have enough resource: memory",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Request memory increase beyond node capacity - expect Infeasible",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem4500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Infeasible",
Message: "Node didn't have enough capacity: memory, requested: 4718592000, capacity: 4294967296",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Request CPU increase beyond node capacity - expect Infeasible",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu5000m, v1.ResourceMemory: mem1000M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Infeasible",
Message: "Node didn't have enough capacity: cpu, requested: 5000, capacity: 4000",
},
},
expectPodSyncTriggered: "true",
},
{
name: "CPU increase in progress, resources unchanged - expect InProgress",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem1000M},
newResourcesAllocated: true,
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem1000M},
originalInProgress: true,
originalPending: true,
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
Message: originalInProgressMsg, // Since the allocation didn't change, the condition isn't reset.
},
},
// Should trigger a sync since the conditions changed (to update status), even though
// the allocated resources are unchanged.
expectPodSyncTriggered: "true",
},
{
name: "CPU increase in progress, new allocation - expect InProgress reset",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem1000M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem1000M},
originalInProgress: true,
originalPending: true,
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
Message: "",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Deferred resize doesn't clear in-progress status",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem2500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
originalInProgress: true,
originalPending: true,
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Deferred",
Message: "Node didn't have enough resource: memory", // Deferred condition should be updated.
},
{
Type: v1.PodResizeInProgress,
Status: "True",
Message: originalInProgressMsg, // InProgress condition should be unchanged.
},
},
expectPodSyncTriggered: "true",
},
{
name: "No resize",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedResize: nil,
},
{
name: "static pod, expect Infeasible",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
annotations: map[string]string{kubetypes.ConfigSourceAnnotationKey: kubetypes.FileSource},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Infeasible",
Message: "In-place resize of static-pods is not supported",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Increase CPU from min shares",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu2m},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Decrease CPU to min shares",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m},
newRequests: v1.ResourceList{v1.ResourceCPU: cpu2m},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu2m},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Increase CPU from min limit",
originalRequests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("10m")},
originalLimits: v1.ResourceList{v1.ResourceCPU: resource.MustParse("10m")},
newRequests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("10m")}, // Unchanged
newLimits: v1.ResourceList{v1.ResourceCPU: resource.MustParse("20m")},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: resource.MustParse("10m")},
expectedAllocatedLims: v1.ResourceList{v1.ResourceCPU: resource.MustParse("20m")},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
},
},
expectPodSyncTriggered: "true",
},
{
name: "Decrease CPU to min limit",
originalRequests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("10m")},
originalLimits: v1.ResourceList{v1.ResourceCPU: resource.MustParse("20m")},
newRequests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("10m")}, // Unchanged
newLimits: v1.ResourceList{v1.ResourceCPU: resource.MustParse("10m")},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: resource.MustParse("10m")},
expectedAllocatedLims: v1.ResourceList{v1.ResourceCPU: resource.MustParse("10m")},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
},
},
expectPodSyncTriggered: "true",
},
}
for _, tt := range tests {
for _, isSidecarContainer := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/sidecar=%t", tt.name, isSidecarContainer), func(t *testing.T) {
var originalPod *v1.Pod
var originalCtr *v1.Container
if isSidecarContainer {
originalPod = testPod2.DeepCopy()
originalCtr = &originalPod.Spec.InitContainers[0]
} else {
originalPod = testPod1.DeepCopy()
originalCtr = &originalPod.Spec.Containers[0]
}
originalPod.Annotations = tt.annotations
originalCtr.Resources.Requests = tt.originalRequests
originalCtr.Resources.Limits = tt.originalLimits
newPod := originalPod.DeepCopy()
if isSidecarContainer {
newPod.Spec.InitContainers[0].Resources.Requests = tt.newRequests
newPod.Spec.InitContainers[0].Resources.Limits = tt.newLimits
} else {
newPod.Spec.Containers[0].Resources.Requests = tt.newRequests
newPod.Spec.Containers[0].Resources.Limits = tt.newLimits
}
podStatus := &kubecontainer.PodStatus{
ID: originalPod.UID,
Name: originalPod.Name,
Namespace: originalPod.Namespace,
}
podStatus.ContainerStatuses = make([]*kubecontainer.Status, len(originalPod.Spec.Containers)+len(originalPod.Spec.InitContainers))
for i, c := range originalPod.Spec.InitContainers {
setContainerStatus(podStatus, &c, i)
}
for i, c := range originalPod.Spec.Containers {
setContainerStatus(podStatus, &c, i+len(originalPod.Spec.InitContainers))
}
allocationManager := makeAllocationManager(t, &containertest.FakeRuntime{PodStatus: *podStatus}, []*v1.Pod{testPod1, testPod2, testPod3}, nil)
if !tt.newResourcesAllocated {
require.NoError(t, allocationManager.SetAllocatedResources(originalPod))
} else {
require.NoError(t, allocationManager.SetAllocatedResources(newPod))
}
t.Cleanup(func() { allocationManager.RemovePod(originalPod.UID) })
if tt.originalInProgress {
allocationManager.(*manager).statusManager.SetPodResizeInProgressCondition(originalPod.UID, "", originalInProgressMsg, 0)
}
if tt.originalPending {
allocationManager.(*manager).statusManager.SetPodResizePendingCondition(originalPod.UID, v1.PodReasonDeferred, originalPendingMsg, 0)
}
allocationManager.(*manager).getPodByUID = func(uid types.UID) (*v1.Pod, bool) {
return newPod, true
}
allocationManager.PushPendingResize(originalPod.UID)
allocationManager.RetryPendingResizes(TriggerReasonPodUpdated)
var updatedPod *v1.Pod
if allocationManager.(*manager).statusManager.IsPodResizeInfeasible(newPod.UID) || allocationManager.(*manager).statusManager.IsPodResizeDeferred(newPod.UID) {
updatedPod = originalPod
} else {
updatedPod = newPod
}
var updatedPodCtr v1.Container
if isSidecarContainer {
updatedPodCtr = updatedPod.Spec.InitContainers[0]
} else {
updatedPodCtr = updatedPod.Spec.Containers[0]
}
assert.Equal(t, tt.expectedAllocatedReqs, updatedPodCtr.Resources.Requests, "updated pod spec requests")
assert.Equal(t, tt.expectedAllocatedLims, updatedPodCtr.Resources.Limits, "updated pod spec limits")
alloc, found := allocationManager.GetContainerResourceAllocation(newPod.UID, updatedPodCtr.Name)
require.True(t, found, "container allocation")
assert.Equal(t, tt.expectedAllocatedReqs, alloc.Requests, "stored container request allocation")
assert.Equal(t, tt.expectedAllocatedLims, alloc.Limits, "stored container limit allocation")
resizeStatus := allocationManager.(*manager).statusManager.GetPodResizeConditions(newPod.UID)
if assert.Len(t, resizeStatus, len(tt.expectedResize), "different number of resize conditions") {
for i := range resizeStatus {
// Ignore probe time and last transition time during comparison.
resizeStatus[i].LastProbeTime = metav1.Time{}
resizeStatus[i].LastTransitionTime = metav1.Time{}
// Message is a substring assertion, since it can change slightly.
if tt.expectedResize[i].Message != "" {
assert.Contains(t, resizeStatus[i].Message, tt.expectedResize[i].Message)
} else {
assert.Empty(t, resizeStatus[i].Message)
}
resizeStatus[i].Message = tt.expectedResize[i].Message
}
assert.Equal(t, tt.expectedResize, resizeStatus)
}
assert.Equal(t, tt.expectPodSyncTriggered, newPod.Annotations["pod-sync-triggered"], "pod sync annotation should be set")
})
}
}
expectedMetrics := `
# HELP kubelet_pod_infeasible_resizes_total [ALPHA] Number of infeasible resizes for pods.
# TYPE kubelet_pod_infeasible_resizes_total counter
kubelet_pod_infeasible_resizes_total{reason_detail="insufficient_node_allocatable"} 4
kubelet_pod_infeasible_resizes_total{reason_detail="static_pod"} 2
`
assert.NoError(t, testutil.GatherAndCompare(
legacyregistry.DefaultGatherer, strings.NewReader(expectedMetrics), "kubelet_pod_infeasible_resizes_total",
))
}
func TestRetryPendingResizesGuanteedQOSPods(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
}
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true)
nodeConfig := cm.NodeConfig{}
nodeConfig.CPUManagerPolicy = string(cpumanager.PolicyStatic)
nodeConfig.MemoryManagerPolicy = string(memorymanager.PolicyTypeStatic)
createTestPod := func(uid types.UID, name, namespace string, req, lim v1.ResourceList, isSidecar bool) *v1.Pod {
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{UID: uid, Name: name, Namespace: namespace},
Status: v1.PodStatus{Phase: v1.PodRunning},
}
if isSidecar {
restartPolicy := v1.ContainerRestartPolicyAlways
pod.Spec.InitContainers = []v1.Container{
{
Name: "c1",
Image: "test-image",
Resources: v1.ResourceRequirements{Requests: req, Limits: lim},
RestartPolicy: &restartPolicy,
},
}
pod.Status.InitContainerStatuses = []v1.ContainerStatus{{Name: "c1", AllocatedResources: req}}
} else {
pod.Spec.Containers = []v1.Container{
{
Name: "c1",
Image: "test-image",
Resources: v1.ResourceRequirements{Requests: req, Limits: lim},
},
}
pod.Status.ContainerStatuses = []v1.ContainerStatus{{Name: "c1", AllocatedResources: req}}
}
return pod
}
cpu500m := resource.MustParse("500m")
cpu1000m := resource.MustParse("1")
cpu2000m := resource.MustParse("2")
mem500M := resource.MustParse("500Mi")
mem1000M := resource.MustParse("1Gi")
mem2000M := resource.MustParse("2Gi")
// Set requests and limits to be the same for guaranteed QOS pods
guaranteedQOSPod := createTestPod("1111", "pod1", "ns1", v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M}, v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M}, false)
guaranteedQOSPodWithSidecar := createTestPod("2222", "pod2", "ns2", v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M}, v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M}, true)
// Set requests and limits to be different for best effort pods
bestEffortPod := createTestPod("333", "pod3", "ns3", v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M}, v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M}, false)
bestEffortPodWithSidecar := createTestPod("444", "pod4", "ns4", v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M}, v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M}, true)
tests := []struct {
name string
originalRequests v1.ResourceList
newRequests v1.ResourceList
originalLimits v1.ResourceList
podsToTest []*v1.Pod
newLimits v1.ResourceList
expectedAllocatedReqs v1.ResourceList
expectedAllocatedLims v1.ResourceList
expectedResize []*v1.PodCondition
ipprExclusiveCPUsFeatureGate bool
ipprExclusiveMemoryFeatureGate bool
}{
{
name: "Guaranteed QOS pod - CPU resize with `InPlacePodVerticalScalingExclusiveCPUs`feature gate off and only CPU resize. Infeasible due to static cpu policy",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
originalLimits: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
podsToTest: []*v1.Pod{guaranteedQOSPod, guaranteedQOSPodWithSidecar},
ipprExclusiveCPUsFeatureGate: false,
ipprExclusiveMemoryFeatureGate: true,
newRequests: v1.ResourceList{v1.ResourceCPU: cpu2000m, v1.ResourceMemory: mem1000M},
newLimits: v1.ResourceList{v1.ResourceCPU: cpu2000m, v1.ResourceMemory: mem1000M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedAllocatedLims: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Infeasible",
Message: "Resize is infeasible for Guaranteed Pods alongside CPU Manager policy \"static\"",
},
},
},
{
name: "Guaranteed QOS pod - Memory resize with `InPlacePodVerticalScalingExclusiveMemory` feature gates off and only memory resize. Infeasible due to static memory policy",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
originalLimits: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
podsToTest: []*v1.Pod{guaranteedQOSPod, guaranteedQOSPodWithSidecar},
ipprExclusiveCPUsFeatureGate: true,
ipprExclusiveMemoryFeatureGate: false,
newRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem2000M},
newLimits: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem2000M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedAllocatedLims: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Infeasible",
Message: "Resize is infeasible for Guaranteed Pods alongside Memory Manager policy \"Static\"",
},
},
},
{
name: "Guaranteed QOS pod - CPU and Memory resize with both feature gates off. Infeasible due to static cpu policy",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
originalLimits: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
podsToTest: []*v1.Pod{guaranteedQOSPod, guaranteedQOSPodWithSidecar},
ipprExclusiveCPUsFeatureGate: false,
ipprExclusiveMemoryFeatureGate: false,
newRequests: v1.ResourceList{v1.ResourceCPU: cpu2000m, v1.ResourceMemory: mem2000M},
newLimits: v1.ResourceList{v1.ResourceCPU: cpu2000m, v1.ResourceMemory: mem2000M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedAllocatedLims: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Infeasible",
Message: "Resize is infeasible for Guaranteed Pods alongside CPU Manager policy \"static\"",
},
},
},
{
name: "BestEffortPod - CPU and Memory limit resize. Resize in progess. Static CPU and Memory policy feature gate settings should not affect best effort pods",
originalRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
originalLimits: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
podsToTest: []*v1.Pod{bestEffortPod, bestEffortPodWithSidecar},
ipprExclusiveCPUsFeatureGate: false,
ipprExclusiveMemoryFeatureGate: false,
newRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
newLimits: v1.ResourceList{v1.ResourceCPU: cpu2000m, v1.ResourceMemory: mem1000M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
expectedAllocatedLims: v1.ResourceList{v1.ResourceCPU: cpu2000m, v1.ResourceMemory: mem1000M},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
},
},
},
}
for _, tt := range tests {
for _, originalPod := range tt.podsToTest {
isSidecarContainer := len(originalPod.Spec.InitContainers) > 0
t.Run(fmt.Sprintf("%s/sidecar=%t", tt.name, isSidecarContainer), func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.InPlacePodVerticalScalingExclusiveCPUs: tt.ipprExclusiveCPUsFeatureGate,
features.InPlacePodVerticalScalingExclusiveMemory: tt.ipprExclusiveMemoryFeatureGate,
})
var originalCtr *v1.Container
if isSidecarContainer {
originalCtr = &originalPod.Spec.InitContainers[0]
} else {
originalCtr = &originalPod.Spec.Containers[0]
}
originalCtr.Resources.Requests = tt.originalRequests
originalCtr.Resources.Limits = tt.originalLimits
newPod := originalPod.DeepCopy()
if isSidecarContainer {
newPod.Spec.InitContainers[0].Resources.Requests = tt.newRequests
newPod.Spec.InitContainers[0].Resources.Limits = tt.newLimits
} else {
newPod.Spec.Containers[0].Resources.Requests = tt.newRequests
newPod.Spec.Containers[0].Resources.Limits = tt.newLimits
}
podStatus := &kubecontainer.PodStatus{
ID: originalPod.UID,
Name: originalPod.Name,
Namespace: originalPod.Namespace,
}
setContainerStatus := func(podStatus *kubecontainer.PodStatus, c *v1.Container, idx int) {
podStatus.ContainerStatuses[idx] = &kubecontainer.Status{
Name: c.Name,
State: kubecontainer.ContainerStateRunning,
Resources: &kubecontainer.ContainerResources{
CPURequest: c.Resources.Requests.Cpu(),
CPULimit: c.Resources.Limits.Cpu(),
MemoryLimit: c.Resources.Limits.Memory(),
},
}
}
podStatus.ContainerStatuses = make([]*kubecontainer.Status, len(originalPod.Spec.Containers)+len(originalPod.Spec.InitContainers))
for i, c := range originalPod.Spec.InitContainers {
setContainerStatus(podStatus, &c, i)
}
for i, c := range originalPod.Spec.Containers {
setContainerStatus(podStatus, &c, i+len(originalPod.Spec.InitContainers))
}
allocationManager := makeAllocationManager(t, &containertest.FakeRuntime{PodStatus: *podStatus}, []*v1.Pod{guaranteedQOSPod, guaranteedQOSPodWithSidecar, bestEffortPod}, &nodeConfig)
require.NoError(t, allocationManager.SetAllocatedResources(originalPod))
t.Cleanup(func() { allocationManager.RemovePod(originalPod.UID) })
allocationManager.(*manager).getPodByUID = func(uid types.UID) (*v1.Pod, bool) {
return newPod, true
}
allocationManager.PushPendingResize(originalPod.UID)
allocationManager.RetryPendingResizes(TriggerReasonPodUpdated)
var updatedPod *v1.Pod
if allocationManager.(*manager).statusManager.IsPodResizeInfeasible(newPod.UID) || allocationManager.(*manager).statusManager.IsPodResizeDeferred(newPod.UID) {
updatedPod = originalPod
} else {
updatedPod = newPod
}
var updatedPodCtr v1.Container
if isSidecarContainer {
updatedPodCtr = updatedPod.Spec.InitContainers[0]
} else {
updatedPodCtr = updatedPod.Spec.Containers[0]
}
assert.Equal(t, tt.expectedAllocatedReqs, updatedPodCtr.Resources.Requests, "updated pod spec requests")
assert.Equal(t, tt.expectedAllocatedLims, updatedPodCtr.Resources.Limits, "updated pod spec limits")
alloc, found := allocationManager.GetContainerResourceAllocation(newPod.UID, updatedPodCtr.Name)
require.True(t, found, "container allocation")
assert.Equal(t, tt.expectedAllocatedReqs, alloc.Requests, "stored container request allocation")
assert.Equal(t, tt.expectedAllocatedLims, alloc.Limits, "stored container limit allocation")
resizeStatus := allocationManager.(*manager).statusManager.GetPodResizeConditions(newPod.UID)
for i := range resizeStatus {
// Ignore probe time and last transition time during comparison.
resizeStatus[i].LastProbeTime = metav1.Time{}
resizeStatus[i].LastTransitionTime = metav1.Time{}
// Message is a substring assertion, since it can change slightly.
assert.Contains(t, resizeStatus[i].Message, tt.expectedResize[i].Message)
resizeStatus[i].Message = tt.expectedResize[i].Message
}
assert.Equal(t, tt.expectedResize, resizeStatus)
})
}
}
}
func TestRetryPendingResizesWithSwap(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
}
metrics.Register()
metrics.PodInfeasibleResizes.Reset()
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.InPlacePodVerticalScaling: true,
features.NodeSwap: true,
})
noSwapContainerName, swapContainerName := "test-container-noswap", "test-container-limitedswap"
cpu500m := resource.MustParse("500m")
cpu1000m := resource.MustParse("1")
mem500M := resource.MustParse("500Mi")
mem1000M := resource.MustParse("1Gi")
testPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
UID: "1111",
Name: "pod1",
Namespace: "ns1",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "c1",
Image: "i1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
},
},
},
},
Status: v1.PodStatus{
Phase: v1.PodRunning,
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
AllocatedResources: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
Resources: &v1.ResourceRequirements{},
},
},
},
}
tests := []struct {
name string
newRequests v1.ResourceList
expectedAllocatedReqs v1.ResourceList
resizePolicy v1.ContainerResizePolicy
swapBehavior kubetypes.SwapBehavior
expectedResize []*v1.PodCondition
expectedMetrics string
}{
{
name: "NoSwap Request Memory decrease ResizePolicy RestartContainer - expect InProgress",
newRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
swapBehavior: kubetypes.NoSwap,
resizePolicy: v1.ContainerResizePolicy{ResourceName: v1.ResourceMemory, RestartPolicy: v1.RestartContainer},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
},
},
},
{
name: "LimitedSwap Request Memory increase with ResizePolicy RestartContainer - expect InProgress",
newRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
swapBehavior: kubetypes.LimitedSwap,
resizePolicy: v1.ContainerResizePolicy{ResourceName: v1.ResourceMemory, RestartPolicy: v1.RestartContainer},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizeInProgress,
Status: "True",
},
},
},
{
name: "LimitedSwap Request Memory increase with ResizePolicy NotRequired - expect Infeasible",
newRequests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
expectedAllocatedReqs: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
swapBehavior: kubetypes.LimitedSwap,
resizePolicy: v1.ContainerResizePolicy{ResourceName: v1.ResourceMemory, RestartPolicy: v1.NotRequired},
expectedResize: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: "Infeasible",
Message: "In-place resize of containers with swap is not supported",
},
},
expectedMetrics: `
# HELP kubelet_pod_infeasible_resizes_total [ALPHA] Number of infeasible resizes for pods.
# TYPE kubelet_pod_infeasible_resizes_total counter
kubelet_pod_infeasible_resizes_total{reason_detail="swap_limitation"} 1
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
originalPod := testPod.DeepCopy()
originalPod.Spec.Containers[0].ResizePolicy = []v1.ContainerResizePolicy{tt.resizePolicy}
if tt.swapBehavior == kubetypes.NoSwap {
originalPod.Spec.Containers[0].Name = noSwapContainerName
} else {
originalPod.Spec.Containers[0].Name = swapContainerName
}
newPod := originalPod.DeepCopy()
newPod.Spec.Containers[0].Resources.Requests = tt.newRequests
podStatus := &kubecontainer.PodStatus{
ID: originalPod.UID,
Name: originalPod.Name,
Namespace: originalPod.Namespace,
}
podStatus.ContainerStatuses = make([]*kubecontainer.Status, len(originalPod.Spec.Containers))
for i, c := range originalPod.Spec.Containers {
setContainerStatus(podStatus, &c, i)
}
runtime := &containertest.FakeRuntime{
SwapBehavior: map[string]kubetypes.SwapBehavior{
noSwapContainerName: kubetypes.NoSwap,
swapContainerName: kubetypes.LimitedSwap,
},
PodStatus: *podStatus,
}
allocationManager := makeAllocationManager(t, runtime, []*v1.Pod{testPod}, nil)
require.NoError(t, allocationManager.SetAllocatedResources(originalPod))
t.Cleanup(func() { allocationManager.RemovePod(originalPod.UID) })
allocationManager.(*manager).getPodByUID = func(uid types.UID) (*v1.Pod, bool) {
return newPod, true
}
allocationManager.PushPendingResize(testPod.UID)
allocationManager.RetryPendingResizes(TriggerReasonPodUpdated)
var updatedPod *v1.Pod
if allocationManager.(*manager).statusManager.IsPodResizeInfeasible(newPod.UID) {
updatedPod = originalPod
} else {
updatedPod = newPod
}
updatedPodCtr := updatedPod.Spec.Containers[0]
assert.Equal(t, tt.expectedAllocatedReqs, updatedPodCtr.Resources.Requests, "updated pod spec requests")
alloc, found := allocationManager.GetContainerResourceAllocation(newPod.UID, updatedPodCtr.Name)
require.True(t, found, "container allocation")
assert.Equal(t, tt.expectedAllocatedReqs, alloc.Requests, "stored container request allocation")
resizeStatus := allocationManager.(*manager).statusManager.GetPodResizeConditions(newPod.UID)
for i := range resizeStatus {
// Ignore probe time and last transition time during comparison.
resizeStatus[i].LastProbeTime = metav1.Time{}
resizeStatus[i].LastTransitionTime = metav1.Time{}
assert.Contains(t, resizeStatus[i].Message, tt.expectedResize[i].Message)
resizeStatus[i].Message = tt.expectedResize[i].Message
}
assert.Equal(t, tt.expectedResize, resizeStatus)
assert.Equal(t, "true", newPod.Annotations["pod-sync-triggered"], "pod sync annotation should be set")
assert.NoError(t, testutil.GatherAndCompare(
legacyregistry.DefaultGatherer, strings.NewReader(tt.expectedMetrics), "kubelet_pod_infeasible_resizes_total",
))
})
}
}
func TestRetryPendingResizesMultipleConditions(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
}
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true)
cpu500m := resource.MustParse("500m")
cpu1000m := resource.MustParse("1")
cpu5000m := resource.MustParse("5000m")
mem500M := resource.MustParse("500Mi")
mem1000M := resource.MustParse("1Gi")
mem4500M := resource.MustParse("4500Mi")
testPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
UID: "1111",
Name: "pod",
Namespace: "ns",
Generation: 1,
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "c1",
Image: "i1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
},
},
},
},
Status: v1.PodStatus{
Phase: v1.PodRunning,
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
AllocatedResources: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
Resources: &v1.ResourceRequirements{},
},
},
},
}
podStatus := &kubecontainer.PodStatus{
ID: testPod.UID,
Name: testPod.Name,
Namespace: testPod.Namespace,
}
podStatus.ContainerStatuses = make([]*kubecontainer.Status, len(testPod.Spec.Containers)+len(testPod.Spec.InitContainers))
for i, c := range testPod.Spec.InitContainers {
setContainerStatus(podStatus, &c, i)
}
for i, c := range testPod.Spec.Containers {
setContainerStatus(podStatus, &c, i+len(testPod.Spec.InitContainers))
}
allocationManager := makeAllocationManager(t, &containertest.FakeRuntime{PodStatus: *podStatus}, []*v1.Pod{testPod}, nil)
require.NoError(t, allocationManager.SetAllocatedResources(testPod))
allocationManager.(*manager).getPodByUID = func(uid types.UID) (*v1.Pod, bool) {
return testPod, true
}
testCases := []struct {
name string
cpu resource.Quantity
mem resource.Quantity
generation int64
expectedConditions []*v1.PodCondition
expectedEvent string
}{
{
name: "allocated != actuated, pod resize should be in progress",
cpu: cpu1000m,
mem: mem1000M,
generation: 1,
expectedConditions: []*v1.PodCondition{{
Type: v1.PodResizeInProgress,
Status: "True",
ObservedGeneration: 1,
}},
expectedEvent: `Normal ResizeStarted Pod resize started: {"containers":[{"name":"c1","resources":{"requests":{"cpu":"1","memory":"1Gi"}}}],"generation":1}`,
},
{
name: "desired != allocated != actuated, both conditions should be present in the pod status",
cpu: cpu5000m,
mem: mem4500M,
generation: 2,
expectedConditions: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: v1.PodReasonInfeasible,
Message: "Node didn't have enough capacity: memory, requested: 4718592000, capacity: 4294967296",
ObservedGeneration: 2,
},
{
Type: v1.PodResizeInProgress,
Status: "True",
ObservedGeneration: 1,
},
},
expectedEvent: `Warning ResizeInfeasible Pod resize Infeasible: {"containers":[{"name":"c1","resources":{"requests":{"cpu":"5","memory":"4500Mi"}}}],"generation":2,"error":"Node didn't have enough capacity: memory, requested: 4718592000, capacity: 4294967296"}`,
},
{
name: "same as previous case to ensure no new event is generated",
cpu: cpu5000m,
mem: mem4500M,
generation: 2,
expectedConditions: []*v1.PodCondition{
{
Type: v1.PodResizePending,
Status: "True",
Reason: v1.PodReasonInfeasible,
Message: "Node didn't have enough capacity: memory, requested: 4718592000, capacity: 4294967296",
ObservedGeneration: 2,
},
{
Type: v1.PodResizeInProgress,
Status: "True",
ObservedGeneration: 1,
},
},
},
{
name: "revert back to the original resize request",
cpu: cpu1000m,
mem: mem1000M,
generation: 3,
expectedConditions: []*v1.PodCondition{{
Type: v1.PodResizeInProgress,
Status: "True",
ObservedGeneration: 1,
}},
},
{
name: "no changes except generation",
cpu: cpu1000m,
mem: mem1000M,
generation: 4,
expectedConditions: []*v1.PodCondition{{
Type: v1.PodResizeInProgress,
Status: "True",
ObservedGeneration: 1,
}},
},
{
name: "allocate a new resize",
cpu: cpu500m,
mem: mem500M,
generation: 5,
expectedConditions: []*v1.PodCondition{{
Type: v1.PodResizeInProgress,
Status: "True",
ObservedGeneration: 5,
}},
expectedEvent: `Normal ResizeStarted Pod resize started: {"containers":[{"name":"c1","resources":{"requests":{"cpu":"500m","memory":"500Mi"}}}],"generation":5}`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testPod.Generation = tc.generation
testPod.Spec = v1.PodSpec{
Containers: []v1.Container{{
Name: "c1",
Image: "i1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: tc.cpu, v1.ResourceMemory: tc.mem},
}},
},
}
allocationManager.PushPendingResize(testPod.UID)
allocationManager.RetryPendingResizes(TriggerReasonPodUpdated)
conditions := allocationManager.(*manager).statusManager.GetPodResizeConditions(testPod.UID)
require.Len(t, conditions, len(tc.expectedConditions))
for _, c := range conditions {
c.LastProbeTime = metav1.Time{}
c.LastTransitionTime = metav1.Time{}
}
require.Equal(t, tc.expectedConditions, conditions)
fakeRecorder := allocationManager.(*manager).recorder.(*record.FakeRecorder)
if tc.expectedEvent != "" {
require.Len(t, fakeRecorder.Events, 1)
event := <-fakeRecorder.Events
require.Equal(t, tc.expectedEvent, event)
} else {
require.Empty(t, fakeRecorder.Events)
}
})
}
}
// testPodAdmitHandler is a lifecycle.PodAdmitHandler for testing.
type testPodAdmitHandler struct {
// admitFunc contains the custom logic for admitting or rejecting a pod.
admitFunc func(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult
}
// Admit rejects all pods in the podsToReject list with a matching UID.
func (a *testPodAdmitHandler) Admit(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
if a.admitFunc == nil {
return lifecycle.PodAdmitResult{Admit: true}
}
return a.admitFunc(attrs)
}
func TestAllocationManagerAddPod(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
}
const containerName = "c1"
cpu1Mem1G := v1.ResourceList{v1.ResourceCPU: resource.MustParse("1"), v1.ResourceMemory: resource.MustParse("1Gi")}
cpu2Mem2G := v1.ResourceList{v1.ResourceCPU: resource.MustParse("2"), v1.ResourceMemory: resource.MustParse("2Gi")}
createTestPod := func(uid types.UID, name, namespace string, resources v1.ResourceList) *v1.Pod {
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{UID: uid, Name: name, Namespace: namespace},
}
container := v1.Container{
Name: containerName,
Image: "i1",
Resources: v1.ResourceRequirements{
Requests: resources,
},
}
pod.Spec.Containers = append(pod.Spec.Containers, container)
return pod
}
pod1UID := types.UID("1111")
pod2UID := types.UID("2222")
pod1Small := createTestPod(pod1UID, "pod1", "ns1", cpu1Mem1G)
pod1Large := createTestPod(pod1UID, "pod1", "ns1", cpu2Mem2G)
pod2Small := createTestPod(pod2UID, "pod2", "ns2", cpu1Mem1G)
pod2Large := createTestPod(pod2UID, "pod2", "ns2", cpu2Mem2G)
testCases := []struct {
name string
ipprFeatureGate bool
initialAllocatedResourcesState map[types.UID]v1.ResourceList
currentActivePods []*v1.Pod
podToAdd *v1.Pod
admitFunc func(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult
expectAdmit bool
admissionFailureReason string
admissionFailureMessage string
expectedAllocatedResourcesState map[types.UID]v1.ResourceList
}{
{
name: "IPPR Enabled - New pod admitted and allocated resources updated",
ipprFeatureGate: true,
initialAllocatedResourcesState: map[types.UID]v1.ResourceList{},
currentActivePods: []*v1.Pod{},
podToAdd: pod1Small,
admitFunc: nil,
expectAdmit: true,
// allocated resources updated with pod1's resources
expectedAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G},
},
{
name: "IPPR Disabled - New pod admitted but allocated resources not updated",
ipprFeatureGate: false,
initialAllocatedResourcesState: map[types.UID]v1.ResourceList{},
currentActivePods: []*v1.Pod{},
podToAdd: pod1Small,
admitFunc: nil,
expectAdmit: true,
expectedAllocatedResourcesState: map[types.UID]v1.ResourceList{},
},
{
name: "IPPR Enabled - New pod not admititted due to insufficient resources",
ipprFeatureGate: true,
initialAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G},
currentActivePods: []*v1.Pod{pod1Small},
podToAdd: pod2Large,
admitFunc: func(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
cpuRequest := attrs.Pod.Spec.Containers[0].Resources.Requests.Cpu().Value()
if cpuRequest > 1 {
return lifecycle.PodAdmitResult{
Admit: false,
Reason: "OutOfcpu",
Message: fmt.Sprintf("not enough CPUs available for pod %s/%s, requested: %d, available:1", attrs.Pod.Namespace, attrs.Pod.Name, cpuRequest),
}
}
return lifecycle.PodAdmitResult{Admit: true}
},
expectAdmit: false,
admissionFailureReason: "OutOfcpu",
admissionFailureMessage: "not enough CPUs available for pod ns2/pod2, requested: 2, available:1",
// allocated resources not modified
expectedAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G},
},
{
name: "IPPR Disabled - New pod not admitted due to insufficient resources",
ipprFeatureGate: false,
initialAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G},
currentActivePods: []*v1.Pod{pod1Small},
podToAdd: pod2Large,
admitFunc: func(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
cpuRequest := attrs.Pod.Spec.Containers[0].Resources.Requests.Cpu().Value()
if cpuRequest > 1 {
return lifecycle.PodAdmitResult{
Admit: false,
Reason: "OutOfcpu",
Message: fmt.Sprintf("not enough CPUs available for pod %s/%s, requested: %d, available:1", attrs.Pod.Namespace, attrs.Pod.Name, cpuRequest),
}
}
return lifecycle.PodAdmitResult{Admit: true}
},
expectAdmit: false,
admissionFailureReason: "OutOfcpu",
admissionFailureMessage: "not enough CPUs available for pod ns2/pod2, requested: 2, available:1",
// allocated resources not modified
expectedAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G},
},
{
name: "IPPR Enabled - no pod resize request. Resource request same as existing allocation",
ipprFeatureGate: true,
initialAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G, pod2UID: cpu1Mem1G},
currentActivePods: []*v1.Pod{pod1Small},
podToAdd: pod1Small,
admitFunc: nil,
expectAdmit: true,
expectedAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G, pod2UID: cpu1Mem1G},
},
{
name: "IPPR Enabled - current allocation not found for added pod. Allocated resources updated.",
ipprFeatureGate: true,
initialAllocatedResourcesState: map[types.UID]v1.ResourceList{},
currentActivePods: []*v1.Pod{pod1Small},
podToAdd: pod1Large,
admitFunc: nil,
expectAdmit: true,
// pod2's resources added to allocated resources
expectedAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu2Mem2G},
},
{
name: "IPPR Enabled - request different from current allocation. Pod still admitted based on existing allocation, but allocated resources remains unchanges.",
ipprFeatureGate: true,
initialAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G, pod2UID: cpu1Mem1G},
currentActivePods: []*v1.Pod{pod1Small, pod2Small},
podToAdd: pod1Large,
admitFunc: func(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
cpuRequest := attrs.Pod.Spec.Containers[0].Resources.Requests.Cpu().Value()
if cpuRequest > 1 {
return lifecycle.PodAdmitResult{
Admit: false,
Reason: "OutOfcpu",
Message: fmt.Sprintf("not enough CPUs available for pod %s/%s, requested: %d, available:1", attrs.Pod.Namespace, attrs.Pod.Name, cpuRequest),
}
}
return lifecycle.PodAdmitResult{Admit: true}
},
// pod is still admitted as allocated resources are considered during admission.
expectAdmit: true,
// allocated Resources state must not be updated.
expectedAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G, pod2UID: cpu1Mem1G},
},
{
name: "IPPR Disabled - request different from current allocation. Admission fails. Allocated resources not updated.",
ipprFeatureGate: false,
initialAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G},
currentActivePods: []*v1.Pod{pod1Small},
podToAdd: pod1Large,
admitFunc: func(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
cpuRequest := attrs.Pod.Spec.Containers[0].Resources.Requests.Cpu().Value()
if cpuRequest > 1 {
return lifecycle.PodAdmitResult{
Admit: false,
Reason: "OutOfcpu",
Message: fmt.Sprintf("not enough CPUs available for pod %s/%s, requested: %d, available:1", attrs.Pod.Namespace, attrs.Pod.Name, cpuRequest),
}
}
return lifecycle.PodAdmitResult{Admit: true}
},
// pod is still admitted as allocated resources are considered during admission.
expectAdmit: false,
admissionFailureReason: "OutOfcpu",
admissionFailureMessage: "not enough CPUs available for pod ns1/pod1, requested: 2, available:1",
// allocated Resources state must not be updated.
expectedAllocatedResourcesState: map[types.UID]v1.ResourceList{pod1UID: cpu1Mem1G},
},
}
for _, tc := range testCases {
featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.34"))
t.Run(tc.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, tc.ipprFeatureGate)
allocationManager := makeAllocationManager(t, &containertest.FakeRuntime{}, []*v1.Pod{}, nil)
podForAllocation := func(uid types.UID, resources v1.ResourceList) *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{UID: uid},
Spec: v1.PodSpec{
Containers: []v1.Container{{
Name: containerName,
Resources: v1.ResourceRequirements{Requests: resources},
}},
},
}
}
for podUID, resources := range tc.initialAllocatedResourcesState {
err := allocationManager.SetAllocatedResources(podForAllocation(podUID, resources))
require.NoError(t, err)
}
if tc.admitFunc != nil {
handler := &testPodAdmitHandler{admitFunc: tc.admitFunc}
allocationManager.AddPodAdmitHandlers(lifecycle.PodAdmitHandlers{handler})
}
ok, reason, message := allocationManager.AddPod(tc.currentActivePods, tc.podToAdd)
require.Equal(t, tc.expectAdmit, ok)
require.Equal(t, tc.admissionFailureReason, reason)
require.Equal(t, tc.admissionFailureMessage, message)
for podUID, resources := range tc.expectedAllocatedResourcesState {
pod := podForAllocation(podUID, resources)
for _, container := range pod.Spec.Containers {
allocatedResources, found := allocationManager.GetContainerResourceAllocation(pod.UID, container.Name)
if pod.UID == tc.podToAdd.UID {
if tc.expectAdmit && !found {
t.Fatalf("resource allocation should exist for pod: %s", tc.podToAdd.Name)
}
if !tc.expectAdmit && found {
initialResources := tc.initialAllocatedResourcesState[pod.UID]
// allocated resources should not be modified when the pod is not admitted
assert.Equal(t, initialResources, allocatedResources.Requests, tc.name)
}
}
assert.Equal(t, container.Resources, allocatedResources, tc.name)
}
}
})
}
}
func TestIsResizeIncreasingRequests(t *testing.T) {
cpu500m := resource.MustParse("500m")
cpu1000m := resource.MustParse("1")
cpu1500m := resource.MustParse("1500m")
mem500M := resource.MustParse("500Mi")
mem1000M := resource.MustParse("1Gi")
mem1500M := resource.MustParse("1500Mi")
tests := []struct {
name string
newRequests map[int]v1.ResourceList
expected bool
}{
{
name: "increase requests, one container",
newRequests: map[int]v1.ResourceList{0: {v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem1500M}},
expected: true,
},
{
name: "decrease requests, one container",
newRequests: map[int]v1.ResourceList{0: {v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M}},
expected: false,
},
{
name: "increase cpu, decrease memory, one container",
newRequests: map[int]v1.ResourceList{0: {v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem500M}},
expected: true,
},
{
name: "increase memory, decrease cpu, one container",
newRequests: map[int]v1.ResourceList{0: {v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem1500M}},
expected: true,
},
{
name: "increase one container, decrease another container, net neutral",
newRequests: map[int]v1.ResourceList{
0: {v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem1500M},
1: {v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
},
expected: false,
},
{
name: "decrease requests, two containers",
newRequests: map[int]v1.ResourceList{
0: {v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
1: {v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
},
expected: false,
},
{
name: "remove requests, set as empty struct",
newRequests: map[int]v1.ResourceList{0: {}},
expected: false,
},
{
name: "remove requests, set as nil",
newRequests: map[int]v1.ResourceList{0: nil},
expected: false,
},
{
name: "add requests, set as empty struct",
newRequests: map[int]v1.ResourceList{2: {v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M}},
expected: true,
},
{
name: "add requests, set as nil",
newRequests: map[int]v1.ResourceList{3: {v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M}},
expected: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
testPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
UID: "1111",
Name: "pod1",
Namespace: "ns1",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "c1",
Image: "i1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
},
},
{
Name: "c2",
Image: "i2",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
},
},
{
Name: "c3",
Image: "i3",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{},
},
},
{
Name: "c4",
Image: "i4",
Resources: v1.ResourceRequirements{
Requests: nil,
},
},
},
},
}
allocationManager := makeAllocationManager(t, &containertest.FakeRuntime{}, []*v1.Pod{testPod}, nil)
require.NoError(t, allocationManager.SetAllocatedResources(testPod))
for k, v := range tc.newRequests {
testPod.Spec.Containers[k].Resources.Requests = v
}
require.Equal(t, tc.expected, allocationManager.(*manager).isResizeIncreasingRequests(testPod))
})
}
}
func TestSortPendingResizes(t *testing.T) {
cpu500m := resource.MustParse("500m")
cpu1000m := resource.MustParse("1")
cpu1500m := resource.MustParse("1500m")
mem500M := resource.MustParse("500Mi")
mem1000M := resource.MustParse("1Gi")
mem1500M := resource.MustParse("1500Mi")
createTestPod := func(podNumber int) *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID(fmt.Sprintf("%d", podNumber)),
Name: fmt.Sprintf("pod%d", podNumber),
Namespace: fmt.Sprintf("ns%d", podNumber),
},
Spec: v1.PodSpec{
Containers: []v1.Container{{
Name: fmt.Sprintf("c%d", podNumber),
Image: fmt.Sprintf("i%d", podNumber),
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
},
}},
},
}
}
testPods := []*v1.Pod{createTestPod(0), createTestPod(1), createTestPod(2), createTestPod(3), createTestPod(4), createTestPod(5)}
allocationManager := makeAllocationManager(t, &containertest.FakeRuntime{}, testPods, nil)
for _, testPod := range testPods {
require.NoError(t, allocationManager.SetAllocatedResources(testPod))
}
// testPods[0] has the highest priority, as it doesn't increase resource requests.
// testPods[1] has the highest PriorityClass.
// testPods[2] is the only pod with QoS class "guaranteed" (all others are burstable).
// testPods[3] has been in a "deferred" state for longer than testPods[4].
// testPods[5] has no resize conditions yet, indicating it is a newer request than the other deferred resizes.
testPods[0].Spec.Containers[0].Resources.Requests = v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M}
for i := 1; i < len(testPods); i++ {
testPods[i].Spec.Containers[0].Resources.Requests = v1.ResourceList{v1.ResourceCPU: cpu1500m, v1.ResourceMemory: mem1500M}
}
testPods[1].Spec.Priority = ptr.To(int32(100))
testPods[2].Status.QOSClass = v1.PodQOSGuaranteed
allocationManager.(*manager).statusManager.SetPodResizePendingCondition(testPods[3].UID, v1.PodReasonDeferred, "some-message", 1)
time.Sleep(5 * time.Millisecond)
allocationManager.(*manager).statusManager.SetPodResizePendingCondition(testPods[4].UID, v1.PodReasonDeferred, "some-message", 1)
allocationManager.(*manager).getPodByUID = func(uid types.UID) (*v1.Pod, bool) {
pods := map[types.UID]*v1.Pod{
testPods[0].UID: testPods[0],
testPods[1].UID: testPods[1],
testPods[2].UID: testPods[2],
testPods[3].UID: testPods[3],
testPods[4].UID: testPods[4],
testPods[5].UID: testPods[5],
}
pod, found := pods[uid]
return pod, found
}
expected := []types.UID{testPods[0].UID, testPods[1].UID, testPods[2].UID, testPods[3].UID, testPods[4].UID, testPods[5].UID}
// Push all the pods to the queue.
for i := range testPods {
allocationManager.PushPendingResize(testPods[i].UID)
}
require.Equal(t, expected, allocationManager.(*manager).podsWithPendingResizes)
// Clear the queue and push the pods in reverse order to spice things up.
allocationManager.(*manager).podsWithPendingResizes = nil
for i := 5; i >= 0; i-- {
allocationManager.PushPendingResize(testPods[i].UID)
}
require.Equal(t, expected, allocationManager.(*manager).podsWithPendingResizes)
}
func TestRecordPodDeferredAcceptedResizes(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
}
metrics.Register()
metrics.PodDeferredAcceptedResizes.Reset()
cpu500m := resource.MustParse("500m")
cpu1000m := resource.MustParse("1")
mem500M := resource.MustParse("500Mi")
mem1000M := resource.MustParse("1Gi")
for _, tc := range []struct {
name string
trigger string
hasPendingCondition bool
expectedMetrics string
expectedEvent string
}{
{
name: "trigger reason: pod updated, no pending condition",
trigger: TriggerReasonPodUpdated,
expectedEvent: `Normal ResizeStarted Pod resize started: {"containers":[{"name":"c1","resources":{"requests":{"cpu":"500m","memory":"500Mi"}}}],"generation":0}`,
},
{
name: "trigger reason: pod resized, pending condition",
trigger: TriggerReasonPodResized,
hasPendingCondition: true,
expectedMetrics: `
# HELP kubelet_pod_deferred_accepted_resizes_total [ALPHA] Cumulative number of resizes that were accepted after being deferred.
# TYPE kubelet_pod_deferred_accepted_resizes_total counter
kubelet_pod_deferred_accepted_resizes_total{retry_trigger="pod_resized"} 1
`,
expectedEvent: `Normal ResizeStarted Pod resize started: {"containers":[{"name":"c1","resources":{"requests":{"cpu":"500m","memory":"500Mi"}}}],"generation":0}`,
},
{
name: "trigger reason: periodic retry, pending condition",
trigger: triggerReasonPeriodic,
hasPendingCondition: true,
expectedMetrics: `
# HELP kubelet_pod_deferred_accepted_resizes_total [ALPHA] Cumulative number of resizes that were accepted after being deferred.
# TYPE kubelet_pod_deferred_accepted_resizes_total counter
kubelet_pod_deferred_accepted_resizes_total{retry_trigger="periodic_retry"} 1
kubelet_pod_deferred_accepted_resizes_total{retry_trigger="pod_resized"} 1
`,
expectedEvent: `Normal ResizeStarted Pod resize started: {"containers":[{"name":"c1","resources":{"requests":{"cpu":"500m","memory":"500Mi"}}}],"generation":0}`,
},
} {
t.Run(tc.name, func(t *testing.T) {
original := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
UID: "1111",
Name: "pod1",
Namespace: "ns1",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "c1",
Image: "i1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
},
},
},
},
Status: v1.PodStatus{
Phase: v1.PodRunning,
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
AllocatedResources: v1.ResourceList{v1.ResourceCPU: cpu1000m, v1.ResourceMemory: mem1000M},
Resources: &v1.ResourceRequirements{},
},
},
},
}
resizedPod := original.DeepCopy()
resizedPod.Spec.Containers[0].Resources.Requests = v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M}
am := makeAllocationManager(t, &containertest.FakeRuntime{}, []*v1.Pod{original}, nil)
require.NoError(t, am.SetAllocatedResources(original))
if tc.hasPendingCondition {
am.(*manager).statusManager.SetPodResizePendingCondition(original.UID, v1.PodReasonDeferred, "message", 1)
}
am.(*manager).getPodByUID = func(uid types.UID) (*v1.Pod, bool) {
return resizedPod, true
}
am.PushPendingResize(original.UID)
resizedPods := am.(*manager).retryPendingResizes(tc.trigger)
require.Len(t, resizedPods, 1)
require.Equal(t, original.UID, resizedPods[0].UID)
require.NoError(t, testutil.GatherAndCompare(
legacyregistry.DefaultGatherer, strings.NewReader(tc.expectedMetrics), "kubelet_pod_deferred_accepted_resizes_total",
))
fakeRecorder := am.(*manager).recorder.(*record.FakeRecorder)
require.Len(t, fakeRecorder.Events, 1)
event := <-fakeRecorder.Events
require.Equal(t, tc.expectedEvent, event)
})
}
}
func makeAllocationManager(t *testing.T, runtime *containertest.FakeRuntime, allocatedPods []*v1.Pod, nodeConfig *cm.NodeConfig) Manager {
t.Helper()
statusManager := status.NewManager(&fake.Clientset{}, kubepod.NewBasicPodManager(), &statustest.FakePodDeletionSafetyProvider{}, kubeletutil.NewPodStartupLatencyTracker())
var containerManager *cm.FakeContainerManager
if nodeConfig == nil {
containerManager = cm.NewFakeContainerManager()
} else {
containerManager = cm.NewFakeContainerManagerWithNodeConfig(*nodeConfig)
}
allocationManager := NewInMemoryManager(
containerManager.GetNodeConfig(),
containerManager.GetNodeAllocatableAbsolute(),
statusManager,
func(pod *v1.Pod) {
/* For testing, just mark the pod as having a pod sync triggered in an annotation. */
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
pod.Annotations["pod-sync-triggered"] = "true"
},
func() []*v1.Pod { return allocatedPods },
func(uid types.UID) (*v1.Pod, bool) {
for _, p := range allocatedPods {
if p.UID == uid {
return p, true
}
}
return nil, false
},
config.NewSourcesReady(func(_ sets.Set[string]) bool { return true }),
record.NewFakeRecorder(20),
)
allocationManager.SetContainerRuntime(runtime)
getNode := func(context.Context, bool) (*v1.Node, error) {
return &v1.Node{
Status: v1.NodeStatus{
Capacity: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("8"),
v1.ResourceMemory: resource.MustParse("8Gi"),
},
Allocatable: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("4Gi"),
v1.ResourcePods: *resource.NewQuantity(40, resource.DecimalSI),
},
},
}, nil
}
handler := lifecycle.NewPredicateAdmitHandler(getNode, lifecycle.NewAdmissionFailureHandlerStub(), containerManager.UpdatePluginResources)
allocationManager.AddPodAdmitHandlers(lifecycle.PodAdmitHandlers{handler})
return allocationManager
}
func setContainerStatus(podStatus *kubecontainer.PodStatus, c *v1.Container, idx int) {
podStatus.ContainerStatuses[idx] = &kubecontainer.Status{
Name: c.Name,
State: kubecontainer.ContainerStateRunning,
Resources: &kubecontainer.ContainerResources{
CPURequest: c.Resources.Requests.Cpu(),
CPULimit: c.Resources.Limits.Cpu(),
MemoryLimit: c.Resources.Limits.Memory(),
},
}
}