From 9f5b09eb7bb95c61b32680bbf433faa8824c9e99 Mon Sep 17 00:00:00 2001 From: Kevin Torres Date: Wed, 25 Jun 2025 17:49:50 +0000 Subject: [PATCH] Unit test pod level hugepage Default and Validation logic --- .../validate_pod_level_defaults_test.go | 366 ++++++++++++++++++ pkg/apis/core/v1/defaults_test.go | 64 ++- pkg/apis/core/validation/validation_test.go | 42 +- 3 files changed, 469 insertions(+), 3 deletions(-) create mode 100644 pkg/api/testing/validate_pod_level_defaults_test.go diff --git a/pkg/api/testing/validate_pod_level_defaults_test.go b/pkg/api/testing/validate_pod_level_defaults_test.go new file mode 100644 index 00000000000..44b8c43c0fd --- /dev/null +++ b/pkg/api/testing/validate_pod_level_defaults_test.go @@ -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 +} diff --git a/pkg/apis/core/v1/defaults_test.go b/pkg/apis/core/v1/defaults_test.go index 3af21ca06ed..9f070a49650 100644 --- a/pkg/apis/core/v1/defaults_test.go +++ b/pkg/apis/core/v1/defaults_test.go @@ -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, diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 0f1560641f7..ba34eff5d8f 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -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", },