Fix handling of empty pod-level resources

This commit is contained in:
Kevin Torres 2026-02-20 01:02:04 +00:00
parent 473f341c9e
commit eff8673537
10 changed files with 522 additions and 10 deletions

View file

@ -22,6 +22,7 @@ import (
"strconv"
"strings"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion"
@ -29,9 +30,37 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
resourcehelper "k8s.io/component-helpers/resource"
"k8s.io/kubernetes/pkg/apis/core"
)
// IsPodLevelResourcesSet check if PodLevelResources pod-level resources are set.
// It returns true if either the Requests or Limits maps are non-empty.
// Note: keep this in sync with k8s.io/component-helpers/resource.IsPodLevelResourcesSet
func IsPodLevelResourcesSet(pod *core.Pod) bool {
if pod.Spec.Resources == nil {
return false
}
if (len(pod.Spec.Resources.Requests) + len(pod.Spec.Resources.Limits)) == 0 {
return false
}
for name := range pod.Spec.Resources.Requests {
if resourcehelper.IsSupportedPodLevelResource(v1.ResourceName(name)) {
return true
}
}
for name := range pod.Spec.Resources.Limits {
if resourcehelper.IsSupportedPodLevelResource(v1.ResourceName(name)) {
return true
}
}
return false
}
// IsHugePageResourceName returns true if the resource name has the huge page
// resource prefix.
func IsHugePageResourceName(name core.ResourceName) bool {

View file

@ -22,6 +22,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/helper"
"k8s.io/kubernetes/pkg/features"
)
@ -45,7 +46,7 @@ func GetPodQOS(pod *core.Pod) core.PodQOSClass {
func ComputePodQOS(pod *core.Pod) core.PodQOSClass {
// When pod-level resources are specified, we use them to determine QoS class.
if utilfeature.DefaultFeatureGate.Enabled(features.PodLevelResources) &&
pod.Spec.Resources != nil {
(pod.Spec.Resources != nil && (!utilfeature.DefaultFeatureGate.Enabled(features.PodLevelResourcesFixKubeletQOSClass) || helper.IsPodLevelResourcesSet(pod))) {
return requirementsQOS(pod.Spec.Resources)
}

View file

@ -20,6 +20,7 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
utilfeature "k8s.io/apiserver/pkg/util/feature"
resourcehelper "k8s.io/component-helpers/resource"
"k8s.io/kubernetes/pkg/features"
)
@ -42,7 +43,7 @@ func GetPodQOS(pod *v1.Pod) v1.PodQOSClass {
func ComputePodQOS(pod *v1.Pod) v1.PodQOSClass {
// When pod-level resources are specified, we use them to determine QoS class.
if utilfeature.DefaultFeatureGate.Enabled(features.PodLevelResources) &&
pod.Spec.Resources != nil {
(pod.Spec.Resources != nil && (!utilfeature.DefaultFeatureGate.Enabled(features.PodLevelResourcesFixKubeletQOSClass) || resourcehelper.IsPodLevelResourcesSet(pod))) {
return requirementsQOS(pod.Spec.Resources)
}

View file

@ -28,6 +28,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
resourcehelper "k8s.io/component-helpers/resource"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/helper/qos"
corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
@ -35,6 +36,13 @@ import (
)
func TestComputePodQOS(t *testing.T) {
type testCase struct {
pod *v1.Pod
expected v1.PodQOSClass
podLevelResourcesEnabled bool
podLevelResourcesFixKubeletQOSClassEnabled bool
}
guaranteedResources := v1.ResourceRequirements{
// Ephemeral storage request should not affect QOS.
Requests: v1.ResourceList{
@ -78,12 +86,6 @@ func TestComputePodQOS(t *testing.T) {
},
}
type testCase struct {
pod *v1.Pod
expected v1.PodQOSClass
podLevelResourcesEnabled bool
}
// Container level test cases
singleContainerTests := []testCase{{
pod: newPod("guaranteed", []v1.Container{
@ -178,6 +180,7 @@ func TestComputePodQOS(t *testing.T) {
plrTest := tc
plrTest.pod = pod
plrTest.podLevelResourcesEnabled = true
plrTest.podLevelResourcesFixKubeletQOSClassEnabled = true
tests = append(tests, plrTest)
}
@ -188,10 +191,16 @@ func TestComputePodQOS(t *testing.T) {
pod.Name = "pod-" + pod.Name + "-plr"
pod.Spec.Resources = pod.Spec.Containers[0].Resources.DeepCopy()
pod.Spec.Containers[0].Resources = guaranteedResources
expectedQOS := tc.expected
if !resourcehelper.IsPodLevelResourcesSet(pod) {
expectedQOS = v1.PodQOSGuaranteed
pod.Name += "-fallback"
}
tests = append(tests, testCase{
pod: pod,
expected: tc.expected,
expected: expectedQOS,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: true,
})
// variant 2: PLR disabled, guaranteed pod
@ -202,12 +211,175 @@ func TestComputePodQOS(t *testing.T) {
pod: pod,
expected: tc.expected,
podLevelResourcesEnabled: false,
podLevelResourcesFixKubeletQOSClassEnabled: false,
})
}
// Explicit empty and zero pod-level resource test cases
explicitPLRTests := []testCase{
{
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "empty-pod-level-resources-should-fallback-to-containers"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "guaranteed", Resources: v1.ResourceRequirements{Requests: getResourceList("100m", "100Mi"), Limits: getResourceList("100m", "100Mi")}},
},
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{},
Limits: v1.ResourceList{},
},
},
},
expected: v1.PodQOSGuaranteed,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: true,
},
{
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "completely-empty-pod-level-resources-should-fallback-to-containers"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "guaranteed", Resources: v1.ResourceRequirements{Requests: getResourceList("100m", "100Mi"), Limits: getResourceList("100m", "100Mi")}},
},
Resources: &v1.ResourceRequirements{},
},
},
expected: v1.PodQOSGuaranteed,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: true,
},
{
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "completely-empty-pod-level-resources-old-behavior"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "guaranteed", Resources: v1.ResourceRequirements{Requests: getResourceList("100m", "100Mi"), Limits: getResourceList("100m", "100Mi")}},
},
Resources: &v1.ResourceRequirements{},
},
},
expected: v1.PodQOSBestEffort,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: false,
},
{
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "nil-pod-level-resources-should-fallback-to-containers"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "guaranteed", Resources: v1.ResourceRequirements{Requests: getResourceList("100m", "100Mi"), Limits: getResourceList("100m", "100Mi")}},
},
},
},
expected: v1.PodQOSGuaranteed,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: true,
},
{
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "zero-pod-level-resources-should-not-fallback-to-containers"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "guaranteed", Resources: v1.ResourceRequirements{Requests: getResourceList("100m", "100Mi"), Limits: getResourceList("100m", "100Mi")}},
},
Resources: &v1.ResourceRequirements{
Requests: getResourceList("0", "0"),
Limits: getResourceList("0", "0"),
},
},
},
expected: v1.PodQOSBestEffort,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: true,
},
{
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "zero-pod-level-resources-should-not-fallback-to-containers-fix-disabled"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "guaranteed", Resources: v1.ResourceRequirements{Requests: getResourceList("100m", "100Mi"), Limits: getResourceList("100m", "100Mi")}},
},
Resources: &v1.ResourceRequirements{
Requests: getResourceList("0", "0"),
Limits: getResourceList("0", "0"),
},
},
},
expected: v1.PodQOSBestEffort,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: false,
},
{
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "empty-pod-level-resources-old-behavior"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "guaranteed", Resources: v1.ResourceRequirements{Requests: getResourceList("100m", "100Mi"), Limits: getResourceList("100m", "100Mi")}},
},
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{},
Limits: v1.ResourceList{},
},
},
},
expected: v1.PodQOSBestEffort,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: false,
},
{
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "guaranteed-with-pod-level-resources-and-hugepages"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "best-effort"},
},
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("10m"),
v1.ResourceMemory: resource.MustParse("100Mi"),
v1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
},
Limits: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("10m"),
v1.ResourceMemory: resource.MustParse("100Mi"),
v1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
},
},
},
},
expected: v1.PodQOSGuaranteed,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: true,
},
{
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "burstable-with-pod-level-resources-and-hugepages"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "best-effort"},
},
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("10m"),
v1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
},
Limits: v1.ResourceList{
v1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
},
},
},
},
expected: v1.PodQOSBurstable,
podLevelResourcesEnabled: true,
podLevelResourcesFixKubeletQOSClassEnabled: true,
},
}
tests = append(tests, explicitPLRTests...)
for _, testCase := range tests {
t.Run(testCase.pod.Name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLevelResources, testCase.podLevelResourcesEnabled)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLevelResourcesFixKubeletQOSClass, testCase.podLevelResourcesFixKubeletQOSClassEnabled)
if actual := ComputePodQOS(testCase.pod); testCase.expected != actual {
t.Errorf("ComputePodQOS error: expected: %s, actual: %s;\npod = %s", testCase.expected, actual, prettyPrintPod(testCase.pod))
}

