325 lines
15 KiB
Go
325 lines
15 KiB
Go
/*
|
|
Copyright 2022 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package validation
|
|
|
|
import (
|
|
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
|
"k8s.io/kubernetes/pkg/apis/resource"
|
|
)
|
|
|
|
// validateResourceDriverName reuses the validation of a CSI driver because
|
|
// the allowed values are exactly the same.
|
|
var validateResourceDriverName = corevalidation.ValidateCSIDriverName
|
|
|
|
// ValidateClaim validates a ResourceClaim.
|
|
func ValidateClaim(resourceClaim *resource.ResourceClaim) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMeta(&resourceClaim.ObjectMeta, true, corevalidation.ValidateResourceClaimName, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, validateResourceClaimSpec(&resourceClaim.Spec, field.NewPath("spec"))...)
|
|
return allErrs
|
|
}
|
|
|
|
func validateResourceClaimSpec(spec *resource.ResourceClaimSpec, fldPath *field.Path) field.ErrorList {
|
|
allErrs := field.ErrorList{}
|
|
for _, msg := range corevalidation.ValidateClassName(spec.ResourceClassName, false) {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceClassName"), spec.ResourceClassName, msg))
|
|
}
|
|
allErrs = append(allErrs, validateResourceClaimParameters(spec.ParametersRef, fldPath.Child("parametersRef"))...)
|
|
if !supportedAllocationModes.Has(string(spec.AllocationMode)) {
|
|
allErrs = append(allErrs, field.NotSupported(fldPath.Child("allocationMode"), spec.AllocationMode, supportedAllocationModes.List()))
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
var supportedAllocationModes = sets.NewString(string(resource.AllocationModeImmediate), string(resource.AllocationModeWaitForFirstConsumer))
|
|
|
|
// It would have been nice to use Go generics to reuse the same validation
|
|
// function for Kind and Name in both types, but generics cannot be used to
|
|
// access common fields in structs.
|
|
|
|
func validateResourceClaimParameters(ref *resource.ResourceClaimParametersReference, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
if ref != nil {
|
|
if ref.Kind == "" {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("kind"), ""))
|
|
}
|
|
if ref.Name == "" {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
|
|
}
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
func validateClassParameters(ref *resource.ResourceClassParametersReference, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
if ref != nil {
|
|
if ref.Kind == "" {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("kind"), ""))
|
|
}
|
|
if ref.Name == "" {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
|
|
}
|
|
if ref.Namespace != "" {
|
|
for _, msg := range apimachineryvalidation.ValidateNamespaceName(ref.Namespace, false) {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("namespace"), ref.Namespace, msg))
|
|
}
|
|
}
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateClass validates a ResourceClass.
|
|
func ValidateClass(resourceClass *resource.ResourceClass) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMeta(&resourceClass.ObjectMeta, false, corevalidation.ValidateClassName, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, validateResourceDriverName(resourceClass.DriverName, field.NewPath("driverName"))...)
|
|
allErrs = append(allErrs, validateClassParameters(resourceClass.ParametersRef, field.NewPath("parametersRef"))...)
|
|
if resourceClass.SuitableNodes != nil {
|
|
allErrs = append(allErrs, corevalidation.ValidateNodeSelector(resourceClass.SuitableNodes, field.NewPath("suitableNodes"))...)
|
|
}
|
|
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateClassUpdate tests if an update to ResourceClass is valid.
|
|
func ValidateClassUpdate(resourceClass, oldClass *resource.ResourceClass) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClass.ObjectMeta, &oldClass.ObjectMeta, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, ValidateClass(resourceClass)...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateClaimUpdate tests if an update to ResourceClaim is valid.
|
|
func ValidateClaimUpdate(resourceClaim, oldClaim *resource.ResourceClaim) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldClaim.ObjectMeta, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(resourceClaim.Spec, oldClaim.Spec, field.NewPath("spec"))...)
|
|
allErrs = append(allErrs, ValidateClaim(resourceClaim)...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateClaimStatusUpdate tests if an update to the status of a ResourceClaim is valid.
|
|
func ValidateClaimStatusUpdate(resourceClaim, oldClaim *resource.ResourceClaim) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldClaim.ObjectMeta, field.NewPath("metadata"))
|
|
fldPath := field.NewPath("status")
|
|
// The name might not be set yet.
|
|
if resourceClaim.Status.DriverName != "" {
|
|
allErrs = append(allErrs, validateResourceDriverName(resourceClaim.Status.DriverName, fldPath.Child("driverName"))...)
|
|
} else if resourceClaim.Status.Allocation != nil {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("driverName"), "must be specified when `allocation` is set"))
|
|
}
|
|
|
|
allErrs = append(allErrs, validateAllocationResult(resourceClaim.Status.Allocation, fldPath.Child("allocation"))...)
|
|
allErrs = append(allErrs, validateResourceClaimConsumers(resourceClaim.Status.ReservedFor, resource.ResourceClaimReservedForMaxSize, fldPath.Child("reservedFor"))...)
|
|
|
|
// Now check for invariants that must be valid for a ResourceClaim.
|
|
if len(resourceClaim.Status.ReservedFor) > 0 {
|
|
if resourceClaim.Status.Allocation == nil {
|
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "may not be specified when `allocated` is not set"))
|
|
} else {
|
|
if !resourceClaim.Status.Allocation.Shareable && len(resourceClaim.Status.ReservedFor) > 1 {
|
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "may not be reserved more than once"))
|
|
}
|
|
// Items may be removed from ReservedFor while the claim is meant to be deallocated,
|
|
// but not added.
|
|
if resourceClaim.DeletionTimestamp != nil || resourceClaim.Status.DeallocationRequested {
|
|
oldSet := sets.New(oldClaim.Status.ReservedFor...)
|
|
newSet := sets.New(resourceClaim.Status.ReservedFor...)
|
|
newItems := newSet.Difference(oldSet)
|
|
if len(newItems) > 0 {
|
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "new entries may not be added while `deallocationRequested` or `deletionTimestamp` are set"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !oldClaim.Status.DeallocationRequested &&
|
|
resourceClaim.Status.DeallocationRequested &&
|
|
len(resourceClaim.Status.ReservedFor) > 0 {
|
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("deallocationRequested"), "deallocation cannot be requested while `reservedFor` is set"))
|
|
}
|
|
|
|
if resourceClaim.Status.Allocation == nil &&
|
|
resourceClaim.Status.DeallocationRequested {
|
|
// Either one or the other field was modified incorrectly.
|
|
// For the sake of simplicity this only reports the invalid
|
|
// end result.
|
|
allErrs = append(allErrs, field.Forbidden(fldPath, "`allocation` must be set when `deallocationRequested` is set"))
|
|
}
|
|
|
|
// Once deallocation has been requested, that request cannot be removed
|
|
// anymore because the deallocation may already have started. The field
|
|
// can only get reset by the driver together with removing the
|
|
// allocation.
|
|
if oldClaim.Status.DeallocationRequested &&
|
|
!resourceClaim.Status.DeallocationRequested &&
|
|
resourceClaim.Status.Allocation != nil {
|
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("deallocationRequested"), "may not be cleared when `allocation` is set"))
|
|
}
|
|
|
|
return allErrs
|
|
}
|
|
|
|
func validateAllocationResult(allocation *resource.AllocationResult, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
if allocation != nil {
|
|
if len(allocation.ResourceHandle) > resource.ResourceHandleMaxSize {
|
|
allErrs = append(allErrs, field.TooLongMaxLength(fldPath.Child("resourceHandle"), len(allocation.ResourceHandle), resource.ResourceHandleMaxSize))
|
|
}
|
|
if allocation.AvailableOnNodes != nil {
|
|
allErrs = append(allErrs, corevalidation.ValidateNodeSelector(allocation.AvailableOnNodes, fldPath.Child("availableOnNodes"))...)
|
|
}
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
func validateResourceClaimUserReference(ref resource.ResourceClaimConsumerReference, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
if ref.Resource == "" {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("resource"), ""))
|
|
}
|
|
if ref.Name == "" {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
|
|
}
|
|
if ref.UID == "" {
|
|
allErrs = append(allErrs, field.Required(fldPath.Child("uid"), ""))
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
// validateSliceIsASet ensures that a slice contains no duplicates and does not exceed a certain maximum size.
|
|
func validateSliceIsASet[T comparable](slice []T, maxSize int, validateItem func(item T, fldPath *field.Path) field.ErrorList, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
allItems := sets.New[T]()
|
|
for i, item := range slice {
|
|
idxPath := fldPath.Index(i)
|
|
if allItems.Has(item) {
|
|
allErrs = append(allErrs, field.Duplicate(idxPath, item))
|
|
} else {
|
|
allErrs = append(allErrs, validateItem(item, idxPath)...)
|
|
allItems.Insert(item)
|
|
}
|
|
}
|
|
if len(slice) > maxSize {
|
|
// Dumping the entire field into the error message is likely to be too long,
|
|
// in particular when it is already beyond the maximum size. Instead this
|
|
// just shows the number of entries.
|
|
allErrs = append(allErrs, field.TooLongMaxLength(fldPath, len(slice), maxSize))
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
// validateResourceClaimConsumers ensures that the slice contains no duplicate UIDs and does not exceed a certain maximum size.
|
|
func validateResourceClaimConsumers(consumers []resource.ResourceClaimConsumerReference, maxSize int, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
allUIDs := sets.New[types.UID]()
|
|
for i, consumer := range consumers {
|
|
idxPath := fldPath.Index(i)
|
|
if allUIDs.Has(consumer.UID) {
|
|
allErrs = append(allErrs, field.Duplicate(idxPath.Child("uid"), consumer.UID))
|
|
} else {
|
|
allErrs = append(allErrs, validateResourceClaimUserReference(consumer, idxPath)...)
|
|
allUIDs.Insert(consumer.UID)
|
|
}
|
|
}
|
|
if len(consumers) > maxSize {
|
|
// Dumping the entire field into the error message is likely to be too long,
|
|
// in particular when it is already beyond the maximum size. Instead this
|
|
// just shows the number of entries.
|
|
allErrs = append(allErrs, field.TooLongMaxLength(fldPath, len(consumers), maxSize))
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
// ValidatePodSchedulingContext validates a PodSchedulingContext.
|
|
func ValidatePodSchedulingContexts(schedulingCtx *resource.PodSchedulingContext) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMeta(&schedulingCtx.ObjectMeta, true, corevalidation.ValidatePodName, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, validatePodSchedulingSpec(&schedulingCtx.Spec, field.NewPath("spec"))...)
|
|
return allErrs
|
|
}
|
|
|
|
func validatePodSchedulingSpec(spec *resource.PodSchedulingContextSpec, fldPath *field.Path) field.ErrorList {
|
|
allErrs := validateSliceIsASet(spec.PotentialNodes, resource.PodSchedulingNodeListMaxSize, validateNodeName, fldPath.Child("potentialNodes"))
|
|
return allErrs
|
|
}
|
|
|
|
// ValidatePodSchedulingContextUpdate tests if an update to PodSchedulingContext is valid.
|
|
func ValidatePodSchedulingContextUpdate(schedulingCtx, oldSchedulingCtx *resource.PodSchedulingContext) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&schedulingCtx.ObjectMeta, &oldSchedulingCtx.ObjectMeta, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, ValidatePodSchedulingContexts(schedulingCtx)...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidatePodSchedulingContextStatusUpdate tests if an update to the status of a PodSchedulingContext is valid.
|
|
func ValidatePodSchedulingContextStatusUpdate(schedulingCtx, oldSchedulingCtx *resource.PodSchedulingContext) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&schedulingCtx.ObjectMeta, &oldSchedulingCtx.ObjectMeta, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, validatePodSchedulingStatus(&schedulingCtx.Status, field.NewPath("status"))...)
|
|
return allErrs
|
|
}
|
|
|
|
func validatePodSchedulingStatus(status *resource.PodSchedulingContextStatus, fldPath *field.Path) field.ErrorList {
|
|
return validatePodSchedulingClaims(status.ResourceClaims, fldPath.Child("claims"))
|
|
}
|
|
|
|
func validatePodSchedulingClaims(claimStatuses []resource.ResourceClaimSchedulingStatus, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
names := sets.NewString()
|
|
for i, claimStatus := range claimStatuses {
|
|
allErrs = append(allErrs, validatePodSchedulingClaim(claimStatus, fldPath.Index(i))...)
|
|
if names.Has(claimStatus.Name) {
|
|
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), claimStatus.Name))
|
|
} else {
|
|
names.Insert(claimStatus.Name)
|
|
}
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
func validatePodSchedulingClaim(status resource.ResourceClaimSchedulingStatus, fldPath *field.Path) field.ErrorList {
|
|
allErrs := validateSliceIsASet(status.UnsuitableNodes, resource.PodSchedulingNodeListMaxSize, validateNodeName, fldPath.Child("unsuitableNodes"))
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateClaimTemplace validates a ResourceClaimTemplate.
|
|
func ValidateClaimTemplate(template *resource.ResourceClaimTemplate) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMeta(&template.ObjectMeta, true, corevalidation.ValidateResourceClaimTemplateName, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, validateResourceClaimTemplateSpec(&template.Spec, field.NewPath("spec"))...)
|
|
return allErrs
|
|
}
|
|
|
|
func validateResourceClaimTemplateSpec(spec *resource.ResourceClaimTemplateSpec, fldPath *field.Path) field.ErrorList {
|
|
allErrs := corevalidation.ValidateTemplateObjectMeta(&spec.ObjectMeta, fldPath.Child("metadata"))
|
|
allErrs = append(allErrs, validateResourceClaimSpec(&spec.Spec, fldPath.Child("spec"))...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateClaimTemplateUpdate tests if an update to template is valid.
|
|
func ValidateClaimTemplateUpdate(template, oldTemplate *resource.ResourceClaimTemplate) field.ErrorList {
|
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&template.ObjectMeta, &oldTemplate.ObjectMeta, field.NewPath("metadata"))
|
|
allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(template.Spec, oldTemplate.Spec, field.NewPath("spec"))...)
|
|
allErrs = append(allErrs, ValidateClaimTemplate(template)...)
|
|
return allErrs
|
|
}
|
|
|
|
func validateNodeName(name string, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
for _, msg := range corevalidation.ValidateNodeName(name, false) {
|
|
allErrs = append(allErrs, field.Invalid(fldPath, name, msg))
|
|
}
|
|
return allErrs
|
|
}
|