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 }