Unit test pod level hugepage Default and Validation logic

This commit is contained in:
Kevin Torres
2025-06-25 17:49:50 +00:00
parent 845e94d370
commit 9f5b09eb7b
3 changed files with 469 additions and 3 deletions

View File

@@ -0,0 +1,366 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package testing
import (
"fmt"
"testing"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/api/legacyscheme"
apisapps1 "k8s.io/kubernetes/pkg/apis/apps/v1"
"k8s.io/kubernetes/pkg/apis/core"
corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
"k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/features"
)
func TestPodLevelResourcesDefaults(t *testing.T) {
resCPU := v1.ResourceName(v1.ResourceCPU)
valCPU1 := resource.MustParse("1")
resMemory := v1.ResourceName(v1.ResourceMemory)
valMem100Mi := resource.MustParse("100Mi")
resHugepage2Mi := v1.ResourceName("hugepages-2Mi")
valhugepage2Mi := resource.MustParse("2Mi")
valhugepage10Mi := resource.MustParse("10Mi")
resHugepage1Gi := v1.ResourceName("hugepages-1Gi")
valHugepage1Gi := resource.MustParse("1Gi")
testCases := []struct {
name string
podResources *v1.ResourceRequirements
containerResources []v1.ResourceRequirements
expectErr bool
}{
{
name: "no pod-level resources, container hugepages-2Mi:R",
containerResources: []v1.ResourceRequirements{
{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
},
},
},
expectErr: true,
},
{
name: "no pod-level resources, container hugepages-2Mi:L",
containerResources: []v1.ResourceRequirements{
{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
},
},
},
expectErr: false,
},
{
name: "no pod-level resources, container hugepages-2Mi:R/L",
containerResources: []v1.ResourceRequirements{
{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
},
},
},
expectErr: false,
},
{
name: "no pod-level resources, container hugepages-2Mi:R/L hugepages-1Gi:R/L",
containerResources: []v1.ResourceRequirements{
{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
resHugepage1Gi: valHugepage1Gi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
resHugepage1Gi: valHugepage1Gi,
},
},
},
expectErr: false,
},
{
name: "pod-level resources hugepages-2Mi:R, container hugepages-2Mi:none",
podResources: &v1.ResourceRequirements{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage10Mi,
},
},
containerResources: []v1.ResourceRequirements{
{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
},
},
},
expectErr: true,
},
{
name: "pod-level resources hugepages-2Mi:L, container hugepages-2Mi:L",
podResources: &v1.ResourceRequirements{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage10Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
},
},
containerResources: []v1.ResourceRequirements{
{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
},
},
},
expectErr: false,
},
{
name: "pod-level resources hugepages-2Mi:R/L, container hugepages-2Mi:R/L",
podResources: &v1.ResourceRequirements{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage10Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage10Mi,
},
},
containerResources: []v1.ResourceRequirements{
{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
},
},
},
expectErr: false,
},
{
name: "pod-level resources hugepages-2Mi:R/L hugepages-1Gi:R/L, container hugepages-2Mi:R/L hugepages-1Gi:R/L",
podResources: &v1.ResourceRequirements{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage10Mi,
resHugepage1Gi: valHugepage1Gi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage10Mi,
resHugepage1Gi: valHugepage1Gi,
},
},
containerResources: []v1.ResourceRequirements{
{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
resHugepage1Gi: valHugepage1Gi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
resHugepage1Gi: valHugepage1Gi,
},
},
},
expectErr: false,
},
{
name: "pod-level resources hugepages-2Mi:R, container hugepages-2Mi:L",
podResources: &v1.ResourceRequirements{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage10Mi,
},
},
containerResources: []v1.ResourceRequirements{
{
Limits: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
resHugepage2Mi: valhugepage2Mi,
},
Requests: v1.ResourceList{
resCPU: valCPU1,
resMemory: valMem100Mi,
},
},
},
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLevelResources, true)
opts := validation.PodValidationOptions{PodLevelResourcesEnabled: true}
// Step 1: Pod defaulting and validation
podForPodValidation1 := makePod(tc.podResources, tc.containerResources)
corev1.SetDefaults_Pod(podForPodValidation1)
corev1.SetDefaults_PodSpec(&podForPodValidation1.Spec)
internalPod := &core.Pod{}
if err := legacyscheme.Scheme.Convert(podForPodValidation1, internalPod, nil); err != nil {
t.Fatalf("Step 1: Failed to convert v1.Pod to core.Pod: %v", err)
}
podErrs := validation.ValidatePodSpec(&internalPod.Spec, &internalPod.ObjectMeta, field.NewPath("spec"), opts)
if len(podErrs) > 0 != tc.expectErr {
t.Errorf("Step 1: Pod validation failed. expectErr=%v, got errs: %v", tc.expectErr, podErrs.ToAggregate())
}
// Step 2: Deployment defaulting and validation
podForPodValidation2 := makePod(tc.podResources, tc.containerResources)
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "test-deploy", Namespace: "test-ns"},
Spec: appsv1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: podForPodValidation2.Spec,
},
},
}
apisapps1.SetDefaults_Deployment(deployment)
corev1.SetDefaults_PodSpec(&deployment.Spec.Template.Spec)
internalPodTemplateSpec := &core.PodTemplateSpec{}
if err := legacyscheme.Scheme.Convert(&deployment.Spec.Template, internalPodTemplateSpec, nil); err != nil {
t.Fatalf("Step 2: Failed to convert v1.PodTemplateSpec to core.PodTemplateSpec: %v", err)
}
podErrs = validation.ValidatePodTemplateSpec(internalPodTemplateSpec, field.NewPath("template"), opts)
if len(podErrs) > 0 != tc.expectErr {
t.Errorf("Step 2: Pod Template validation failed. expectErr=%v, got errs: %v", tc.expectErr, podErrs.ToAggregate())
}
// Step 3: Validate the defaulted pod spec from the deployment
podForPodValidation3 := &v1.Pod{
Spec: deployment.Spec.Template.Spec,
}
corev1.SetDefaults_Pod(podForPodValidation3)
corev1.SetDefaults_PodSpec(&podForPodValidation3.Spec)
internalPod = &core.Pod{}
if err := legacyscheme.Scheme.Convert(podForPodValidation3, internalPod, nil); err != nil {
t.Fatalf("Step 3: Failed to convert v1.Pod to core.Pod: %v", err)
}
podErrs = validation.ValidatePodSpec(&internalPod.Spec, &internalPod.ObjectMeta, field.NewPath("spec"), opts)
if len(podErrs) > 0 != tc.expectErr {
t.Errorf("Step 3: Pod validation failed. expectErr=%v, got errs: %v", tc.expectErr, podErrs.ToAggregate())
}
})
}
}
func makePod(podResources *v1.ResourceRequirements, containersResources []v1.ResourceRequirements) *v1.Pod {
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "test-pod", Namespace: "test-ns"},
Spec: v1.PodSpec{
Containers: makeContainers(containersResources),
},
}
if podResources != nil {
pod.Spec.Resources = podResources
}
return pod
}
func makeContainers(resourceRequirements []v1.ResourceRequirements) []v1.Container {
containers := []v1.Container{}
for idx, containerResources := range resourceRequirements {
container := v1.Container{
Name: fmt.Sprintf("container-%d", idx),
Image: "img", ImagePullPolicy: "Never", TerminationMessagePolicy: "File",
Resources: containerResources,
}
containers = append(containers, container)
}
return containers
}

