
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.
318 lines
15 KiB
Go
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
|
|
}
|