k8s.io/kubernetes@v1.29.3/pkg/apis/resource/validation/validation.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package validation
    18  
    19  import (
    20  	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
    21  	"k8s.io/apimachinery/pkg/types"
    22  	"k8s.io/apimachinery/pkg/util/sets"
    23  	"k8s.io/apimachinery/pkg/util/validation/field"
    24  	corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
    25  	"k8s.io/kubernetes/pkg/apis/resource"
    26  )
    27  
    28  // validateResourceDriverName reuses the validation of a CSI driver because
    29  // the allowed values are exactly the same.
    30  var validateResourceDriverName = corevalidation.ValidateCSIDriverName
    31  
    32  // ValidateClaim validates a ResourceClaim.
    33  func ValidateClaim(resourceClaim *resource.ResourceClaim) field.ErrorList {
    34  	allErrs := corevalidation.ValidateObjectMeta(&resourceClaim.ObjectMeta, true, corevalidation.ValidateResourceClaimName, field.NewPath("metadata"))
    35  	allErrs = append(allErrs, validateResourceClaimSpec(&resourceClaim.Spec, field.NewPath("spec"))...)
    36  	return allErrs
    37  }
    38  
    39  func validateResourceClaimSpec(spec *resource.ResourceClaimSpec, fldPath *field.Path) field.ErrorList {
    40  	allErrs := field.ErrorList{}
    41  	for _, msg := range corevalidation.ValidateClassName(spec.ResourceClassName, false) {
    42  		allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceClassName"), spec.ResourceClassName, msg))
    43  	}
    44  	allErrs = append(allErrs, validateResourceClaimParameters(spec.ParametersRef, fldPath.Child("parametersRef"))...)
    45  	if !supportedAllocationModes.Has(string(spec.AllocationMode)) {
    46  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("allocationMode"), spec.AllocationMode, supportedAllocationModes.List()))
    47  	}
    48  	return allErrs
    49  }
    50  
    51  var supportedAllocationModes = sets.NewString(string(resource.AllocationModeImmediate), string(resource.AllocationModeWaitForFirstConsumer))
    52  
    53  // It would have been nice to use Go generics to reuse the same validation
    54  // function for Kind and Name in both types, but generics cannot be used to
    55  // access common fields in structs.
    56  
    57  func validateResourceClaimParameters(ref *resource.ResourceClaimParametersReference, fldPath *field.Path) field.ErrorList {
    58  	var allErrs field.ErrorList
    59  	if ref != nil {
    60  		if ref.Kind == "" {
    61  			allErrs = append(allErrs, field.Required(fldPath.Child("kind"), ""))
    62  		}
    63  		if ref.Name == "" {
    64  			allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
    65  		}
    66  	}
    67  	return allErrs
    68  }
    69  
    70  func validateClassParameters(ref *resource.ResourceClassParametersReference, fldPath *field.Path) field.ErrorList {
    71  	var allErrs field.ErrorList
    72  	if ref != nil {
    73  		if ref.Kind == "" {
    74  			allErrs = append(allErrs, field.Required(fldPath.Child("kind"), ""))
    75  		}
    76  		if ref.Name == "" {
    77  			allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
    78  		}
    79  		if ref.Namespace != "" {
    80  			for _, msg := range apimachineryvalidation.ValidateNamespaceName(ref.Namespace, false) {
    81  				allErrs = append(allErrs, field.Invalid(fldPath.Child("namespace"), ref.Namespace, msg))
    82  			}
    83  		}
    84  	}
    85  	return allErrs
    86  }
    87  
    88  // ValidateClass validates a ResourceClass.
    89  func ValidateClass(resourceClass *resource.ResourceClass) field.ErrorList {
    90  	allErrs := corevalidation.ValidateObjectMeta(&resourceClass.ObjectMeta, false, corevalidation.ValidateClassName, field.NewPath("metadata"))
    91  	allErrs = append(allErrs, validateResourceDriverName(resourceClass.DriverName, field.NewPath("driverName"))...)
    92  	allErrs = append(allErrs, validateClassParameters(resourceClass.ParametersRef, field.NewPath("parametersRef"))...)
    93  	if resourceClass.SuitableNodes != nil {
    94  		allErrs = append(allErrs, corevalidation.ValidateNodeSelector(resourceClass.SuitableNodes, field.NewPath("suitableNodes"))...)
    95  	}
    96  
    97  	return allErrs
    98  }
    99  
   100  // ValidateClassUpdate tests if an update to ResourceClass is valid.
   101  func ValidateClassUpdate(resourceClass, oldClass *resource.ResourceClass) field.ErrorList {
   102  	allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClass.ObjectMeta, &oldClass.ObjectMeta, field.NewPath("metadata"))
   103  	allErrs = append(allErrs, ValidateClass(resourceClass)...)
   104  	return allErrs
   105  }
   106  
   107  // ValidateClaimUpdate tests if an update to ResourceClaim is valid.
   108  func ValidateClaimUpdate(resourceClaim, oldClaim *resource.ResourceClaim) field.ErrorList {
   109  	allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldClaim.ObjectMeta, field.NewPath("metadata"))
   110  	allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(resourceClaim.Spec, oldClaim.Spec, field.NewPath("spec"))...)
   111  	allErrs = append(allErrs, ValidateClaim(resourceClaim)...)
   112  	return allErrs
   113  }
   114  
   115  // ValidateClaimStatusUpdate tests if an update to the status of a ResourceClaim is valid.
   116  func ValidateClaimStatusUpdate(resourceClaim, oldClaim *resource.ResourceClaim) field.ErrorList {
   117  	allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldClaim.ObjectMeta, field.NewPath("metadata"))
   118  	fldPath := field.NewPath("status")
   119  	// The name might not be set yet.
   120  	if resourceClaim.Status.DriverName != "" {
   121  		allErrs = append(allErrs, validateResourceDriverName(resourceClaim.Status.DriverName, fldPath.Child("driverName"))...)
   122  	} else if resourceClaim.Status.Allocation != nil {
   123  		allErrs = append(allErrs, field.Required(fldPath.Child("driverName"), "must be specified when `allocation` is set"))
   124  	}
   125  
   126  	allErrs = append(allErrs, validateAllocationResult(resourceClaim.Status.Allocation, fldPath.Child("allocation"))...)
   127  	allErrs = append(allErrs, validateResourceClaimConsumers(resourceClaim.Status.ReservedFor, resource.ResourceClaimReservedForMaxSize, fldPath.Child("reservedFor"))...)
   128  
   129  	// Now check for invariants that must be valid for a ResourceClaim.
   130  	if len(resourceClaim.Status.ReservedFor) > 0 {
   131  		if resourceClaim.Status.Allocation == nil {
   132  			allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "may not be specified when `allocated` is not set"))
   133  		} else {
   134  			if !resourceClaim.Status.Allocation.Shareable && len(resourceClaim.Status.ReservedFor) > 1 {
   135  				allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "may not be reserved more than once"))
   136  			}
   137  			// Items may be removed from ReservedFor while the claim is meant to be deallocated,
   138  			// but not added.
   139  			if resourceClaim.DeletionTimestamp != nil || resourceClaim.Status.DeallocationRequested {
   140  				oldSet := sets.New(oldClaim.Status.ReservedFor...)
   141  				newSet := sets.New(resourceClaim.Status.ReservedFor...)
   142  				newItems := newSet.Difference(oldSet)
   143  				if len(newItems) > 0 {
   144  					allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "new entries may not be added while `deallocationRequested` or `deletionTimestamp` are set"))
   145  				}
   146  			}
   147  		}
   148  	}
   149  
   150  	// Updates to a populated resourceClaim.Status.Allocation are not allowed
   151  	if oldClaim.Status.Allocation != nil && resourceClaim.Status.Allocation != nil {
   152  		allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(resourceClaim.Status.Allocation, oldClaim.Status.Allocation, fldPath.Child("allocation"))...)
   153  	}
   154  
   155  	if !oldClaim.Status.DeallocationRequested &&
   156  		resourceClaim.Status.DeallocationRequested &&
   157  		len(resourceClaim.Status.ReservedFor) > 0 {
   158  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("deallocationRequested"), "deallocation cannot be requested while `reservedFor` is set"))
   159  	}
   160  
   161  	if resourceClaim.Status.Allocation == nil &&
   162  		resourceClaim.Status.DeallocationRequested {
   163  		// Either one or the other field was modified incorrectly.
   164  		// For the sake of simplicity this only reports the invalid
   165  		// end result.
   166  		allErrs = append(allErrs, field.Forbidden(fldPath, "`allocation` must be set when `deallocationRequested` is set"))
   167  	}
   168  
   169  	// Once deallocation has been requested, that request cannot be removed
   170  	// anymore because the deallocation may already have started. The field
   171  	// can only get reset by the driver together with removing the
   172  	// allocation.
   173  	if oldClaim.Status.DeallocationRequested &&
   174  		!resourceClaim.Status.DeallocationRequested &&
   175  		resourceClaim.Status.Allocation != nil {
   176  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("deallocationRequested"), "may not be cleared when `allocation` is set"))
   177  	}
   178  
   179  	return allErrs
   180  }
   181  
   182  func validateAllocationResult(allocation *resource.AllocationResult, fldPath *field.Path) field.ErrorList {
   183  	var allErrs field.ErrorList
   184  	if allocation != nil {
   185  		if len(allocation.ResourceHandles) > 0 {
   186  			allErrs = append(allErrs, validateResourceHandles(allocation.ResourceHandles, resource.AllocationResultResourceHandlesMaxSize, fldPath.Child("resourceHandles"))...)
   187  		}
   188  		if allocation.AvailableOnNodes != nil {
   189  			allErrs = append(allErrs, corevalidation.ValidateNodeSelector(allocation.AvailableOnNodes, fldPath.Child("availableOnNodes"))...)
   190  		}
   191  	}
   192  	return allErrs
   193  }
   194  
   195  func validateResourceHandles(resourceHandles []resource.ResourceHandle, maxSize int, fldPath *field.Path) field.ErrorList {
   196  	var allErrs field.ErrorList
   197  	for i, resourceHandle := range resourceHandles {
   198  		idxPath := fldPath.Index(i)
   199  		allErrs = append(allErrs, validateResourceDriverName(resourceHandle.DriverName, idxPath.Child("driverName"))...)
   200  		if len(resourceHandle.Data) > resource.ResourceHandleDataMaxSize {
   201  			allErrs = append(allErrs, field.TooLongMaxLength(idxPath.Child("data"), len(resourceHandle.Data), resource.ResourceHandleDataMaxSize))
   202  		}
   203  	}
   204  	if len(resourceHandles) > maxSize {
   205  		// Dumping the entire field into the error message is likely to be too long,
   206  		// in particular when it is already beyond the maximum size. Instead this
   207  		// just shows the number of entries.
   208  		allErrs = append(allErrs, field.TooLongMaxLength(fldPath, len(resourceHandles), maxSize))
   209  	}
   210  	return allErrs
   211  }
   212  
   213  func validateResourceClaimUserReference(ref resource.ResourceClaimConsumerReference, fldPath *field.Path) field.ErrorList {
   214  	var allErrs field.ErrorList
   215  	if ref.Resource == "" {
   216  		allErrs = append(allErrs, field.Required(fldPath.Child("resource"), ""))
   217  	}
   218  	if ref.Name == "" {
   219  		allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
   220  	}
   221  	if ref.UID == "" {
   222  		allErrs = append(allErrs, field.Required(fldPath.Child("uid"), ""))
   223  	}
   224  	return allErrs
   225  }
   226  
   227  // validateSliceIsASet ensures that a slice contains no duplicates and does not exceed a certain maximum size.
   228  func validateSliceIsASet[T comparable](slice []T, maxSize int, validateItem func(item T, fldPath *field.Path) field.ErrorList, fldPath *field.Path) field.ErrorList {
   229  	var allErrs field.ErrorList
   230  	allItems := sets.New[T]()
   231  	for i, item := range slice {
   232  		idxPath := fldPath.Index(i)
   233  		if allItems.Has(item) {
   234  			allErrs = append(allErrs, field.Duplicate(idxPath, item))
   235  		} else {
   236  			allErrs = append(allErrs, validateItem(item, idxPath)...)
   237  			allItems.Insert(item)
   238  		}
   239  	}
   240  	if len(slice) > maxSize {
   241  		// Dumping the entire field into the error message is likely to be too long,
   242  		// in particular when it is already beyond the maximum size. Instead this
   243  		// just shows the number of entries.
   244  		allErrs = append(allErrs, field.TooLongMaxLength(fldPath, len(slice), maxSize))
   245  	}
   246  	return allErrs
   247  }
   248  
   249  // validateResourceClaimConsumers ensures that the slice contains no duplicate UIDs and does not exceed a certain maximum size.
   250  func validateResourceClaimConsumers(consumers []resource.ResourceClaimConsumerReference, maxSize int, fldPath *field.Path) field.ErrorList {
   251  	var allErrs field.ErrorList
   252  	allUIDs := sets.New[types.UID]()
   253  	for i, consumer := range consumers {
   254  		idxPath := fldPath.Index(i)
   255  		if allUIDs.Has(consumer.UID) {
   256  			allErrs = append(allErrs, field.Duplicate(idxPath.Child("uid"), consumer.UID))
   257  		} else {
   258  			allErrs = append(allErrs, validateResourceClaimUserReference(consumer, idxPath)...)
   259  			allUIDs.Insert(consumer.UID)
   260  		}
   261  	}
   262  	if len(consumers) > maxSize {
   263  		// Dumping the entire field into the error message is likely to be too long,
   264  		// in particular when it is already beyond the maximum size. Instead this
   265  		// just shows the number of entries.
   266  		allErrs = append(allErrs, field.TooLongMaxLength(fldPath, len(consumers), maxSize))
   267  	}
   268  	return allErrs
   269  }
   270  
   271  // ValidatePodSchedulingContext validates a PodSchedulingContext.
   272  func ValidatePodSchedulingContexts(schedulingCtx *resource.PodSchedulingContext) field.ErrorList {
   273  	allErrs := corevalidation.ValidateObjectMeta(&schedulingCtx.ObjectMeta, true, corevalidation.ValidatePodName, field.NewPath("metadata"))
   274  	allErrs = append(allErrs, validatePodSchedulingSpec(&schedulingCtx.Spec, field.NewPath("spec"))...)
   275  	return allErrs
   276  }
   277  
   278  func validatePodSchedulingSpec(spec *resource.PodSchedulingContextSpec, fldPath *field.Path) field.ErrorList {
   279  	allErrs := validateSliceIsASet(spec.PotentialNodes, resource.PodSchedulingNodeListMaxSize, validateNodeName, fldPath.Child("potentialNodes"))
   280  	return allErrs
   281  }
   282  
   283  // ValidatePodSchedulingContextUpdate tests if an update to PodSchedulingContext is valid.
   284  func ValidatePodSchedulingContextUpdate(schedulingCtx, oldSchedulingCtx *resource.PodSchedulingContext) field.ErrorList {
   285  	allErrs := corevalidation.ValidateObjectMetaUpdate(&schedulingCtx.ObjectMeta, &oldSchedulingCtx.ObjectMeta, field.NewPath("metadata"))
   286  	allErrs = append(allErrs, ValidatePodSchedulingContexts(schedulingCtx)...)
   287  	return allErrs
   288  }
   289  
   290  // ValidatePodSchedulingContextStatusUpdate tests if an update to the status of a PodSchedulingContext is valid.
   291  func ValidatePodSchedulingContextStatusUpdate(schedulingCtx, oldSchedulingCtx *resource.PodSchedulingContext) field.ErrorList {
   292  	allErrs := corevalidation.ValidateObjectMetaUpdate(&schedulingCtx.ObjectMeta, &oldSchedulingCtx.ObjectMeta, field.NewPath("metadata"))
   293  	allErrs = append(allErrs, validatePodSchedulingStatus(&schedulingCtx.Status, field.NewPath("status"))...)
   294  	return allErrs
   295  }
   296  
   297  func validatePodSchedulingStatus(status *resource.PodSchedulingContextStatus, fldPath *field.Path) field.ErrorList {
   298  	return validatePodSchedulingClaims(status.ResourceClaims, fldPath.Child("claims"))
   299  }
   300  
   301  func validatePodSchedulingClaims(claimStatuses []resource.ResourceClaimSchedulingStatus, fldPath *field.Path) field.ErrorList {
   302  	var allErrs field.ErrorList
   303  	names := sets.NewString()
   304  	for i, claimStatus := range claimStatuses {
   305  		allErrs = append(allErrs, validatePodSchedulingClaim(claimStatus, fldPath.Index(i))...)
   306  		if names.Has(claimStatus.Name) {
   307  			allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), claimStatus.Name))
   308  		} else {
   309  			names.Insert(claimStatus.Name)
   310  		}
   311  	}
   312  	return allErrs
   313  }
   314  
   315  func validatePodSchedulingClaim(status resource.ResourceClaimSchedulingStatus, fldPath *field.Path) field.ErrorList {
   316  	allErrs := validateSliceIsASet(status.UnsuitableNodes, resource.PodSchedulingNodeListMaxSize, validateNodeName, fldPath.Child("unsuitableNodes"))
   317  	return allErrs
   318  }
   319  
   320  // ValidateClaimTemplace validates a ResourceClaimTemplate.
   321  func ValidateClaimTemplate(template *resource.ResourceClaimTemplate) field.ErrorList {
   322  	allErrs := corevalidation.ValidateObjectMeta(&template.ObjectMeta, true, corevalidation.ValidateResourceClaimTemplateName, field.NewPath("metadata"))
   323  	allErrs = append(allErrs, validateResourceClaimTemplateSpec(&template.Spec, field.NewPath("spec"))...)
   324  	return allErrs
   325  }
   326  
   327  func validateResourceClaimTemplateSpec(spec *resource.ResourceClaimTemplateSpec, fldPath *field.Path) field.ErrorList {
   328  	allErrs := corevalidation.ValidateTemplateObjectMeta(&spec.ObjectMeta, fldPath.Child("metadata"))
   329  	allErrs = append(allErrs, validateResourceClaimSpec(&spec.Spec, fldPath.Child("spec"))...)
   330  	return allErrs
   331  }
   332  
   333  // ValidateClaimTemplateUpdate tests if an update to template is valid.
   334  func ValidateClaimTemplateUpdate(template, oldTemplate *resource.ResourceClaimTemplate) field.ErrorList {
   335  	allErrs := corevalidation.ValidateObjectMetaUpdate(&template.ObjectMeta, &oldTemplate.ObjectMeta, field.NewPath("metadata"))
   336  	allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(template.Spec, oldTemplate.Spec, field.NewPath("spec"))...)
   337  	allErrs = append(allErrs, ValidateClaimTemplate(template)...)
   338  	return allErrs
   339  }
   340  
   341  func validateNodeName(name string, fldPath *field.Path) field.ErrorList {
   342  	var allErrs field.ErrorList
   343  	for _, msg := range corevalidation.ValidateNodeName(name, false) {
   344  		allErrs = append(allErrs, field.Invalid(fldPath, name, msg))
   345  	}
   346  	return allErrs
   347  }