Repository: kubernetes/kubernetes
Pull request #130831: disable in-place pod vertical scaling for swap enabled pods
Author: login: ajaysundark
Labels: name: kind/bug
description: Categorizes issue or PR as related to a bug.
name: area/test
name: priority/important-soon
description: Must be staffed and worked on either currently, or very soon, ideally in time for the next release.
name: area/kubelet
name: lgtm
description: "Looks good to me", indicates that a PR is ready to be merged.
name: sig/node
description: Categorizes an issue or PR as relevant to SIG Node.
name: release-note
description: Denotes a PR that will be considered when it comes time to generate release notes.
name: size/XL
description: Denotes a PR that changes 500-999 lines, ignoring generated files.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: cncf-cla: yes
description: Indicates the PR's author has signed the CNCF CLA.
name: sig/testing
description: Categorizes an issue or PR as relevant to SIG Testing.
name: ok-to-test
description: Indicates a non-member PR verified by an org member that is safe to test.
name: needs-triage
description: Indicates an issue or PR lacks a `triage/foo` label and requires one.
Created: 2025-03-14T21:30:45Z | Merged: 2025-03-21T02:38:38Z

#### What type of PR is this?

/kind bug

#### What this PR does / why we need it:
Resizing swap needs more consideration (https://github.com/kubernetes/kubernetes/issues/130111). For the purposes of unblocking InPlacePodVerticalScaling beta, resizes affecting swap limit configuration will be marked as infeasible. This is a temporary change and will be revisited in the future after beta.

#### Which issue(s) this PR fixes:
Fixes #130659

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
Disable InPlace Pod Resize for Swap enabled containers that does not have memory ResizePolicy as RestartContainer
```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:

```docs

```


Linked issue #130659: [FG:InPlacePodVerticalScaling] Make resizes affecting swap limits infeasible
Resizing swap needs more consideration (https://github.com/kubernetes/kubernetes/issues/130111). For the purposes of unblocking InPlacePodVerticalScaling beta, let's mark resizes affecting swap limit configuration as infeasible.

I thought this would just be a simple addition to `IsInPlacePodVerticalScalingAllowed`, but then realized we need to look at which resources on which containers are being resized, so it doesn't make sense with how that function is used. We should just add an additional check here:  https://github.com/kubernetes/kubernetes/blob/22ff6b4918e9cec09842d6ca94584b81a03042cd/pkg/kubelet/kubelet.go#L2940

In case it's useful, here's my previous attempt: https://github.com/kubernetes/kubernetes/compare/master...tallclair:disable-swap-resize?expand=1

/sig node
/priority important-soon
/milestone v1.33
Linked issue comments: user: login: tallclair
created_at: 2025-03-08T00:05:31Z
body: /assign
/triage accepted

(@ajaysundark will own this but is not yet an org member)
author_association: MEMBER
user: login: wendy-ha18
created_at: 2025-03-08T08:29:35Z
body: Hello @tallclair, with this newly open issue target for v1.33 milestone, a friendly reminder that code freeze is starting at 02:00 UTC Friday 21st March 2025 (about 2 weeks from now), and while there is still time, we want to ensure that each PR has a chance to be merged on time.

Please make sure the PR for this issue has both `lgtm` and `approved` labels before the code freeze. Thank Tim!
author_association: MEMBER
user: login: wendy-ha18
created_at: 2025-03-17T11:35:15Z
body: Hello @tallclair @ajaysundark

Thanks a lot for creating PR https://github.com/kubernetes/kubernetes/pull/130831 for this issue.

A gentle reminder that the code freeze has started [02:00 UTC Friday 21st March 2025](https://everytimezone.com/s/2c5e9275) . Please make sure the related PR targeting for v1.33 has both `lgtm` and `approved` labels ASAP, and file an [Exception](https://github.com/kubernetes/sig-release/blob/master/releases/EXCEPTIONS.md) if you haven't done it yet.
Thanks!
author_association: MEMBER

Diff:
diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go
index a7ed8e0014a56..3bd3ac16eb765 100644
--- a/cmd/kubelet/app/server.go
+++ b/cmd/kubelet/app/server.go
@@ -834,7 +834,7 @@ func run(ctx context.Context, s *options.KubeletServer, kubeDeps *kubelet.Depend
 				s.TopologyManagerPolicyOptions, features.TopologyManagerPolicyOptions)
 		}
 		if utilfeature.DefaultFeatureGate.Enabled(features.NodeSwap) {
-			if !kubeletutil.IsCgroup2UnifiedMode() && s.MemorySwap.SwapBehavior == kubelettypes.LimitedSwap {
+			if !kubeletutil.IsCgroup2UnifiedMode() && s.MemorySwap.SwapBehavior == string(kubelettypes.LimitedSwap) {
 				// This feature is not supported for cgroupv1 so we are failing early.
 				return fmt.Errorf("swap feature is enabled and LimitedSwap but it is only supported with cgroupv2")
 			}
diff --git a/pkg/kubelet/apis/config/validation/validation.go b/pkg/kubelet/apis/config/validation/validation.go
index 6f5197ca92c47..8d1c286947577 100644
--- a/pkg/kubelet/apis/config/validation/validation.go
+++ b/pkg/kubelet/apis/config/validation/validation.go
@@ -203,8 +203,8 @@ func ValidateKubeletConfiguration(kc *kubeletconfig.KubeletConfiguration, featur
 	if localFeatureGate.Enabled(features.NodeSwap) {
 		switch kc.MemorySwap.SwapBehavior {
 		case "":
-		case kubetypes.NoSwap:
-		case kubetypes.LimitedSwap:
+		case string(kubetypes.NoSwap):
+		case string(kubetypes.LimitedSwap):
 		default:
 			allErrors = append(allErrors, fmt.Errorf("invalid configuration: memorySwap.swapBehavior %q must be one of: \"\", %q or %q", kc.MemorySwap.SwapBehavior, kubetypes.LimitedSwap, kubetypes.NoSwap))
 		}
diff --git a/pkg/kubelet/apis/config/validation/validation_test.go b/pkg/kubelet/apis/config/validation/validation_test.go
index 08da05c2efc0d..75fccec60ec22 100644
--- a/pkg/kubelet/apis/config/validation/validation_test.go
+++ b/pkg/kubelet/apis/config/validation/validation_test.go
@@ -385,7 +385,7 @@ func TestValidateKubeletConfiguration(t *testing.T) {
 		name: "specify MemorySwap.SwapBehavior without enabling NodeSwap",
 		configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
 			conf.FeatureGates = map[string]bool{"NodeSwap": false}
-			conf.MemorySwap.SwapBehavior = kubetypes.LimitedSwap
+			conf.MemorySwap.SwapBehavior = string(kubetypes.LimitedSwap)
 			return conf
 		},
 		errMsg: "invalid configuration: memorySwap.swapBehavior cannot be set when NodeSwap feature flag is disabled",
diff --git a/pkg/kubelet/container/runtime.go b/pkg/kubelet/container/runtime.go
index 025579ab80eaa..1073f734031c3 100644
--- a/pkg/kubelet/container/runtime.go
+++ b/pkg/kubelet/container/runtime.go
@@ -34,6 +34,7 @@ import (
 	runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
 	"k8s.io/klog/v2"
 	"k8s.io/kubernetes/pkg/credentialprovider"
+	kubelettypes "k8s.io/kubernetes/pkg/kubelet/types"
 	"k8s.io/kubernetes/pkg/volume"
 )
 
@@ -138,6 +139,9 @@ type Runtime interface {
 	ListPodSandboxMetrics(ctx context.Context) ([]*runtimeapi.PodSandboxMetrics, error)
 	// GetContainerStatus returns the status for the container.
 	GetContainerStatus(ctx context.Context, id ContainerID) (*Status, error)
+	// GetContainerSwapBehavior reports whether a container could be swappable.
+	// This is used to decide whether to handle InPlacePodVerticalScaling for containers.
+	GetContainerSwapBehavior(pod *v1.Pod, container *v1.Container) kubelettypes.SwapBehavior
 }
 
 // StreamingRuntime is the interface implemented by runtimes that handle the serving of the
diff --git a/pkg/kubelet/container/testing/fake_runtime.go b/pkg/kubelet/container/testing/fake_runtime.go
index e360ba40bb87a..226972a44a7af 100644
--- a/pkg/kubelet/container/testing/fake_runtime.go
+++ b/pkg/kubelet/container/testing/fake_runtime.go
@@ -30,6 +30,7 @@ import (
 	runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
 	"k8s.io/kubernetes/pkg/credentialprovider"
 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
+	kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
 	"k8s.io/kubernetes/pkg/volume"
 )
 
@@ -70,6 +71,7 @@ type FakeRuntime struct {
 	// from container runtime.
 	BlockImagePulls      bool
 	imagePullTokenBucket chan bool
+	SwapBehavior         map[string]kubetypes.SwapBehavior
 	T                    TB
 }
 
@@ -536,3 +538,10 @@ func (f *FakeRuntime) GetContainerStatus(_ context.Context, _ kubecontainer.Cont
 	f.CalledFunctions = append(f.CalledFunctions, "GetContainerStatus")
 	return nil, f.Err
 }
+
+func (f *FakeRuntime) GetContainerSwapBehavior(pod *v1.Pod, container *v1.Container) kubetypes.SwapBehavior {
+	if f.SwapBehavior != nil && f.SwapBehavior[container.Name] != "" {
+		return f.SwapBehavior[container.Name]
+	}
+	return kubetypes.NoSwap
+}
diff --git a/pkg/kubelet/container/testing/runtime_mock.go b/pkg/kubelet/container/testing/runtime_mock.go
index 1f021ea06d737..d53f2089b1a39 100644
--- a/pkg/kubelet/container/testing/runtime_mock.go
+++ b/pkg/kubelet/container/testing/runtime_mock.go
@@ -33,7 +33,9 @@ import (
 
 	mock "github.com/stretchr/testify/mock"
 
-	types "k8s.io/apimachinery/pkg/types"
+	pkgtypes "k8s.io/apimachinery/pkg/types"
+
+	types "k8s.io/kubernetes/pkg/kubelet/types"
 
 	v1 "k8s.io/cri-api/pkg/apis/runtime/v1"
 )
@@ -419,6 +421,53 @@ func (_c *MockRuntime_GetContainerStatus_Call) RunAndReturn(run func(context.Con
 	return _c
 }
 
+// GetContainerSwapBehavior provides a mock function with given fields: pod, _a1
+func (_m *MockRuntime) GetContainerSwapBehavior(pod *corev1.Pod, _a1 *corev1.Container) types.SwapBehavior {
+	ret := _m.Called(pod, _a1)
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetContainerSwapBehavior")
+	}
+
+	var r0 types.SwapBehavior
+	if rf, ok := ret.Get(0).(func(*corev1.Pod, *corev1.Container) types.SwapBehavior); ok {
+		r0 = rf(pod, _a1)
+	} else {
+		r0 = ret.Get(0).(types.SwapBehavior)
+	}
+
+	return r0
+}
+
+// MockRuntime_GetContainerSwapBehavior_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContainerSwapBehavior'
+type MockRuntime_GetContainerSwapBehavior_Call struct {
+	*mock.Call
+}
+
+// GetContainerSwapBehavior is a helper method to define mock.On call
+//   - pod *corev1.Pod
+//   - _a1 *corev1.Container
+func (_e *MockRuntime_Expecter) GetContainerSwapBehavior(pod interface{}, _a1 interface{}) *MockRuntime_GetContainerSwapBehavior_Call {
+	return &MockRuntime_GetContainerSwapBehavior_Call{Call: _e.mock.On("GetContainerSwapBehavior", pod, _a1)}
+}
+
+func (_c *MockRuntime_GetContainerSwapBehavior_Call) Run(run func(pod *corev1.Pod, _a1 *corev1.Container)) *MockRuntime_GetContainerSwapBehavior_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*corev1.Pod), args[1].(*corev1.Container))
+	})
+	return _c
+}
+
+func (_c *MockRuntime_GetContainerSwapBehavior_Call) Return(_a0 types.SwapBehavior) *MockRuntime_GetContainerSwapBehavior_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockRuntime_GetContainerSwapBehavior_Call) RunAndReturn(run func(*corev1.Pod, *corev1.Container) types.SwapBehavior) *MockRuntime_GetContainerSwapBehavior_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // GetImageRef provides a mock function with given fields: ctx, image
 func (_m *MockRuntime) GetImageRef(ctx context.Context, image container.ImageSpec) (string, error) {
 	ret := _m.Called(ctx, image)
@@ -534,7 +583,7 @@ func (_c *MockRuntime_GetImageSize_Call) RunAndReturn(run func(context.Context,
 }
 
 // GetPodStatus provides a mock function with given fields: ctx, uid, name, namespace
-func (_m *MockRuntime) GetPodStatus(ctx context.Context, uid types.UID, name string, namespace string) (*container.PodStatus, error) {
+func (_m *MockRuntime) GetPodStatus(ctx context.Context, uid pkgtypes.UID, name string, namespace string) (*container.PodStatus, error) {
 	ret := _m.Called(ctx, uid, name, namespace)
 
 	if len(ret) == 0 {
@@ -543,10 +592,10 @@ func (_m *MockRuntime) GetPodStatus(ctx context.Context, uid types.UID, name str
 
 	var r0 *container.PodStatus
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, types.UID, string, string) (*container.PodStatus, error)); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, pkgtypes.UID, string, string) (*container.PodStatus, error)); ok {
 		return rf(ctx, uid, name, namespace)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, types.UID, string, string) *container.PodStatus); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, pkgtypes.UID, string, string) *container.PodStatus); ok {
 		r0 = rf(ctx, uid, name, namespace)
 	} else {
 		if ret.Get(0) != nil {
@@ -554,7 +603,7 @@ func (_m *MockRuntime) GetPodStatus(ctx context.Context, uid types.UID, name str
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, types.UID, string, string) error); ok {
+	if rf, ok := ret.Get(1).(func(context.Context, pkgtypes.UID, string, string) error); ok {
 		r1 = rf(ctx, uid, name, namespace)
 	} else {
 		r1 = ret.Error(1)
@@ -570,16 +619,16 @@ type MockRuntime_GetPodStatus_Call struct {
 
 // GetPodStatus is a helper method to define mock.On call
 //   - ctx context.Context
-//   - uid types.UID
+//   - uid pkgtypes.UID
 //   - name string
 //   - namespace string
 func (_e *MockRuntime_Expecter) GetPodStatus(ctx interface{}, uid interface{}, name interface{}, namespace interface{}) *MockRuntime_GetPodStatus_Call {
 	return &MockRuntime_GetPodStatus_Call{Call: _e.mock.On("GetPodStatus", ctx, uid, name, namespace)}
 }
 
-func (_c *MockRuntime_GetPodStatus_Call) Run(run func(ctx context.Context, uid types.UID, name string, namespace string)) *MockRuntime_GetPodStatus_Call {
+func (_c *MockRuntime_GetPodStatus_Call) Run(run func(ctx context.Context, uid pkgtypes.UID, name string, namespace string)) *MockRuntime_GetPodStatus_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(types.UID), args[2].(string), args[3].(string))
+		run(args[0].(context.Context), args[1].(pkgtypes.UID), args[2].(string), args[3].(string))
 	})
 	return _c
 }
@@ -589,7 +638,7 @@ func (_c *MockRuntime_GetPodStatus_Call) Return(_a0 *container.PodStatus, _a1 er
 	return _c
 }
 
-func (_c *MockRuntime_GetPodStatus_Call) RunAndReturn(run func(context.Context, types.UID, string, string) (*container.PodStatus, error)) *MockRuntime_GetPodStatus_Call {
+func (_c *MockRuntime_GetPodStatus_Call) RunAndReturn(run func(context.Context, pkgtypes.UID, string, string) (*container.PodStatus, error)) *MockRuntime_GetPodStatus_Call {
 	_c.Call.Return(run)
 	return _c
 }
diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go
index 6d999fa6def5a..fedf14af5034d 100644
--- a/pkg/kubelet/kubelet.go
+++ b/pkg/kubelet/kubelet.go
@@ -2920,6 +2920,40 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, string, string) {
 	return true, "", ""
 }
 
+func disallowResizeForSwappableContainers(runtime kubecontainer.Runtime, desiredPod, allocatedPod *v1.Pod) (bool, string) {
+	if desiredPod == nil || allocatedPod == nil {
+		return false, ""
+	}
+	restartableMemoryResizePolicy := func(resizePolicies []v1.ContainerResizePolicy) bool {
+		for _, policy := range resizePolicies {
+			if policy.ResourceName == v1.ResourceMemory {
+				return policy.RestartPolicy == v1.RestartContainer
+			}
+		}
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
+	for _, container := range append(allocatedPod.Spec.Containers, allocatedPod.Spec.InitContainers...) {
+		allocatedContainers[container.Name] = container
+	}
+	for _, desiredContainer := range append(desiredPod.Spec.Containers, desiredPod.Spec.InitContainers...) {
+		allocatedContainer, ok := allocatedContainers[desiredContainer.Name]
+		if !ok {
+			continue
+		}
+		origMemRequest := desiredContainer.Resources.Requests[v1.ResourceMemory]
+		newMemRequest := allocatedContainer.Resources.Requests[v1.ResourceMemory]
+		if !origMemRequest.Equal(newMemRequest) && !restartableMemoryResizePolicy(allocatedContainer.ResizePolicy) {
+			aSwapBehavior := runtime.GetContainerSwapBehavior(desiredPod, &desiredContainer)
+			bSwapBehavior := runtime.GetContainerSwapBehavior(allocatedPod, &allocatedContainer)
+			if aSwapBehavior != kubetypes.NoSwap || bSwapBehavior != kubetypes.NoSwap {
+				return true, "In-place resize of containers with swap is not supported."
+			}
+		}
+	}
+	return false, ""
+}
+
 // handlePodResourcesResize returns the "allocated pod", which should be used for all resource
 // calculations after this function is called. It also updates the cached ResizeStatus according to
 // the allocation decision and pod status.
@@ -2949,6 +2983,10 @@ func (kl *Kubelet) handlePodResourcesResize(pod *v1.Pod, podStatus *kubecontaine
 		// If there is a pending resize but the resize is not allowed, always use the allocated resources.
 		kl.statusManager.SetPodResizePendingCondition(pod.UID, v1.PodReasonInfeasible, msg)
 		return podFromAllocation, nil
+	} else if resizeNotAllowed, msg := disallowResizeForSwappableContainers(kl.containerRuntime, pod, podFromAllocation); resizeNotAllowed {
+		// If this resize involve swap recalculation, set as infeasible, as IPPR with swap is not supported for beta.
+		kl.statusManager.SetPodResizePendingCondition(pod.UID, v1.PodReasonInfeasible, msg)
+		return podFromAllocation, nil
 	}
 
 	kl.podResizeMutex.Lock()
diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go
index 6dbe8ee6456eb..0fb6b124a8bb5 100644
--- a/pkg/kubelet/kubelet_test.go
+++ b/pkg/kubelet/kubelet_test.go
@@ -2612,6 +2612,185 @@ func TestPodResourceAllocationReset(t *testing.T) {
 	}
 }
 
+func TestHandlePodResourcesResizeWithSwap(t *testing.T) {
+	if goruntime.GOOS == "windows" {
+		t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
+	}
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true)
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeSwap, true)
+	testKubelet := newTestKubelet(t, false)
+	defer testKubelet.Cleanup()
+	noSwapContainerName, swapContainerName := "test-container-noswap", "test-container-limitedswap"
+	runtime := testKubelet.fakeRuntime
+	runtime.SwapBehavior = map[string]kubetypes.SwapBehavior{
+		noSwapContainerName: kubetypes.NoSwap,
+		swapContainerName:   kubetypes.LimitedSwap,
+	}
+	kubelet := testKubelet.kubelet
+	cpu500m := resource.MustParse("500m")
+	cpu1000m := resource.MustParse("1")
+	mem500M := resource.MustParse("500Mi")
+	mem1000M := resource.MustParse("1Gi")
+	nodes := []*v1.Node{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: testKubeletHostname},
+			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),
+				},
+			},
+		},
+	}
+	kubelet.nodeLister = testNodeLister{nodes: nodes}
+	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{},
+				},
+			},
+		},
+	}
+
+	testKubelet.fakeKubeClient = fake.NewSimpleClientset(testPod)
+	kubelet.kubeClient = testKubelet.fakeKubeClient
+	defer testKubelet.fakeKubeClient.ClearActions()
+	kubelet.podManager.AddPod(testPod)
+	kubelet.podWorkers.(*fakePodWorkers).running = map[types.UID]bool{
+		testPod.UID: true,
+	}
+	defer kubelet.podManager.RemovePod(testPod)
+	tests := []struct {
+		name                  string
+		newRequests           v1.ResourceList
+		expectedAllocatedReqs v1.ResourceList
+		resizePolicy          v1.ContainerResizePolicy
+		swapBehavior          kubetypes.SwapBehavior
+		expectedResize        []*v1.PodCondition
+	}{
+		{
+			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",
+				},
+			},
+		},
+	}
+	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
+			}
+			kubelet.podManager.UpdatePod(originalPod)
+			newPod := originalPod.DeepCopy()
+			newPod.Spec.Containers[0].Resources.Requests = tt.newRequests
+			require.NoError(t, kubelet.allocationManager.SetAllocatedResources(originalPod))
+			require.NoError(t, kubelet.allocationManager.SetActuatedResources(originalPod, nil))
+			t.Cleanup(func() { kubelet.allocationManager.RemovePod(originalPod.UID) })
+			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))
+			for i, c := range originalPod.Spec.Containers {
+				setContainerStatus(podStatus, &c, i)
+			}
+			updatedPod, err := kubelet.handlePodResourcesResize(newPod, podStatus)
+			require.NoError(t, err)
+			updatedPodCtr := updatedPod.Spec.Containers[0]
+			assert.Equal(t, tt.expectedAllocatedReqs, updatedPodCtr.Resources.Requests, "updated pod spec requests")
+
+			alloc, found := kubelet.allocationManager.GetContainerResourceAllocation(newPod.UID, updatedPodCtr.Name)
+			require.True(t, found, "container allocation")
+			assert.Equal(t, tt.expectedAllocatedReqs, alloc.Requests, "stored container request allocation")
+			resizeStatus := kubelet.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)
+		})
+	}
+}
+
 func TestHandlePodResourcesResize(t *testing.T) {
 	if goruntime.GOOS == "windows" {
 		t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
diff --git a/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go b/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go
index d44432b7e61ed..92321bd9bd460 100644
--- a/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go
+++ b/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go
@@ -45,7 +45,7 @@ import (
 	"k8s.io/kubernetes/pkg/kubelet/cm"
 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
 	"k8s.io/kubernetes/pkg/kubelet/qos"
-	kubelettypes "k8s.io/kubernetes/pkg/kubelet/types"
+	"k8s.io/kubernetes/pkg/kubelet/types"
 	"k8s.io/utils/ptr"
 )
 
@@ -206,35 +206,45 @@ func (m *kubeGenericRuntimeManager) configureContainerSwapResources(lcr *runtime
 	}
 
 	swapConfigurationHelper := newSwapConfigurationHelper(*m.machineInfo)
-	if m.memorySwapBehavior == kubelettypes.LimitedSwap {
-		if !isCgroup2UnifiedMode() {
-			swapConfigurationHelper.ConfigureNoSwap(lcr)
-			return
-		}
-	}
-
-	if !utilfeature.DefaultFeatureGate.Enabled(kubefeatures.NodeSwap) {
-		swapConfigurationHelper.ConfigureNoSwap(lcr)
-		return
-	}
-
-	if kubelettypes.IsCriticalPod(pod) {
-		swapConfigurationHelper.ConfigureNoSwap(lcr)
-		return
-	}
-
 	// NOTE(ehashman): Behavior is defined in the opencontainers runtime spec:
 	// https://github.com/opencontainers/runtime-spec/blob/1c3f411f041711bbeecf35ff7e93461ea6789220/config-linux.md#memory
-	switch m.memorySwapBehavior {
-	case kubelettypes.NoSwap:
+	switch m.GetContainerSwapBehavior(pod, container) {
+	case types.NoSwap:
 		swapConfigurationHelper.ConfigureNoSwap(lcr)
-	case kubelettypes.LimitedSwap:
+	case types.LimitedSwap:
 		swapConfigurationHelper.ConfigureLimitedSwap(lcr, pod, container)
 	default:
 		swapConfigurationHelper.ConfigureNoSwap(lcr)
 	}
 }
 
+// GetContainerSwapBehavior checks what swap behavior should be configured for a container,
+// considering the requirements for enabling swap.
+func (m *kubeGenericRuntimeManager) GetContainerSwapBehavior(pod *v1.Pod, container *v1.Container) types.SwapBehavior {
+	c := types.SwapBehavior(m.memorySwapBehavior)
+	if c == types.LimitedSwap {
+		if !utilfeature.DefaultFeatureGate.Enabled(kubefeatures.NodeSwap) || !swapControllerAvailable() {
+			return types.NoSwap
+		}
+
+		if !isCgroup2UnifiedMode() {
+			return types.NoSwap
+		}
+
+		if types.IsCriticalPod(pod) {
+			return types.NoSwap
+		}
+		podQos := kubeapiqos.GetPodQOS(pod)
+		containerDoesNotRequestMemory := container.Resources.Requests.Memory().IsZero() && container.Resources.Limits.Memory().IsZero()
+		memoryRequestEqualsToLimit := container.Resources.Requests.Memory().Cmp(*container.Resources.Limits.Memory()) == 0
+		if podQos != v1.PodQOSBurstable || containerDoesNotRequestMemory || memoryRequestEqualsToLimit {
+			return types.NoSwap
+		}
+		return c
+	}
+	return types.NoSwap
+}
+
 // generateContainerResources generates platform specific (linux) container resources config for runtime
 func (m *kubeGenericRuntimeManager) generateContainerResources(pod *v1.Pod, container *v1.Container) *runtimeapi.ContainerResources {
 	enforceMemoryQoS := false
@@ -428,15 +438,6 @@ func newSwapConfigurationHelper(machineInfo cadvisorv1.MachineInfo) *swapConfigu
 }
 
 func (m swapConfigurationHelper) ConfigureLimitedSwap(lcr *runtimeapi.LinuxContainerResources, pod *v1.Pod, container *v1.Container) {
-	podQos := kubeapiqos.GetPodQOS(pod)
-	containerDoesNotRequestMemory := container.Resources.Requests.Memory().IsZero() && container.Resources.Limits.Memory().IsZero()
-	memoryRequestEqualsToLimit := container.Resources.Requests.Memory().Cmp(*container.Resources.Limits.Memory()) == 0
-
-	if podQos != v1.PodQOSBurstable || containerDoesNotRequestMemory || !isCgroup2UnifiedMode() || memoryRequestEqualsToLimit {
-		m.ConfigureNoSwap(lcr)
-		return
-	}
-
 	containerMemoryRequest := container.Resources.Requests.Memory()
 	swapLimit, err := calcSwapForBurstablePods(containerMemoryRequest.Value(), int64(m.machineInfo.MemoryCapacity), int64(m.machineInfo.SwapCapacity))
 	if err != nil {
diff --git a/pkg/kubelet/kuberuntime/kuberuntime_container_linux_test.go b/pkg/kubelet/kuberuntime/kuberuntime_container_linux_test.go
index 5c0549e20b07f..8d21488e9ed38 100644
--- a/pkg/kubelet/kuberuntime/kuberuntime_container_linux_test.go
+++ b/pkg/kubelet/kuberuntime/kuberuntime_container_linux_test.go
@@ -924,6 +924,145 @@ func TestGenerateLinuxContainerResources(t *testing.T) {
 	}
 }
 
+func TestGetContainerSwapBehavior(t *testing.T) {
+	_, _, m, err := createTestRuntimeManager()
+	require.NoError(t, err)
+	pod := &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			UID:       "12345678",
+			Name:      "foo",
+			Namespace: "bar",
+		},
+		Spec: v1.PodSpec{
+			Containers: []v1.Container{
+				{
+					Name: "c1",
+					Resources: v1.ResourceRequirements{
+						Requests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("1Gi")},
+						Limits:   v1.ResourceList{v1.ResourceMemory: resource.MustParse("2Gi")},
+					},
+				},
+			},
+		},
+		Status: v1.PodStatus{},
+	}
+	tests := []struct {
+		name                       string
+		configuredMemorySwap       types.SwapBehavior
+		nodeSwapFeatureGateEnabled bool
+		isSwapControllerAvailable  bool
+		cgroupVersion              CgroupVersion
+		isCriticalPod              bool
+		qosClass                   v1.PodQOSClass
+		containerResourceOverride  func(container *v1.Container)
+		expected                   types.SwapBehavior
+	}{
+		{
+			name:                       "NoSwap, user set NoSwap behavior",
+			configuredMemorySwap:       types.NoSwap,
+			nodeSwapFeatureGateEnabled: false,
+			expected:                   types.NoSwap,
+		},
+		{
+			name:                       "NoSwap, feature gate turned off",
+			configuredMemorySwap:       types.LimitedSwap,
+			nodeSwapFeatureGateEnabled: false,
+			expected:                   types.NoSwap,
+		},
+		{
+			name:                       "NoSwap, swap controller unavailable",
+			configuredMemorySwap:       types.LimitedSwap,
+			nodeSwapFeatureGateEnabled: true,
+			isSwapControllerAvailable:  false,
+			expected:                   types.NoSwap,
+		},
+		{
+			name:                       "NoSwap, cgroup v1",
+			configuredMemorySwap:       types.LimitedSwap,
+			nodeSwapFeatureGateEnabled: true,
+			isSwapControllerAvailable:  true,
+			cgroupVersion:              cgroupV1,
+			expected:                   types.NoSwap,
+		},
+		{
+			name:                       "NoSwap, qos is Best-Effort",
+			configuredMemorySwap:       types.LimitedSwap,
+			nodeSwapFeatureGateEnabled: true,
+			isSwapControllerAvailable:  true,
+			cgroupVersion:              cgroupV2,
+			qosClass:                   v1.PodQOSBestEffort,
+			expected:                   types.NoSwap,
+		},
+		{
+			name:                       "NoSwap, qos is Guaranteed",
+			configuredMemorySwap:       types.LimitedSwap,
+			nodeSwapFeatureGateEnabled: true,
+			isSwapControllerAvailable:  true,
+			cgroupVersion:              cgroupV2,
+			qosClass:                   v1.PodQOSGuaranteed,
+			expected:                   types.NoSwap,
+		},
+		{
+			name:                       "NoSwap, zero memory",
+			configuredMemorySwap:       types.LimitedSwap,
+			nodeSwapFeatureGateEnabled: true,
+			isSwapControllerAvailable:  true,
+			cgroupVersion:              cgroupV2,
+			qosClass:                   v1.PodQOSBurstable,
+			containerResourceOverride: func(c *v1.Container) {
+				c.Resources.Requests = v1.ResourceList{
+					v1.ResourceMemory: resource.MustParse("0"),
+				}
+				c.Resources.Limits = v1.ResourceList{
+					v1.ResourceMemory: resource.MustParse("0"),
+				}
+			},
+			expected: types.NoSwap,
+		},
+		{
+			name:                       "NoSwap, memory request equal to limit",
+			configuredMemorySwap:       types.LimitedSwap,
+			nodeSwapFeatureGateEnabled: true,
+			isSwapControllerAvailable:  true,
+			cgroupVersion:              cgroupV2,
+			qosClass:                   v1.PodQOSBurstable,
+			containerResourceOverride: func(c *v1.Container) {
+				c.Resources.Requests = v1.ResourceList{
+					v1.ResourceMemory: resource.MustParse("100Mi"),
+				}
+				c.Resources.Limits = v1.ResourceList{
+					v1.ResourceMemory: resource.MustParse("100Mi"),
+				}
+			},
+			expected: types.NoSwap,
+		},
+		{
+			name:                       "LimitedSwap, cgroup v2",
+			configuredMemorySwap:       types.LimitedSwap,
+			nodeSwapFeatureGateEnabled: true,
+			isSwapControllerAvailable:  true,
+			cgroupVersion:              cgroupV2,
+			qosClass:                   v1.PodQOSBurstable,
+			expected:                   types.LimitedSwap,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			m.memorySwapBehavior = string(tt.configuredMemorySwap)
+			setCgroupVersionDuringTest(tt.cgroupVersion)
+			defer setSwapControllerAvailableDuringTest(tt.isSwapControllerAvailable)()
+			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeSwap, tt.nodeSwapFeatureGateEnabled)
+			testpod := pod.DeepCopy()
+			testpod.Status.QOSClass = tt.qosClass
+			if tt.containerResourceOverride != nil {
+				tt.containerResourceOverride(&testpod.Spec.Containers[0])
+			}
+			assert.Equal(t, tt.expected, m.GetContainerSwapBehavior(testpod, &testpod.Spec.Containers[0]))
+		})
+	}
+}
+
 func TestGenerateLinuxContainerResourcesWithSwap(t *testing.T) {
 	_, _, m, err := createTestRuntimeManager()
 	assert.NoError(t, err)
@@ -999,7 +1138,7 @@ func TestGenerateLinuxContainerResourcesWithSwap(t *testing.T) {
 		qosClass                    v1.PodQOSClass
 		swapDisabledOnNode          bool
 		nodeSwapFeatureGateEnabled  bool
-		swapBehavior                string
+		swapBehavior                types.SwapBehavior
 		addContainerWithoutRequests bool
 		addGuaranteedContainer      bool
 		isCriticalPod               bool
@@ -1195,7 +1334,7 @@ func TestGenerateLinuxContainerResourcesWithSwap(t *testing.T) {
 			setCgroupVersionDuringTest(tc.cgroupVersion)
 			defer setSwapControllerAvailableDuringTest(!tc.swapDisabledOnNode)()
 			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeSwap, tc.nodeSwapFeatureGateEnabled)
-			m.memorySwapBehavior = tc.swapBehavior
+			m.memorySwapBehavior = string(tc.swapBehavior)
 
 			var resourceReqsC1, resourceReqsC2 v1.ResourceRequirements
 			switch tc.qosClass {
diff --git a/pkg/kubelet/kuberuntime/kuberuntime_container_unsupported.go b/pkg/kubelet/kuberuntime/kuberuntime_container_unsupported.go
index 8d3b483f7b0d2..bf7ccaba635d8 100644
--- a/pkg/kubelet/kuberuntime/kuberuntime_container_unsupported.go
+++ b/pkg/kubelet/kuberuntime/kuberuntime_container_unsupported.go
@@ -24,6 +24,7 @@ import (
 	runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
 	"k8s.io/kubernetes/pkg/kubelet/cm"
 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
+	"k8s.io/kubernetes/pkg/kubelet/types"
 )
 
 // applyPlatformSpecificContainerConfig applies platform specific configurations to runtimeapi.ContainerConfig.
@@ -48,3 +49,7 @@ func toKubeContainerResources(statusResources *runtimeapi.ContainerResources) *k
 func toKubeContainerUser(statusUser *runtimeapi.ContainerUser) *kubecontainer.ContainerUser {
 	return nil
 }
+
+func (m *kubeGenericRuntimeManager) GetContainerSwapBehavior(pod *v1.Pod, container *v1.Container) types.SwapBehavior {
+	return types.NoSwap
+}
diff --git a/pkg/kubelet/kuberuntime/kuberuntime_container_windows.go b/pkg/kubelet/kuberuntime/kuberuntime_container_windows.go
index abf71f55b7198..d4a77555c2878 100644
--- a/pkg/kubelet/kuberuntime/kuberuntime_container_windows.go
+++ b/pkg/kubelet/kuberuntime/kuberuntime_container_windows.go
@@ -26,6 +26,7 @@ import (
 	"k8s.io/klog/v2"
 	"k8s.io/kubernetes/pkg/kubelet/cm"
 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
+	"k8s.io/kubernetes/pkg/kubelet/types"
 	"k8s.io/kubernetes/pkg/kubelet/winstats"
 	"k8s.io/kubernetes/pkg/securitycontext"
 )
@@ -183,3 +184,7 @@ func toKubeContainerResources(statusResources *runtimeapi.ContainerResources) *k
 func toKubeContainerUser(statusUser *runtimeapi.ContainerUser) *kubecontainer.ContainerUser {
 	return nil
 }
+
+func (m *kubeGenericRuntimeManager) GetContainerSwapBehavior(pod *v1.Pod, container *v1.Container) types.SwapBehavior {
+	return types.NoSwap
+}
diff --git a/pkg/kubelet/types/constants.go b/pkg/kubelet/types/constants.go
index 56cba9c43b869..791052dbbcece 100644
--- a/pkg/kubelet/types/constants.go
+++ b/pkg/kubelet/types/constants.go
@@ -32,7 +32,9 @@ const (
 )
 
 // SwapBehavior types
+type SwapBehavior string
+
 const (
-	LimitedSwap = "LimitedSwap"
-	NoSwap      = "NoSwap"
+	LimitedSwap SwapBehavior = "LimitedSwap"
+	NoSwap      SwapBehavior = "NoSwap"
 )
diff --git a/test/e2e_node/swap_test.go b/test/e2e_node/swap_test.go
index eb1cd3d536755..5b8eb1452fb96 100644
--- a/test/e2e_node/swap_test.go
+++ b/test/e2e_node/swap_test.go
@@ -76,7 +76,7 @@ var _ = SIGDescribe("Swap", "[LinuxOnly]", ginkgo.Ordered, feature.Swap, framewo
 			if !isPodCgroupV2(f, pod) {
 				e2eskipper.Skipf("swap tests require cgroup v2")
 			}
-			gomega.Expect(getSwapBehavior()).To(gomega.Or(gomega.Equal(types.NoSwap), gomega.BeEmpty()), "NodeConformance is expected to run with NoSwap")
+			gomega.Expect(getSwapBehavior()).To(gomega.Or(gomega.Equal(string(types.NoSwap)), gomega.BeEmpty()), "NodeConformance is expected to run with NoSwap")
 
 			expectNoSwap(f, pod)
 		},
@@ -105,8 +105,8 @@ var _ = SIGDescribe("Swap", "[LinuxOnly]", ginkgo.Ordered, feature.Swap, framewo
 		enableLimitedSwap := func(ctx context.Context, initialConfig *config.KubeletConfiguration) {
 			msg := "swap behavior is already set to LimitedSwap"
 
-			if swapBehavior := initialConfig.MemorySwap.SwapBehavior; swapBehavior != types.LimitedSwap {
-				initialConfig.MemorySwap.SwapBehavior = types.LimitedSwap
+			if swapBehavior := initialConfig.MemorySwap.SwapBehavior; swapBehavior != string(types.LimitedSwap) {
+				initialConfig.MemorySwap.SwapBehavior = string(types.LimitedSwap)
 				msg = "setting swap behavior to LimitedSwap"
 			}
 
@@ -124,7 +124,7 @@ var _ = SIGDescribe("Swap", "[LinuxOnly]", ginkgo.Ordered, feature.Swap, framewo
 				if !isPodCgroupV2(f, pod) {
 					e2eskipper.Skipf("swap tests require cgroup v2")
 				}
-				gomega.Expect(getSwapBehavior()).To(gomega.Equal(types.LimitedSwap))
+				gomega.Expect(getSwapBehavior()).To(gomega.Equal(string(types.LimitedSwap)))
 
 				expectedSwapLimit := calcSwapForBurstablePod(f, pod)
 				expectLimitedSwap(f, pod, expectedSwapLimit)


Reviews:
user: login: tallclair
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T03:45:03Z
user: login: ajaysundark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T00:23:40Z
user: login: ajaysundark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T01:19:49Z
user: login: ajaysundark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T03:09:58Z
user: login: ajaysundark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T03:13:41Z
user: login: ajaysundark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T02:20:30Z
user: login: haircommander
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:54:49Z
user: login: tallclair
body: looks good, just a couple little things
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T16:57:05Z
user: login: ajaysundark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T19:01:12Z
user: login: tallclair
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T19:06:51Z
user: login: ajaysundark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T19:08:37Z

Review comments:
diff_hunk: @@ -2892,6 +2892,58 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, v1.PodResizeStatus, string)
 	return true, v1.PodResizeStatusInProgress, ""
 }
 
+func IsMemoryResize(currentPod, allocatedPod *v1.Pod) bool {
path: pkg/kubelet/kubelet.go
user: login: tallclair
body: nit: we've been using the term "desired resources" to refer to the requested resize resources before they're allocated, so I'd prefer to call this `desiredPod`
```suggestion
func IsMemoryResize(desiredPod, allocatedPod *v1.Pod) bool {
```
created_at: 2025-03-18T03:03:46Z
author_association: MEMBER
diff_hunk: @@ -2892,6 +2892,58 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, v1.PodResizeStatus, string)
 	return true, v1.PodResizeStatusInProgress, ""
 }
 
+func IsMemoryResize(currentPod, allocatedPod *v1.Pod) bool {
+	if currentPod == nil || allocatedPod == nil {
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
path: pkg/kubelet/kubelet.go
user: login: tallclair
body: container order isn't allowed to change, so you can skip the map and just compare containers by index. Probably still a good idea to make sure the names match and throw an error if they don't though.
created_at: 2025-03-18T03:05:09Z
author_association: MEMBER
diff_hunk: @@ -2892,6 +2892,58 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, v1.PodResizeStatus, string)
 	return true, v1.PodResizeStatusInProgress, ""
 }
 
+func IsMemoryResize(currentPod, allocatedPod *v1.Pod) bool {
+	if currentPod == nil || allocatedPod == nil {
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
+	for _, container := range append(allocatedPod.Spec.Containers, allocatedPod.Spec.InitContainers...) {
+		allocatedContainers[container.Name] = container
+	}
+
+	memoryChanged := false
+	for _, originalContainer := range append(currentPod.Spec.Containers, currentPod.Spec.InitContainers...) {
+		allocatedContainer, ok := allocatedContainers[originalContainer.Name]
+		if !ok {
+			continue
+		}
+		origMemRequest := originalContainer.Resources.Requests[v1.ResourceMemory]
+		newMemRequest := allocatedContainer.Resources.Requests[v1.ResourceMemory]
+		if !origMemRequest.Equal(newMemRequest) {
+			memoryChanged = true
+			break
+		}
+		origMemLimit := originalContainer.Resources.Limits[v1.ResourceMemory]
+		newMemLimit := allocatedContainer.Resources.Limits[v1.ResourceMemory]
+		if !origMemLimit.Equal(newMemLimit) {
+			memoryChanged = true
+			break
+		}
path: pkg/kubelet/kubelet.go
user: login: tallclair
body: We only care about memory requests for swap, right? Or do memory limits also factor into the swap calculation?
created_at: 2025-03-18T03:05:41Z
author_association: MEMBER
diff_hunk: @@ -2909,11 +2961,17 @@ func (kl *Kubelet) handlePodResourcesResize(pod *v1.Pod, podStatus *kubecontaine
 		}
 		// Pod allocation does not need to be updated.
 		return allocatedPod, nil
-	} else if resizable, msg := kuberuntime.IsInPlacePodVerticalScalingAllowed(pod); !resizable {
+	} else if resizable, msg := kl.containerRuntime.IsInPlacePodVerticalScalingAllowed(pod); !resizable {
 		// If there is a pending resize but the resize is not allowed, always use the allocated resources.
 		kl.recorder.Eventf(pod, v1.EventTypeWarning, events.ResizeInfeasible, msg)
 		kl.statusManager.SetPodResizeStatus(pod.UID, v1.PodResizeStatusInfeasible)
 		return allocatedPod, nil
+	} else if IsMemoryResize(pod, allocatedPod) && swapReconfigurationRequired(kl.containerRuntime, pod, allocatedPod) {
+		// If this resize involve swap set as infeasible, as IPPR with swap is not supported for beta.
+		msg = "In-place resize of pods with swap is not supported."
+		kl.recorder.Eventf(pod, v1.EventTypeWarning, events.ResizeInfeasible, msg)
+		kl.statusManager.SetPodResizeStatus(pod.UID, v1.PodResizeStatusInfeasible)
path: pkg/kubelet/kubelet.go
user: login: tallclair
body: Make sure to rebase when you get a chance. This will need to be updated with https://github.com/kubernetes/kubernetes/pull/130733
created_at: 2025-03-18T03:08:02Z
author_association: MEMBER
diff_hunk: @@ -2909,11 +2961,17 @@ func (kl *Kubelet) handlePodResourcesResize(pod *v1.Pod, podStatus *kubecontaine
 		}
 		// Pod allocation does not need to be updated.
 		return allocatedPod, nil
-	} else if resizable, msg := kuberuntime.IsInPlacePodVerticalScalingAllowed(pod); !resizable {
+	} else if resizable, msg := kl.containerRuntime.IsInPlacePodVerticalScalingAllowed(pod); !resizable {
 		// If there is a pending resize but the resize is not allowed, always use the allocated resources.
 		kl.recorder.Eventf(pod, v1.EventTypeWarning, events.ResizeInfeasible, msg)
 		kl.statusManager.SetPodResizeStatus(pod.UID, v1.PodResizeStatusInfeasible)
 		return allocatedPod, nil
+	} else if IsMemoryResize(pod, allocatedPod) && swapReconfigurationRequired(kl.containerRuntime, pod, allocatedPod) {
+		// If this resize involve swap set as infeasible, as IPPR with swap is not supported for beta.
+		msg = "In-place resize of pods with swap is not supported."
path: pkg/kubelet/kubelet.go
user: login: tallclair
body: Is swap just configured at the container level today? Should we allow resizing swap if the resize restart policy is `RestartContainer`? That would mean that when memory is resized, the container would be restarted, and the resource configuration would be recomputed before it starts. That's what we did for memory limit decreases.
created_at: 2025-03-18T03:21:34Z
author_association: MEMBER
diff_hunk: @@ -235,6 +218,33 @@ func (m *kubeGenericRuntimeManager) configureContainerSwapResources(lcr *runtime
 	}
 }
 
+// getPodSwapBehavior checks what swap behavior should be configured for the pod, considering the
+// requirements for enabling swap.
+func (m *kubeGenericRuntimeManager) GetPodSwapBehavior(pod *v1.Pod, container *v1.Container) kubelettypes.SwapBehavior {
+	c := kubelettypes.SwapBehavior(m.memorySwapBehavior)
+	if c == kubelettypes.LimitedSwap {
+		if !utilfeature.DefaultFeatureGate.Enabled(kubefeatures.NodeSwap) || !swapControllerAvailable() {
+			return kubelettypes.NoSwap
+		}
+
+		if !isCgroup2UnifiedMode() {
+			return kubelettypes.NoSwap
+		}
+
+		if kubelettypes.IsCriticalPod(pod) {
+			return kubelettypes.NoSwap
+		}
+		podQos := kubeapiqos.GetPodQOS(pod)
+		containerDoesNotRequestMemory := container.Resources.Requests.Memory().IsZero() && container.Resources.Limits.Memory().IsZero()
+		memoryRequestEqualsToLimit := container.Resources.Requests.Memory().Cmp(*container.Resources.Limits.Memory()) == 0
+		if podQos != v1.PodQOSBurstable || containerDoesNotRequestMemory || !isCgroup2UnifiedMode() || memoryRequestEqualsToLimit {
path: pkg/kubelet/kuberuntime/kuberuntime_container_linux.go
user: login: tallclair
body: `isCgroup2UnifiedMode` is already checked above
```suggestion
		if podQos != v1.PodQOSBurstable || containerDoesNotRequestMemory || memoryRequestEqualsToLimit {
```
created_at: 2025-03-18T03:29:02Z
author_association: MEMBER
diff_hunk: @@ -235,6 +218,33 @@ func (m *kubeGenericRuntimeManager) configureContainerSwapResources(lcr *runtime
 	}
 }
 
+// getPodSwapBehavior checks what swap behavior should be configured for the pod, considering the
+// requirements for enabling swap.
+func (m *kubeGenericRuntimeManager) GetPodSwapBehavior(pod *v1.Pod, container *v1.Container) kubelettypes.SwapBehavior {
path: pkg/kubelet/kuberuntime/kuberuntime_container_linux.go
user: login: tallclair
body: nit: rename
```suggestion
func (m *kubeGenericRuntimeManager) GetContainerSwapBehavior(pod *v1.Pod, container *v1.Container) kubelettypes.SwapBehavior {
```
created_at: 2025-03-18T03:30:29Z
author_association: MEMBER
diff_hunk: @@ -206,26 +206,9 @@ func (m *kubeGenericRuntimeManager) configureContainerSwapResources(lcr *runtime
 	}
path: pkg/kubelet/kuberuntime/kuberuntime_container_linux.go
user: login: tallclair
body: Should the `!swapControllerAvailable()` check be moved into `GetPodSwapBehavior` too?
created_at: 2025-03-18T03:32:14Z
author_association: MEMBER
diff_hunk: @@ -26,7 +26,7 @@ import (
 	kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
 )
 
-func IsInPlacePodVerticalScalingAllowed(pod *v1.Pod) (allowed bool, msg string) {
+func (m *kubeGenericRuntimeManager) IsInPlacePodVerticalScalingAllowed(pod *v1.Pod) (allowed bool, msg string) {
path: pkg/kubelet/kuberuntime/kuberuntime_features_linux.go
user: login: tallclair
body: You can revert this
created_at: 2025-03-18T03:33:27Z
author_association: MEMBER
diff_hunk: @@ -21,6 +21,6 @@ package kuberuntime
 
 import v1 "k8s.io/api/core/v1"
 
-func IsInPlacePodVerticalScalingAllowed(_ *v1.Pod) (allowed bool, msg string) {
+func (m *kubeGenericRuntimeManager) IsInPlacePodVerticalScalingAllowed(_ *v1.Pod) (allowed bool, msg string) {
path: pkg/kubelet/kuberuntime/kuberuntime_features_unsupported.go
user: login: tallclair
body: revert
created_at: 2025-03-18T03:33:31Z
author_association: MEMBER
diff_hunk: @@ -21,6 +21,6 @@ package kuberuntime
 
 import v1 "k8s.io/api/core/v1"
 
-func IsInPlacePodVerticalScalingAllowed(_ *v1.Pod) (allowed bool, msg string) {
+func (m *kubeGenericRuntimeManager) IsInPlacePodVerticalScalingAllowed(_ *v1.Pod) (allowed bool, msg string) {
path: pkg/kubelet/kuberuntime/kuberuntime_features_windows.go
user: login: tallclair
body: revert
created_at: 2025-03-18T03:33:34Z
author_association: MEMBER
path: pkg/kubelet/kuberuntime/kuberuntime_manager.go
user: login: tallclair
body: Revert
created_at: 2025-03-18T03:33:42Z
author_association: MEMBER
diff_hunk: @@ -137,6 +138,12 @@ type Runtime interface {
 	ListPodSandboxMetrics(ctx context.Context) ([]*runtimeapi.PodSandboxMetrics, error)
 	// GetContainerStatus returns the status for the container.
 	GetContainerStatus(ctx context.Context, id ContainerID) (*Status, error)
+	// IsInPlacePodVerticalScalingAllowed reports whether resizing the given pod is allowed on this node.
+	// If it is not allowed, a message explaining why is returned.
+	IsInPlacePodVerticalScalingAllowed(pod *v1.Pod) (allowed bool, msg string)
path: pkg/kubelet/container/runtime.go
user: login: tallclair
body: You can remove this
created_at: 2025-03-18T03:34:09Z
author_association: MEMBER
path: pkg/kubelet/container/testing/runtime_mock.go
user: login: tallclair
body: This mock needs to be regenerated. Just run `hack/update-mocks.sh`. Do it after you remove `IsInPlacePodVerticalScalingAllowed` from the Runtime interface though.
created_at: 2025-03-18T03:35:42Z
author_association: MEMBER
diff_hunk: @@ -235,6 +218,33 @@ func (m *kubeGenericRuntimeManager) configureContainerSwapResources(lcr *runtime
 	}
 }
 
+// getPodSwapBehavior checks what swap behavior should be configured for the pod, considering the
+// requirements for enabling swap.
+func (m *kubeGenericRuntimeManager) GetPodSwapBehavior(pod *v1.Pod, container *v1.Container) kubelettypes.SwapBehavior {
path: pkg/kubelet/kuberuntime/kuberuntime_container_linux.go
user: login: tallclair
body: You need to add a method stub to `kuberuntime_container_unsupported.go` and `kuberuntime_container_windows.go` too.
created_at: 2025-03-18T03:37:03Z
author_association: MEMBER
diff_hunk: @@ -2892,6 +2892,58 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, v1.PodResizeStatus, string)
 	return true, v1.PodResizeStatusInProgress, ""
 }
 
+func IsMemoryResize(currentPod, allocatedPod *v1.Pod) bool {
path: pkg/kubelet/kubelet.go
user: login: tallclair
body: nit: doesn't need to be exported
```suggestion
func isMemoryResize(currentPod, allocatedPod *v1.Pod) bool {
```
created_at: 2025-03-18T03:37:37Z
author_association: MEMBER
diff_hunk: @@ -2892,6 +2892,58 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, v1.PodResizeStatus, string)
 	return true, v1.PodResizeStatusInProgress, ""
 }
 
+func IsMemoryResize(currentPod, allocatedPod *v1.Pod) bool {
+	if currentPod == nil || allocatedPod == nil {
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
+	for _, container := range append(allocatedPod.Spec.Containers, allocatedPod.Spec.InitContainers...) {
+		allocatedContainers[container.Name] = container
+	}
+
+	memoryChanged := false
+	for _, originalContainer := range append(currentPod.Spec.Containers, currentPod.Spec.InitContainers...) {
+		allocatedContainer, ok := allocatedContainers[originalContainer.Name]
+		if !ok {
+			continue
+		}
+		origMemRequest := originalContainer.Resources.Requests[v1.ResourceMemory]
+		newMemRequest := allocatedContainer.Resources.Requests[v1.ResourceMemory]
+		if !origMemRequest.Equal(newMemRequest) {
+			memoryChanged = true
+			break
+		}
+		origMemLimit := originalContainer.Resources.Limits[v1.ResourceMemory]
+		newMemLimit := allocatedContainer.Resources.Limits[v1.ResourceMemory]
+		if !origMemLimit.Equal(newMemLimit) {
+			memoryChanged = true
+			break
+		}
path: pkg/kubelet/kubelet.go
user: login: ajaysundark
body: only requests are relevant for swap calculation. makes sense to allow patching limit 
created_at: 2025-03-19T00:23:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2909,11 +2961,17 @@ func (kl *Kubelet) handlePodResourcesResize(pod *v1.Pod, podStatus *kubecontaine
 		}
 		// Pod allocation does not need to be updated.
 		return allocatedPod, nil
-	} else if resizable, msg := kuberuntime.IsInPlacePodVerticalScalingAllowed(pod); !resizable {
+	} else if resizable, msg := kl.containerRuntime.IsInPlacePodVerticalScalingAllowed(pod); !resizable {
 		// If there is a pending resize but the resize is not allowed, always use the allocated resources.
 		kl.recorder.Eventf(pod, v1.EventTypeWarning, events.ResizeInfeasible, msg)
 		kl.statusManager.SetPodResizeStatus(pod.UID, v1.PodResizeStatusInfeasible)
 		return allocatedPod, nil
+	} else if IsMemoryResize(pod, allocatedPod) && swapReconfigurationRequired(kl.containerRuntime, pod, allocatedPod) {
+		// If this resize involve swap set as infeasible, as IPPR with swap is not supported for beta.
+		msg = "In-place resize of pods with swap is not supported."
path: pkg/kubelet/kubelet.go
user: login: ajaysundark
body: > Is swap just configured at the container level today?

Though the KEP references swap-usage at 'pods',  swap limits are set only at container granularity today.

> Should we allow resizing swap if the resize restart policy is RestartContainer?

This idea sounds okay to me. IIUC, resizePolicy is also at the container level, so should work clean.

Is this how you see the restriction look like: `in-place /resize requests on burstable pods are disallowed for containers with resizePolicy: NotRequired`    
created_at: 2025-03-19T01:19:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2892,6 +2892,58 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, v1.PodResizeStatus, string)
 	return true, v1.PodResizeStatusInProgress, ""
 }
 
+func IsMemoryResize(currentPod, allocatedPod *v1.Pod) bool {
+	if currentPod == nil || allocatedPod == nil {
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
path: pkg/kubelet/kubelet.go
user: login: ajaysundark
body: throwing an error for name change doesn't seem to fit here in this context. I'm keeping the lookup by name as it looks cleaner to me.. plmk if you think look up by index should be preferred.
created_at: 2025-03-19T03:09:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,26 +206,9 @@ func (m *kubeGenericRuntimeManager) configureContainerSwapResources(lcr *runtime
 	}
path: pkg/kubelet/kuberuntime/kuberuntime_container_linux.go
user: login: ajaysundark
body: `!swapControllerAvailable()` check is still relevant here as it is a precondition for _any_ swap-configuration at the node. However for other features, absence of which should be same as 'NoSwap', so is defined at both places.
created_at: 2025-03-19T03:13:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2909,11 +2961,17 @@ func (kl *Kubelet) handlePodResourcesResize(pod *v1.Pod, podStatus *kubecontaine
 		}
 		// Pod allocation does not need to be updated.
 		return allocatedPod, nil
-	} else if resizable, msg := kuberuntime.IsInPlacePodVerticalScalingAllowed(pod); !resizable {
+	} else if resizable, msg := kl.containerRuntime.IsInPlacePodVerticalScalingAllowed(pod); !resizable {
 		// If there is a pending resize but the resize is not allowed, always use the allocated resources.
 		kl.recorder.Eventf(pod, v1.EventTypeWarning, events.ResizeInfeasible, msg)
 		kl.statusManager.SetPodResizeStatus(pod.UID, v1.PodResizeStatusInfeasible)
 		return allocatedPod, nil
+	} else if IsMemoryResize(pod, allocatedPod) && swapReconfigurationRequired(kl.containerRuntime, pod, allocatedPod) {
+		// If this resize involve swap set as infeasible, as IPPR with swap is not supported for beta.
+		msg = "In-place resize of pods with swap is not supported."
path: pkg/kubelet/kubelet.go
user: login: ajaysundark
body: Done. 

Updated to restrict IPPR only for only containers that will not restart in resize 
created_at: 2025-03-20T02:20:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,35 +206,45 @@ func (m *kubeGenericRuntimeManager) configureContainerSwapResources(lcr *runtime
 	}
 
 	swapConfigurationHelper := newSwapConfigurationHelper(*m.machineInfo)
-	if m.memorySwapBehavior == kubelettypes.LimitedSwap {
-		if !isCgroup2UnifiedMode() {
-			swapConfigurationHelper.ConfigureNoSwap(lcr)
-			return
-		}
-	}
-
-	if !utilfeature.DefaultFeatureGate.Enabled(kubefeatures.NodeSwap) {
-		swapConfigurationHelper.ConfigureNoSwap(lcr)
-		return
-	}
-
-	if kubelettypes.IsCriticalPod(pod) {
-		swapConfigurationHelper.ConfigureNoSwap(lcr)
-		return
-	}
-
 	// NOTE(ehashman): Behavior is defined in the opencontainers runtime spec:
 	// https://github.com/opencontainers/runtime-spec/blob/1c3f411f041711bbeecf35ff7e93461ea6789220/config-linux.md#memory
-	switch m.memorySwapBehavior {
-	case kubelettypes.NoSwap:
+	switch m.GetContainerSwapBehavior(pod, container) {
+	case types.NoSwap:
 		swapConfigurationHelper.ConfigureNoSwap(lcr)
-	case kubelettypes.LimitedSwap:
+	case types.LimitedSwap:
 		swapConfigurationHelper.ConfigureLimitedSwap(lcr, pod, container)
 	default:
 		swapConfigurationHelper.ConfigureNoSwap(lcr)
 	}
 }
 
+// GetContainerSwapBehavior checks what swap behavior should be configured for a container,
+// considering the requirements for enabling swap.
+func (m *kubeGenericRuntimeManager) GetContainerSwapBehavior(pod *v1.Pod, container *v1.Container) types.SwapBehavior {
path: pkg/kubelet/kuberuntime/kuberuntime_container_linux.go
user: login: tallclair
body: This function could use a unit test
created_at: 2025-03-20T16:41:15Z
author_association: MEMBER
diff_hunk: @@ -2892,6 +2892,58 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, v1.PodResizeStatus, string)
 	return true, v1.PodResizeStatusInProgress, ""
 }
 
+func IsMemoryResize(currentPod, allocatedPod *v1.Pod) bool {
+	if currentPod == nil || allocatedPod == nil {
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
path: pkg/kubelet/kubelet.go
user: login: tallclair
body: You can leave it as is, but comparison by index is more efficient. That's the only reason to prefer it.
created_at: 2025-03-20T16:45:53Z
author_association: MEMBER
diff_hunk: @@ -2920,6 +2920,41 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, string, string) {
 	return true, "", ""
 }
 
+func disallowResizeForSwappableContainers(runtime kubecontainer.Runtime, desiredPod, allocatedPod *v1.Pod) (bool, string) {
+	if desiredPod == nil || allocatedPod == nil {
+		return false, ""
+	}
+	msg := "In-place resize of containers with swap is not supported."
path: pkg/kubelet/kubelet.go
user: login: tallclair
body: nit: inline this where it's used.
created_at: 2025-03-20T16:52:44Z
author_association: MEMBER
diff_hunk: @@ -2612,6 +2612,189 @@ func TestPodResourceAllocationReset(t *testing.T) {
 	}
 }
 
+func TestHandlePodResourcesResizeWithSwap(t *testing.T) {
+	if goruntime.GOOS == "windows" {
+		t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
+	}
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true)
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeSwap, true)
+	testKubelet := newTestKubelet(t, false)
+	defer testKubelet.Cleanup()
+	noSwapContainerName, swapContainerName := "test-container-noswap", "test-container-limitedswap"
+	runtime := testKubelet.fakeRuntime
+	runtime.SwapBehavior = map[string]kubetypes.SwapBehavior{
+		noSwapContainerName: kubetypes.NoSwap,
+		swapContainerName:   kubetypes.LimitedSwap,
+	}
+	kubelet := testKubelet.kubelet
+	// containerRestartPolicyAlways := v1.ContainerRestartPolicyAlways
path: pkg/kubelet/kubelet_test.go
user: login: tallclair
body: cleanup
created_at: 2025-03-20T16:53:18Z
author_association: MEMBER
diff_hunk: @@ -2920,6 +2920,41 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, string, string) {
 	return true, "", ""
 }
 
+func disallowResizeForSwappableContainers(runtime kubecontainer.Runtime, desiredPod, allocatedPod *v1.Pod) (bool, string) {
+	if desiredPod == nil || allocatedPod == nil {
+		return false, ""
+	}
+	msg := "In-place resize of containers with swap is not supported."
+	restartableMemoryResizePolicy := func(resizePolicies []v1.ContainerResizePolicy) bool {
+		for _, policy := range resizePolicies {
+			if policy.ResourceName == v1.ResourceMemory {
+				return policy.RestartPolicy == v1.RestartContainer
+			}
+		}
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
+	for _, container := range append(allocatedPod.Spec.Containers, allocatedPod.Spec.InitContainers...) {
+		allocatedContainers[container.Name] = container
+	}
+	for _, desiredContainer := range append(desiredPod.Spec.Containers, desiredPod.Spec.InitContainers...) {
+		allocatedContainer, ok := allocatedContainers[desiredContainer.Name]
+		if !ok {
+			continue
+		}
+		origMemRequest := desiredContainer.Resources.Requests[v1.ResourceMemory]
+		newMemRequest := allocatedContainer.Resources.Requests[v1.ResourceMemory]
+		if !origMemRequest.Equal(newMemRequest) && !restartableMemoryResizePolicy(allocatedContainer.ResizePolicy) {
+			aSwapBehavior := runtime.GetContainerSwapBehavior(allocatedPod, &desiredContainer)
+			bSwapBehavior := runtime.GetContainerSwapBehavior(allocatedPod, &allocatedContainer)
path: pkg/kubelet/kubelet.go
user: login: haircommander
body: it's not immediately apparent to me why this condition would happen. my understanding is we're not moving qos classes so either the pod is burstable and has swap or it is not and doesn't (in the limited swap case). I can see this partially future proofing against api level swap, but even then we'd probably need to check the swap field and not interpret based on memory. am I missing something here?
created_at: 2025-03-20T16:54:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2612,6 +2612,189 @@ func TestPodResourceAllocationReset(t *testing.T) {
 	}
 }
 
+func TestHandlePodResourcesResizeWithSwap(t *testing.T) {
+	if goruntime.GOOS == "windows" {
+		t.Skip("InPlacePodVerticalScaling is not currently supported for Windows")
+	}
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true)
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeSwap, true)
+	testKubelet := newTestKubelet(t, false)
+	defer testKubelet.Cleanup()
+	noSwapContainerName, swapContainerName := "test-container-noswap", "test-container-limitedswap"
+	runtime := testKubelet.fakeRuntime
+	runtime.SwapBehavior = map[string]kubetypes.SwapBehavior{
+		noSwapContainerName: kubetypes.NoSwap,
+		swapContainerName:   kubetypes.LimitedSwap,
+	}
+	kubelet := testKubelet.kubelet
+	// containerRestartPolicyAlways := v1.ContainerRestartPolicyAlways
+
+	cpu500m := resource.MustParse("500m")
+	cpu1000m := resource.MustParse("1")
+	mem500M := resource.MustParse("500Mi")
+	mem1000M := resource.MustParse("1Gi")
+	nodes := []*v1.Node{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: testKubeletHostname},
+			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),
+				},
+			},
+		},
+	}
+	kubelet.nodeLister = testNodeLister{nodes: nodes}
+	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{},
+				},
+			},
+		},
+	}
+
+	testKubelet.fakeKubeClient = fake.NewSimpleClientset(testPod)
+	kubelet.kubeClient = testKubelet.fakeKubeClient
+	defer testKubelet.fakeKubeClient.ClearActions()
+	kubelet.podManager.AddPod(testPod)
+	kubelet.podWorkers.(*fakePodWorkers).running = map[types.UID]bool{
+		testPod.UID: true,
+	}
+	defer kubelet.podManager.RemovePod(testPod)
+	tests := []struct {
+		name                  string
+		originalRequests      v1.ResourceList
path: pkg/kubelet/kubelet_test.go
user: login: tallclair
body: unused
created_at: 2025-03-20T16:54:57Z
author_association: MEMBER
diff_hunk: @@ -2920,6 +2920,41 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, string, string) {
 	return true, "", ""
 }
 
+func disallowResizeForSwappableContainers(runtime kubecontainer.Runtime, desiredPod, allocatedPod *v1.Pod) (bool, string) {
+	if desiredPod == nil || allocatedPod == nil {
+		return false, ""
+	}
+	msg := "In-place resize of containers with swap is not supported."
+	restartableMemoryResizePolicy := func(resizePolicies []v1.ContainerResizePolicy) bool {
+		for _, policy := range resizePolicies {
+			if policy.ResourceName == v1.ResourceMemory {
+				return policy.RestartPolicy == v1.RestartContainer
+			}
+		}
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
+	for _, container := range append(allocatedPod.Spec.Containers, allocatedPod.Spec.InitContainers...) {
+		allocatedContainers[container.Name] = container
+	}
+	for _, desiredContainer := range append(desiredPod.Spec.Containers, desiredPod.Spec.InitContainers...) {
+		allocatedContainer, ok := allocatedContainers[desiredContainer.Name]
+		if !ok {
+			continue
+		}
+		origMemRequest := desiredContainer.Resources.Requests[v1.ResourceMemory]
+		newMemRequest := allocatedContainer.Resources.Requests[v1.ResourceMemory]
+		if !origMemRequest.Equal(newMemRequest) && !restartableMemoryResizePolicy(allocatedContainer.ResizePolicy) {
+			aSwapBehavior := runtime.GetContainerSwapBehavior(allocatedPod, &desiredContainer)
+			bSwapBehavior := runtime.GetContainerSwapBehavior(allocatedPod, &allocatedContainer)
path: pkg/kubelet/kubelet.go
user: login: ajaysundark
body: >  either the pod is burstable and has swap or it is not 

Thanks for raising this. You're right that swap is applicable only when the pod's QoS is Burstable. However, the important detail is that the current implementation handles swap calculation per container. This means a Burstable pod can contain individual containers with differing swap behaviors based on their specific resource requests/limits. This function allows IPPR to be less restrictive for such containers, even within a Burstable pod, where swap is not presently configured.
created_at: 2025-03-20T19:01:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2920,6 +2920,40 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, string, string) {
 	return true, "", ""
 }
 
+func disallowResizeForSwappableContainers(runtime kubecontainer.Runtime, desiredPod, allocatedPod *v1.Pod) (bool, string) {
+	if desiredPod == nil || allocatedPod == nil {
+		return false, ""
+	}
+	restartableMemoryResizePolicy := func(resizePolicies []v1.ContainerResizePolicy) bool {
+		for _, policy := range resizePolicies {
+			if policy.ResourceName == v1.ResourceMemory {
+				return policy.RestartPolicy == v1.RestartContainer
+			}
+		}
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
+	for _, container := range append(allocatedPod.Spec.Containers, allocatedPod.Spec.InitContainers...) {
+		allocatedContainers[container.Name] = container
+	}
+	for _, desiredContainer := range append(desiredPod.Spec.Containers, desiredPod.Spec.InitContainers...) {
+		allocatedContainer, ok := allocatedContainers[desiredContainer.Name]
+		if !ok {
+			continue
+		}
+		origMemRequest := desiredContainer.Resources.Requests[v1.ResourceMemory]
+		newMemRequest := allocatedContainer.Resources.Requests[v1.ResourceMemory]
+		if !origMemRequest.Equal(newMemRequest) && !restartableMemoryResizePolicy(allocatedContainer.ResizePolicy) {
+			aSwapBehavior := runtime.GetContainerSwapBehavior(allocatedPod, &desiredContainer)
path: pkg/kubelet/kubelet.go
user: login: tallclair
body: Shouldn't this be desiredPod, desiredContainer?
```suggestion
			aSwapBehavior := runtime.GetContainerSwapBehavior(desiredPod, &desiredContainer)
```
created_at: 2025-03-20T19:06:50Z
author_association: MEMBER
diff_hunk: @@ -2920,6 +2920,40 @@ func (kl *Kubelet) canResizePod(pod *v1.Pod) (bool, string, string) {
 	return true, "", ""
 }
 
+func disallowResizeForSwappableContainers(runtime kubecontainer.Runtime, desiredPod, allocatedPod *v1.Pod) (bool, string) {
+	if desiredPod == nil || allocatedPod == nil {
+		return false, ""
+	}
+	restartableMemoryResizePolicy := func(resizePolicies []v1.ContainerResizePolicy) bool {
+		for _, policy := range resizePolicies {
+			if policy.ResourceName == v1.ResourceMemory {
+				return policy.RestartPolicy == v1.RestartContainer
+			}
+		}
+		return false
+	}
+	allocatedContainers := make(map[string]v1.Container)
+	for _, container := range append(allocatedPod.Spec.Containers, allocatedPod.Spec.InitContainers...) {
+		allocatedContainers[container.Name] = container
+	}
+	for _, desiredContainer := range append(desiredPod.Spec.Containers, desiredPod.Spec.InitContainers...) {
+		allocatedContainer, ok := allocatedContainers[desiredContainer.Name]
+		if !ok {
+			continue
+		}
+		origMemRequest := desiredContainer.Resources.Requests[v1.ResourceMemory]
+		newMemRequest := allocatedContainer.Resources.Requests[v1.ResourceMemory]
+		if !origMemRequest.Equal(newMemRequest) && !restartableMemoryResizePolicy(allocatedContainer.ResizePolicy) {
+			aSwapBehavior := runtime.GetContainerSwapBehavior(allocatedPod, &desiredContainer)
path: pkg/kubelet/kubelet.go
user: login: ajaysundark
body: You're right!
created_at: 2025-03-20T19:08:37Z
author_association: CONTRIBUTOR

PR comments:
user: login: k8s-ci-robot
created_at: 2025-03-14T21:30:54Z
body: This issue is currently awaiting triage.

If a SIG or subproject determines this is a relevant issue, they will accept it by applying the `triage/accepted` label and provide further guidance.

The `triage/accepted` label can be added by org members by writing `/triage accepted` in a comment.


<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: CONTRIBUTOR
user: login: k8s-ci-robot
created_at: 2025-03-14T21:30:55Z
body: Hi @ajaysundark. Thanks for your PR.

I'm waiting for a [kubernetes](https://github.com/orgs/kubernetes/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fkubernetes).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>

author_association: CONTRIBUTOR
user: login: tallclair
created_at: 2025-03-18T02:59:03Z
body: /ok-to-test
author_association: MEMBER
user: login: tallclair
created_at: 2025-03-19T23:50:43Z
body: /assign
/priority important-soon
/milestone v1.33
author_association: MEMBER
user: login: tico88612
created_at: 2025-03-20T03:04:06Z
body: Hello @tallclair @ajaysundark 
Appreciate all of your efforts with this PR! I know v1.33 milestone has just been added, and as usual we will still remind and tracked.
The code freeze has started [02:00 UTC Friday 21st March 2025](https://everytimezone.com/s/2c5e9275) . Please make sure any PRs have both lgtm and approved labels ASAP, and file an [Exception](https://github.com/kubernetes/sig-release/blob/master/releases/EXCEPTIONS.md) if you haven't done it yet.
Thanks!
author_association: MEMBER
user: login: tallclair
created_at: 2025-03-20T16:39:57Z
body: /cc @haircommander 
author_association: MEMBER
user: login: tallclair
created_at: 2025-03-20T21:10:40Z
body: /lgtm
/approve

Thanks!
author_association: MEMBER
user: login: k8s-ci-robot
created_at: 2025-03-20T21:10:46Z
body: LGTM label has been added.  <details>Git tree hash: 3c51acf35a331192643d00c36aac4eaa6a843dc7</details>
author_association: CONTRIBUTOR
user: login: tallclair
created_at: 2025-03-20T21:59:01Z
body: /test pull-kubernetes-node-kubelet-serial-podresize
/test pull-kubernetes-node-kubelet-podresize
author_association: MEMBER
user: login: tallclair
created_at: 2025-03-20T21:59:08Z
body: /hold
author_association: MEMBER
user: login: tallclair
created_at: 2025-03-20T23:06:55Z
body: /retest
author_association: MEMBER
user: login: tallclair
created_at: 2025-03-20T23:07:10Z
body: /unhold
author_association: MEMBER
user: login: k8s-ci-robot
created_at: 2025-03-21T00:07:06Z
body: @ajaysundark: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kubernetes-e2e-capz-windows-master | a09cb22e4dbca6a907d9efb7317f55d51344a4f8 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/130831/pull-kubernetes-e2e-capz-windows-master/1902859436142104576) | false | `/test pull-kubernetes-e2e-capz-windows-master`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes&repo=kubernetes&pr=130831). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aajaysundark). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes/kubernetes/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: CONTRIBUTOR
user: login: thockin
created_at: 2025-03-21T01:18:29Z
body: /approve
author_association: MEMBER
user: login: k8s-ci-robot
created_at: 2025-03-21T01:18:42Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/kubernetes/pull/130831#" title="Author self-approved">ajaysundark</a>*, *<a href="https://github.com/kubernetes/kubernetes/pull/130831#issuecomment-2741667891" title="Approved">tallclair</a>*, *<a href="https://github.com/kubernetes/kubernetes/pull/130831#issuecomment-2741999550" title="Approved">thockin</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fkubernetes).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[cmd/kubelet/OWNERS](https://github.com/kubernetes/kubernetes/blob/master/cmd/kubelet/OWNERS)~~ [tallclair,thockin]
- ~~[pkg/kubelet/OWNERS](https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/OWNERS)~~ [tallclair,thockin]
- ~~[pkg/kubelet/apis/config/OWNERS](https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/apis/config/OWNERS)~~ [thockin]
- ~~[test/e2e_node/OWNERS](https://github.com/kubernetes/kubernetes/blob/master/test/e2e_node/OWNERS)~~ [tallclair,thockin]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: ajaysundark
created_at: 2025-03-21T01:41:14Z
body: > The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:
> 
> Test name	Commit	Details	Required	Rerun command
> pull-kubernetes-e2e-capz-windows-master	[a09cb22](https://github.com/kubernetes/kubernetes/commit/a09cb22e4dbca6a907d9efb7317f55d51344a4f8)	[link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/130831/pull-kubernetes-e2e-capz-windows-master/1902859436142104576)	false	`/test pull-kubernetes-e2e-capz-windows-master`
> [Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes&repo=kubernetes&pr=130831). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aajaysundark). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes/kubernetes/issues?q=is:issue+is:open) when you hit one in your PR.

Unrelated: https://github.com/kubernetes/kubernetes/issues/130960

author_association: CONTRIBUTOR
user: login: pacoxu
created_at: 2025-03-21T02:19:05Z
body: /milestone v1.33
approved before code freeze
/skip
author_association: MEMBER