View File

@@ -1229,7 +1229,7 @@ func TestPodResourcesDefaults(t *testing.T) {
},
},
}, {
name: "pod hugepages requests=unset limits=unset, container hugepages requests=unset limits=set",
name: "pod has cpu limit with hugepages requests=unset limits=unset, container hugepages requests=unset limits=set",
podLevelResourcesEnabled: true,
podResources: &v1.ResourceRequirements{
Limits: v1.ResourceList{
@@ -1290,6 +1290,68 @@ func TestPodResourcesDefaults(t *testing.T) {
},
},
},
}, {
name: "pod has cpu request with hugepages requests=unset limits=unset, container hugepages requests=unset limits=set",
podLevelResourcesEnabled: true,
podResources: &v1.ResourceRequirements{
Requests: v1.ResourceList{
"cpu": resource.MustParse("5m"),
},
},
containers: []v1.Container{
{
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
"cpu": resource.MustParse("2m"),
v1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("4Mi"),
},
},
}, {
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
"cpu": resource.MustParse("1m"),
v1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("2Mi"),
},
},
},
},
expectedPodSpec: v1.PodSpec{
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{
"cpu": resource.MustParse("5m"),
v1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("6Mi"),
},
Limits: v1.ResourceList{
"cpu": resource.MustParse("5m"),
v1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("6Mi"),
},
},
Containers: []v1.Container{
{
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
"cpu": resource.MustParse("2m"),
v1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("4Mi"),
},
Limits: v1.ResourceList{
"cpu": resource.MustParse("2m"),
v1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("4Mi"),
},
},
}, {
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
"cpu": resource.MustParse("1m"),
v1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("2Mi"),
},
Limits: v1.ResourceList{
"cpu": resource.MustParse("1m"),
v1.ResourceHugePagesPrefix + "2Mi": resource.MustParse("2Mi"),
},
},
},
},
},
}, {
name: "pod hugepages requests=unset limits=set, container hugepages requests=unset limits=set",
podLevelResourcesEnabled: true,

View File

@@ -20173,12 +20173,12 @@ func TestValidatePodResourceConsistency(t *testing.T) {
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceCPU: resource.MustParse("8"),
core.ResourceMemory: resource.MustParse("3Mi"),
core.ResourceMemory: resource.MustParse("7Mi"),
},
},
},
},
expectedErrors: []string{"must be greater than or equal to aggregate container requests"},
expectedErrors: []string{"must be greater than or equal to aggregate container requests", "must be greater than or equal to aggregate container requests"},
}, {
name: "container requests with resources unsupported at pod-level",
podResources: core.ResourceRequirements{
@@ -20212,6 +20212,7 @@ func TestValidatePodResourceConsistency(t *testing.T) {
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("10"),
core.ResourceMemory: resource.MustParse("10Mi"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("10Mi"),
},
},
containers: []core.Container{
@@ -20220,6 +20221,7 @@ func TestValidatePodResourceConsistency(t *testing.T) {
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("5"),
core.ResourceMemory: resource.MustParse("5Mi"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("4Mi"),
},
},
}, {
@@ -20227,6 +20229,7 @@ func TestValidatePodResourceConsistency(t *testing.T) {
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("4"),
core.ResourceMemory: resource.MustParse("3Mi"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("4Mi"),
},
},
},
@@ -20237,6 +20240,7 @@ func TestValidatePodResourceConsistency(t *testing.T) {
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("10"),
core.ResourceMemory: resource.MustParse("10Mi"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("8Mi"),
},
},
containers: []core.Container{
@@ -20245,6 +20249,7 @@ func TestValidatePodResourceConsistency(t *testing.T) {
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("5"),
core.ResourceMemory: resource.MustParse("5Mi"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("4Mi"),
},
},
}, {
@@ -20252,6 +20257,7 @@ func TestValidatePodResourceConsistency(t *testing.T) {
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("5"),
core.ResourceMemory: resource.MustParse("5Mi"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("4Mi"),
},
},
},
@@ -20281,12 +20287,41 @@ func TestValidatePodResourceConsistency(t *testing.T) {
},
},
},
}, {
name: "hugepage aggregate container limits greater than pod limits",
podResources: core.ResourceRequirements{
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("10"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("4Mi"),
},
},
containers: []core.Container{
{
Resources: core.ResourceRequirements{
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("6"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("4Mi"),
},
},
}, {
Resources: core.ResourceRequirements{
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("6"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("4Mi"),
},
},
},
},
expectedErrors: []string{
"must be greater than or equal to aggregate container limits",
},
}, {
name: "individual container limits greater than pod limits",
podResources: core.ResourceRequirements{
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("10"),
core.ResourceMemory: resource.MustParse("10Mi"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("2Mi"),
},
},
containers: []core.Container{
@@ -20295,11 +20330,14 @@ func TestValidatePodResourceConsistency(t *testing.T) {
Limits: core.ResourceList{
core.ResourceCPU: resource.MustParse("11"),
core.ResourceMemory: resource.MustParse("12Mi"),
core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("4Mi"),
},
},
},
},
expectedErrors: []string{
"must be greater than or equal to aggregate container limits",
"must be less than or equal to pod limits",
"must be less than or equal to pod limits",
"must be less than or equal to pod limits",
},