kubernetes/pkg/apis/resource/validation/validation.go
Patrick Ohly 5cca60f0b8 api: dynamic resource allocation API
This adds a new resource.k8s.io API group with v1alpha1 as version. It contains
four new types: resource.ResourceClaim, resource.ResourceClass, resource.ResourceClaimTemplate, and
resource.PodScheduling.
2022-11-10 20:08:24 +01:00

318 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/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/apis/resource"
)
// validateResourceClaimName can be used to check whether the given
// name for a ResourceClaim is valid.
var validateResourceClaimName = apimachineryvalidation.NameIsDNSSubdomain
// validateResourceClaimTemplateName can be used to check whether the given
// name for a ResourceClaimTemplate is valid.
var validateResourceClaimTemplateName = apimachineryvalidation.NameIsDNSSubdomain
// 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, 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, validateSliceIsASet(resourceClaim.Status.ReservedFor, resource.ResourceClaimReservedForMaxSize,
validateResourceClaimUserReference, 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
}
// ValidatePodScheduling validates a PodScheduling.
func ValidatePodScheduling(resourceClaim *resource.PodScheduling) field.ErrorList {
allErrs := corevalidation.ValidateObjectMeta(&resourceClaim.ObjectMeta, true, corevalidation.ValidatePodName, field.NewPath("metadata"))
allErrs = append(allErrs, validatePodSchedulingSpec(&resourceClaim.Spec, field.NewPath("spec"))...)
return allErrs
}
func validatePodSchedulingSpec(spec *resource.PodSchedulingSpec, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
// Checking PotentialNodes for duplicates is intentionally not done. It
// could be fairly expensive and the only component which normally has
// permissions to set this field, kube-scheduler, is a trusted
// component. Also, if it gets this wrong because of a bug, then the
// effect is limited (same semantic).
if len(spec.PotentialNodes) > resource.PodSchedulingNodeListMaxSize {
allErrs = append(allErrs, field.TooLongMaxLength(fldPath.Child("potentialNodes"), nil, resource.PodSchedulingNodeListMaxSize))
}
return allErrs
}
// ValidatePodSchedulingUpdate tests if an update to PodScheduling is valid.
func ValidatePodSchedulingUpdate(resourceClaim, oldPodScheduling *resource.PodScheduling) field.ErrorList {
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldPodScheduling.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, ValidatePodScheduling(resourceClaim)...)
return allErrs
}
// ValidatePodSchedulingStatusUpdate tests if an update to the status of a PodScheduling is valid.
func ValidatePodSchedulingStatusUpdate(resourceClaim, oldPodScheduling *resource.PodScheduling) field.ErrorList {
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldPodScheduling.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, validatePodSchedulingStatus(&resourceClaim.Status, field.NewPath("status"))...)
return allErrs
}
func validatePodSchedulingStatus(status *resource.PodSchedulingStatus, 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(claim resource.ResourceClaimSchedulingStatus, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
// Checking UnsuitableNodes for duplicates is intentionally not done. It
// could be fairly expensive and if a resource driver gets this wrong,
// then it is only going to have a negative effect for the pods relying
// on this driver.
if len(claim.UnsuitableNodes) > resource.PodSchedulingNodeListMaxSize {
allErrs = append(allErrs, field.TooLongMaxLength(fldPath.Child("unsuitableNodes"), nil, resource.PodSchedulingNodeListMaxSize))
}
return allErrs
}
// ValidateClaimTemplace validates a ResourceClaimTemplate.
func ValidateClaimTemplate(template *resource.ResourceClaimTemplate) field.ErrorList {
allErrs := corevalidation.ValidateObjectMeta(&template.ObjectMeta, true, 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
}