diff --git a/pkg/apis/core/helper/helpers.go b/pkg/apis/core/helper/helpers.go index 00592c1e4b0..af4b5b8db2a 100644 --- a/pkg/apis/core/helper/helpers.go +++ b/pkg/apis/core/helper/helpers.go @@ -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 { diff --git a/pkg/apis/core/helper/qos/qos.go b/pkg/apis/core/helper/qos/qos.go index c8f12439477..ef419d5d02c 100644 --- a/pkg/apis/core/helper/qos/qos.go +++ b/pkg/apis/core/helper/qos/qos.go @@ -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) } diff --git a/pkg/apis/core/v1/helper/qos/qos.go b/pkg/apis/core/v1/helper/qos/qos.go index c2871605108..654a1d68167 100644 --- a/pkg/apis/core/v1/helper/qos/qos.go +++ b/pkg/apis/core/v1/helper/qos/qos.go @@ -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) } diff --git a/pkg/apis/core/v1/helper/qos/qos_test.go b/pkg/apis/core/v1/helper/qos/qos_test.go index 6e45aa1242c..4796f493149 100644 --- a/pkg/apis/core/v1/helper/qos/qos_test.go +++ b/pkg/apis/core/v1/helper/qos/qos_test.go @@ -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)) } diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 56bf70a429d..145fcc1f4bb 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -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: {}, diff --git a/staging/src/k8s.io/component-helpers/resource/helpers.go b/staging/src/k8s.io/component-helpers/resource/helpers.go index 23f57a7fca1..fd2960f27d2 100644 --- a/staging/src/k8s.io/component-helpers/resource/helpers.go +++ b/staging/src/k8s.io/component-helpers/resource/helpers.go @@ -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 diff --git a/staging/src/k8s.io/kubectl/pkg/util/qos/qos.go b/staging/src/k8s.io/kubectl/pkg/util/qos/qos.go index 1761f769904..e2254062378 100644 --- a/staging/src/k8s.io/kubectl/pkg/util/qos/qos.go +++ b/staging/src/k8s.io/kubectl/pkg/util/qos/qos.go @@ -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) } diff --git a/staging/src/k8s.io/kubectl/pkg/util/qos/qos_test.go b/staging/src/k8s.io/kubectl/pkg/util/qos/qos_test.go index 0c0a630056c..187ca6b5b13 100644 --- a/staging/src/k8s.io/kubectl/pkg/util/qos/qos_test.go +++ b/staging/src/k8s.io/kubectl/pkg/util/qos/qos_test.go @@ -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 { diff --git a/test/compatibility_lifecycle/reference/feature_list.md b/test/compatibility_lifecycle/reference/feature_list.md index 3ee2c8b59e2..e2d94e17dba 100644 --- a/test/compatibility_lifecycle/reference/feature_list.md +++ b/test/compatibility_lifecycle/reference/feature_list.md @@ -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.32–1.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) | diff --git a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml index 2c268c06abc..60193037d87 100644 --- a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml +++ b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml @@ -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