kubernetes/pkg/api/testing/validate_pod_level_defaults...

367 lines
11 KiB
Go

/*
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
}