diff --git a/pkg/api/pod/BUILD b/pkg/api/pod/BUILD index 4205b162618..1b59f73e07e 100644 --- a/pkg/api/pod/BUILD +++ b/pkg/api/pod/BUILD @@ -12,9 +12,13 @@ go_library( importpath = "k8s.io/kubernetes/pkg/api/pod", deps = [ "//pkg/apis/core:go_default_library", + "//pkg/apis/core/helper:go_default_library", + "//pkg/apis/core/v1/helper:go_default_library", + "//pkg/apis/core/validation:go_default_library", "//pkg/features:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", ], ) diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go index 623debf6c36..8e8092d228e 100644 --- a/pkg/api/pod/util.go +++ b/pkg/api/pod/util.go @@ -21,8 +21,13 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" utilfeature "k8s.io/apiserver/pkg/util/feature" api "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/apis/core/helper" + v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper" + "k8s.io/kubernetes/pkg/apis/core/validation" + apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" "k8s.io/kubernetes/pkg/features" ) @@ -283,6 +288,91 @@ func UpdatePodCondition(status *api.PodStatus, condition *api.PodCondition) bool return !isEqual } +// usesHugePagesInProjectedVolume returns true if hugepages are used in downward api for volume +func usesHugePagesInProjectedVolume(podSpec *api.PodSpec) bool { + // determine if any container is using hugepages in downward api volume + for _, volumeSource := range podSpec.Volumes { + if volumeSource.DownwardAPI != nil { + for _, item := range volumeSource.DownwardAPI.Items { + if item.ResourceFieldRef != nil { + if strings.HasPrefix(item.ResourceFieldRef.Resource, "requests.hugepages-") || strings.HasPrefix(item.ResourceFieldRef.Resource, "limits.hugepages-") { + return true + } + } + } + } + } + return false +} + +// usesHugePagesInProjectedEnv returns true if hugepages are used in downward api for volume +func usesHugePagesInProjectedEnv(item api.Container) bool { + for _, env := range item.Env { + if env.ValueFrom != nil { + if env.ValueFrom.ResourceFieldRef != nil { + if strings.HasPrefix(env.ValueFrom.ResourceFieldRef.Resource, "requests.hugepages-") || strings.HasPrefix(env.ValueFrom.ResourceFieldRef.Resource, "limits.hugepages-") { + return true + } + } + } + } + return false +} + +// usesMultipleHugePageResources returns true if the pod spec uses more than +// one size of hugepage +func usesMultipleHugePageResources(podSpec *api.PodSpec) bool { + hugePageResources := sets.NewString() + resourceSet := helper.ToPodResourcesSet(podSpec) + for resourceStr := range resourceSet { + if v1helper.IsHugePageResourceName(v1.ResourceName(resourceStr)) { + hugePageResources.Insert(resourceStr) + } + } + return len(hugePageResources) > 1 +} + +// GetValidationOptionsFromPodSpec returns validation options based on pod specs +func GetValidationOptionsFromPodSpec(podSpec, oldPodSpec *api.PodSpec) apivalidation.PodValidationOptions { + // default pod validation options based on feature gate + opts := validation.PodValidationOptions{ + // Allow multiple huge pages on pod create if feature is enabled + AllowMultipleHugePageResources: utilfeature.DefaultFeatureGate.Enabled(features.HugePageStorageMediumSize), + // Allow pod spec to use hugepages in downward API if feature is enabled + AllowDownwardAPIHugePages: utilfeature.DefaultFeatureGate.Enabled(features.DownwardAPIHugePages), + } + // if we are not doing an update operation, just return with default options + if oldPodSpec == nil { + return opts + } + // if old spec used multiple huge page sizes, we must allow it + opts.AllowMultipleHugePageResources = opts.AllowMultipleHugePageResources || usesMultipleHugePageResources(oldPodSpec) + // if old spec used hugepages in downward api, we must allow it + opts.AllowDownwardAPIHugePages = opts.AllowDownwardAPIHugePages || usesHugePagesInProjectedVolume(oldPodSpec) + // determine if any container is using hugepages in env var + if !opts.AllowDownwardAPIHugePages { + VisitContainers(oldPodSpec, AllContainers, func(c *api.Container, containerType ContainerType) bool { + opts.AllowDownwardAPIHugePages = opts.AllowDownwardAPIHugePages || usesHugePagesInProjectedEnv(*c) + return !opts.AllowDownwardAPIHugePages + }) + } + return opts +} + +// GetValidationOptionsFromPodTemplate will return pod validation options for specified template. +func GetValidationOptionsFromPodTemplate(podTemplate, oldPodTemplate *api.PodTemplateSpec) apivalidation.PodValidationOptions { + var newPodSpec, oldPodSpec *api.PodSpec + // we have to be careful about nil pointers here + // replication controller in particular is prone to passing nil + if podTemplate != nil { + newPodSpec = &podTemplate.Spec + } + if oldPodTemplate != nil { + oldPodSpec = &oldPodTemplate.Spec + } + return GetValidationOptionsFromPodSpec(newPodSpec, oldPodSpec) +} + // DropDisabledTemplateFields removes disabled fields from the pod template metadata and spec. // This should be called from PrepareForCreate/PrepareForUpdate for all resources containing a PodTemplateSpec func DropDisabledTemplateFields(podTemplate, oldPodTemplate *api.PodTemplateSpec) { diff --git a/pkg/api/testing/BUILD b/pkg/api/testing/BUILD index 6f3548cc6b4..f2664685548 100644 --- a/pkg/api/testing/BUILD +++ b/pkg/api/testing/BUILD @@ -98,6 +98,7 @@ go_test( embed = [":go_default_library"], deps = [ "//pkg/api/legacyscheme:go_default_library", + "//pkg/api/pod:go_default_library", "//pkg/api/testing/compat:go_default_library", "//pkg/apis/apps:go_default_library", "//pkg/apis/apps/v1:go_default_library", diff --git a/pkg/api/testing/backward_compatibility_test.go b/pkg/api/testing/backward_compatibility_test.go index 633b5ee5b9d..c9cfb888a39 100644 --- a/pkg/api/testing/backward_compatibility_test.go +++ b/pkg/api/testing/backward_compatibility_test.go @@ -19,14 +19,14 @@ package testing import ( "testing" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" + podutil "k8s.io/kubernetes/pkg/api/pod" "k8s.io/kubernetes/pkg/api/testing/compat" api "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/apis/core/validation" - _ "k8s.io/kubernetes/pkg/apis/core/install" + "k8s.io/kubernetes/pkg/apis/core/validation" ) func TestCompatibility_v1_PodSecurityContext(t *testing.T) { @@ -159,7 +159,8 @@ func TestCompatibility_v1_PodSecurityContext(t *testing.T) { } validator := func(obj runtime.Object) field.ErrorList { - return validation.ValidatePodSpec(&(obj.(*api.Pod).Spec), &(obj.(*api.Pod).ObjectMeta), field.NewPath("spec")) + opts := podutil.GetValidationOptionsFromPodSpec(&(obj.(*api.Pod).Spec), nil) + return validation.ValidatePodSpec(&(obj.(*api.Pod).Spec), &(obj.(*api.Pod).ObjectMeta), field.NewPath("spec"), opts) } for _, tc := range cases { diff --git a/pkg/api/v1/resource/helpers.go b/pkg/api/v1/resource/helpers.go index 0464b84f98f..9d69a16588a 100644 --- a/pkg/api/v1/resource/helpers.go +++ b/pkg/api/v1/resource/helpers.go @@ -20,8 +20,9 @@ import ( "fmt" "math" "strconv" + "strings" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/features" @@ -193,7 +194,20 @@ func ExtractContainerResourceValue(fs *v1.ResourceFieldSelector, container *v1.C case "requests.ephemeral-storage": return convertResourceEphemeralStorageToString(container.Resources.Requests.StorageEphemeral(), divisor) } - + // handle extended standard resources with dynamic names + // example: requests.hugepages- or limits.hugepages- + if strings.HasPrefix(fs.Resource, "requests.") { + resourceName := v1.ResourceName(strings.TrimPrefix(fs.Resource, "requests.")) + if IsHugePageResourceName(resourceName) { + return convertResourceHugePagesToString(container.Resources.Requests.Name(resourceName, resource.BinarySI), divisor) + } + } + if strings.HasPrefix(fs.Resource, "limits.") { + resourceName := v1.ResourceName(strings.TrimPrefix(fs.Resource, "limits.")) + if IsHugePageResourceName(resourceName) { + return convertResourceHugePagesToString(container.Resources.Limits.Name(resourceName, resource.BinarySI), divisor) + } + } return "", fmt.Errorf("unsupported container resource : %v", fs.Resource) } @@ -211,6 +225,13 @@ func convertResourceMemoryToString(memory *resource.Quantity, divisor resource.Q return strconv.FormatInt(m, 10), nil } +// convertResourceHugePagesToString converts hugepages value to the format of divisor and returns +// ceiling of the value. +func convertResourceHugePagesToString(hugePages *resource.Quantity, divisor resource.Quantity) (string, error) { + m := int64(math.Ceil(float64(hugePages.Value()) / float64(divisor.Value()))) + return strconv.FormatInt(m, 10), nil +} + // convertResourceEphemeralStorageToString converts ephemeral storage value to the format of divisor and returns // ceiling of the value. func convertResourceEphemeralStorageToString(ephemeralStorage *resource.Quantity, divisor resource.Quantity) (string, error) { @@ -240,6 +261,8 @@ func MergeContainerResourceLimits(container *v1.Container, if container.Resources.Limits == nil { container.Resources.Limits = make(v1.ResourceList) } + // NOTE: we exclude hugepages-* resources because hugepages are never overcommitted. + // This means that the container always has a limit specified. for _, resource := range []v1.ResourceName{v1.ResourceCPU, v1.ResourceMemory, v1.ResourceEphemeralStorage} { if quantity, exists := container.Resources.Limits[resource]; !exists || quantity.IsZero() { if cap, exists := allocatable[resource]; exists { @@ -248,3 +271,9 @@ func MergeContainerResourceLimits(container *v1.Container, } } } + +// IsHugePageResourceName returns true if the resource name has the huge page +// resource prefix. +func IsHugePageResourceName(name v1.ResourceName) bool { + return strings.HasPrefix(string(name), v1.ResourceHugePagesPrefix) +} diff --git a/pkg/apis/apps/validation/BUILD b/pkg/apis/apps/validation/BUILD index c34f52f3987..b6967f8b796 100644 --- a/pkg/apis/apps/validation/BUILD +++ b/pkg/apis/apps/validation/BUILD @@ -32,6 +32,7 @@ go_test( deps = [ "//pkg/apis/apps:go_default_library", "//pkg/apis/core:go_default_library", + "//pkg/apis/core/validation:go_default_library", "//pkg/features:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/pkg/apis/apps/validation/validation.go b/pkg/apis/apps/validation/validation.go index 6ac73cb6b7e..e297c8a75b9 100644 --- a/pkg/apis/apps/validation/validation.go +++ b/pkg/apis/apps/validation/validation.go @@ -241,17 +241,17 @@ func ValidateControllerRevisionUpdate(newHistory, oldHistory *apps.ControllerRev } // ValidateDaemonSet tests if required fields in the DaemonSet are set. -func ValidateDaemonSet(ds *apps.DaemonSet) field.ErrorList { +func ValidateDaemonSet(ds *apps.DaemonSet, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&ds.ObjectMeta, true, ValidateDaemonSetName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateDaemonSetSpec(&ds.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateDaemonSetSpec(&ds.Spec, field.NewPath("spec"), opts)...) return allErrs } // ValidateDaemonSetUpdate tests if required fields in the DaemonSet are set. -func ValidateDaemonSetUpdate(ds, oldDS *apps.DaemonSet) field.ErrorList { +func ValidateDaemonSetUpdate(ds, oldDS *apps.DaemonSet, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := apivalidation.ValidateObjectMetaUpdate(&ds.ObjectMeta, &oldDS.ObjectMeta, field.NewPath("metadata")) allErrs = append(allErrs, ValidateDaemonSetSpecUpdate(&ds.Spec, &oldDS.Spec, field.NewPath("spec"))...) - allErrs = append(allErrs, ValidateDaemonSetSpec(&ds.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateDaemonSetSpec(&ds.Spec, field.NewPath("spec"), opts)...) return allErrs } @@ -307,7 +307,7 @@ func ValidateDaemonSetStatusUpdate(ds, oldDS *apps.DaemonSet) field.ErrorList { } // ValidateDaemonSetSpec tests if required fields in the DaemonSetSpec are set. -func ValidateDaemonSetSpec(spec *apps.DaemonSetSpec, fldPath *field.Path) field.ErrorList { +func ValidateDaemonSetSpec(spec *apps.DaemonSetSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, fldPath.Child("selector"))...) @@ -320,7 +320,7 @@ func ValidateDaemonSetSpec(spec *apps.DaemonSetSpec, fldPath *field.Path) field. allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "empty selector is invalid for daemonset")) } - allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"))...) + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"), opts)...) // Daemons typically run on more than one node, so mark Read-Write persistent disks as invalid. allErrs = append(allErrs, apivalidation.ValidateReadOnlyPersistentDisks(spec.Template.Spec.Volumes, fldPath.Child("template", "spec", "volumes"))...) // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). @@ -474,7 +474,7 @@ func ValidateRollback(rollback *apps.RollbackConfig, fldPath *field.Path) field. } // ValidateDeploymentSpec validates given deployment spec. -func ValidateDeploymentSpec(spec *apps.DeploymentSpec, fldPath *field.Path) field.ErrorList { +func ValidateDeploymentSpec(spec *apps.DeploymentSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...) @@ -491,7 +491,7 @@ func ValidateDeploymentSpec(spec *apps.DeploymentSpec, fldPath *field.Path) fiel if err != nil { allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "invalid label selector")) } else { - allErrs = append(allErrs, ValidatePodTemplateSpecForReplicaSet(&spec.Template, selector, spec.Replicas, fldPath.Child("template"))...) + allErrs = append(allErrs, ValidatePodTemplateSpecForReplicaSet(&spec.Template, selector, spec.Replicas, fldPath.Child("template"), opts)...) } allErrs = append(allErrs, ValidateDeploymentStrategy(&spec.Strategy, fldPath.Child("strategy"))...) @@ -541,9 +541,9 @@ func ValidateDeploymentStatus(status *apps.DeploymentStatus, fldPath *field.Path } // ValidateDeploymentUpdate tests if an update to a Deployment is valid. -func ValidateDeploymentUpdate(update, old *apps.Deployment) field.ErrorList { +func ValidateDeploymentUpdate(update, old *apps.Deployment, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateDeploymentSpec(&update.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateDeploymentSpec(&update.Spec, field.NewPath("spec"), opts)...) return allErrs } @@ -564,9 +564,9 @@ func ValidateDeploymentStatusUpdate(update, old *apps.Deployment) field.ErrorLis } // ValidateDeployment validates a given Deployment. -func ValidateDeployment(obj *apps.Deployment) field.ErrorList { +func ValidateDeployment(obj *apps.Deployment, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateDeploymentName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateDeploymentSpec(&obj.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateDeploymentSpec(&obj.Spec, field.NewPath("spec"), opts)...) return allErrs } @@ -587,17 +587,17 @@ func ValidateDeploymentRollback(obj *apps.DeploymentRollback) field.ErrorList { var ValidateReplicaSetName = apimachineryvalidation.NameIsDNSSubdomain // ValidateReplicaSet tests if required fields in the ReplicaSet are set. -func ValidateReplicaSet(rs *apps.ReplicaSet) field.ErrorList { +func ValidateReplicaSet(rs *apps.ReplicaSet, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&rs.ObjectMeta, true, ValidateReplicaSetName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateReplicaSetSpec(&rs.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateReplicaSetSpec(&rs.Spec, field.NewPath("spec"), opts)...) return allErrs } // ValidateReplicaSetUpdate tests if required fields in the ReplicaSet are set. -func ValidateReplicaSetUpdate(rs, oldRs *apps.ReplicaSet) field.ErrorList { +func ValidateReplicaSetUpdate(rs, oldRs *apps.ReplicaSet, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&rs.ObjectMeta, &oldRs.ObjectMeta, field.NewPath("metadata"))...) - allErrs = append(allErrs, ValidateReplicaSetSpec(&rs.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateReplicaSetSpec(&rs.Spec, field.NewPath("spec"), opts)...) return allErrs } @@ -634,7 +634,7 @@ func ValidateReplicaSetStatus(status apps.ReplicaSetStatus, fldPath *field.Path) } // ValidateReplicaSetSpec tests if required fields in the ReplicaSet spec are set. -func ValidateReplicaSetSpec(spec *apps.ReplicaSetSpec, fldPath *field.Path) field.ErrorList { +func ValidateReplicaSetSpec(spec *apps.ReplicaSetSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...) @@ -653,13 +653,13 @@ func ValidateReplicaSetSpec(spec *apps.ReplicaSetSpec, fldPath *field.Path) fiel if err != nil { allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "invalid label selector")) } else { - allErrs = append(allErrs, ValidatePodTemplateSpecForReplicaSet(&spec.Template, selector, spec.Replicas, fldPath.Child("template"))...) + allErrs = append(allErrs, ValidatePodTemplateSpecForReplicaSet(&spec.Template, selector, spec.Replicas, fldPath.Child("template"), opts)...) } return allErrs } // ValidatePodTemplateSpecForReplicaSet validates the given template and ensures that it is in accordance with the desired selector and replicas. -func ValidatePodTemplateSpecForReplicaSet(template *api.PodTemplateSpec, selector labels.Selector, replicas int32, fldPath *field.Path) field.ErrorList { +func ValidatePodTemplateSpecForReplicaSet(template *api.PodTemplateSpec, selector labels.Selector, replicas int32, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} if template == nil { allErrs = append(allErrs, field.Required(fldPath, "")) @@ -671,7 +671,7 @@ func ValidatePodTemplateSpecForReplicaSet(template *api.PodTemplateSpec, selecto allErrs = append(allErrs, field.Invalid(fldPath.Child("metadata", "labels"), template.Labels, "`selector` does not match template `labels`")) } } - allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(template, fldPath)...) + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(template, fldPath, opts)...) if replicas > 1 { allErrs = append(allErrs, apivalidation.ValidateReadOnlyPersistentDisks(template.Spec.Volumes, fldPath.Child("spec", "volumes"))...) } diff --git a/pkg/apis/apps/validation/validation_test.go b/pkg/apis/apps/validation/validation_test.go index 41ecdc12034..10a5d23b83f 100644 --- a/pkg/apis/apps/validation/validation_test.go +++ b/pkg/apis/apps/validation/validation_test.go @@ -30,6 +30,7 @@ import ( featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/kubernetes/pkg/apis/apps" api "k8s.io/kubernetes/pkg/apis/core" + corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" "k8s.io/kubernetes/pkg/features" ) @@ -1552,7 +1553,7 @@ func TestValidateDaemonSetUpdate(t *testing.T) { if len(successCase.old.ObjectMeta.ResourceVersion) == 0 || len(successCase.update.ObjectMeta.ResourceVersion) == 0 { t.Errorf("%q has incorrect test setup with no resource version set", testName) } - if errs := ValidateDaemonSetUpdate(&successCase.update, &successCase.old); len(errs) != 0 { + if errs := ValidateDaemonSetUpdate(&successCase.update, &successCase.old, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("%q expected no error, but got: %v", testName, errs) } } @@ -1770,7 +1771,7 @@ func TestValidateDaemonSetUpdate(t *testing.T) { t.Errorf("%q has incorrect test setup with no resource version set", testName) } // Run the tests - if errs := ValidateDaemonSetUpdate(&errorCase.update, &errorCase.old); len(errs) != errorCase.expectedErrNum { + if errs := ValidateDaemonSetUpdate(&errorCase.update, &errorCase.old, corevalidation.PodValidationOptions{}); len(errs) != errorCase.expectedErrNum { t.Errorf("%q expected %d errors, but got %d error: %v", testName, errorCase.expectedErrNum, len(errs), errs) } else { t.Logf("(PASS) %q got errors %v", testName, errs) @@ -1829,7 +1830,7 @@ func TestValidateDaemonSet(t *testing.T) { }, } for _, successCase := range successCases { - if errs := ValidateDaemonSet(&successCase); len(errs) != 0 { + if errs := ValidateDaemonSet(&successCase, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -1973,7 +1974,7 @@ func TestValidateDaemonSet(t *testing.T) { }, } for k, v := range errorCases { - errs := ValidateDaemonSet(&v) + errs := ValidateDaemonSet(&v, corevalidation.PodValidationOptions{}) if len(errs) == 0 { t.Errorf("expected failure for %s", k) } @@ -2049,7 +2050,7 @@ func TestValidateDeployment(t *testing.T) { validDeployment(), } for _, successCase := range successCases { - if errs := ValidateDeployment(successCase); len(errs) != 0 { + if errs := ValidateDeployment(successCase, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -2142,7 +2143,7 @@ func TestValidateDeployment(t *testing.T) { errorCases["ephemeral containers not allowed"] = invalidEphemeralContainersDeployment for k, v := range errorCases { - errs := ValidateDeployment(v) + errs := ValidateDeployment(v, corevalidation.PodValidationOptions{}) if len(errs) == 0 { t.Errorf("[%s] expected failure", k) } else if !strings.Contains(errs[0].Error(), k) { @@ -2671,7 +2672,7 @@ func TestValidateReplicaSetUpdate(t *testing.T) { for _, successCase := range successCases { successCase.old.ObjectMeta.ResourceVersion = "1" successCase.update.ObjectMeta.ResourceVersion = "1" - if errs := ValidateReplicaSetUpdate(&successCase.update, &successCase.old); len(errs) != 0 { + if errs := ValidateReplicaSetUpdate(&successCase.update, &successCase.old, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -2746,7 +2747,7 @@ func TestValidateReplicaSetUpdate(t *testing.T) { }, } for testName, errorCase := range errorCases { - if errs := ValidateReplicaSetUpdate(&errorCase.update, &errorCase.old); len(errs) == 0 { + if errs := ValidateReplicaSetUpdate(&errorCase.update, &errorCase.old, corevalidation.PodValidationOptions{}); len(errs) == 0 { t.Errorf("expected failure: %s", testName) } } @@ -2816,7 +2817,7 @@ func TestValidateReplicaSet(t *testing.T) { }, } for _, successCase := range successCases { - if errs := ValidateReplicaSet(&successCase); len(errs) != 0 { + if errs := ValidateReplicaSet(&successCase, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -2948,7 +2949,7 @@ func TestValidateReplicaSet(t *testing.T) { }, } for k, v := range errorCases { - errs := ValidateReplicaSet(&v) + errs := ValidateReplicaSet(&v, corevalidation.PodValidationOptions{}) if len(errs) == 0 { t.Errorf("expected failure for %s", k) } diff --git a/pkg/apis/batch/validation/BUILD b/pkg/apis/batch/validation/BUILD index 9ec6be8f660..4fc203a8b91 100644 --- a/pkg/apis/batch/validation/BUILD +++ b/pkg/apis/batch/validation/BUILD @@ -30,6 +30,7 @@ go_test( deps = [ "//pkg/apis/batch:go_default_library", "//pkg/apis/core:go_default_library", + "//pkg/apis/core/validation:go_default_library", "//pkg/features:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", diff --git a/pkg/apis/batch/validation/validation.go b/pkg/apis/batch/validation/validation.go index dc646129ed2..03135c2a206 100644 --- a/pkg/apis/batch/validation/validation.go +++ b/pkg/apis/batch/validation/validation.go @@ -76,17 +76,17 @@ func ValidateGeneratedSelector(obj *batch.Job) field.ErrorList { } // ValidateJob validates a Job and returns an ErrorList with any errors. -func ValidateJob(job *batch.Job) field.ErrorList { +func ValidateJob(job *batch.Job, opts apivalidation.PodValidationOptions) field.ErrorList { // Jobs and rcs have the same name validation allErrs := apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata")) allErrs = append(allErrs, ValidateGeneratedSelector(job)...) - allErrs = append(allErrs, ValidateJobSpec(&job.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateJobSpec(&job.Spec, field.NewPath("spec"), opts)...) return allErrs } // ValidateJobSpec validates a JobSpec and returns an ErrorList with any errors. -func ValidateJobSpec(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList { - allErrs := validateJobSpec(spec, fldPath) +func ValidateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList { + allErrs := validateJobSpec(spec, fldPath, opts) if spec.Selector == nil { allErrs = append(allErrs, field.Required(fldPath.Child("selector"), "")) @@ -104,7 +104,7 @@ func ValidateJobSpec(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList { return allErrs } -func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList { +func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} if spec.Parallelism != nil { @@ -123,7 +123,7 @@ func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList { allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.TTLSecondsAfterFinished), fldPath.Child("ttlSecondsAfterFinished"))...) } - allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"))...) + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"), opts)...) if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure && spec.Template.Spec.RestartPolicy != api.RestartPolicyNever { allErrs = append(allErrs, field.NotSupported(fldPath.Child("template", "spec", "restartPolicy"), @@ -142,9 +142,9 @@ func ValidateJobStatus(status *batch.JobStatus, fldPath *field.Path) field.Error } // ValidateJobUpdate validates an update to a Job and returns an ErrorList with any errors. -func ValidateJobUpdate(job, oldJob *batch.Job) field.ErrorList { +func ValidateJobUpdate(job, oldJob *batch.Job, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateJobSpecUpdate(job.Spec, oldJob.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateJobSpecUpdate(job.Spec, oldJob.Spec, field.NewPath("spec"), opts)...) return allErrs } @@ -156,9 +156,9 @@ func ValidateJobUpdateStatus(job, oldJob *batch.Job) field.ErrorList { } // ValidateJobSpecUpdate validates an update to a JobSpec and returns an ErrorList with any errors. -func ValidateJobSpecUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path) field.ErrorList { +func ValidateJobSpecUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} - allErrs = append(allErrs, ValidateJobSpec(&spec, fldPath)...) + allErrs = append(allErrs, ValidateJobSpec(&spec, fldPath, opts)...) allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Completions, oldSpec.Completions, fldPath.Child("completions"))...) allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Selector, oldSpec.Selector, fldPath.Child("selector"))...) allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.Template, oldSpec.Template, fldPath.Child("template"))...) @@ -173,10 +173,10 @@ func ValidateJobStatusUpdate(status, oldStatus batch.JobStatus) field.ErrorList } // ValidateCronJob validates a CronJob and returns an ErrorList with any errors. -func ValidateCronJob(cronJob *batch.CronJob) field.ErrorList { +func ValidateCronJob(cronJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList { // CronJobs and rcs have the same name validation allErrs := apivalidation.ValidateObjectMeta(&cronJob.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateCronJobSpec(&cronJob.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateCronJobSpec(&cronJob.Spec, field.NewPath("spec"), opts)...) if len(cronJob.ObjectMeta.Name) > apimachineryvalidation.DNS1035LabelMaxLength-11 { // The cronjob controller appends a 11-character suffix to the cronjob (`-$TIMESTAMP`) when // creating a job. The job name length limit is 63 characters. @@ -188,16 +188,16 @@ func ValidateCronJob(cronJob *batch.CronJob) field.ErrorList { } // ValidateCronJobUpdate validates an update to a CronJob and returns an ErrorList with any errors. -func ValidateCronJobUpdate(job, oldJob *batch.CronJob) field.ErrorList { +func ValidateCronJobUpdate(job, oldJob *batch.CronJob, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateCronJobSpec(&job.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateCronJobSpec(&job.Spec, field.NewPath("spec"), opts)...) // skip the 52-character name validation limit on update validation // to allow old cronjobs with names > 52 chars to be updated/deleted return allErrs } // ValidateCronJobSpec validates a CronJobSpec and returns an ErrorList with any errors. -func ValidateCronJobSpec(spec *batch.CronJobSpec, fldPath *field.Path) field.ErrorList { +func ValidateCronJobSpec(spec *batch.CronJobSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} if len(spec.Schedule) == 0 { @@ -209,7 +209,7 @@ func ValidateCronJobSpec(spec *batch.CronJobSpec, fldPath *field.Path) field.Err allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.StartingDeadlineSeconds), fldPath.Child("startingDeadlineSeconds"))...) } allErrs = append(allErrs, validateConcurrencyPolicy(&spec.ConcurrencyPolicy, fldPath.Child("concurrencyPolicy"))...) - allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"))...) + allErrs = append(allErrs, ValidateJobTemplateSpec(&spec.JobTemplate, fldPath.Child("jobTemplate"), opts)...) if spec.SuccessfulJobsHistoryLimit != nil { // zero is a valid SuccessfulJobsHistoryLimit @@ -248,16 +248,16 @@ func validateScheduleFormat(schedule string, fldPath *field.Path) field.ErrorLis } // ValidateJobTemplate validates a JobTemplate and returns an ErrorList with any errors. -func ValidateJobTemplate(job *batch.JobTemplate) field.ErrorList { +func ValidateJobTemplate(job *batch.JobTemplate, opts apivalidation.PodValidationOptions) field.ErrorList { // this method should be identical to ValidateJob allErrs := apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateJobTemplateSpec(&job.Template, field.NewPath("template"))...) + allErrs = append(allErrs, ValidateJobTemplateSpec(&job.Template, field.NewPath("template"), opts)...) return allErrs } // ValidateJobTemplateSpec validates a JobTemplateSpec and returns an ErrorList with any errors. -func ValidateJobTemplateSpec(spec *batch.JobTemplateSpec, fldPath *field.Path) field.ErrorList { - allErrs := validateJobSpec(&spec.Spec, fldPath.Child("spec")) +func ValidateJobTemplateSpec(spec *batch.JobTemplateSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList { + allErrs := validateJobSpec(&spec.Spec, fldPath.Child("spec"), opts) // jobtemplate will always have the selector automatically generated if spec.Spec.Selector != nil { diff --git a/pkg/apis/batch/validation/validation_test.go b/pkg/apis/batch/validation/validation_test.go index 5c54322d9e3..e261c831a72 100644 --- a/pkg/apis/batch/validation/validation_test.go +++ b/pkg/apis/batch/validation/validation_test.go @@ -26,6 +26,7 @@ import ( featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/kubernetes/pkg/apis/batch" api "k8s.io/kubernetes/pkg/apis/core" + corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" "k8s.io/kubernetes/pkg/features" ) @@ -99,7 +100,7 @@ func TestValidateJob(t *testing.T) { }, } for k, v := range successCases { - if errs := ValidateJob(&v); len(errs) != 0 { + if errs := ValidateJob(&v, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success for %s: %v", k, errs) } } @@ -238,7 +239,7 @@ func TestValidateJob(t *testing.T) { } for k, v := range errorCases { - errs := ValidateJob(&v) + errs := ValidateJob(&v, corevalidation.PodValidationOptions{}) if len(errs) == 0 { t.Errorf("expected failure for %s", k) } else { @@ -368,7 +369,7 @@ func TestValidateCronJob(t *testing.T) { }, } for k, v := range successCases { - if errs := ValidateCronJob(&v); len(errs) != 0 { + if errs := ValidateCronJob(&v, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success for %s: %v", k, errs) } @@ -376,7 +377,7 @@ func TestValidateCronJob(t *testing.T) { // copy to avoid polluting the testcase object, set a resourceVersion to allow validating update, and test a no-op update v = *v.DeepCopy() v.ResourceVersion = "1" - if errs := ValidateCronJobUpdate(&v, &v); len(errs) != 0 { + if errs := ValidateCronJobUpdate(&v, &v, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success for %s: %v", k, errs) } } @@ -629,7 +630,7 @@ func TestValidateCronJob(t *testing.T) { } for k, v := range errorCases { - errs := ValidateCronJob(&v) + errs := ValidateCronJob(&v, corevalidation.PodValidationOptions{}) if len(errs) == 0 { t.Errorf("expected failure for %s", k) } else { @@ -644,7 +645,7 @@ func TestValidateCronJob(t *testing.T) { // copy to avoid polluting the testcase object, set a resourceVersion to allow validating update, and test a no-op update v = *v.DeepCopy() v.ResourceVersion = "1" - errs = ValidateCronJobUpdate(&v, &v) + errs = ValidateCronJobUpdate(&v, &v, corevalidation.PodValidationOptions{}) if len(errs) == 0 { if k == "metadata.name: must be no more than 52 characters" { continue diff --git a/pkg/apis/core/helper/helpers.go b/pkg/apis/core/helper/helpers.go index 1ed2f59facd..adf5f28eca4 100644 --- a/pkg/apis/core/helper/helpers.go +++ b/pkg/apis/core/helper/helpers.go @@ -497,3 +497,38 @@ func PersistentVolumeClaimHasClass(claim *core.PersistentVolumeClaim) bool { return false } + +func toResourceNames(resources core.ResourceList) []core.ResourceName { + result := []core.ResourceName{} + for resourceName := range resources { + result = append(result, resourceName) + } + return result +} + +func toSet(resourceNames []core.ResourceName) sets.String { + result := sets.NewString() + for _, resourceName := range resourceNames { + result.Insert(string(resourceName)) + } + return result +} + +// toContainerResourcesSet returns a set of resources names in container resource requirements +func toContainerResourcesSet(ctr *core.Container) sets.String { + resourceNames := toResourceNames(ctr.Resources.Requests) + resourceNames = append(resourceNames, toResourceNames(ctr.Resources.Limits)...) + return toSet(resourceNames) +} + +// ToPodResourcesSet returns a set of resource names in all containers in a pod. +func ToPodResourcesSet(podSpec *core.PodSpec) sets.String { + result := sets.NewString() + for i := range podSpec.InitContainers { + result = result.Union(toContainerResourcesSet(&podSpec.InitContainers[i])) + } + for i := range podSpec.Containers { + result = result.Union(toContainerResourcesSet(&podSpec.Containers[i])) + } + return result +} diff --git a/pkg/apis/core/resource.go b/pkg/apis/core/resource.go index d5006e5c8aa..bde1e24ca53 100644 --- a/pkg/apis/core/resource.go +++ b/pkg/apis/core/resource.go @@ -26,40 +26,33 @@ func (rn ResourceName) String() string { // CPU returns the CPU limit if specified. func (rl *ResourceList) CPU() *resource.Quantity { - if val, ok := (*rl)[ResourceCPU]; ok { - return &val - } - return &resource.Quantity{Format: resource.DecimalSI} + return rl.Name(ResourceCPU, resource.DecimalSI) } // Memory returns the Memory limit if specified. func (rl *ResourceList) Memory() *resource.Quantity { - if val, ok := (*rl)[ResourceMemory]; ok { - return &val - } - return &resource.Quantity{Format: resource.BinarySI} + return rl.Name(ResourceMemory, resource.BinarySI) } // Storage returns the Storage limit if specified. func (rl *ResourceList) Storage() *resource.Quantity { - if val, ok := (*rl)[ResourceStorage]; ok { - return &val - } - return &resource.Quantity{Format: resource.BinarySI} + return rl.Name(ResourceStorage, resource.BinarySI) } // Pods returns the list of pods func (rl *ResourceList) Pods() *resource.Quantity { - if val, ok := (*rl)[ResourcePods]; ok { - return &val - } - return &resource.Quantity{} + return rl.Name(ResourcePods, resource.DecimalSI) } // StorageEphemeral returns the list of ephemeral storage volumes, if any func (rl *ResourceList) StorageEphemeral() *resource.Quantity { - if val, ok := (*rl)[ResourceEphemeralStorage]; ok { + return rl.Name(ResourceEphemeralStorage, resource.BinarySI) +} + +// Name returns the resource with name if specified, otherwise it returns a nil quantity with default format. +func (rl *ResourceList) Name(name ResourceName, defaultFormat resource.Format) *resource.Quantity { + if val, ok := (*rl)[name]; ok { return &val } - return &resource.Quantity{} + return &resource.Quantity{Format: defaultFormat} } diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index 930185b9084..9dd26a3b88d 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -350,7 +350,7 @@ func ValidateObjectMetaUpdate(newMeta, oldMeta *metav1.ObjectMeta, fldPath *fiel return allErrs } -func ValidateVolumes(volumes []core.Volume, podMeta *metav1.ObjectMeta, fldPath *field.Path) (map[string]core.VolumeSource, field.ErrorList) { +func ValidateVolumes(volumes []core.Volume, podMeta *metav1.ObjectMeta, fldPath *field.Path, opts PodValidationOptions) (map[string]core.VolumeSource, field.ErrorList) { allErrs := field.ErrorList{} allNames := sets.String{} @@ -369,7 +369,7 @@ func ValidateVolumes(volumes []core.Volume, podMeta *metav1.ObjectMeta, fldPath for i, vol := range volumes { idxPath := fldPath.Index(i) namePath := idxPath.Child("name") - el := validateVolumeSource(&vol.VolumeSource, idxPath, vol.Name, podMeta) + el := validateVolumeSource(&vol.VolumeSource, idxPath, vol.Name, podMeta, opts) if len(vol.Name) == 0 { el = append(el, field.Required(namePath, "")) } else { @@ -446,7 +446,7 @@ func devicePathAlreadyExists(devicePath string, mounts map[string]string) bool { return false } -func validateVolumeSource(source *core.VolumeSource, fldPath *field.Path, volName string, podMeta *metav1.ObjectMeta) field.ErrorList { +func validateVolumeSource(source *core.VolumeSource, fldPath *field.Path, volName string, podMeta *metav1.ObjectMeta, opts PodValidationOptions) field.ErrorList { numVolumes := 0 allErrs := field.ErrorList{} if source.EmptyDir != nil { @@ -576,7 +576,7 @@ func validateVolumeSource(source *core.VolumeSource, fldPath *field.Path, volNam allErrs = append(allErrs, field.Forbidden(fldPath.Child("downwarAPI"), "may not specify more than 1 volume type")) } else { numVolumes++ - allErrs = append(allErrs, validateDownwardAPIVolumeSource(source.DownwardAPI, fldPath.Child("downwardAPI"))...) + allErrs = append(allErrs, validateDownwardAPIVolumeSource(source.DownwardAPI, fldPath.Child("downwardAPI"), opts)...) } } if source.FC != nil { @@ -658,7 +658,7 @@ func validateVolumeSource(source *core.VolumeSource, fldPath *field.Path, volNam allErrs = append(allErrs, field.Forbidden(fldPath.Child("projected"), "may not specify more than 1 volume type")) } else { numVolumes++ - allErrs = append(allErrs, validateProjectedVolumeSource(source.Projected, fldPath.Child("projected"))...) + allErrs = append(allErrs, validateProjectedVolumeSource(source.Projected, fldPath.Child("projected"), opts)...) } } if source.ScaleIO != nil { @@ -1007,9 +1007,8 @@ var validVolumeDownwardAPIFieldPathExpressions = sets.NewString( "metadata.annotations", "metadata.uid") -func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *field.Path) field.ErrorList { +func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} - if len(file.Path) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("path"), "")) } @@ -1020,7 +1019,11 @@ func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *fi allErrs = append(allErrs, field.Invalid(fldPath, "resource", "fieldRef and resourceFieldRef can not be specified simultaneously")) } } else if file.ResourceFieldRef != nil { - allErrs = append(allErrs, validateContainerResourceFieldSelector(file.ResourceFieldRef, &validContainerResourceFieldPathExpressions, fldPath.Child("resourceFieldRef"), true)...) + localValidContainerResourceFieldPathPrefixes := validContainerResourceFieldPathPrefixes + if opts.AllowDownwardAPIHugePages { + localValidContainerResourceFieldPathPrefixes = validContainerResourceFieldPathPrefixesWithDownwardAPIHugePages + } + allErrs = append(allErrs, validateContainerResourceFieldSelector(file.ResourceFieldRef, &validContainerResourceFieldPathExpressions, &localValidContainerResourceFieldPathPrefixes, fldPath.Child("resourceFieldRef"), true)...) } else { allErrs = append(allErrs, field.Required(fldPath, "one of fieldRef and resourceFieldRef is required")) } @@ -1031,7 +1034,7 @@ func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *fi return allErrs } -func validateDownwardAPIVolumeSource(downwardAPIVolume *core.DownwardAPIVolumeSource, fldPath *field.Path) field.ErrorList { +func validateDownwardAPIVolumeSource(downwardAPIVolume *core.DownwardAPIVolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} downwardAPIMode := downwardAPIVolume.DefaultMode @@ -1040,12 +1043,12 @@ func validateDownwardAPIVolumeSource(downwardAPIVolume *core.DownwardAPIVolumeSo } for _, file := range downwardAPIVolume.Items { - allErrs = append(allErrs, validateDownwardAPIVolumeFile(&file, fldPath)...) + allErrs = append(allErrs, validateDownwardAPIVolumeFile(&file, fldPath, opts)...) } return allErrs } -func validateProjectionSources(projection *core.ProjectedVolumeSource, projectionMode *int32, fldPath *field.Path) field.ErrorList { +func validateProjectionSources(projection *core.ProjectedVolumeSource, projectionMode *int32, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} allPaths := sets.String{} @@ -1093,7 +1096,7 @@ func validateProjectionSources(projection *core.ProjectedVolumeSource, projectio if projPath := srcPath.Child("downwardAPI"); source.DownwardAPI != nil { numSources++ for _, file := range source.DownwardAPI.Items { - allErrs = append(allErrs, validateDownwardAPIVolumeFile(&file, projPath)...) + allErrs = append(allErrs, validateDownwardAPIVolumeFile(&file, projPath, opts)...) if len(file.Path) > 0 { curPath := file.Path if !allPaths.Has(curPath) { @@ -1123,7 +1126,7 @@ func validateProjectionSources(projection *core.ProjectedVolumeSource, projectio return allErrs } -func validateProjectedVolumeSource(projection *core.ProjectedVolumeSource, fldPath *field.Path) field.ErrorList { +func validateProjectedVolumeSource(projection *core.ProjectedVolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} projectionMode := projection.DefaultMode @@ -1131,7 +1134,7 @@ func validateProjectedVolumeSource(projection *core.ProjectedVolumeSource, fldPa allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *projectionMode, fileModeErrorMsg)) } - allErrs = append(allErrs, validateProjectionSources(projection, projectionMode, fldPath)...) + allErrs = append(allErrs, validateProjectionSources(projection, projectionMode, fldPath, opts)...) return allErrs } @@ -2138,7 +2141,7 @@ func validateContainerPorts(ports []core.ContainerPort, fldPath *field.Path) fie } // ValidateEnv validates env vars -func ValidateEnv(vars []core.EnvVar, fldPath *field.Path) field.ErrorList { +func ValidateEnv(vars []core.EnvVar, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} for i, ev := range vars { @@ -2150,7 +2153,7 @@ func ValidateEnv(vars []core.EnvVar, fldPath *field.Path) field.ErrorList { allErrs = append(allErrs, field.Invalid(idxPath.Child("name"), ev.Name, msg)) } } - allErrs = append(allErrs, validateEnvVarValueFrom(ev, idxPath.Child("valueFrom"))...) + allErrs = append(allErrs, validateEnvVarValueFrom(ev, idxPath.Child("valueFrom"), opts)...) } return allErrs } @@ -2166,9 +2169,17 @@ var validEnvDownwardAPIFieldPathExpressions = sets.NewString( // status.podIPs is populated even if IPv6DualStack feature gate // is not enabled. This will work for single stack and dual stack. "status.podIPs") + var validContainerResourceFieldPathExpressions = sets.NewString("limits.cpu", "limits.memory", "limits.ephemeral-storage", "requests.cpu", "requests.memory", "requests.ephemeral-storage") -func validateEnvVarValueFrom(ev core.EnvVar, fldPath *field.Path) field.ErrorList { +// NOTE: this is only valid with DownwardAPIHugePages enabled +var validContainerResourceFieldPathPrefixes = sets.NewString() +var validContainerResourceFieldPathPrefixesWithDownwardAPIHugePages = sets.NewString(hugepagesRequestsPrefixDownwardAPI, hugepagesLimitsPrefixDownwardAPI) + +const hugepagesRequestsPrefixDownwardAPI string = `requests.hugepages-` +const hugepagesLimitsPrefixDownwardAPI string = `limits.hugepages-` + +func validateEnvVarValueFrom(ev core.EnvVar, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} if ev.ValueFrom == nil { @@ -2183,7 +2194,11 @@ func validateEnvVarValueFrom(ev core.EnvVar, fldPath *field.Path) field.ErrorLis } if ev.ValueFrom.ResourceFieldRef != nil { numSources++ - allErrs = append(allErrs, validateContainerResourceFieldSelector(ev.ValueFrom.ResourceFieldRef, &validContainerResourceFieldPathExpressions, fldPath.Child("resourceFieldRef"), false)...) + localValidContainerResourceFieldPathPrefixes := validContainerResourceFieldPathPrefixes + if opts.AllowDownwardAPIHugePages { + localValidContainerResourceFieldPathPrefixes = validContainerResourceFieldPathPrefixesWithDownwardAPIHugePages + } + allErrs = append(allErrs, validateContainerResourceFieldSelector(ev.ValueFrom.ResourceFieldRef, &validContainerResourceFieldPathExpressions, &localValidContainerResourceFieldPathPrefixes, fldPath.Child("resourceFieldRef"), false)...) } if ev.ValueFrom.ConfigMapKeyRef != nil { numSources++ @@ -2246,7 +2261,7 @@ func validateObjectFieldSelector(fs *core.ObjectFieldSelector, expressions *sets return allErrs } -func validateContainerResourceFieldSelector(fs *core.ResourceFieldSelector, expressions *sets.String, fldPath *field.Path, volume bool) field.ErrorList { +func validateContainerResourceFieldSelector(fs *core.ResourceFieldSelector, expressions *sets.String, prefixes *sets.String, fldPath *field.Path, volume bool) field.ErrorList { allErrs := field.ErrorList{} if volume && len(fs.ContainerName) == 0 { @@ -2254,7 +2269,18 @@ func validateContainerResourceFieldSelector(fs *core.ResourceFieldSelector, expr } else if len(fs.Resource) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("resource"), "")) } else if !expressions.Has(fs.Resource) { - allErrs = append(allErrs, field.NotSupported(fldPath.Child("resource"), fs.Resource, expressions.List())) + // check if the prefix is present + foundPrefix := false + if prefixes != nil { + for _, prefix := range prefixes.List() { + if strings.HasPrefix(fs.Resource, prefix) { + foundPrefix = true + } + } + } + if !foundPrefix { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("resource"), fs.Resource, expressions.List())) + } } allErrs = append(allErrs, validateContainerResourceDivisor(fs.Resource, fs.Divisor, fldPath)...) return allErrs @@ -2315,6 +2341,7 @@ func validateSecretEnvSource(secretSource *core.SecretEnvSource, fldPath *field. var validContainerResourceDivisorForCPU = sets.NewString("1m", "1") var validContainerResourceDivisorForMemory = sets.NewString("1", "1k", "1M", "1G", "1T", "1P", "1E", "1Ki", "1Mi", "1Gi", "1Ti", "1Pi", "1Ei") +var validContainerResourceDivisorForHugePages = sets.NewString("1", "1k", "1M", "1G", "1T", "1P", "1E", "1Ki", "1Mi", "1Gi", "1Ti", "1Pi", "1Ei") var validContainerResourceDivisorForEphemeralStorage = sets.NewString("1", "1k", "1M", "1G", "1T", "1P", "1E", "1Ki", "1Mi", "1Gi", "1Ti", "1Pi", "1Ei") func validateContainerResourceDivisor(rName string, divisor resource.Quantity, fldPath *field.Path) field.ErrorList { @@ -2337,6 +2364,11 @@ func validateContainerResourceDivisor(rName string, divisor resource.Quantity, f allErrs = append(allErrs, field.Invalid(fldPath.Child("divisor"), rName, "only divisor's values 1, 1k, 1M, 1G, 1T, 1P, 1E, 1Ki, 1Mi, 1Gi, 1Ti, 1Pi, 1Ei are supported with the local ephemeral storage resource")) } } + if strings.HasPrefix(rName, hugepagesRequestsPrefixDownwardAPI) || strings.HasPrefix(rName, hugepagesLimitsPrefixDownwardAPI) { + if !validContainerResourceDivisorForHugePages.Has(divisor.String()) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("divisor"), rName, "only divisor's values 1, 1k, 1M, 1G, 1T, 1P, 1E, 1Ki, 1Mi, 1Gi, 1Ti, 1Pi, 1Ei are supported with the hugepages resource")) + } + } return allErrs } @@ -2674,7 +2706,7 @@ func validatePullPolicy(policy core.PullPolicy, fldPath *field.Path) field.Error return allErrors } -func validateEphemeralContainers(ephemeralContainers []core.EphemeralContainer, containers, initContainers []core.Container, volumes map[string]core.VolumeSource, fldPath *field.Path) field.ErrorList { +func validateEphemeralContainers(ephemeralContainers []core.EphemeralContainer, containers, initContainers []core.Container, volumes map[string]core.VolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} if len(ephemeralContainers) == 0 { @@ -2706,7 +2738,7 @@ func validateEphemeralContainers(ephemeralContainers []core.EphemeralContainer, // of ephemeralContainers[0].spec.name) // TODO(verb): factor a validateContainer() out of validateContainers() to be used here c := core.Container(ec.EphemeralContainerCommon) - allErrs = append(allErrs, validateContainers([]core.Container{c}, false, volumes, idxPath)...) + allErrs = append(allErrs, validateContainers([]core.Container{c}, false, volumes, idxPath, opts)...) // EphemeralContainers don't require the backwards-compatibility distinction between pod/podTemplate validation allErrs = append(allErrs, validateContainersOnlyForPod([]core.Container{c}, idxPath)...) @@ -2748,10 +2780,10 @@ func validateFieldAllowList(value interface{}, allowedFields map[string]bool, er return allErrs } -func validateInitContainers(containers, otherContainers []core.Container, deviceVolumes map[string]core.VolumeSource, fldPath *field.Path) field.ErrorList { +func validateInitContainers(containers, otherContainers []core.Container, deviceVolumes map[string]core.VolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { var allErrs field.ErrorList if len(containers) > 0 { - allErrs = append(allErrs, validateContainers(containers, true, deviceVolumes, fldPath)...) + allErrs = append(allErrs, validateContainers(containers, true, deviceVolumes, fldPath, opts)...) } allNames := sets.String{} @@ -2782,7 +2814,7 @@ func validateInitContainers(containers, otherContainers []core.Container, device return allErrs } -func validateContainers(containers []core.Container, isInitContainers bool, volumes map[string]core.VolumeSource, fldPath *field.Path) field.ErrorList { +func validateContainers(containers []core.Container, isInitContainers bool, volumes map[string]core.VolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} if len(containers) == 0 { @@ -2836,7 +2868,7 @@ func validateContainers(containers []core.Container, isInitContainers bool, volu allErrs = append(allErrs, validateProbe(ctr.ReadinessProbe, idxPath.Child("readinessProbe"))...) allErrs = append(allErrs, validateContainerPorts(ctr.Ports, idxPath.Child("ports"))...) - allErrs = append(allErrs, ValidateEnv(ctr.Env, idxPath.Child("env"))...) + allErrs = append(allErrs, ValidateEnv(ctr.Env, idxPath.Child("env"), opts)...) allErrs = append(allErrs, ValidateEnvFrom(ctr.EnvFrom, idxPath.Child("envFrom"))...) allErrs = append(allErrs, ValidateVolumeMounts(ctr.VolumeMounts, volDevices, volumes, &ctr, idxPath.Child("volumeMounts"))...) allErrs = append(allErrs, ValidateVolumeDevices(ctr.VolumeDevices, volMounts, volumes, idxPath.Child("volumeDevices"))...) @@ -3133,28 +3165,6 @@ func ValidateTolerations(tolerations []core.Toleration, fldPath *field.Path) fie return allErrors } -func toResourceNames(resources core.ResourceList) []core.ResourceName { - result := []core.ResourceName{} - for resourceName := range resources { - result = append(result, resourceName) - } - return result -} - -func toSet(resourceNames []core.ResourceName) sets.String { - result := sets.NewString() - for _, resourceName := range resourceNames { - result.Insert(string(resourceName)) - } - return result -} - -func toContainerResourcesSet(ctr *core.Container) sets.String { - resourceNames := toResourceNames(ctr.Resources.Requests) - resourceNames = append(resourceNames, toResourceNames(ctr.Resources.Limits)...) - return toSet(resourceNames) -} - // validateContainersOnlyForPod does additional validation for containers on a pod versus a pod template // it only does additive validation of fields not covered in validateContainers func validateContainersOnlyForPod(containers []core.Container, fldPath *field.Path) field.ErrorList { @@ -3172,19 +3182,19 @@ func validateContainersOnlyForPod(containers []core.Container, fldPath *field.Pa type PodValidationOptions struct { // Allow pod spec to have more than one huge page resource (with different sizes) AllowMultipleHugePageResources bool + // Allow pod spec to use hugepages in downward API + AllowDownwardAPIHugePages bool } // ValidatePodSingleHugePageResources checks if there are multiple huge // pages resources in the pod object. func ValidatePodSingleHugePageResources(pod *core.Pod, specPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} + resourceSet := helper.ToPodResourcesSet(&pod.Spec) hugePageResources := sets.NewString() - for i := range pod.Spec.Containers { - resourceSet := toContainerResourcesSet(&pod.Spec.Containers[i]) - for resourceStr := range resourceSet { - if v1helper.IsHugePageResourceName(v1.ResourceName(resourceStr)) { - hugePageResources.Insert(resourceStr) - } + for resourceStr := range resourceSet { + if v1helper.IsHugePageResourceName(v1.ResourceName(resourceStr)) { + hugePageResources.Insert(resourceStr) } } if len(hugePageResources) > 1 { @@ -3199,7 +3209,7 @@ func validatePodMetadataAndSpec(pod *core.Pod, opts PodValidationOptions) field. fldPath := field.NewPath("metadata") allErrs := ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName, fldPath) allErrs = append(allErrs, ValidatePodSpecificAnnotations(pod.ObjectMeta.Annotations, &pod.Spec, fldPath.Child("annotations"))...) - allErrs = append(allErrs, ValidatePodSpec(&pod.Spec, &pod.ObjectMeta, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidatePodSpec(&pod.Spec, &pod.ObjectMeta, field.NewPath("spec"), opts)...) // we do additional validation only pertinent for pods and not pod templates // this was done to preserve backwards compatibility @@ -3280,14 +3290,14 @@ func validatePodIPs(pod *core.Pod) field.ErrorList { // tricks. // The pod metadata is needed to validate generic ephemeral volumes. It is optional // and should be left empty unless the spec is from a real pod object. -func ValidatePodSpec(spec *core.PodSpec, podMeta *metav1.ObjectMeta, fldPath *field.Path) field.ErrorList { +func ValidatePodSpec(spec *core.PodSpec, podMeta *metav1.ObjectMeta, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} - vols, vErrs := ValidateVolumes(spec.Volumes, podMeta, fldPath.Child("volumes")) + vols, vErrs := ValidateVolumes(spec.Volumes, podMeta, fldPath.Child("volumes"), opts) allErrs = append(allErrs, vErrs...) - allErrs = append(allErrs, validateContainers(spec.Containers, false, vols, fldPath.Child("containers"))...) - allErrs = append(allErrs, validateInitContainers(spec.InitContainers, spec.Containers, vols, fldPath.Child("initContainers"))...) - allErrs = append(allErrs, validateEphemeralContainers(spec.EphemeralContainers, spec.Containers, spec.InitContainers, vols, fldPath.Child("ephemeralContainers"))...) + allErrs = append(allErrs, validateContainers(spec.Containers, false, vols, fldPath.Child("containers"), opts)...) + allErrs = append(allErrs, validateInitContainers(spec.InitContainers, spec.Containers, vols, fldPath.Child("initContainers"), opts)...) + allErrs = append(allErrs, validateEphemeralContainers(spec.EphemeralContainers, spec.Containers, spec.InitContainers, vols, fldPath.Child("ephemeralContainers"), opts)...) allErrs = append(allErrs, validateRestartPolicy(&spec.RestartPolicy, fldPath.Child("restartPolicy"))...) allErrs = append(allErrs, validateDNSPolicy(&spec.DNSPolicy, fldPath.Child("dnsPolicy"))...) allErrs = append(allErrs, unversionedvalidation.ValidateLabels(spec.NodeSelector, fldPath.Child("nodeSelector"))...) @@ -4077,7 +4087,7 @@ func validatePodConditions(conditions []core.PodCondition, fldPath *field.Path) // ValidatePodEphemeralContainersUpdate tests that a user update to EphemeralContainers is valid. // newPod and oldPod must only differ in their EphemeralContainers. -func ValidatePodEphemeralContainersUpdate(newPod, oldPod *core.Pod) field.ErrorList { +func ValidatePodEphemeralContainersUpdate(newPod, oldPod *core.Pod, opts PodValidationOptions) field.ErrorList { spec := newPod.Spec specPath := field.NewPath("spec").Child("ephemeralContainers") @@ -4085,7 +4095,7 @@ func ValidatePodEphemeralContainersUpdate(newPod, oldPod *core.Pod) field.ErrorL for _, vol := range spec.Volumes { vols[vol.Name] = vol.VolumeSource } - allErrs := validateEphemeralContainers(spec.EphemeralContainers, spec.Containers, spec.InitContainers, vols, specPath) + allErrs := validateEphemeralContainers(spec.EphemeralContainers, spec.Containers, spec.InitContainers, vols, specPath, opts) // Existing EphemeralContainers may not be changed. Order isn't preserved by patch, so check each individually. newContainerIndex := make(map[string]*core.EphemeralContainer) @@ -4121,17 +4131,17 @@ func ValidatePodBinding(binding *core.Binding) field.ErrorList { } // ValidatePodTemplate tests if required fields in the pod template are set. -func ValidatePodTemplate(pod *core.PodTemplate) field.ErrorList { +func ValidatePodTemplate(pod *core.PodTemplate, opts PodValidationOptions) field.ErrorList { allErrs := ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidatePodTemplateSpec(&pod.Template, field.NewPath("template"))...) + allErrs = append(allErrs, ValidatePodTemplateSpec(&pod.Template, field.NewPath("template"), opts)...) return allErrs } // ValidatePodTemplateUpdate tests to see if the update is legal for an end user to make. newPod is updated with fields // that cannot be changed. -func ValidatePodTemplateUpdate(newPod, oldPod *core.PodTemplate) field.ErrorList { +func ValidatePodTemplateUpdate(newPod, oldPod *core.PodTemplate, opts PodValidationOptions) field.ErrorList { allErrs := ValidateObjectMetaUpdate(&newPod.ObjectMeta, &oldPod.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, ValidatePodTemplateSpec(&newPod.Template, field.NewPath("template"))...) + allErrs = append(allErrs, ValidatePodTemplateSpec(&newPod.Template, field.NewPath("template"), opts)...) return allErrs } @@ -4490,16 +4500,16 @@ func ValidateServiceStatusUpdate(service, oldService *core.Service) field.ErrorL } // ValidateReplicationController tests if required fields in the replication controller are set. -func ValidateReplicationController(controller *core.ReplicationController) field.ErrorList { +func ValidateReplicationController(controller *core.ReplicationController, opts PodValidationOptions) field.ErrorList { allErrs := ValidateObjectMeta(&controller.ObjectMeta, true, ValidateReplicationControllerName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateReplicationControllerSpec(&controller.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateReplicationControllerSpec(&controller.Spec, field.NewPath("spec"), opts)...) return allErrs } // ValidateReplicationControllerUpdate tests if required fields in the replication controller are set. -func ValidateReplicationControllerUpdate(controller, oldController *core.ReplicationController) field.ErrorList { +func ValidateReplicationControllerUpdate(controller, oldController *core.ReplicationController, opts PodValidationOptions) field.ErrorList { allErrs := ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateReplicationControllerSpec(&controller.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateReplicationControllerSpec(&controller.Spec, field.NewPath("spec"), opts)...) return allErrs } @@ -4544,7 +4554,7 @@ func ValidateNonEmptySelector(selectorMap map[string]string, fldPath *field.Path } // Validates the given template and ensures that it is in accordance with the desired selector and replicas. -func ValidatePodTemplateSpecForRC(template *core.PodTemplateSpec, selectorMap map[string]string, replicas int32, fldPath *field.Path) field.ErrorList { +func ValidatePodTemplateSpecForRC(template *core.PodTemplateSpec, selectorMap map[string]string, replicas int32, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} if template == nil { allErrs = append(allErrs, field.Required(fldPath, "")) @@ -4557,7 +4567,7 @@ func ValidatePodTemplateSpecForRC(template *core.PodTemplateSpec, selectorMap ma allErrs = append(allErrs, field.Invalid(fldPath.Child("metadata", "labels"), template.Labels, "`selector` does not match template `labels`")) } } - allErrs = append(allErrs, ValidatePodTemplateSpec(template, fldPath)...) + allErrs = append(allErrs, ValidatePodTemplateSpec(template, fldPath, opts)...) if replicas > 1 { allErrs = append(allErrs, ValidateReadOnlyPersistentDisks(template.Spec.Volumes, fldPath.Child("spec", "volumes"))...) } @@ -4573,22 +4583,22 @@ func ValidatePodTemplateSpecForRC(template *core.PodTemplateSpec, selectorMap ma } // ValidateReplicationControllerSpec tests if required fields in the replication controller spec are set. -func ValidateReplicationControllerSpec(spec *core.ReplicationControllerSpec, fldPath *field.Path) field.ErrorList { +func ValidateReplicationControllerSpec(spec *core.ReplicationControllerSpec, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...) allErrs = append(allErrs, ValidateNonEmptySelector(spec.Selector, fldPath.Child("selector"))...) allErrs = append(allErrs, ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...) - allErrs = append(allErrs, ValidatePodTemplateSpecForRC(spec.Template, spec.Selector, spec.Replicas, fldPath.Child("template"))...) + allErrs = append(allErrs, ValidatePodTemplateSpecForRC(spec.Template, spec.Selector, spec.Replicas, fldPath.Child("template"), opts)...) return allErrs } // ValidatePodTemplateSpec validates the spec of a pod template -func ValidatePodTemplateSpec(spec *core.PodTemplateSpec, fldPath *field.Path) field.ErrorList { +func ValidatePodTemplateSpec(spec *core.PodTemplateSpec, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, unversionedvalidation.ValidateLabels(spec.Labels, fldPath.Child("labels"))...) allErrs = append(allErrs, ValidateAnnotations(spec.Annotations, fldPath.Child("annotations"))...) allErrs = append(allErrs, ValidatePodSpecificAnnotations(spec.Annotations, &spec.Spec, fldPath.Child("annotations"))...) - allErrs = append(allErrs, ValidatePodSpec(&spec.Spec, nil, fldPath.Child("spec"))...) + allErrs = append(allErrs, ValidatePodSpec(&spec.Spec, nil, fldPath.Child("spec"), opts)...) allErrs = append(allErrs, validateSeccompAnnotationsAndFields(spec.ObjectMeta, &spec.Spec, fldPath.Child("spec"))...) if len(spec.Spec.EphemeralContainers) > 0 { diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 7f73d3f6877..d4dec3827ce 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -1307,7 +1307,8 @@ func testValidatePVC(t *testing.T, ephemeral bool) { }, }, } - _, errs = ValidateVolumes(volumes, nil, field.NewPath("")) + opts := PodValidationOptions{} + _, errs = ValidateVolumes(volumes, nil, field.NewPath(""), opts) } else { errs = ValidatePersistentVolumeClaim(scenario.claim) } @@ -2199,6 +2200,7 @@ func TestValidateVolumes(t *testing.T) { name string vol core.Volume errs []verr + opts PodValidationOptions }{ // EmptyDir and basic volume names { @@ -3294,6 +3296,79 @@ func TestValidateVolumes(t *testing.T) { }, }, }, + { + name: "hugepages-downwardAPI-enabled", + vol: core.Volume{ + Name: "downwardapi", + VolumeSource: core.VolumeSource{ + DownwardAPI: &core.DownwardAPIVolumeSource{ + Items: []core.DownwardAPIVolumeFile{ + { + Path: "hugepages_request", + ResourceFieldRef: &core.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "requests.hugepages-2Mi", + }, + }, + { + Path: "hugepages_limit", + ResourceFieldRef: &core.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "limits.hugepages-2Mi", + }, + }, + }, + }, + }, + }, + opts: PodValidationOptions{AllowDownwardAPIHugePages: true}, + }, + { + name: "hugepages-downwardAPI-requests-disabled", + vol: core.Volume{ + Name: "downwardapi", + VolumeSource: core.VolumeSource{ + DownwardAPI: &core.DownwardAPIVolumeSource{ + Items: []core.DownwardAPIVolumeFile{ + { + Path: "hugepages_request", + ResourceFieldRef: &core.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "requests.hugepages-2Mi", + }, + }, + }, + }, + }, + }, + errs: []verr{{ + etype: field.ErrorTypeNotSupported, + field: "downwardAPI.resourceFieldRef.resource", + }}, + }, + { + name: "hugepages-downwardAPI-limits-disabled", + vol: core.Volume{ + Name: "downwardapi", + VolumeSource: core.VolumeSource{ + DownwardAPI: &core.DownwardAPIVolumeSource{ + Items: []core.DownwardAPIVolumeFile{ + { + Path: "hugepages_limit", + ResourceFieldRef: &core.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "limits.hugepages-2Mi", + }, + }, + }, + }, + }, + }, + errs: []verr{{ + etype: field.ErrorTypeNotSupported, + field: "downwardAPI.resourceFieldRef.resource", + }}, + }, { name: "downapi valid defaultMode", vol: core.Volume{ @@ -3990,7 +4065,7 @@ func TestValidateVolumes(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - names, errs := ValidateVolumes([]core.Volume{tc.vol}, nil, field.NewPath("field")) + names, errs := ValidateVolumes([]core.Volume{tc.vol}, nil, field.NewPath("field"), tc.opts) if len(errs) != len(tc.errs) { t.Fatalf("unexpected error(s): got %d, want %d: %v", len(tc.errs), len(errs), errs) } @@ -4016,7 +4091,7 @@ func TestValidateVolumes(t *testing.T) { {Name: "abc", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}, {Name: "abc", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}, } - _, errs := ValidateVolumes(dupsCase, nil, field.NewPath("field")) + _, errs := ValidateVolumes(dupsCase, nil, field.NewPath("field"), PodValidationOptions{}) if len(errs) == 0 { t.Errorf("expected error") } else if len(errs) != 1 { @@ -4029,7 +4104,7 @@ func TestValidateVolumes(t *testing.T) { hugePagesCase := core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{Medium: core.StorageMediumHugePages}} // Enable HugePages - if errs := validateVolumeSource(&hugePagesCase, field.NewPath("field").Index(0), "working", nil); len(errs) != 0 { + if errs := validateVolumeSource(&hugePagesCase, field.NewPath("field").Index(0), "working", nil, PodValidationOptions{}); len(errs) != 0 { t.Errorf("Unexpected error when HugePages feature is enabled.") } @@ -4208,7 +4283,7 @@ func TestHugePagesIsolation(t *testing.T) { for tcName, tc := range testCases { t.Run(tcName, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HugePageStorageMediumSize, tc.enableHugePageStorageMediumSize)() - errs := ValidatePodCreate(tc.pod, PodValidationOptions{tc.enableHugePageStorageMediumSize}) + errs := ValidatePodCreate(tc.pod, PodValidationOptions{AllowMultipleHugePageResources: tc.enableHugePageStorageMediumSize}) if tc.expectError && len(errs) == 0 { t.Errorf("Unexpected success") } @@ -4359,7 +4434,7 @@ func TestAlphaLocalStorageCapacityIsolation(t *testing.T) { } for _, tc := range testCases { - if errs := validateVolumeSource(&tc, field.NewPath("spec"), "tmpvol", nil); len(errs) != 0 { + if errs := validateVolumeSource(&tc, field.NewPath("spec"), "tmpvol", nil, PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -4529,12 +4604,55 @@ func TestLocalStorageEnvWithFeatureGate(t *testing.T) { }, } for _, testCase := range testCases { - if errs := validateEnvVarValueFrom(testCase, field.NewPath("field")); len(errs) != 0 { + if errs := validateEnvVarValueFrom(testCase, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success, got: %v", errs) } } } +func TestHugePagesEnv(t *testing.T) { + testCases := []core.EnvVar{ + { + Name: "hugepages-limits", + ValueFrom: &core.EnvVarSource{ + ResourceFieldRef: &core.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "limits.hugepages-2Mi", + }, + }, + }, + { + Name: "hugepages-requests", + ValueFrom: &core.EnvVarSource{ + ResourceFieldRef: &core.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "requests.hugepages-2Mi", + }, + }, + }, + } + // enable gate + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DownwardAPIHugePages, true)() + opts := PodValidationOptions{AllowDownwardAPIHugePages: true} + if errs := validateEnvVarValueFrom(testCase, field.NewPath("field"), opts); len(errs) != 0 { + t.Errorf("expected success, got: %v", errs) + } + }) + } + // disable gate + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DownwardAPIHugePages, false)() + opts := PodValidationOptions{AllowDownwardAPIHugePages: false} + if errs := validateEnvVarValueFrom(testCase, field.NewPath("field"), opts); len(errs) == 0 { + t.Errorf("expected failure") + } + }) + } +} + func TestValidateEnv(t *testing.T) { successCase := []core.EnvVar{ {Name: "abc", Value: "value"}, @@ -4656,7 +4774,7 @@ func TestValidateEnv(t *testing.T) { }, }, } - if errs := ValidateEnv(successCase, field.NewPath("field")); len(errs) != 0 { + if errs := ValidateEnv(successCase, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success, got: %v", errs) } @@ -4920,7 +5038,7 @@ func TestValidateEnv(t *testing.T) { }, } for _, tc := range errorCases { - if errs := ValidateEnv(tc.envs, field.NewPath("field")); len(errs) == 0 { + if errs := ValidateEnv(tc.envs, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { t.Errorf("expected failure for %s", tc.name) } else { for i := range errs { @@ -5101,7 +5219,7 @@ func TestValidateVolumeMounts(t *testing.T) { {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, {Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, } - vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field")) + vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) if len(v1err) > 0 { t.Errorf("Invalid test volume - expected success %v", v1err) return @@ -5164,7 +5282,7 @@ func TestValidateDisabledSubpath(t *testing.T) { {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, {Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, } - vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field")) + vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) if len(v1err) > 0 { t.Errorf("Invalid test volume - expected success %v", v1err) return @@ -5226,7 +5344,7 @@ func TestValidateSubpathMutuallyExclusive(t *testing.T) { {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, {Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, } - vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field")) + vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) if len(v1err) > 0 { t.Errorf("Invalid test volume - expected success %v", v1err) return @@ -5307,7 +5425,7 @@ func TestValidateDisabledSubpathExpr(t *testing.T) { {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, {Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, } - vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field")) + vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) if len(v1err) > 0 { t.Errorf("Invalid test volume - expected success %v", v1err) return @@ -5501,7 +5619,7 @@ func TestValidateMountPropagation(t *testing.T) { volumes := []core.Volume{ {Name: "foo", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, } - vols2, v2err := ValidateVolumes(volumes, nil, field.NewPath("field")) + vols2, v2err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) if len(v2err) > 0 { t.Errorf("Invalid test volume - expected success %v", v2err) return @@ -5524,7 +5642,7 @@ func TestAlphaValidateVolumeDevices(t *testing.T) { {Name: "def", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, } - vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field")) + vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) if len(v1err) > 0 { t.Errorf("Invalid test volumes - expected success %v", v1err) return @@ -5737,7 +5855,7 @@ func TestValidateEphemeralContainers(t *testing.T) { }, }, } { - if errs := validateEphemeralContainers(ephemeralContainers, containers, initContainers, vols, field.NewPath("ephemeralContainers")); len(errs) != 0 { + if errs := validateEphemeralContainers(ephemeralContainers, containers, initContainers, vols, field.NewPath("ephemeralContainers"), PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success for '%s' but got errors: %v", title, errs) } } @@ -5901,7 +6019,7 @@ func TestValidateEphemeralContainers(t *testing.T) { } for _, tc := range tcs { - errs := validateEphemeralContainers(tc.ephemeralContainers, containers, initContainers, vols, field.NewPath("ephemeralContainers")) + errs := validateEphemeralContainers(tc.ephemeralContainers, containers, initContainers, vols, field.NewPath("ephemeralContainers"), PodValidationOptions{}) if len(errs) == 0 { t.Errorf("for test %q, expected error but received none", tc.title) @@ -6070,7 +6188,7 @@ func TestValidateContainers(t *testing.T) { }, {Name: "abc-1234", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", SecurityContext: fakeValidSecurityContext(true)}, } - if errs := validateContainers(successCase, false, volumeDevices, field.NewPath("field")); len(errs) != 0 { + if errs := validateContainers(successCase, false, volumeDevices, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -6310,7 +6428,7 @@ func TestValidateContainers(t *testing.T) { }, } for k, v := range errorCases { - if errs := validateContainers(v, false, volumeDevices, field.NewPath("field")); len(errs) == 0 { + if errs := validateContainers(v, false, volumeDevices, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { t.Errorf("expected failure for %s", k) } } @@ -6344,7 +6462,7 @@ func TestValidateInitContainers(t *testing.T) { TerminationMessagePolicy: "File", }, } - if errs := validateContainers(successCase, true, volumeDevices, field.NewPath("field")); len(errs) != 0 { + if errs := validateContainers(successCase, true, volumeDevices, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -6370,7 +6488,7 @@ func TestValidateInitContainers(t *testing.T) { }, } for k, v := range errorCases { - if errs := validateContainers(v, true, volumeDevices, field.NewPath("field")); len(errs) == 0 { + if errs := validateContainers(v, true, volumeDevices, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { t.Errorf("expected failure for %s", k) } } @@ -6881,7 +6999,7 @@ func TestValidatePodSpec(t *testing.T) { } for k, v := range successCases { t.Run(k, func(t *testing.T) { - if errs := ValidatePodSpec(&v, nil, field.NewPath("field")); len(errs) != 0 { + if errs := ValidatePodSpec(&v, nil, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } }) @@ -7085,7 +7203,7 @@ func TestValidatePodSpec(t *testing.T) { }, } for k, v := range failureCases { - if errs := ValidatePodSpec(&v, nil, field.NewPath("field")); len(errs) == 0 { + if errs := ValidatePodSpec(&v, nil, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { t.Errorf("expected failure for %q", k) } } @@ -9875,7 +9993,7 @@ func TestValidatePodEphemeralContainersUpdate(t *testing.T) { for _, test := range tests { new := core.Pod{Spec: core.PodSpec{EphemeralContainers: test.new}} old := core.Pod{Spec: core.PodSpec{EphemeralContainers: test.old}} - errs := ValidatePodEphemeralContainersUpdate(&new, &old) + errs := ValidatePodEphemeralContainersUpdate(&new, &old, PodValidationOptions{}) if test.err == "" { if len(errs) != 0 { t.Errorf("unexpected invalid: %s (%+v)\nA: %+v\nB: %+v", test.test, errs, test.new, test.old) @@ -11508,7 +11626,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { for _, successCase := range successCases { successCase.old.ObjectMeta.ResourceVersion = "1" successCase.update.ObjectMeta.ResourceVersion = "1" - if errs := ValidateReplicationControllerUpdate(&successCase.update, &successCase.old); len(errs) != 0 { + if errs := ValidateReplicationControllerUpdate(&successCase.update, &successCase.old, PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -11583,7 +11701,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { }, } for testName, errorCase := range errorCases { - if errs := ValidateReplicationControllerUpdate(&errorCase.update, &errorCase.old); len(errs) == 0 { + if errs := ValidateReplicationControllerUpdate(&errorCase.update, &errorCase.old, PodValidationOptions{}); len(errs) == 0 { t.Errorf("expected failure: %s", testName) } } @@ -11653,7 +11771,7 @@ func TestValidateReplicationController(t *testing.T) { }, } for _, successCase := range successCases { - if errs := ValidateReplicationController(&successCase); len(errs) != 0 { + if errs := ValidateReplicationController(&successCase, PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -11803,7 +11921,7 @@ func TestValidateReplicationController(t *testing.T) { }, } for k, v := range errorCases { - errs := ValidateReplicationController(&v) + errs := ValidateReplicationController(&v, PodValidationOptions{}) if len(errs) == 0 { t.Errorf("expected failure for %s", k) } @@ -16919,7 +17037,7 @@ func TestValidatePodTemplateSpecSeccomp(t *testing.T) { } for i, test := range tests { - err := ValidatePodTemplateSpec(test.spec, rootFld) + err := ValidatePodTemplateSpec(test.spec, rootFld, PodValidationOptions{}) asserttestify.Equal(t, test.expectedErr, err, "TestCase[%d]: %s", i, test.description) } } diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 4dc3f09ee73..665d15aea8d 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -589,6 +589,12 @@ const ( // medium: HugePages-1Gi HugePageStorageMediumSize featuregate.Feature = "HugePageStorageMediumSize" + // owner: @derekwaynecarr + // alpha: v1.20 + // + // Enables usage of hugepages- in downward API. + DownwardAPIHugePages featuregate.Feature = "DownwardAPIHugePages" + // owner: @freehan // GA: v1.18 // @@ -758,6 +764,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS ServiceAppProtocol: {Default: true, PreRelease: featuregate.Beta}, ImmutableEphemeralVolumes: {Default: true, PreRelease: featuregate.Beta}, HugePageStorageMediumSize: {Default: true, PreRelease: featuregate.Beta}, + DownwardAPIHugePages: {Default: false, PreRelease: featuregate.Alpha}, ExternalPolicyForExternalIP: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.22 AnyVolumeDataSource: {Default: false, PreRelease: featuregate.Alpha}, DefaultPodTopologySpread: {Default: true, PreRelease: featuregate.Beta}, diff --git a/pkg/quota/v1/evaluator/core/pods.go b/pkg/quota/v1/evaluator/core/pods.go index b0b85cb5ffd..ede379ca63b 100644 --- a/pkg/quota/v1/evaluator/core/pods.go +++ b/pkg/quota/v1/evaluator/core/pods.go @@ -57,7 +57,7 @@ var podResources = []corev1.ResourceName{ } // podResourcePrefixes are the set of prefixes for resources (Hugepages, and other -// potential extended reources with specific prefix) managed by quota associated with pods. +// potential extended resources with specific prefix) managed by quota associated with pods. var podResourcePrefixes = []string{ corev1.ResourceHugePagesPrefix, corev1.ResourceRequestsHugePagesPrefix, diff --git a/pkg/registry/apps/daemonset/strategy.go b/pkg/registry/apps/daemonset/strategy.go index 436aac5aa45..03bdaa89583 100644 --- a/pkg/registry/apps/daemonset/strategy.go +++ b/pkg/registry/apps/daemonset/strategy.go @@ -115,7 +115,8 @@ func (daemonSetStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime. // Validate validates a new daemon set. func (daemonSetStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { daemonSet := obj.(*apps.DaemonSet) - return validation.ValidateDaemonSet(daemonSet) + opts := pod.GetValidationOptionsFromPodTemplate(&daemonSet.Spec.Template, nil) + return validation.ValidateDaemonSet(daemonSet, opts) } // Canonicalize normalizes the object after validation. @@ -132,8 +133,10 @@ func (daemonSetStrategy) AllowCreateOnUpdate() bool { func (daemonSetStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { newDaemonSet := obj.(*apps.DaemonSet) oldDaemonSet := old.(*apps.DaemonSet) - allErrs := validation.ValidateDaemonSet(obj.(*apps.DaemonSet)) - allErrs = append(allErrs, validation.ValidateDaemonSetUpdate(newDaemonSet, oldDaemonSet)...) + + opts := pod.GetValidationOptionsFromPodTemplate(&newDaemonSet.Spec.Template, &oldDaemonSet.Spec.Template) + allErrs := validation.ValidateDaemonSet(obj.(*apps.DaemonSet), opts) + allErrs = append(allErrs, validation.ValidateDaemonSetUpdate(newDaemonSet, oldDaemonSet, opts)...) // Update is not allowed to set Spec.Selector for apps/v1 and apps/v1beta2 (allowed for extensions/v1beta1). // If RequestInfo is nil, it is better to revert to old behavior (i.e. allow update to set Spec.Selector) diff --git a/pkg/registry/apps/deployment/strategy.go b/pkg/registry/apps/deployment/strategy.go index 8b1af655ee0..e3aab529a25 100644 --- a/pkg/registry/apps/deployment/strategy.go +++ b/pkg/registry/apps/deployment/strategy.go @@ -79,7 +79,8 @@ func (deploymentStrategy) PrepareForCreate(ctx context.Context, obj runtime.Obje // Validate validates a new deployment. func (deploymentStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { deployment := obj.(*apps.Deployment) - return validation.ValidateDeployment(deployment) + opts := pod.GetValidationOptionsFromPodTemplate(&deployment.Spec.Template, nil) + return validation.ValidateDeployment(deployment, opts) } // Canonicalize normalizes the object after validation. @@ -112,7 +113,9 @@ func (deploymentStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime func (deploymentStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { newDeployment := obj.(*apps.Deployment) oldDeployment := old.(*apps.Deployment) - allErrs := validation.ValidateDeploymentUpdate(newDeployment, oldDeployment) + + opts := pod.GetValidationOptionsFromPodTemplate(&newDeployment.Spec.Template, &oldDeployment.Spec.Template) + allErrs := validation.ValidateDeploymentUpdate(newDeployment, oldDeployment, opts) // Update is not allowed to set Spec.Selector for all groups/versions except extensions/v1beta1. // If RequestInfo is nil, it is better to revert to old behavior (i.e. allow update to set Spec.Selector) diff --git a/pkg/registry/apps/replicaset/strategy.go b/pkg/registry/apps/replicaset/strategy.go index 163b786eb1d..e1328fe46cb 100644 --- a/pkg/registry/apps/replicaset/strategy.go +++ b/pkg/registry/apps/replicaset/strategy.go @@ -108,7 +108,8 @@ func (rsStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) // Validate validates a new ReplicaSet. func (rsStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { rs := obj.(*apps.ReplicaSet) - return validation.ValidateReplicaSet(rs) + opts := pod.GetValidationOptionsFromPodTemplate(&rs.Spec.Template, nil) + return validation.ValidateReplicaSet(rs, opts) } // Canonicalize normalizes the object after validation. @@ -125,8 +126,10 @@ func (rsStrategy) AllowCreateOnUpdate() bool { func (rsStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { newReplicaSet := obj.(*apps.ReplicaSet) oldReplicaSet := old.(*apps.ReplicaSet) - allErrs := validation.ValidateReplicaSet(obj.(*apps.ReplicaSet)) - allErrs = append(allErrs, validation.ValidateReplicaSetUpdate(newReplicaSet, oldReplicaSet)...) + + opts := pod.GetValidationOptionsFromPodTemplate(&newReplicaSet.Spec.Template, &oldReplicaSet.Spec.Template) + allErrs := validation.ValidateReplicaSet(obj.(*apps.ReplicaSet), opts) + allErrs = append(allErrs, validation.ValidateReplicaSetUpdate(newReplicaSet, oldReplicaSet, opts)...) // Update is not allowed to set Spec.Selector for all groups/versions except extensions/v1beta1. // If RequestInfo is nil, it is better to revert to old behavior (i.e. allow update to set Spec.Selector) diff --git a/pkg/registry/batch/cronjob/strategy.go b/pkg/registry/batch/cronjob/strategy.go index e103067c76d..983932834d7 100644 --- a/pkg/registry/batch/cronjob/strategy.go +++ b/pkg/registry/batch/cronjob/strategy.go @@ -83,7 +83,8 @@ func (cronJobStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Ob // Validate validates a new scheduled job. func (cronJobStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { cronJob := obj.(*batch.CronJob) - return validation.ValidateCronJob(cronJob) + opts := pod.GetValidationOptionsFromPodTemplate(&cronJob.Spec.JobTemplate.Spec.Template, nil) + return validation.ValidateCronJob(cronJob, opts) } // Canonicalize normalizes the object after validation. @@ -103,7 +104,9 @@ func (cronJobStrategy) AllowCreateOnUpdate() bool { func (cronJobStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { newCronJob := obj.(*batch.CronJob) oldCronJob := old.(*batch.CronJob) - return validation.ValidateCronJobUpdate(newCronJob, oldCronJob) + + opts := pod.GetValidationOptionsFromPodTemplate(&newCronJob.Spec.JobTemplate.Spec.Template, &oldCronJob.Spec.JobTemplate.Spec.Template) + return validation.ValidateCronJobUpdate(newCronJob, oldCronJob, opts) } type cronJobStatusStrategy struct { diff --git a/pkg/registry/batch/job/strategy.go b/pkg/registry/batch/job/strategy.go index e755178a289..906ca95c4a8 100644 --- a/pkg/registry/batch/job/strategy.go +++ b/pkg/registry/batch/job/strategy.go @@ -103,7 +103,8 @@ func (jobStrategy) Validate(ctx context.Context, obj runtime.Object) field.Error if job.Spec.ManualSelector == nil || *job.Spec.ManualSelector == false { generateSelector(job) } - return validation.ValidateJob(job) + opts := pod.GetValidationOptionsFromPodTemplate(&job.Spec.Template, nil) + return validation.ValidateJob(job, opts) } // generateSelector adds a selector to a job and labels to its template @@ -173,8 +174,10 @@ func (jobStrategy) AllowCreateOnUpdate() bool { func (jobStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { job := obj.(*batch.Job) oldJob := old.(*batch.Job) - validationErrorList := validation.ValidateJob(job) - updateErrorList := validation.ValidateJobUpdate(job, oldJob) + + opts := pod.GetValidationOptionsFromPodTemplate(&job.Spec.Template, &oldJob.Spec.Template) + validationErrorList := validation.ValidateJob(job, opts) + updateErrorList := validation.ValidateJobUpdate(job, oldJob, opts) return append(validationErrorList, updateErrorList...) } diff --git a/pkg/registry/core/pod/strategy.go b/pkg/registry/core/pod/strategy.go index fe9fe3c8496..cdf42f4a0de 100644 --- a/pkg/registry/core/pod/strategy.go +++ b/pkg/registry/core/pod/strategy.go @@ -91,10 +91,7 @@ func (podStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object // Validate validates a new pod. func (podStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { pod := obj.(*api.Pod) - opts := validation.PodValidationOptions{ - // Allow multiple huge pages on pod create if feature is enabled - AllowMultipleHugePageResources: utilfeature.DefaultFeatureGate.Enabled(features.HugePageStorageMediumSize), - } + opts := podutil.GetValidationOptionsFromPodSpec(&pod.Spec, nil) return validation.ValidatePodCreate(pod, opts) } @@ -109,11 +106,10 @@ func (podStrategy) AllowCreateOnUpdate() bool { // ValidateUpdate is the default update validation for an end user. func (podStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - oldFailsSingleHugepagesValidation := len(validation.ValidatePodSingleHugePageResources(old.(*api.Pod), field.NewPath("spec"))) > 0 - opts := validation.PodValidationOptions{ - // Allow multiple huge pages on pod create if feature is enabled or if the old pod already has multiple hugepages specified - AllowMultipleHugePageResources: oldFailsSingleHugepagesValidation || utilfeature.DefaultFeatureGate.Enabled(features.HugePageStorageMediumSize), - } + // Allow downward api usage of hugepages on pod update if feature is enabled or if the old pod already had used them. + pod := obj.(*api.Pod) + oldPod := old.(*api.Pod) + opts := podutil.GetValidationOptionsFromPodSpec(&pod.Spec, &oldPod.Spec) return validation.ValidatePodUpdate(obj.(*api.Pod), old.(*api.Pod), opts) } @@ -182,7 +178,10 @@ type podEphemeralContainersStrategy struct { var EphemeralContainersStrategy = podEphemeralContainersStrategy{Strategy} func (podEphemeralContainersStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return validation.ValidatePodEphemeralContainersUpdate(obj.(*api.Pod), old.(*api.Pod)) + newPod := obj.(*api.Pod) + oldPod := old.(*api.Pod) + opts := podutil.GetValidationOptionsFromPodSpec(&newPod.Spec, &oldPod.Spec) + return validation.ValidatePodEphemeralContainersUpdate(newPod, oldPod, opts) } // GetAttrs returns labels and fields of a given object for filtering purposes. diff --git a/pkg/registry/core/podtemplate/strategy.go b/pkg/registry/core/podtemplate/strategy.go index eb79e1ee3c6..65055785a06 100644 --- a/pkg/registry/core/podtemplate/strategy.go +++ b/pkg/registry/core/podtemplate/strategy.go @@ -53,7 +53,8 @@ func (podTemplateStrategy) PrepareForCreate(ctx context.Context, obj runtime.Obj // Validate validates a new pod template. func (podTemplateStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { template := obj.(*api.PodTemplate) - return corevalidation.ValidatePodTemplate(template) + opts := pod.GetValidationOptionsFromPodTemplate(&template.Template, nil) + return corevalidation.ValidatePodTemplate(template, opts) } // Canonicalize normalizes the object after validation. @@ -77,7 +78,10 @@ func (podTemplateStrategy) PrepareForUpdate(ctx context.Context, obj, old runtim func (podTemplateStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { template := obj.(*api.PodTemplate) oldTemplate := old.(*api.PodTemplate) - return corevalidation.ValidatePodTemplateUpdate(template, oldTemplate) + + // Allow downward api usage of hugepages on pod update if feature is enabled or if the old pod already had used them. + opts := pod.GetValidationOptionsFromPodTemplate(&template.Template, &oldTemplate.Template) + return corevalidation.ValidatePodTemplateUpdate(template, oldTemplate, opts) } func (podTemplateStrategy) AllowUnconditionalUpdate() bool { diff --git a/pkg/registry/core/replicationcontroller/strategy.go b/pkg/registry/core/replicationcontroller/strategy.go index 38c1cc911f3..3ae193d6a71 100644 --- a/pkg/registry/core/replicationcontroller/strategy.go +++ b/pkg/registry/core/replicationcontroller/strategy.go @@ -108,7 +108,8 @@ func (rcStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) // Validate validates a new replication controller. func (rcStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { controller := obj.(*api.ReplicationController) - return validation.ValidateReplicationController(controller) + opts := pod.GetValidationOptionsFromPodTemplate(controller.Spec.Template, nil) + return validation.ValidateReplicationController(controller, opts) } // Canonicalize normalizes the object after validation. @@ -126,8 +127,9 @@ func (rcStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) f oldRc := old.(*api.ReplicationController) newRc := obj.(*api.ReplicationController) - validationErrorList := validation.ValidateReplicationController(newRc) - updateErrorList := validation.ValidateReplicationControllerUpdate(newRc, oldRc) + opts := pod.GetValidationOptionsFromPodTemplate(newRc.Spec.Template, oldRc.Spec.Template) + validationErrorList := validation.ValidateReplicationController(newRc, opts) + updateErrorList := validation.ValidateReplicationControllerUpdate(newRc, oldRc, opts) errs := append(validationErrorList, updateErrorList...) for key, value := range helper.NonConvertibleFields(oldRc.Annotations) { diff --git a/staging/src/k8s.io/api/core/v1/resource.go b/staging/src/k8s.io/api/core/v1/resource.go index 6a8c7062053..4e249d03ab2 100644 --- a/staging/src/k8s.io/api/core/v1/resource.go +++ b/staging/src/k8s.io/api/core/v1/resource.go @@ -25,40 +25,35 @@ func (rn ResourceName) String() string { return string(rn) } -// Returns the CPU limit if specified. +// Cpu returns the Cpu limit if specified. func (rl *ResourceList) Cpu() *resource.Quantity { - if val, ok := (*rl)[ResourceCPU]; ok { - return &val - } - return &resource.Quantity{Format: resource.DecimalSI} + return rl.Name(ResourceCPU, resource.DecimalSI) } -// Returns the Memory limit if specified. +// Memory returns the Memory limit if specified. func (rl *ResourceList) Memory() *resource.Quantity { - if val, ok := (*rl)[ResourceMemory]; ok { - return &val - } - return &resource.Quantity{Format: resource.BinarySI} + return rl.Name(ResourceMemory, resource.BinarySI) } -// Returns the Storage limit if specified. +// Storage returns the Storage limit if specified. func (rl *ResourceList) Storage() *resource.Quantity { - if val, ok := (*rl)[ResourceStorage]; ok { - return &val - } - return &resource.Quantity{Format: resource.BinarySI} + return rl.Name(ResourceStorage, resource.BinarySI) } +// Pods returns the list of pods func (rl *ResourceList) Pods() *resource.Quantity { - if val, ok := (*rl)[ResourcePods]; ok { - return &val - } - return &resource.Quantity{} + return rl.Name(ResourcePods, resource.DecimalSI) } +// StorageEphemeral returns the list of ephemeral storage volumes, if any func (rl *ResourceList) StorageEphemeral() *resource.Quantity { - if val, ok := (*rl)[ResourceEphemeralStorage]; ok { + return rl.Name(ResourceEphemeralStorage, resource.BinarySI) +} + +// Name returns the resource with name if specified, otherwise it returns a nil quantity with default format. +func (rl *ResourceList) Name(name ResourceName, defaultFormat resource.Format) *resource.Quantity { + if val, ok := (*rl)[name]; ok { return &val } - return &resource.Quantity{} + return &resource.Quantity{Format: defaultFormat} } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_resolve.go b/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_resolve.go index 93e28f98a26..e45b686b391 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_resolve.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_resolve.go @@ -199,7 +199,20 @@ func extractContainerResourceValue(fs *corev1.ResourceFieldSelector, container * case "requests.ephemeral-storage": return convertResourceEphemeralStorageToString(container.Resources.Requests.StorageEphemeral(), divisor) } - + // handle extended standard resources with dynamic names + // example: requests.hugepages- or limits.hugepages- + if strings.HasPrefix(fs.Resource, "requests.") { + resourceName := corev1.ResourceName(strings.TrimPrefix(fs.Resource, "requests.")) + if IsHugePageResourceName(resourceName) { + return convertResourceHugePagesToString(container.Resources.Requests.Name(resourceName, resource.BinarySI), divisor) + } + } + if strings.HasPrefix(fs.Resource, "limits.") { + resourceName := corev1.ResourceName(strings.TrimPrefix(fs.Resource, "limits.")) + if IsHugePageResourceName(resourceName) { + return convertResourceHugePagesToString(container.Resources.Limits.Name(resourceName, resource.BinarySI), divisor) + } + } return "", fmt.Errorf("Unsupported container resource : %v", fs.Resource) } @@ -217,6 +230,13 @@ func convertResourceMemoryToString(memory *resource.Quantity, divisor resource.Q return strconv.FormatInt(m, 10), nil } +// convertResourceHugePagesToString converts hugepages value to the format of divisor and returns +// ceiling of the value. +func convertResourceHugePagesToString(hugePages *resource.Quantity, divisor resource.Quantity) (string, error) { + m := int64(math.Ceil(float64(hugePages.Value()) / float64(divisor.Value()))) + return strconv.FormatInt(m, 10), nil +} + // convertResourceEphemeralStorageToString converts ephemeral storage value to the format of divisor and returns // ceiling of the value. func convertResourceEphemeralStorageToString(ephemeralStorage *resource.Quantity, divisor resource.Quantity) (string, error) { @@ -269,3 +289,9 @@ func GetEnvVarRefString(from *corev1.EnvVarSource) string { return "invalid valueFrom" } + +// IsHugePageResourceName returns true if the resource name has the huge page +// resource prefix. +func IsHugePageResourceName(name corev1.ResourceName) bool { + return strings.HasPrefix(string(name), corev1.ResourceHugePagesPrefix) +} diff --git a/staging/src/k8s.io/kubectl/pkg/util/resource/resource.go b/staging/src/k8s.io/kubectl/pkg/util/resource/resource.go index 8308f1583f7..44ddf96ac91 100644 --- a/staging/src/k8s.io/kubectl/pkg/util/resource/resource.go +++ b/staging/src/k8s.io/kubectl/pkg/util/resource/resource.go @@ -108,7 +108,20 @@ func ExtractContainerResourceValue(fs *corev1.ResourceFieldSelector, container * case "requests.ephemeral-storage": return convertResourceEphemeralStorageToString(container.Resources.Requests.StorageEphemeral(), divisor) } - + // handle extended standard resources with dynamic names + // example: requests.hugepages- or limits.hugepages- + if strings.HasPrefix(fs.Resource, "requests.") { + resourceName := corev1.ResourceName(strings.TrimPrefix(fs.Resource, "requests.")) + if IsHugePageResourceName(resourceName) { + return convertResourceHugePagesToString(container.Resources.Requests.Name(resourceName, resource.BinarySI), divisor) + } + } + if strings.HasPrefix(fs.Resource, "limits.") { + resourceName := corev1.ResourceName(strings.TrimPrefix(fs.Resource, "limits.")) + if IsHugePageResourceName(resourceName) { + return convertResourceHugePagesToString(container.Resources.Limits.Name(resourceName, resource.BinarySI), divisor) + } + } return "", fmt.Errorf("Unsupported container resource : %v", fs.Resource) } @@ -126,6 +139,13 @@ func convertResourceMemoryToString(memory *resource.Quantity, divisor resource.Q return strconv.FormatInt(m, 10), nil } +// convertResourceHugePagesToString converts hugepages value to the format of divisor and returns +// ceiling of the value. +func convertResourceHugePagesToString(hugePages *resource.Quantity, divisor resource.Quantity) (string, error) { + m := int64(math.Ceil(float64(hugePages.Value()) / float64(divisor.Value()))) + return strconv.FormatInt(m, 10), nil +} + // convertResourceEphemeralStorageToString converts ephemeral storage value to the format of divisor and returns // ceiling of the value. func convertResourceEphemeralStorageToString(ephemeralStorage *resource.Quantity, divisor resource.Quantity) (string, error) { diff --git a/test/e2e/common/downwardapi_volume.go b/test/e2e/common/downwardapi_volume.go index c802e730845..c2f4c0713a1 100644 --- a/test/e2e/common/downwardapi_volume.go +++ b/test/e2e/common/downwardapi_volume.go @@ -261,7 +261,6 @@ var _ = ginkgo.Describe("[sig-storage] Downward API volume", func() { f.TestContainerOutputRegexp("downward API volume plugin", pod, 0, []string{"[1-9]"}) }) - }) func downwardAPIVolumePodForModeTest(name, filePath string, itemMode, defaultMode *int32) *v1.Pod {