View file

@ -767,6 +767,12 @@ const (
// Enables specifying resources at pod-level.
PodLevelResources featuregate.Feature = "PodLevelResources"
// owner: @KevinTMtz
// key: https://kep.k8s.io/2837
//
// Fixes handling of nil/empty pod-level resources values for QoS classes.
PodLevelResourcesFixKubeletQOSClass featuregate.Feature = "PodLevelResourcesFixKubeletQOSClass"
// owner: @knight42
// kep: https://kep.k8s.io/3288
//
@ -1730,6 +1736,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
{Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.Beta},
},
PodLevelResourcesFixKubeletQOSClass: {
{Version: version.MustParse("1.37"), Default: true, PreRelease: featuregate.Beta},
},
PodLogsQuerySplitStreams: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
@ -2484,6 +2494,8 @@ var defaultKubernetesFeatureGateDependencies = map[featuregate.Feature][]feature
PodLevelResources: {},
PodLevelResourcesFixKubeletQOSClass: {PodLevelResources},
PodLogsQuerySplitStreams: {},
PodObservedGenerationTracking: {},

View file

@ -79,6 +79,7 @@ func IsSupportedPodLevelResource(name v1.ResourceName) bool {
// IsPodLevelResourcesSet check if PodLevelResources pod-level resources are set.
// It returns true if either the Requests or Limits maps are non-empty.
// Note: keep this in sync with k8s.io/kubernetes/pkg/apis/core/helper.IsPodLevelResourcesSet
func IsPodLevelResourcesSet(pod *v1.Pod) bool {
if pod.Spec.Resources == nil {
return false

View file

@ -19,6 +19,7 @@ package qos
import (
core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
resourcehelper "k8s.io/component-helpers/resource"
)
var supportedQoSComputeResources = sets.New(core.ResourceCPU, core.ResourceMemory)
@ -39,7 +40,7 @@ func GetPodQOS(pod *core.Pod) core.PodQOSClass {
// A pod is Burstable if cpu & memory limits and requests do not match across all containers.
func ComputePodQOS(pod *core.Pod) core.PodQOSClass {
// When pod-level resources are specified, we use them to determine QoS class.
if pod.Spec.Resources != nil {
if resourcehelper.IsPodLevelResourcesSet(pod) {
return requirementsQOS(pod.Spec.Resources)
}

View file

@ -189,6 +189,294 @@ func TestGetPodQOS(t *testing.T) {
},
want: corev1.PodQOSBestEffort,
},
{
name: "empty pod level resources should fallback to containers",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{},
Limits: corev1.ResourceList{},
},
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "completely empty pod level resources should fallback to containers",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{},
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "nil pod level resources should fallback to containers",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "zero pod level resources should not fallback to containers",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
},
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
},
},
want: corev1.PodQOSBestEffort,
},
{
name: "guaranteed with pod level resources",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("100Mi"),
},
},
},
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "burstable with pod level resources",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("100Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "burstable with pod level requests only",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "container with zero requests limits",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
},
},
},
},
},
want: corev1.PodQOSBestEffort,
},
{
name: "guaranteed with pod level resources and hugepages",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("10m"),
corev1.ResourceMemory: resource.MustParse("100Mi"),
corev1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("10m"),
corev1.ResourceMemory: resource.MustParse("100Mi"),
corev1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
},
},
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "burstable with pod level resources and hugepages",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("10m"),
corev1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceName("hugepages-2Mi"): resource.MustParse("100Mi"),
},
},
Containers: []corev1.Container{
{
Name: "container1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
},
},
},
want: corev1.PodQOSBurstable,
},
}
for _, tt := range tests {

View file

@ -149,6 +149,7 @@
| PodGroupPreemptionPolicy | | | 1.37 | | | | GenericWorkload | [code](https://cs.k8s.io/?q=%5CbPodGroupPreemptionPolicy%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbPodGroupPreemptionPolicy%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| PodLevelResourceManagers | | | 1.36 | | | | PodLevelResources | [code](https://cs.k8s.io/?q=%5CbPodLevelResourceManagers%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbPodLevelResourceManagers%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| PodLevelResources | :ballot_box_with_check: 1.34+ | | 1.321.33 | 1.34 | | | | [code](https://cs.k8s.io/?q=%5CbPodLevelResources%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbPodLevelResources%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| PodLevelResourcesFixKubeletQOSClass | :ballot_box_with_check: 1.37+ | | | 1.37 | | | PodLevelResources | [code](https://cs.k8s.io/?q=%5CbPodLevelResourcesFixKubeletQOSClass%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbPodLevelResourcesFixKubeletQOSClass%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| PodLogsQuerySplitStreams | | | 1.32 | | | | | [code](https://cs.k8s.io/?q=%5CbPodLogsQuerySplitStreams%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbPodLogsQuerySplitStreams%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| PodObservedGenerationTracking | :ballot_box_with_check: 1.34+ | :closed_lock_with_key: 1.35+ | 1.33 | 1.34 | 1.35 | | | [code](https://cs.k8s.io/?q=%5CbPodObservedGenerationTracking%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbPodObservedGenerationTracking%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
| PodReadyToStartContainersCondition | :ballot_box_with_check: 1.29+ | | 1.28 | 1.29 | | | | [code](https://cs.k8s.io/?q=%5CbPodReadyToStartContainersCondition%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbPodReadyToStartContainersCondition%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |

View file

@ -1375,6 +1375,12 @@
lockToDefault: false
preRelease: Beta
version: "1.34"
- name: PodLevelResourcesFixKubeletQOSClass
versionedSpecs:
- default: true
lockToDefault: false
preRelease: Beta
version: "1.37"
- name: PodLogsQuerySplitStreams
versionedSpecs:
- default: false