/
githubmirror
/
kubernetes
Обзор
Документация
Войти
/
githubmirror
/
kubernetes
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
pkg/apis/resource/validation/validation.go
2 034 строки
91 KB
Gaurav Ghildiyal
api: Run make-update to regenerate API files
24 июл 2026, 10:46
24 июл 2026, 10:46
346f439
Код
Авторство
О чём код?
/* 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 ( "context" "encoding/json" "errors" "fmt" "math" "regexp" "slices" "strconv" "strings" "github.com/google/uuid" "gopkg.in/inf.v0" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/operation" apiresource "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/validate" "k8s.io/apimachinery/pkg/api/validate/content" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel/environment" utilfeature "k8s.io/apiserver/pkg/util/feature" dracel "k8s.io/dynamic-resource-allocation/cel" "k8s.io/dynamic-resource-allocation/structured" core "k8s.io/kubernetes/pkg/apis/core" corehelper "k8s.io/kubernetes/pkg/apis/core/helper" v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper" corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" "k8s.io/kubernetes/pkg/apis/resource" "k8s.io/kubernetes/pkg/features" ) // ResourceNormalizationRules handles the structural differences between v1beta1 // (flattened fields) and v1/v1beta2 (fields under 'exactly') for validation error paths. var ResourceNormalizationRules = []field.NormalizationRule{ { // ResourceClaim Regexp: regexp.MustCompile(`spec\.devices\.requests\[(\d+)\]\.(deviceClassName|selectors|allocationMode|count|adminAccess|tolerations|derivedAttributes)`), Replacement: "spec.devices.requests[$1].exactly.$2", }, { // ResourceClaimTemplate (RCT.Spec.Spec adds an extra "spec." prefix) Regexp: regexp.MustCompile(`spec\.spec\.devices\.requests\[(\d+)\]\.(deviceClassName|selectors|allocationMode|count|adminAccess|tolerations|derivedAttributes)`), Replacement: "spec.spec.devices.requests[$1].exactly.$2", }, { // This v1beta1 'basic' to flattened rule is to support ResourceSlice Regexp: regexp.MustCompile(`spec\.devices\[(\d+)\]\.basic\.`), Replacement: "spec.devices[$1].", }, } var ( // validateResourceDriverName reuses the validation of a CSI driver because // the allowed values are exactly the same. validateDriverName = corevalidation.ValidateCSIDriverName validateDeviceName = corevalidation.ValidateDNS1123Label validateDeviceClassName = corevalidation.ValidateDNS1123Subdomain validateRequestName = corevalidation.ValidateDNS1123Label validateCounterName = corevalidation.ValidateDNS1123Label // this is the max length limit for domain/ID attributeAndCapacityMaxKeyLength = resource.DeviceMaxDomainLength + 1 + resource.DeviceMaxIDLength ) func validatePoolName(name string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if name == "" { allErrs = append(allErrs, field.Required(fldPath, "")) } else { if len(name) > resource.PoolNameMaxLength { allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.PoolNameMaxLength).WithOrigin("format=k8s-resource-pool-name")) } parts := strings.Split(name, "/") for _, part := range parts { allErrs = append(allErrs, corevalidation.ValidateDNS1123Subdomain(part, fldPath).WithOrigin("format=k8s-resource-pool-name")...) } } return allErrs } func validateUID(uid string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if err := uuid.Validate(uid); err != nil { allErrs = append(allErrs, field.Invalid(fldPath, uid, fmt.Sprintf("error validating uid: %v", err))) } else if len(uid) != 36 || uid != strings.ToLower(uid) { allErrs = append(allErrs, field.Invalid(fldPath, uid, "uid must be in RFC 4122 normalized form, `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` with lowercase hexadecimal characters")) } return allErrs.WithOrigin("format=k8s-uuid") } // ValidateResourceClaim validates a ResourceClaim. func ValidateResourceClaim(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"), false)...) return allErrs } // ValidateResourceClaimUpdate tests if an update to ResourceClaim is valid. func ValidateResourceClaimUpdate(resourceClaim, oldClaim *resource.ResourceClaim) field.ErrorList { allErrs := corevalidation.ValidateObjectMeta(&resourceClaim.ObjectMeta, true, corevalidation.ValidateResourceClaimName, field.NewPath("metadata")) allErrs = append(allErrs, corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldClaim.ObjectMeta, field.NewPath("metadata"))...) // The spec is immutable. On update, we only check for immutability. // Re-validating other fields is skipped because the user cannot change them; // the only actionable error is for the immutability violation. allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(resourceClaim.Spec, oldClaim.Spec, field.NewPath("spec")).WithOrigin("immutable").MarkCoveredByDeclarative()...) return allErrs } // ValidateResourceClaimStatusUpdate tests if an update to the status of a ResourceClaim is valid. func ValidateResourceClaimStatusUpdate(resourceClaim, oldClaim *resource.ResourceClaim) field.ErrorList { allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldClaim.ObjectMeta, field.NewPath("metadata")) requestNames := gatherRequestNames(&resourceClaim.Spec.Devices) allErrs = append(allErrs, validateResourceClaimStatusUpdate(&resourceClaim.Status, &oldClaim.Status, resourceClaim.DeletionTimestamp != nil, requestNames, field.NewPath("status"))...) return allErrs } func validateResourceClaimSpec(spec *resource.ResourceClaimSpec, fldPath *field.Path, stored bool) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, validateDeviceClaim(&spec.Devices, fldPath.Child("devices"), stored)...) return allErrs } // deviceValidationOptions holds validation options and shared state (like accumulators) // that are used during the validation of a DeviceClaim. type deviceValidationOptions struct { // stored indicates whether the object being validated is already stored // in the API server. If true, certain previously valid fields (like CEL expressions) are // considered valid without re-evaluation, allowing for relaxed validation on updates. stored bool // totalDerivedAttrCost tracks the cumulative compilation cost of all derived // attribute CEL expressions across the entire DeviceClaim. This ensures their // combined cost does not exceed the allowed budget. totalDerivedAttrCost *uint64 // constraintAttributes maps a request/subrequest reference ("req" or // "req/subreq") to the set of attribute names referenced by constraints // targeting it: // - If an attribute name is in the "" key, it applies to all requests. // - If an attribute name is in the "req" key, it applies to all subrequests // of "req". // - If an attribute name is in the "req/subreq" key, it applies to that // specific subrequest. constraintAttributes map[string]sets.Set[string] } func gatherConstraintAttributes(constraints []resource.DeviceConstraint) map[string]sets.Set[string] { attrs := make(map[string]sets.Set[string]) for _, constraint := range constraints { var attrName string if constraint.MatchAttribute != nil { attrName = string(*constraint.MatchAttribute) } else if constraint.DistinctAttribute != nil { attrName = string(*constraint.DistinctAttribute) } else { continue } if len(constraint.Requests) == 0 { // A constraint with an empty requests list applies globally, so we // use the empty string key to represent all requests. if attrs[""] == nil { attrs[""] = sets.New[string]() } attrs[""].Insert(attrName) } else { // A constraint might target multiple explicit requests or subrequests, // so we track the attribute usage for each individual target. for _, req := range constraint.Requests { if attrs[req] == nil { attrs[req] = sets.New[string]() } attrs[req].Insert(attrName) } } } return attrs } func validateDeviceClaim(deviceClaim *resource.DeviceClaim, fldPath *field.Path, stored bool) field.ErrorList { allErrs := field.ErrorList{} requestNames := gatherRequestNames(deviceClaim) var totalDerivedAttrCost uint64 opts := deviceValidationOptions{ stored: stored, totalDerivedAttrCost: &totalDerivedAttrCost, constraintAttributes: gatherConstraintAttributes(deviceClaim.Constraints), } allErrs = append(allErrs, validateSet(deviceClaim.Requests, resource.DeviceRequestsMaxSize, func(request resource.DeviceRequest, fldPath *field.Path) field.ErrorList { return validateDeviceRequest(request, fldPath, opts) }, func(request resource.DeviceRequest) string { return request.Name }, fldPath.Child("requests"), sizeCovered, uniquenessCovered)...) allErrs = append(allErrs, validateSlice(deviceClaim.Constraints, resource.DeviceConstraintsMaxSize, func(constraint resource.DeviceConstraint, fldPath *field.Path) field.ErrorList { return validateDeviceConstraint(constraint, fldPath, requestNames) }, fldPath.Child("constraints"), sizeCovered)...) allErrs = append(allErrs, validateSlice(deviceClaim.Config, resource.DeviceConfigMaxSize, func(config resource.DeviceClaimConfiguration, fldPath *field.Path) field.ErrorList { return validateDeviceClaimConfiguration(config, fldPath, requestNames, stored) }, fldPath.Child("config"), sizeCovered)...) if totalDerivedAttrCost > resource.DeviceClaimDerivedAttributeCELMaxCost { allErrs = append(allErrs, field.Forbidden(fldPath, "too complex, total cost of derived attribute CEL expressions in the claim exceeds cost limit")) } return allErrs } type requestNames map[string]sets.Set[string] func (r requestNames) Has(s string) bool { segments := strings.Split(s, "/") // If there are more than one / in the string, we // know there can't be any match. if len(segments) > 2 { return false } // If the first segment doesn't have a match, we // don't need to check the other one. subRequestNames, found := r[segments[0]] if !found { return false } if len(segments) == 1 { return true } // If the first segment matched and we have another one, // check for a match for that too. return subRequestNames.Has(segments[1]) } func gatherRequestNames(deviceClaim *resource.DeviceClaim) requestNames { requestNames := make(requestNames) for _, request := range deviceClaim.Requests { if len(request.FirstAvailable) == 0 { requestNames[request.Name] = nil continue } subRequestNames := sets.New[string]() for _, subRequest := range request.FirstAvailable { subRequestNames.Insert(subRequest.Name) } requestNames[request.Name] = subRequestNames } return requestNames } func gatherAllocatedDevices(allocationResult *resource.DeviceAllocationResult) sets.Set[structured.SharedDeviceID] { allocatedDevices := sets.New[structured.SharedDeviceID]() for _, result := range allocationResult.Results { deviceName := result.Device deviceID := structured.MakeDeviceID(result.Driver, result.Pool, deviceName) sharedDeviceID := structured.MakeSharedDeviceID(deviceID, result.ShareID) allocatedDevices.Insert(sharedDeviceID) } return allocatedDevices } func validateDeviceRequest(request resource.DeviceRequest, fldPath *field.Path, opts deviceValidationOptions) field.ErrorList { allErrs := validateRequestName(request.Name, fldPath.Child("name")) numDeviceRequestType := 0 hasFirstAvailable := len(request.FirstAvailable) > 0 hasExactly := request.Exactly != nil if hasFirstAvailable { numDeviceRequestType++ } if hasExactly { numDeviceRequestType++ } switch { case numDeviceRequestType == 0: allErrs = append(allErrs, field.Required(fldPath, "exactly one of `exactly` or `firstAvailable` is required")) case numDeviceRequestType > 1: allErrs = append(allErrs, field.Invalid(fldPath, nil, "exactly one of `exactly` or `firstAvailable` is required, but multiple fields are set")) case hasFirstAvailable: allErrs = append(allErrs, validateSet(request.FirstAvailable, resource.FirstAvailableDeviceRequestMaxSize, func(subRequest resource.DeviceSubRequest, fldPath *field.Path) field.ErrorList { return validateDeviceSubRequest(subRequest, fldPath, opts, request.Name) }, func(subRequest resource.DeviceSubRequest) string { return subRequest.Name }, fldPath.Child("firstAvailable"), sizeCovered, uniquenessCovered)...) case hasExactly: allErrs = append(allErrs, validateExactDeviceRequest(*request.Exactly, fldPath.Child("exactly"), opts, request.Name)...) } return allErrs } func validateDeviceSubRequest(subRequest resource.DeviceSubRequest, fldPath *field.Path, opts deviceValidationOptions, parentRequestName string) field.ErrorList { allErrs := validateRequestName(subRequest.Name, fldPath.Child("name")) allErrs = append(allErrs, validateDeviceClass(subRequest.DeviceClassName, fldPath.Child("deviceClassName")).MarkCoveredByDeclarative()...) allErrs = append(allErrs, validateSelectorSlice(subRequest.Selectors, fldPath.Child("selectors"), opts)...) allErrs = append(allErrs, validateDeviceDerivedAttributes(subRequest.DerivedAttributes, fldPath.Child("derivedAttributes"), opts, parentRequestName+"/"+subRequest.Name)...) allErrs = append(allErrs, validateDeviceAllocationMode(subRequest.AllocationMode, subRequest.Count, fldPath.Child("allocationMode"), fldPath.Child("count"))...) for i, toleration := range subRequest.Tolerations { allErrs = append(allErrs, validateDeviceToleration(toleration, fldPath.Child("tolerations").Index(i))...) } return allErrs } func validateExactDeviceRequest(request resource.ExactDeviceRequest, fldPath *field.Path, opts deviceValidationOptions, requestName string) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateDeviceClass(request.DeviceClassName, fldPath.Child("deviceClassName"))...) allErrs = append(allErrs, validateSelectorSlice(request.Selectors, fldPath.Child("selectors"), opts)...) allErrs = append(allErrs, validateDeviceDerivedAttributes(request.DerivedAttributes, fldPath.Child("derivedAttributes"), opts, requestName)...) allErrs = append(allErrs, validateDeviceAllocationMode(request.AllocationMode, request.Count, fldPath.Child("allocationMode"), fldPath.Child("count"))...) for i, toleration := range request.Tolerations { allErrs = append(allErrs, validateDeviceToleration(toleration, fldPath.Child("tolerations").Index(i))...) } return allErrs } func validateDeviceAllocationMode(deviceAllocationMode resource.DeviceAllocationMode, count int64, allocModeFldPath, countFldPath *field.Path) field.ErrorList { var allErrs field.ErrorList switch deviceAllocationMode { case resource.DeviceAllocationModeAll: if count != 0 { allErrs = append(allErrs, field.Invalid(countFldPath, count, fmt.Sprintf("must not be specified when allocationMode is '%s'", deviceAllocationMode))) } case resource.DeviceAllocationModeExactCount: if count <= 0 { allErrs = append(allErrs, field.Invalid(countFldPath, count, "must be greater than zero")) } default: // NOTE: Declarative validation does not (yet) enforce the real requiredness of this field // because v1beta1 uses a bespoke form of union which makes this field truly optional // in some cases and truly required in others. DO NOT REMOVE THIS CODE UNLESS THAT IS RESOLVED. allErrs = append(allErrs, field.NotSupported(allocModeFldPath, deviceAllocationMode, []resource.DeviceAllocationMode{resource.DeviceAllocationModeAll, resource.DeviceAllocationModeExactCount}).MarkCoveredByDeclarative()) } return allErrs } func validateDeviceClass(deviceClass string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if deviceClass == "" { allErrs = append(allErrs, field.Required(fldPath, "")) } else { allErrs = append(allErrs, validateDeviceClassName(deviceClass, fldPath)...) } return allErrs } func validateSelectorSlice(selectors []resource.DeviceSelector, fldPath *field.Path, opts deviceValidationOptions) field.ErrorList { return validateSlice(selectors, resource.DeviceSelectorsMaxSize, func(selector resource.DeviceSelector, fldPath *field.Path) field.ErrorList { return validateSelector(selector, fldPath, opts) }, fldPath, sizeCovered) } func validateSelector(selector resource.DeviceSelector, fldPath *field.Path, opts deviceValidationOptions) field.ErrorList { var allErrs field.ErrorList if selector.CEL == nil { allErrs = append(allErrs, field.Required(fldPath.Child("cel"), "")) } else { allErrs = append(allErrs, validateCELExpression(selector.CEL.Expression, fldPath.Child("cel", "expression"), opts, false)...) } return allErrs } func validateCELExpression(expression string, fldPath *field.Path, opts deviceValidationOptions, derivedAttribute bool) field.ErrorList { if opts.stored { // If a CEL expression is stored, it must have been valid at the // time it was stored and thus must also be valid now. Returning // early here allows relaxing validation more quickly without // ratcheting and is cheaper. return nil } envType := environment.NewExpressions var allErrs field.ErrorList if len(expression) > resource.CELSelectorExpressionMaxLength { allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.CELSelectorExpressionMaxLength)) // Don't bother compiling too long expressions. return allErrs } result := dracel.GetCompiler(dracel.Features{ EnableConsumableCapacity: utilfeature.DefaultFeatureGate.Enabled(features.DRAConsumableCapacity), EnableListTypeAttributes: utilfeature.DefaultFeatureGate.Enabled(features.DRAListTypeAttributes), }).CompileCELExpression(expression, dracel.Options{ EnvType: &envType, DerivedAttribute: derivedAttribute, }) if result.Error != nil { allErrs = append(allErrs, convertCELErrorToValidationError(fldPath, expression, result.Error)) } else { if derivedAttribute { if result.MaxCost > min(resource.CELSelectorExpressionMaxCost, resource.DeviceClaimDerivedAttributeCELMaxCost) { allErrs = append(allErrs, field.Forbidden(fldPath, "too complex, exceeds cost limit")) } if opts.totalDerivedAttrCost != nil { *opts.totalDerivedAttrCost += result.MaxCost } } else if result.MaxCost > resource.CELSelectorExpressionMaxCost { allErrs = append(allErrs, field.Forbidden(fldPath, "too complex, exceeds cost limit")) } } return allErrs } func convertCELErrorToValidationError(fldPath *field.Path, expression string, err error) *field.Error { var celErr *cel.Error if errors.As(err, &celErr) { switch celErr.Type { case cel.ErrorTypeRequired: return field.Required(fldPath, celErr.Detail) case cel.ErrorTypeInvalid: return field.Invalid(fldPath, expression, celErr.Detail) case cel.ErrorTypeInternal: return field.InternalError(fldPath, celErr) } } return field.InternalError(fldPath, fmt.Errorf("unsupported error type: %w", err)) } func isAttributeUsedByConstraint(attrName, reqName string, constraintAttributes map[string]sets.Set[string]) bool { // Check if the attribute is used by a globally applied constraint. if globalAttrs, ok := constraintAttributes[""]; ok && globalAttrs.Has(attrName) { return true } // Check if the attribute is used by a constraint explicitly targeting this // request or subrequest. if reqAttrs, ok := constraintAttributes[reqName]; ok && reqAttrs.Has(attrName) { return true } // If this is a subrequest (e.g. "req/subreq"), check if the attribute is // used by a constraint targeting the parent request ("req"), as that // applies to all of its subrequests. if parentReqName, _, found := strings.Cut(reqName, "/"); found { if parentAttrs, ok := constraintAttributes[parentReqName]; ok && parentAttrs.Has(attrName) { return true } } return false } func validateDeviceDerivedAttributes(attrs []resource.DeviceDerivedAttribute, fldPath *field.Path, opts deviceValidationOptions, requestName string) field.ErrorList { return validateSet(attrs, -1, func(attr resource.DeviceDerivedAttribute, fldPath *field.Path) field.ErrorList { return validateDeviceDerivedAttribute(attr, fldPath, opts, requestName) }, func(attr resource.DeviceDerivedAttribute) string { return string(attr.Name) }, fldPath) } func validateDeviceDerivedAttribute(attr resource.DeviceDerivedAttribute, fldPath *field.Path, opts deviceValidationOptions, requestName string) field.ErrorList { var allErrs field.ErrorList // Declarative validation checks for non-empty value, // no need to check that here. if attr.Name != "" { if !isAttributeUsedByConstraint(string(attr.Name), requestName, opts.constraintAttributes) { allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), attr.Name, "must be referenced by at least one matchAttribute or distinctAttribute constraint targeting this request")) } } // Declarative validation checks for non-empty value, // no need to check that here. if attr.Expression != "" { allErrs = append(allErrs, validateCELExpression(attr.Expression, fldPath.Child("expression"), opts, true)...) } return allErrs } func validateDeviceConstraint(constraint resource.DeviceConstraint, fldPath *field.Path, requestNames requestNames) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateSet(constraint.Requests, resource.DeviceRequestsMaxSize, func(name string, fldPath *field.Path) field.ErrorList { return validateRequestNameRef(name, fldPath, requestNames) }, stringKey, fldPath.Child("requests"), sizeCovered, uniquenessCovered)...) if constraint.MatchAttribute != nil { allErrs = append(allErrs, validateFullyQualifiedName(*constraint.MatchAttribute, fldPath.Child("matchAttribute")).MarkCoveredByDeclarative()...) } else if constraint.DistinctAttribute != nil { allErrs = append(allErrs, validateFullyQualifiedName(*constraint.DistinctAttribute, fldPath.Child("distinctAttribute")).MarkCoveredByDeclarative()...) } else if utilfeature.DefaultFeatureGate.Enabled(features.DRAConsumableCapacity) { allErrs = append(allErrs, field.Required(fldPath, `exactly one of "matchAttribute" or "distinctAttribute" is required, but multiple fields are set`)) } else { allErrs = append(allErrs, field.Required(fldPath.Child("matchAttribute"), "")) } return allErrs } func validateDeviceClaimConfiguration(config resource.DeviceClaimConfiguration, fldPath *field.Path, requestNames requestNames, stored bool) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateSet(config.Requests, resource.DeviceRequestsMaxSize, func(name string, fldPath *field.Path) field.ErrorList { return validateRequestNameRef(name, fldPath, requestNames) }, stringKey, fldPath.Child("requests"), sizeCovered, uniquenessCovered)...) allErrs = append(allErrs, validateDeviceConfiguration(config.DeviceConfiguration, fldPath, stored)...) return allErrs } func validateRequestNameRef(name string, fldPath *field.Path, requestNames requestNames) field.ErrorList { var allErrs field.ErrorList segments := strings.Split(name, "/") if len(segments) > 2 { allErrs = append(allErrs, field.Invalid(fldPath, name, "must be the name of a request in the claim or the name of a request and a subrequest separated by '/'")) return allErrs } for i := range segments { allErrs = append(allErrs, validateRequestName(segments[i], fldPath)...) } if !requestNames.Has(name) { allErrs = append(allErrs, field.Invalid(fldPath, name, "must be the name of a request in the claim or the name of a request and a subrequest separated by '/'")) } return allErrs } func validateDeviceConfiguration(config resource.DeviceConfiguration, fldPath *field.Path, stored bool) field.ErrorList { var allErrs field.ErrorList if config.Opaque == nil { allErrs = append(allErrs, field.Required(fldPath.Child("opaque"), "")) } else { allErrs = append(allErrs, validateOpaqueConfiguration(*config.Opaque, fldPath.Child("opaque"), stored)...) } return allErrs } func validateOpaqueConfiguration(config resource.OpaqueDeviceConfiguration, fldPath *field.Path, stored bool) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateDriverName(config.Driver, fldPath.Child("driver"), corevalidation.RequiredCovered, corevalidation.FormatCovered, corevalidation.SizeCovered)...) allErrs = append(allErrs, validateRawExtension(config.Parameters, fldPath.Child("parameters"), stored, resource.OpaqueParametersMaxLength)...) return allErrs } func validateResourceClaimStatusUpdate(status, oldStatus *resource.ResourceClaimStatus, claimDeleted bool, requestNames requestNames, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateSet(status.ReservedFor, resource.ResourceClaimReservedForMaxSize, validateResourceClaimUserReference, func(consumer resource.ResourceClaimConsumerReference) types.UID { return consumer.UID }, fldPath.Child("reservedFor"), sizeCovered, uniquenessCovered)...) var allocatedDevices sets.Set[structured.SharedDeviceID] if status.Allocation != nil { allocatedDevices = gatherAllocatedDevices(&status.Allocation.Devices) } allErrs = append(allErrs, validateSet(status.Devices, -1, func(device resource.AllocatedDeviceStatus, fldPath *field.Path) field.ErrorList { return validateDeviceStatus(device, fldPath, allocatedDevices) }, func(device resource.AllocatedDeviceStatus) structured.SharedDeviceID { deviceID := structured.MakeDeviceID(device.Driver, device.Pool, device.Device) return structured.MakeSharedDeviceID(deviceID, (*types.UID)(device.ShareID)) }, fldPath.Child("devices"), uniquenessCovered)...) // Now check for invariants that must be valid for a ResourceClaim. if len(status.ReservedFor) > 0 { if status.Allocation == nil { allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "may not be specified when `allocated` is not set")) } else { // Items may be removed from ReservedFor while the claim is meant to be deallocated, // but not added. if claimDeleted { oldSet := sets.New(oldStatus.ReservedFor...) newSet := sets.New(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")) } } } } // Updates to a populated status.Allocation are not allowed. // Unmodified fields don't need to be validated again and, // in this particular case, must not be validated again because // validation for new results is tighter than it was before. if oldStatus.Allocation != nil && status.Allocation != nil { allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(status.Allocation, oldStatus.Allocation, fldPath.Child("allocation")).WithOrigin("update").MarkCoveredByDeclarative()...) } else if status.Allocation != nil { allErrs = append(allErrs, validateAllocationResult(status.Allocation, fldPath.Child("allocation"), requestNames, false)...) } 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 } // validateAllocationResult enforces constraints for *new* results, which in at // least one case (admin access) are more strict than before. Therefore it // may not be called to re-validate results which were stored earlier. func validateAllocationResult(allocation *resource.AllocationResult, fldPath *field.Path, requestNames requestNames, stored bool) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateDeviceAllocationResult(allocation.Devices, fldPath.Child("devices"), requestNames, stored)...) if allocation.NodeSelector != nil { allErrs = append(allErrs, corevalidation.ValidateNodeSelector(allocation.NodeSelector, false, fldPath.Child("nodeSelector"))...) } return allErrs } func validateDeviceAllocationResult(allocation resource.DeviceAllocationResult, fldPath *field.Path, requestNames requestNames, stored bool) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateSlice(allocation.Results, resource.AllocationResultsMaxSize, func(result resource.DeviceRequestAllocationResult, fldPath *field.Path) field.ErrorList { return validateDeviceRequestAllocationResult(result, fldPath, requestNames) }, fldPath.Child("results"), sizeCovered)...) allErrs = append(allErrs, validateSlice(allocation.Config, 2*resource.DeviceConfigMaxSize, /* class + claim */ func(config resource.DeviceAllocationConfiguration, fldPath *field.Path) field.ErrorList { return validateDeviceAllocationConfiguration(config, fldPath, requestNames, stored) }, fldPath.Child("config"), sizeCovered)...) return allErrs } func validateDeviceRequestAllocationResult(result resource.DeviceRequestAllocationResult, fldPath *field.Path, requestNames requestNames) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateRequestNameRef(result.Request, fldPath.Child("request"), requestNames)...) allErrs = append(allErrs, validateDriverName(result.Driver, fldPath.Child("driver"), corevalidation.RequiredCovered, corevalidation.FormatCovered, corevalidation.SizeCovered)...) allErrs = append(allErrs, validatePoolName(result.Pool, fldPath.Child("pool")).MarkCoveredByDeclarative()...) allErrs = append(allErrs, validateDeviceName(result.Device, fldPath.Child("device"))...) allErrs = append(allErrs, validateDeviceBindingParameters(result.BindingConditions, result.BindingFailureConditions, fldPath)...) for i, toleration := range result.Tolerations { allErrs = append(allErrs, validateDeviceToleration(toleration, fldPath.Child("tolerations").Index(i))...) } if result.ShareID != nil { allErrs = append(allErrs, validateUID(string(*result.ShareID), fldPath.Child("shareID")).MarkCoveredByDeclarative()...) } allErrs = append(allErrs, validateSkipNodeOperations(result.SkipNodeOperations, fldPath.Child("skipNodeOperations"))...) return allErrs } func validateDeviceAllocationConfiguration(config resource.DeviceAllocationConfiguration, fldPath *field.Path, requestNames requestNames, stored bool) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateAllocationConfigSource(config.Source, fldPath.Child("source"))...) allErrs = append(allErrs, validateSet(config.Requests, resource.DeviceRequestsMaxSize, func(name string, fldPath *field.Path) field.ErrorList { return validateRequestNameRef(name, fldPath, requestNames) }, stringKey, fldPath.Child("requests"), sizeCovered, uniquenessCovered)...) allErrs = append(allErrs, validateDeviceConfiguration(config.DeviceConfiguration, fldPath, stored)...) return allErrs } func validateAllocationConfigSource(source resource.AllocationConfigSource, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList switch source { case "": allErrs = append(allErrs, field.Required(fldPath, "").MarkCoveredByDeclarative()) case resource.AllocationConfigSourceClaim, resource.AllocationConfigSourceClass: default: allErrs = append(allErrs, field.NotSupported(fldPath, source, []resource.AllocationConfigSource{resource.AllocationConfigSourceClaim, resource.AllocationConfigSourceClass}).MarkCoveredByDeclarative()) } return allErrs } // ValidateDeviceClass validates a DeviceClass. func ValidateDeviceClass(class *resource.DeviceClass) field.ErrorList { validateClassName := func(fldPath *field.Path, name string) field.ErrorList { // validate.LongName doesn't respect operation type currently (CREATE or UPDATE) // so it is ok to use operation.Operation{} here return validate.LongName(context.Background(), operation.Operation{}, fldPath, &name, nil).MarkCoveredByDeclarative() } allErrs := corevalidation.ValidateObjectMetaWithOpts(&class.ObjectMeta, false, validateClassName, field.NewPath("metadata")) allErrs = append(allErrs, validateDeviceClassSpec(&class.Spec, nil, field.NewPath("spec"))...) return allErrs } // ValidateDeviceClassUpdate tests if an update to DeviceClass is valid. func ValidateDeviceClassUpdate(class, oldClass *resource.DeviceClass) field.ErrorList { validateClassName := func(fldPath *field.Path, name string) field.ErrorList { return validate.LongName(context.Background(), operation.Operation{}, fldPath, &name, nil).MarkCoveredByDeclarative() } // TODO(lalitc375): Remove this if decided in https://github.com/kubernetes/kubernetes/issues/134444. allErrs := corevalidation.ValidateObjectMetaWithOpts(&class.ObjectMeta, false, validateClassName, field.NewPath("metadata")) allErrs = append(allErrs, corevalidation.ValidateObjectMetaUpdate(&class.ObjectMeta, &oldClass.ObjectMeta, field.NewPath("metadata"))...) allErrs = append(allErrs, validateDeviceClassSpec(&class.Spec, &oldClass.Spec, field.NewPath("spec"))...) return allErrs } func validateDeviceClassSpec(spec, oldSpec *resource.DeviceClassSpec, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList // If the selectors are exactly as before, we treat the CEL expressions as "stored". // Any change, including merely reordering selectors, triggers validation as new // expressions. stored := false if oldSpec != nil { stored = apiequality.Semantic.DeepEqual(spec.Selectors, oldSpec.Selectors) } allErrs = append(allErrs, validateSlice(spec.Selectors, resource.DeviceSelectorsMaxSize, func(selector resource.DeviceSelector, fldPath *field.Path) field.ErrorList { return validateSelector(selector, fldPath, deviceValidationOptions{stored: stored}) }, fldPath.Child("selectors"), sizeCovered)...) // Same logic as above for configs. stored = false if oldSpec != nil { stored = apiequality.Semantic.DeepEqual(spec.Config, oldSpec.Config) } allErrs = append(allErrs, validateSlice(spec.Config, resource.DeviceConfigMaxSize, func(config resource.DeviceClassConfiguration, fldPath *field.Path) field.ErrorList { return validateDeviceClassConfiguration(config, fldPath, stored) }, fldPath.Child("config"), sizeCovered)...) if spec.ExtendedResourceName != nil && !v1helper.IsExtendedResourceName(corev1.ResourceName(*spec.ExtendedResourceName)) { allErrs = append(allErrs, field.Invalid(fldPath.Child("extendedResourceName"), *spec.ExtendedResourceName, "must be a valid extended resource name").MarkCoveredByDeclarative().WithOrigin("format=k8s-extended-resource-name")) } return allErrs } func validateDeviceClassConfiguration(config resource.DeviceClassConfiguration, fldPath *field.Path, stored bool) field.ErrorList { return validateDeviceConfiguration(config.DeviceConfiguration, fldPath, stored) } // ValidateResourceClaimTemplate validates a ResourceClaimTemplate. func ValidateResourceClaimTemplate(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"), false)...) return allErrs } func validateResourceClaimTemplateSpec(spec *resource.ResourceClaimTemplateSpec, fldPath *field.Path, stored bool) field.ErrorList { allErrs := corevalidation.ValidateTemplateObjectMeta(&spec.ObjectMeta, fldPath.Child("metadata")) allErrs = append(allErrs, validateResourceClaimSpec(&spec.Spec, fldPath.Child("spec"), stored)...) return allErrs } // ValidateResourceClaimTemplateUpdate tests if an update to template is valid. func ValidateResourceClaimTemplateUpdate(template, oldTemplate *resource.ResourceClaimTemplate) field.ErrorList { allErrs := corevalidation.ValidateObjectMetaUpdate(&template.ObjectMeta, &oldTemplate.ObjectMeta, field.NewPath("metadata")) // The spec is immutable. On update, we only check for immutability. // Re-validating other fields is skipped because the user cannot change them; // the only actionable error is for the immutability violation. allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(template.Spec, oldTemplate.Spec, field.NewPath("spec"))...) 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 } // ValidateResourceSlice tests if a ResourceSlice object is valid. func ValidateResourceSlice(slice *resource.ResourceSlice) field.ErrorList { allErrs := corevalidation.ValidateObjectMeta(&slice.ObjectMeta, false, apimachineryvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) allErrs = append(allErrs, validateResourceSliceSpec(&slice.Spec, nil, field.NewPath("spec"))...) return allErrs } // ValidateResourceSliceUpdate tests if a ResourceSlice update is valid. func ValidateResourceSliceUpdate(resourceSlice, oldResourceSlice *resource.ResourceSlice) field.ErrorList { allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceSlice.ObjectMeta, &oldResourceSlice.ObjectMeta, field.NewPath("metadata")) allErrs = append(allErrs, validateResourceSliceSpec(&resourceSlice.Spec, &oldResourceSlice.Spec, field.NewPath("spec"))...) return allErrs } func validateResourceSliceSpec(spec, oldSpec *resource.ResourceSliceSpec, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateDriverName(spec.Driver, fldPath.Child("driver"))...) allErrs = append(allErrs, validateResourcePool(spec.Pool, fldPath.Child("pool"))...) if oldSpec != nil { allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(spec.Pool.Name, oldSpec.Pool.Name, fldPath.Child("pool", "name"))...) allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(spec.Driver, oldSpec.Driver, fldPath.Child("driver"))...) allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(spec.NodeName, oldSpec.NodeName, fldPath.Child("nodeName"))...) } setFields := make([]string, 0, 4) if spec.NodeName != nil { if *spec.NodeName != "" { setFields = append(setFields, "`nodeName`") allErrs = append(allErrs, validateNodeName(*spec.NodeName, fldPath.Child("nodeName"))...) } else { allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeName"), *spec.NodeName, "must be either unset or set to a non-empty string")) } } if spec.NodeSelector != nil { setFields = append(setFields, "`nodeSelector`") allErrs = append(allErrs, corevalidation.ValidateNodeSelector(spec.NodeSelector, false, fldPath.Child("nodeSelector"))...) if len(spec.NodeSelector.NodeSelectorTerms) != 1 { // This additional constraint simplifies merging of different selectors // when devices are allocated from different slices. allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeSelector", "nodeSelectorTerms"), spec.NodeSelector.NodeSelectorTerms, "must have exactly one node selector term")) } } if spec.AllNodes != nil { if *spec.AllNodes { setFields = append(setFields, "`allNodes`") } else { allErrs = append(allErrs, field.Invalid(fldPath.Child("allNodes"), *spec.AllNodes, "must be either unset or set to true")) } } if spec.PerDeviceNodeSelection != nil { if *spec.PerDeviceNodeSelection { setFields = append(setFields, "`perDeviceNodeSelection`") } else { allErrs = append(allErrs, field.Invalid(fldPath.Child("perDeviceNodeSelection"), *spec.PerDeviceNodeSelection, "must be either unset or set to true")) } } switch len(setFields) { case 0: allErrs = append(allErrs, field.Required(fldPath, "exactly one of `nodeName`, `nodeSelector`, `allNodes`, `perDeviceNodeSelection` is required")) case 1: default: allErrs = append(allErrs, field.Invalid(fldPath, fmt.Sprintf("{%s}", strings.Join(setFields, ", ")), "exactly one of `nodeName`, `nodeSelector`, `allNodes`, `perDeviceNodeSelection` is required, but multiple fields are set")) } if spec.SharedCounters != nil && spec.Devices != nil { allErrs = append(allErrs, field.Invalid(fldPath, "", "only one of `sharedCounters` or `devices` is allowed")) } maxDevices := resource.ResourceSliceMaxDevices if haveDeviceTaints(spec) || haveConsumesCounters(spec) || haveListAttributes(spec) { maxDevices = resource.ResourceSliceMaxDevicesWithAdvancedFeatures } allErrs = append(allErrs, validateSet(spec.Devices, maxDevices, func(device resource.Device, fldPath *field.Path) field.ErrorList { oldDevice := lookupDevice(oldSpec, device.Name) return validateDevice(device, oldDevice, fldPath, spec.PerDeviceNodeSelection) }, func(device resource.Device) string { return device.Name }, fldPath.Child("devices"))...) allErrs = append(allErrs, validateSet(spec.SharedCounters, resource.ResourceSliceMaxCounterSets, validateCounterSet, func(counterSet resource.CounterSet) string { return counterSet.Name }, fldPath.Child("sharedCounters"), sizeCovered, uniquenessCovered)...) // The name format is validated declaratively; see the field's tags. if spec.PartitionTypeAttribute != nil { allErrs = append(allErrs, validatePartitionTypeAttribute(spec, fldPath)...) } allErrs = append(allErrs, validateSkipNodeOperations(spec.SkipNodeOperations, fldPath.Child("skipNodeOperations"))...) return allErrs } // validatePartitionTypeAttribute checks that a slice naming a partition type // attribute declares devices which consume counters, and that each of those // devices carries the attribute as a string. Devices which consume no counters // are exempt and may appear alongside them. func validatePartitionTypeAttribute(spec *resource.ResourceSliceSpec, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList name := string(*spec.PartitionTypeAttribute) if !haveConsumesCounters(spec) { return append(allErrs, field.Invalid(fldPath.Child("partitionTypeAttribute"), name, "may only be set on a slice which declares devices that consume counters")) } // A malformed name is reported declaratively; no device can carry it, so // checking each of them would only bury that one error. if len(validateFullyQualifiedName(*spec.PartitionTypeAttribute, nil)) > 0 { return allErrs } // A name in the driver's own domain may also be declared bare. Strip the // prefix once here rather than allocating driver+"/" per device below. bareName, _ := strings.CutPrefix(name, spec.Driver+"/") for i, device := range spec.Devices { if len(device.ConsumesCounters) == 0 { continue } attrPath := fldPath.Child("devices").Index(i).Child("attributes").Key(name) attribute, ok := lookupQualifiedAttribute(device.Attributes, name, bareName) if !ok { allErrs = append(allErrs, field.Required(attrPath, "device consumes counters and `partitionTypeAttribute` names this attribute, so it must be set")) continue } if attribute.StringValue == nil { allErrs = append(allErrs, field.Invalid(attrPath, attribute, "must be a string value because `partitionTypeAttribute` names this attribute")) } } return allErrs } // validateSkipNodeOperations checks business logic constraints on SkipNodeOperations: // NodePrepareResources cannot be skipped unless NodeUnprepareResources is also skipped // (or "*" is specified). Other checks (e.g. valid enum values, set uniqueness) are handled // by declarative validation. func validateSkipNodeOperations(skipOps []resource.SkipNodeOperation, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList skipSet := sets.New(skipOps...) if skipSet.Has(resource.SkipNodeOperationNodePrepareResources) && !skipSet.Has(resource.SkipNodeOperationNodeUnprepareResources) && !skipSet.Has(resource.SkipNodeOperationAll) { allErrs = append(allErrs, field.Invalid(fldPath, skipOps, "NodePrepareResources cannot be skipped unless NodeUnprepareResources is also skipped")) } return allErrs } // lookupQualifiedAttribute finds a device attribute by its fully qualified // name. A key that carries no domain defaults to the driver's own domain, so an // attribute in the driver's domain may be declared either explicitly or bare. // The explicit form wins, keeping the result deterministic when a device // declares both; bareName (the name with the driver's domain stripped, empty // when the name is in another domain) is consulted only when non-empty. func lookupQualifiedAttribute(attributes map[resource.QualifiedName]resource.DeviceAttribute, name, bareName string) (resource.DeviceAttribute, bool) { if attribute, ok := attributes[resource.QualifiedName(name)]; ok { return attribute, true } if bareName != "" { if attribute, ok := attributes[resource.QualifiedName(bareName)]; ok { return attribute, true } } return resource.DeviceAttribute{}, false } func haveListAttributes(spec *resource.ResourceSliceSpec) bool { if spec == nil { return false } for _, device := range spec.Devices { for _, attribute := range device.Attributes { if attribute.BoolValues != nil || attribute.IntValues != nil || attribute.StringValues != nil || attribute.VersionValues != nil { return true } } } return false } func haveDeviceTaints(spec *resource.ResourceSliceSpec) bool { if spec == nil { return false } for _, device := range spec.Devices { if len(device.Taints) > 0 { return true } } return false } func haveConsumesCounters(spec *resource.ResourceSliceSpec) bool { if spec == nil { return false } for _, device := range spec.Devices { if len(device.ConsumesCounters) > 0 { return true } } return false } func lookupDevice(spec *resource.ResourceSliceSpec, deviceName string) *resource.Device { if spec == nil { return nil } for i := range spec.Devices { device := &spec.Devices[i] if device.Name == deviceName { return device } } return nil } func validateCounterSet(counterSet resource.CounterSet, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if counterSet.Name == "" { allErrs = append(allErrs, field.Required(fldPath.Child("name"), "").MarkCoveredByDeclarative()) } else { allErrs = append(allErrs, validateCounterName(counterSet.Name, fldPath.Child("name")).MarkCoveredByDeclarative()...) } if len(counterSet.Counters) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("counters"), "").MarkCoveredByDeclarative()) } else { // The size limit is enforced for across all sets by the caller. allErrs = append(allErrs, validateMap(counterSet.Counters, resource.ResourceSliceMaxCountersPerCounterSet, validation.DNS1123LabelMaxLength, validateCounterName, validateDeviceCounter, fldPath.Child("counters"), keysCovered)...) } return allErrs } func validateResourcePool(pool resource.ResourcePool, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validatePoolName(pool.Name, fldPath.Child("name"))...) if pool.ResourceSliceCount <= 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceSliceCount"), pool.ResourceSliceCount, "must be greater than zero")) } if pool.Generation < 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("generation"), pool.Generation, "must be greater than or equal to zero")) } return allErrs } func validateDevice(device resource.Device, oldDevice *resource.Device, fldPath *field.Path, perDeviceNodeSelection *bool) field.ErrorList { var allErrs field.ErrorList allowMultipleAllocations := device.AllowMultipleAllocations != nil && *device.AllowMultipleAllocations allErrs = append(allErrs, validateDeviceName(device.Name, fldPath.Child("name"))...) // Warn about exceeding the maximum length only once. If any individual // field is too large, then so is the combination. // we count the total number of attribute entries (scalars and list items) attributeAndCapacityLength := len(device.Attributes) + len(device.Capacity) if attributeAndCapacityLength > resource.ResourceSliceMaxAttributesAndCapacitiesPerDevice { allErrs = append(allErrs, field.Invalid(fldPath, attributeAndCapacityLength, fmt.Sprintf("the total number of attributes and capacities must not exceed %d", resource.ResourceSliceMaxAttributesAndCapacitiesPerDevice))) } numAttributeValues := numEntriesDeviceAttribute(device) if numAttributeValues > resource.ResourceSliceMaxAttributeValuesPerDevice { allErrs = append(allErrs, field.Invalid(fldPath, numAttributeValues, fmt.Sprintf("the total number of attribute values must not exceed %d", resource.ResourceSliceMaxAttributeValuesPerDevice))) } allErrs = append(allErrs, validateMap(device.Attributes, -1, attributeAndCapacityMaxKeyLength, validateQualifiedName, validateDeviceAttribute, fldPath.Child("attributes"))...) // If the entire capacity is the same as before then validation can be skipped. // We could also do the DeepEqual on the entire spec, but here it is a bit cheaper. if oldDevice == nil || !apiequality.Semantic.DeepEqual(oldDevice.Capacity, device.Capacity) { if allowMultipleAllocations { allErrs = append(allErrs, validateMap(device.Capacity, -1, attributeAndCapacityMaxKeyLength, validateQualifiedName, validateMultiAllocatableDeviceCapacity, fldPath.Child("capacity"))...) } else { allErrs = append(allErrs, validateMap(device.Capacity, -1, attributeAndCapacityMaxKeyLength, validateQualifiedName, validateSingleAllocatableDeviceCapacity, fldPath.Child("capacity"))...) } } // If the entire set is the same as before then validation can be skipped. // We could also do the DeepEqual on the entire spec, but here it is a bit cheaper. if oldDevice == nil || !apiequality.Semantic.DeepEqual(oldDevice.Taints, device.Taints) { allErrs = append(allErrs, validateSlice(device.Taints, resource.DeviceTaintsMaxLength, func(taint resource.DeviceTaint, fldPath *field.Path) field.ErrorList { return validateDeviceTaint(taint, nil, fldPath) }, fldPath.Child("taints"))...) } allErrs = append(allErrs, validateSet(device.ConsumesCounters, resource.ResourceSliceMaxDeviceCounterConsumptionsPerDevice, validateDeviceCounterConsumption, func(deviceCapacityConsumption resource.DeviceCounterConsumption) string { return deviceCapacityConsumption.CounterSet }, fldPath.Child("consumesCounters"), sizeCovered, uniquenessCovered)...) if perDeviceNodeSelection != nil && *perDeviceNodeSelection { setFields := make([]string, 0, 3) if device.NodeName != nil { if len(*device.NodeName) != 0 { setFields = append(setFields, "`nodeName`") allErrs = append(allErrs, validateNodeName(*device.NodeName, fldPath.Child("nodeName"))...) } else { allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeName"), *device.NodeName, "must not be empty")) } } if device.NodeSelector != nil { setFields = append(setFields, "`nodeSelector`") allErrs = append(allErrs, corevalidation.ValidateNodeSelector(device.NodeSelector, false, fldPath.Child("nodeSelector"))...) } if device.AllNodes != nil { if *device.AllNodes { setFields = append(setFields, "`allNodes`") } else { allErrs = append(allErrs, field.Invalid(fldPath.Child("allNodes"), *device.AllNodes, "must be either unset or set to true")) } } switch len(setFields) { case 0: allErrs = append(allErrs, field.Required(fldPath, "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required when `perDeviceNodeSelection` is set to true in the ResourceSlice spec")) case 1: default: allErrs = append(allErrs, field.Invalid(fldPath, fmt.Sprintf("{%s}", strings.Join(setFields, ", ")), "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required when `perDeviceNodeSelection` is set to true in the ResourceSlice spec")) } } else if (perDeviceNodeSelection == nil || !*perDeviceNodeSelection) && (device.NodeName != nil || device.NodeSelector != nil || device.AllNodes != nil) { allErrs = append(allErrs, field.Invalid(fldPath, nil, "`nodeName`, `nodeSelector` and `allNodes` can only be set if `perDeviceNodeSelection` is set to true in the ResourceSlice spec")) } allErrs = append(allErrs, validateDeviceBindingParameters(device.BindingConditions, device.BindingFailureConditions, fldPath)...) allErrs = append(allErrs, validateNodeAllocatableResources(device.NodeAllocatableResources, device.Capacity, fldPath.Child("nodeAllocatableResources"))...) return allErrs } func validateNodeAllocatableResources(mappings map[corev1.ResourceName]resource.NodeAllocatableResource, capacities map[resource.QualifiedName]resource.DeviceCapacity, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList for resourceName, mapping := range mappings { keyPath := fldPath.Key(string(resourceName)) if !corehelper.IsNodeAllocatableResourceName(core.ResourceName(resourceName)) { allErrs = append(allErrs, field.Invalid(keyPath, resourceName, "must be a node allocatable resource name")) } if mapping.Mapping == nil && mapping.Overhead == nil { allErrs = append(allErrs, field.Required(keyPath, "at least one of mapping or overhead must be set")) } if mapping.Mapping != nil { allErrs = append(allErrs, validateNodeAllocatableMapping(mapping.Mapping, capacities, keyPath.Child("mapping"))...) } if mapping.Overhead != nil { allErrs = append(allErrs, validateNodeAllocatableOverheadMapping(mapping.Overhead, keyPath.Child("overhead"))...) } } return allErrs } // validateNodeAllocatableMapping validates a mapped node allocatable resource configuration func validateNodeAllocatableMapping(mapping *resource.NodeAllocatableMapping, capacities map[resource.QualifiedName]resource.DeviceCapacity, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if mapping.CapacityKey != nil { if *mapping.CapacityKey == "" { allErrs = append(allErrs, field.Invalid(fldPath.Child("capacityKey"), "", "capacityKey must not be an empty string")) } else { var exists bool if capacities != nil { _, exists = capacities[*mapping.CapacityKey] } if !exists { allErrs = append(allErrs, field.NotFound(fldPath.Child("capacityKey"), *mapping.CapacityKey)) } } if mapping.CapacityMultiplier == nil { allErrs = append(allErrs, field.Required(fldPath.Child("capacityMultiplier"), "capacityMultiplier is required when capacityKey is set").WithOrigin("dependentRequired").MarkCoveredByDeclarative()) } else if mapping.CapacityMultiplier.Sign() <= 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("capacityMultiplier"), mapping.CapacityMultiplier.String(), "must be positive")) } } if mapping.DeviceMultiplier != nil { if mapping.DeviceMultiplier.Sign() <= 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("deviceMultiplier"), mapping.DeviceMultiplier.String(), "must be positive")) } } if mapping.CapacityMultiplier != nil && mapping.CapacityKey == nil { allErrs = append(allErrs, field.Required(fldPath.Child("capacityKey"), "capacityKey is required when capacityMultiplier is set").WithOrigin("dependentRequired").MarkCoveredByDeclarative()) } return allErrs } // validateNodeAllocatableOverheadMapping validates an overhead node allocatable resource configuration func validateNodeAllocatableOverheadMapping(overhead *resource.NodeAllocatableOverhead, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if overhead.PerPod == nil && overhead.PerContainer == nil { allErrs = append(allErrs, field.Invalid(fldPath, "", "at least one of perPod or perContainer must be set")) return allErrs } if overhead.PerPod != nil && overhead.PerPod.Sign() < 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("perPod"), overhead.PerPod.String(), "must be non-negative")) } if overhead.PerContainer != nil && overhead.PerContainer.Sign() < 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("perContainer"), overhead.PerContainer.String(), "must be non-negative")) } return allErrs } // numEntriesDeviceAttribute counts the total number of entries in the device's attributes, // counting each item in list values separately. // This is used to enforce the maximum number of attributes and capacities // per device when the DRAListTypeAttributes feature gate is enabled. func numEntriesDeviceAttribute(device resource.Device) int { numEntries := 0 for _, attr := range device.Attributes { if len(attr.BoolValues) == 0 && len(attr.IntValues) == 0 && len(attr.StringValues) == 0 && len(attr.VersionValues) == 0 { numEntries++ continue } // These fields are actually mutually exclusive, but we count the total number of entries across all of them. // If setting multiple fields in the list value, the validation will fail later and report that exactly one value must be specified. numEntries += len(attr.BoolValues) + len(attr.IntValues) + len(attr.StringValues) + len(attr.VersionValues) } return numEntries } func validateDeviceCounterConsumption(deviceCounterConsumption resource.DeviceCounterConsumption, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if len(deviceCounterConsumption.CounterSet) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("counterSet"), "").MarkCoveredByDeclarative()) } else { allErrs = append(allErrs, validateCounterName(deviceCounterConsumption.CounterSet, fldPath.Child("counterSet")).MarkCoveredByDeclarative()...) } if len(deviceCounterConsumption.Counters) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("counters"), "").MarkCoveredByDeclarative()) } else { allErrs = append(allErrs, validateMap(deviceCounterConsumption.Counters, resource.ResourceSliceMaxCountersPerDeviceCounterConsumption, validation.DNS1123LabelMaxLength, validateCounterName, validateDeviceCounter, fldPath.Child("counters"), keysCovered)...) } return allErrs } var ( numericIdentifier = `(0|[1-9]\d*)` preReleaseIdentifier = `(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)` buildIdentifier = `[0-9a-zA-Z-]+` semverRe = regexp.MustCompile(`^` + // dot-separated version segments (e.g. 1.2.3) numericIdentifier + `\.` + numericIdentifier + `\.` + numericIdentifier + // optional dot-separated prerelease segments (e.g. -alpha.PRERELEASE.1) `(-` + preReleaseIdentifier + `(\.` + preReleaseIdentifier + `)*)?` + // optional dot-separated build identifier segments (e.g. +build.id.20240305) `(\+` + buildIdentifier + `(\.` + buildIdentifier + `)*)?` + `$`) ) func validateDeviceAttribute(attribute resource.DeviceAttribute, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList numFields := 0 if attribute.BoolValue != nil { numFields++ } if attribute.IntValue != nil { numFields++ } if attribute.StringValue != nil { numFields++ allErrs = append(allErrs, validateDeviceAttributeStringValue(attribute.StringValue, fldPath.Child("string"))...) } if attribute.VersionValue != nil { numFields++ allErrs = append(allErrs, validateDeviceAttributeVersionValue(attribute.VersionValue, fldPath.Child("version"))...) } if attribute.BoolValues != nil { numFields++ if len(attribute.BoolValues) == 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("bools"), attribute.BoolValues, "must not be empty if specified")) } } if attribute.IntValues != nil { numFields++ if len(attribute.IntValues) == 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("ints"), attribute.IntValues, "must not be empty if specified")) } } if attribute.StringValues != nil { numFields++ if len(attribute.StringValues) == 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("strings"), attribute.StringValues, "must not be empty if specified")) } for i, item := range attribute.StringValues { allErrs = append(allErrs, validateDeviceAttributeStringValue(&item, fldPath.Child("strings").Index(i)).WithOrigin("maxBytes").MarkCoveredByDeclarative()...) } } if attribute.VersionValues != nil { numFields++ if len(attribute.VersionValues) == 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), attribute.VersionValues, "must not be empty if specified")) } for i, item := range attribute.VersionValues { allErrs = append(allErrs, validateDeviceAttributeVersionValue(&item, fldPath.Child("versions").Index(i))...) } } switch numFields { case 0: allErrs = append(allErrs, field.Invalid(fldPath, "", "exactly one value must be specified").WithOrigin("union").MarkCoveredByDeclarative()) case 1: // Okay. default: allErrs = append(allErrs, field.Invalid(fldPath, attribute, "exactly one value must be specified").WithOrigin("union").MarkCoveredByDeclarative()) } return allErrs } func validateDeviceAttributeStringValue(value *string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if len(*value) > resource.DeviceAttributeMaxValueLength { allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.DeviceAttributeMaxValueLength)) } return allErrs } func validateDeviceAttributeVersionValue(value *string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if !semverRe.MatchString(*value) { allErrs = append(allErrs, field.Invalid(fldPath, *value, "must be a string compatible with semver.org spec 2.0.0")) } if len(*value) > resource.DeviceAttributeMaxValueLength { allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.DeviceAttributeMaxValueLength)) } return allErrs } // validateMultiAllocatableDeviceCapacity must check requestPolicy in consumable capacity. // It only gets called for new or modified capacity. func validateMultiAllocatableDeviceCapacity(capacity resource.DeviceCapacity, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if capacity.RequestPolicy != nil { allErrs = append(allErrs, validateRequestPolicy(capacity.Value, capacity.RequestPolicy, fldPath.Child("requestPolicy"))...) } return allErrs } // validateSingleAllocatableDeviceCapacity must not allow consumable capacity. func validateSingleAllocatableDeviceCapacity(capacity resource.DeviceCapacity, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if capacity.RequestPolicy != nil { allErrs = append(allErrs, field.Forbidden(fldPath.Child("requestPolicy"), "allowMultipleAllocations must be true")) } return allErrs } // validateRequestPolicy validates at most one of ValidRequestValues can be defined. // If any ValidRequestValues are defined, Default must also be defined and valid. // Only gets called for new or modified policy. func validateRequestPolicy(maxCapacity apiresource.Quantity, policy *resource.CapacityRequestPolicy, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if len(policy.ValidValues) > 0 && policy.ValidRange != nil { allErrs = append(allErrs, field.Forbidden(fldPath, `exactly one policy can be specified, cannot specify "validValues" and "validRange" at the same time`)) } else { allErrs = append(allErrs, validateValidRequestValues(maxCapacity, policy, fldPath)...) } return allErrs } func validateValidRequestValues(maxCapacity apiresource.Quantity, policy *resource.CapacityRequestPolicy, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList switch { case len(policy.ValidValues) > 0: if policy.Default == nil { allErrs = append(allErrs, field.Required(fldPath.Child("default"), "required when validValues is defined")) } else { allErrs = append(allErrs, validateRequestPolicyValidValues(*policy.Default, maxCapacity, policy.ValidValues, fldPath.Child("validValues"))...) } case policy.ValidRange != nil: if policy.Default == nil { allErrs = append(allErrs, field.Required(fldPath.Child("default"), "required when validRange is defined")) } else { allErrs = append(allErrs, validateRequestPolicyRange(*policy.Default, maxCapacity, *policy.ValidRange, fldPath.Child("validRange"))...) } } return allErrs } func validateRequestPolicyValidValues(defaultValue apiresource.Quantity, maxCapacity apiresource.Quantity, validValues []apiresource.Quantity, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList foundDefault := false // Check if validValues is sorted in ascending order for i := range len(validValues) - 1 { if validValues[i].Cmp(validValues[i+1]) > 0 { allErrs = append(allErrs, field.Invalid( fldPath.Index(i+1), validValues[i+1].String(), "values must be sorted in ascending order")) } } // Choose the key function based on whether fractional capacity values are // supported. When DRAFractionalCapacityRange is enabled, use decimal precision; // otherwise, use integer-based keys. quantityKeyFunc := quantityKeyInt if utilfeature.DefaultFeatureGate.Enabled(features.DRAFractionalCapacityRange) { quantityKeyFunc = quantityKeyAsDec } allErrs = append(allErrs, validateSet(validValues, resource.CapacityRequestPolicyDiscreteMaxOptions, func(option apiresource.Quantity, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if option.Cmp(maxCapacity) > 0 { allErrs = append(allErrs, field.Invalid(fldPath, option.String(), fmt.Sprintf("option is larger than capacity value: %s", maxCapacity.String()))) } if option.Cmp(defaultValue) == 0 { foundDefault = true } return allErrs }, quantityKeyFunc, fldPath)...) if !foundDefault { allErrs = append(allErrs, field.Invalid(fldPath, defaultValue.String(), "default value is not valid according to the requestPolicy")) } return allErrs } // validateRequestPolicyRange validates a CapacityRequestPolicyRange. // Only gets called for a new or modified range. func validateRequestPolicyRange(defaultValue apiresource.Quantity, maxCapacity apiresource.Quantity, valueRange resource.CapacityRequestPolicyRange, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if valueRange.Min == nil { allErrs = append(allErrs, field.Required(fldPath.Child("min"), "required when validRange is defined")) return allErrs } if valueRange.Min.Cmp(maxCapacity) > 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("min"), valueRange.Min.String(), fmt.Sprintf("min is larger than capacity value: %s", maxCapacity.String()))) } if defaultValue.Cmp(*valueRange.Min) < 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), defaultValue.String(), fmt.Sprintf("default is less than min: %s", valueRange.Min.String()))) } if valueRange.Max != nil { if valueRange.Min.Cmp(*valueRange.Max) > 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("max"), valueRange.Min.String(), fmt.Sprintf("min is larger than max: %s", valueRange.Max.String()))) } if valueRange.Max.Cmp(maxCapacity) > 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("max"), valueRange.Max.String(), fmt.Sprintf("max is larger than capacity value: %s", maxCapacity.String()))) } if defaultValue.Cmp(*valueRange.Max) > 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), defaultValue.String(), fmt.Sprintf("default is more than max: %s", valueRange.Max.String()))) } } useMilli := false if utilfeature.DefaultFeatureGate.Enabled(features.DRAFractionalCapacityRange) { // The default is not part of the range, so rangeHasFractional does not see it. // A fractional default with an integer range must still use the milli path, or the // integer path reads default.Value() (which rounds) and accepts a non-multiple. useMilli = rangeHasFractional(valueRange) || isFractionalQuantity(defaultValue) // Overflow guards apply when DRAFractionalCapacityRange is enabled and the incoming // range has fractional values. if useMilli { hasUnsafeValue := false for _, pair := range []struct { q *apiresource.Quantity name string }{ {&defaultValue, "default"}, {valueRange.Min, "min"}, {valueRange.Max, "max"}, {valueRange.Step, "step"}, } { if pair.q != nil { milliQuantity := apiresource.NewMilliQuantity(pair.q.MilliValue(), pair.q.Format) if pair.q.Cmp(*milliQuantity) != 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child(pair.name), pair.q.String(), "value cannot be represented as a milli value")) hasUnsafeValue = true } } } if hasUnsafeValue { // fast return to avoid unsafe calculation return allErrs } } } if valueRange.Step != nil { if valueRange.Step.Sign() <= 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("step"), valueRange.Step.String(), "must be greater than zero")) } else { added := valueRange.Min.DeepCopy() added.Add(*valueRange.Step) if added.Cmp(maxCapacity) > 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("step"), valueRange.Step.String(), fmt.Sprintf("one step %s is larger than capacity value: %s", added.String(), maxCapacity.String()))) } // The integer path of validateRequestPolicyRangeStep below reads step, // min, max and the default value with Quantity.Value(), which only holds // in [0, MaxInt64]: above it a positive multiple of 2^64 reads as 0 and the // step check divides by zero, and a negative decimal-backed bound can read // back positive. Reject those before the arithmetic. The milli path is // bounded by the hasUnsafeValue check above instead. hasUnsafeValue := false if !useMilli { for _, pair := range []struct { q *apiresource.Quantity name string }{ {&defaultValue, "default"}, {valueRange.Min, "min"}, {valueRange.Max, "max"}, {valueRange.Step, "step"}, } { if pair.q != nil { if errs := validateInt64Range(*pair.q, fldPath.Child(pair.name)); len(errs) > 0 { allErrs = append(allErrs, errs...) hasUnsafeValue = true } } } } if !hasUnsafeValue { // Use milli-value arithmetic whenever any field is fractional. // useMilli is set only when the DRAFractionalCapacityRange gate is on. allErrs = append(allErrs, validateRequestPolicyRangeStep(defaultValue, *valueRange.Min, *valueRange.Step, useMilli, fldPath.Child("step"))...) if valueRange.Max != nil { allErrs = append(allErrs, validateRequestPolicyRangeStep(*valueRange.Max, *valueRange.Min, *valueRange.Step, useMilli, fldPath.Child("step"))...) } } } } return allErrs } // validateRequestPolicyRangeStep checks if the value is a valid step increment // starting from the minimum value (i.e., value - min is a multiple of step). // // Pre-requisite: value, min, and step must be pre-validated to ensure they // don't underflow or overflow the supported value range (int64 or milli-value) // and that step is not zero. func validateRequestPolicyRangeStep(value, min, step apiresource.Quantity, useMilli bool, fldPath *field.Path) field.ErrorList { var stepVal, minVal, val int64 if useMilli { stepVal = step.MilliValue() minVal = min.MilliValue() val = value.MilliValue() } else { stepVal = step.Value() minVal = min.Value() val = value.Value() } if (val-minVal)%stepVal != 0 { return field.ErrorList{field.Invalid(fldPath, value.String(), fmt.Sprintf("value is not a multiple of a given step (%s) from (%s)", step.String(), min.String()))} } return nil } // boundOverInt64Message is the field error for a consumable-capacity validRange // bound above the range where Quantity.Value() is usable. const boundOverInt64Message = "must not be larger than 9223372036854775807" // validateInt64Range reports whether q is in [0, MaxInt64], the range where // Quantity.Value() returns a number the bound can be reasoned about with. Above // MaxInt64 it overflows, so a positive multiple of 2^64 reads back as 0; below // zero it does not round as documented once the quantity is decimal-backed and // can read back with the wrong sign (#110653). Sign() alone misses the first // case and CmpInt64 alone misses the second, so the check needs both. func validateInt64Range(q apiresource.Quantity, fldPath *field.Path) field.ErrorList { if q.Sign() < 0 { return field.ErrorList{field.Invalid(fldPath, q.String(), apimachineryvalidation.IsNegativeErrorMsg)} } if q.CmpInt64(math.MaxInt64) > 0 { return field.ErrorList{field.Invalid(fldPath, q.String(), boundOverInt64Message)} } return nil } // rangeHasFractional reports whether any non-nil field of r has sub-integer precision. func rangeHasFractional(r resource.CapacityRequestPolicyRange) bool { for _, q := range []*apiresource.Quantity{r.Min, r.Max, r.Step} { if q != nil && isFractionalQuantity(*q) { return true } } return false } // isFractionalQuantity reports whether q has sub-integer precision. func isFractionalQuantity(q apiresource.Quantity) bool { dec := q.AsDec() infDec := inf.Dec{} rounded := infDec.Round(dec, 0, inf.RoundDown) return dec.Cmp(rounded) != 0 } func validateDeviceCounter(counter resource.Counter, fldPath *field.Path) field.ErrorList { // Any parsed quantity is valid. return nil } func validateQualifiedName(name resource.QualifiedName, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList parts := strings.Split(string(name), "/") switch len(parts) { case 1: allErrs = append(allErrs, validateCIdentifier(parts[0], fldPath)...) case 2: if len(parts[0]) == 0 { allErrs = append(allErrs, field.Invalid(fldPath, "", "the domain must not be empty")) } else { allErrs = append(allErrs, validateDriverName(parts[0], fldPath)...) } if len(parts[1]) == 0 { allErrs = append(allErrs, field.Invalid(fldPath, "", "the name must not be empty")) } else { allErrs = append(allErrs, validateCIdentifier(parts[1], fldPath)...) } // TODO: This validation is incomplete. It should reject qualified names // that contain more than one slash. Currently, names like "a/b/c" are not // handled and are implicitly accepted. // // This needs to be fixed in two places: // 1. Here in this function. // 2. In the corresponding declarative validation utility `resourcesQualifiedName` // in `staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt.go`. // // The fix should be introduced carefully, possibly using ratcheting to avoid // breaking existing, non-compliant objects. } return allErrs } func validateFullyQualifiedName(name resource.FullyQualifiedName, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateQualifiedName(resource.QualifiedName(name), fldPath)...) // validateQualifiedName checks that both parts are valid. // What we need to enforce here is that there really is a domain. if !strings.Contains(string(name), "/") { allErrs = append(allErrs, field.Invalid(fldPath, name, "a fully qualified name must be a domain and a name separated by a slash")) } return allErrs.WithOrigin("format=k8s-resource-fully-qualified-name") } func validateCIdentifier(id string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if len(id) > resource.DeviceMaxIDLength { allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.DeviceMaxIDLength)) } for _, msg := range content.IsCIdentifier(id) { allErrs = append(allErrs, field.Invalid(fldPath, id, msg)) } return allErrs } // validationOption is an option for validation. type validationOption int const ( // The validation of each item is covered by declarative validation. itemsCovered validationOption = iota // The list size check is covered by declarative validation. sizeCovered // The uniqueness check is covered by declarative validation. uniquenessCovered // key validation is covered by declarative validation. keysCovered ) // validateItems validates each item in a slice. func validateItems[T any](slice []T, validateItem func(T, *field.Path) field.ErrorList, fldPath *field.Path, opts ...validationOption) field.ErrorList { var allErrs field.ErrorList for i, item := range slice { idxPath := fldPath.Index(i) errs := validateItem(item, idxPath) if slices.Contains(opts, itemsCovered) { errs = errs.MarkCoveredByDeclarative() } allErrs = append(allErrs, errs...) } return allErrs } // validateSlice ensures that a slice does not exceed a certain maximum size // and that all entries are valid. // A negative maxSize disables the length check. func validateSlice[T any](slice []T, maxSize int, validateItem func(T, *field.Path) field.ErrorList, fldPath *field.Path, opts ...validationOption) field.ErrorList { if maxSize >= 0 && 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. err := field.TooMany(fldPath, len(slice), maxSize).WithOrigin("maxItems") if slices.Contains(opts, sizeCovered) { err = err.MarkCoveredByDeclarative() } // maxSize check short-circuits for DOS protection return field.ErrorList{err} } return validateItems(slice, validateItem, fldPath, opts...) } // validateSet ensures that a slice contains no duplicates, does not // exceed a certain maximum size and that all entries are valid. func validateSet[T any, K comparable](slice []T, maxSize int, validateItem func(item T, fldPath *field.Path) field.ErrorList, itemKey func(T) K, fldPath *field.Path, opts ...validationOption) field.ErrorList { if maxSize >= 0 && 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. err := field.TooMany(fldPath, len(slice), maxSize).WithOrigin("maxItems") if slices.Contains(opts, sizeCovered) { err = err.MarkCoveredByDeclarative() } // maxSize check short-circuits for DOS protection return field.ErrorList{err} } allErrs := validateItems(slice, validateItem, fldPath, opts...) allItems := sets.New[K]() for i, item := range slice { idxPath := fldPath.Index(i) key := itemKey(item) childPath := idxPath if allItems.Has(key) { err := field.Duplicate(childPath, key) if slices.Contains(opts, uniquenessCovered) { err = err.MarkCoveredByDeclarative() } allErrs = append(allErrs, err) } else { allItems.Insert(key) } } return allErrs } // stringKey uses the item itself as a key for validateSet. func stringKey(item string) string { return item } // quantityKeyAsDec uses a scaled inf.Dec of the item as itself // as a key for validateSet. func quantityKeyAsDec(item apiresource.Quantity) string { return item.AsDec().String() } // quantityKeyInt uses base-10 integer of the item itself // as a key for validateSet. func quantityKeyInt(item apiresource.Quantity) string { return strconv.FormatInt(item.Value(), 10) } // validateMap validates keys, items and the maximum length of a map. // A negative maxSize disables the length check. // // Keys larger than truncateKeyLen get truncated in the middle. A very // small limit gets increased because it is okay to include more details. // This is not used for validation of keys, which has to be done by // the callback function. func validateMap[K ~string, T any](m map[K]T, maxSize, truncateKeyLen int, validateKey func(K, *field.Path) field.ErrorList, validateItem func(T, *field.Path) field.ErrorList, fldPath *field.Path, opts ...validationOption) field.ErrorList { var allErrs field.ErrorList if maxSize >= 0 && len(m) > maxSize { allErrs = append(allErrs, field.TooMany(fldPath, len(m), maxSize)) // maxSize check short-circuits for DOS protection return allErrs } for key, item := range m { keyPath := fldPath.Key(truncateIfTooLong(string(key), truncateKeyLen)) keyValidationErrors := validateKey(key, fldPath) if slices.Contains(opts, keysCovered) { keyValidationErrors = keyValidationErrors.MarkCoveredByDeclarative() } allErrs = append(allErrs, keyValidationErrors...) allErrs = append(allErrs, validateItem(item, keyPath)...) } return allErrs } func truncateIfTooLong(str string, maxLen int) string { // The caller was overly restrictive. Increase the length to something reasonable // (https://github.com/kubernetes/kubernetes/pull/127511#discussion_r1826206362). if maxLen < 16 { maxLen = 16 } if len(str) <= maxLen { return str } ellipsis := "..." remaining := maxLen - len(ellipsis) return str[0:(remaining+1)/2] + ellipsis + str[len(str)-remaining/2:] } func validateDeviceStatus(device resource.AllocatedDeviceStatus, fldPath *field.Path, allocatedDevices sets.Set[structured.SharedDeviceID]) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateDriverName(device.Driver, fldPath.Child("driver"))...) allErrs = append(allErrs, validatePoolName(device.Pool, fldPath.Child("pool"))...) allErrs = append(allErrs, validateDeviceName(device.Device, fldPath.Child("device"))...) if device.ShareID != nil { allErrs = append(allErrs, validateUID(*device.ShareID, fldPath.Child("shareID")).MarkCoveredByDeclarative()...) } deviceID := structured.MakeDeviceID(device.Driver, device.Pool, device.Device) sharedDeviceID := structured.MakeSharedDeviceID(deviceID, (*types.UID)(device.ShareID)) if !allocatedDevices.Has(sharedDeviceID) { allErrs = append(allErrs, field.Invalid(fldPath, sharedDeviceID, "must be an allocated device in the claim")) } if len(device.Conditions) > resource.AllocatedDeviceStatusMaxConditions { allErrs = append(allErrs, field.TooMany(fldPath.Child("conditions"), len(device.Conditions), resource.AllocatedDeviceStatusMaxConditions)) } allErrs = append(allErrs, metav1validation.ValidateConditions(device.Conditions, fldPath.Child("conditions"))...) if device.Data != nil && len(device.Data.Raw) > 0 { // Data is an optional field. allErrs = append(allErrs, validateRawExtension(*device.Data, fldPath.Child("data"), false, resource.AllocatedDeviceStatusDataMaxLength)...) } allErrs = append(allErrs, validateNetworkDeviceData(device.NetworkData, fldPath.Child("networkData"))...) return allErrs } // validateRawExtension validates RawExtension as in https://github.com/kubernetes/kubernetes/pull/125549/ func validateRawExtension(rawExtension runtime.RawExtension, fldPath *field.Path, stored bool, rawExtensionMaxLength int) field.ErrorList { var allErrs field.ErrorList var v any if len(rawExtension.Raw) == 0 { allErrs = append(allErrs, field.Required(fldPath, "")) } else if !stored && len(rawExtension.Raw) > rawExtensionMaxLength { // Don't even bother with parsing when too large. // Only applies on create. Existing parameters are grand-fathered in // because the limit was introduced in 1.32. This also means that it // can be changed in the future. allErrs = append(allErrs, field.TooLong(fldPath, "" /* unused */, rawExtensionMaxLength)) } else if err := json.Unmarshal(rawExtension.Raw, &v); err != nil { allErrs = append(allErrs, field.Invalid(fldPath, "<value omitted>", fmt.Sprintf("error parsing data as JSON: %v", err.Error()))) } else if v == nil { allErrs = append(allErrs, field.Required(fldPath, "")) } else if _, isObject := v.(map[string]any); !isObject { allErrs = append(allErrs, field.Invalid(fldPath, "<value omitted>", "must be a valid JSON object")) } return allErrs } func validateNetworkDeviceData(networkDeviceData *resource.NetworkDeviceData, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if networkDeviceData == nil { return allErrs } if len(networkDeviceData.InterfaceName) > resource.NetworkDeviceDataInterfaceNameMaxLength { allErrs = append(allErrs, field.TooLong(fldPath.Child("interfaceName"), "" /* unused */, resource.NetworkDeviceDataInterfaceNameMaxLength).WithOrigin("maxBytes").MarkCoveredByDeclarative()) } if len(networkDeviceData.HardwareAddress) > resource.NetworkDeviceDataHardwareAddressMaxLength { allErrs = append(allErrs, field.TooLong(fldPath.Child("hardwareAddress"), "" /* unused */, resource.NetworkDeviceDataHardwareAddressMaxLength).WithOrigin("maxBytes").MarkCoveredByDeclarative()) } allErrs = append(allErrs, validateSet(networkDeviceData.IPs, resource.NetworkDeviceDataMaxIPs, func(address string, fldPath *field.Path) field.ErrorList { return validation.IsValidInterfaceAddress(fldPath, address) }, stringKey, fldPath.Child("ips"), sizeCovered, uniquenessCovered)...) return allErrs } // ValidateDeviceTaintRule tests if a DeviceTaintRule object is valid. func ValidateDeviceTaintRule(deviceTaint *resource.DeviceTaintRule) field.ErrorList { allErrs := corevalidation.ValidateObjectMeta(&deviceTaint.ObjectMeta, false, apimachineryvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) allErrs = append(allErrs, validateDeviceTaintRuleSpec(&deviceTaint.Spec, nil, field.NewPath("spec"))...) return allErrs } // ValidateDeviceTaintRuleUpdate tests if a DeviceTaintRule update is valid. func ValidateDeviceTaintRuleUpdate(deviceTaint, oldDeviceTaint *resource.DeviceTaintRule) field.ErrorList { allErrs := corevalidation.ValidateObjectMetaUpdate(&deviceTaint.ObjectMeta, &oldDeviceTaint.ObjectMeta, field.NewPath("metadata")) allErrs = append(allErrs, validateDeviceTaintRuleSpec(&deviceTaint.Spec, &oldDeviceTaint.Spec, field.NewPath("spec"))...) return allErrs } func validateDeviceTaintRuleSpec(spec, oldSpec *resource.DeviceTaintRuleSpec, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList var oldFilter *resource.DeviceTaintSelector if oldSpec != nil { oldFilter = oldSpec.DeviceSelector // +k8s:verify-mutation:reason=clone } allErrs = append(allErrs, validateDeviceTaintSelector(spec.DeviceSelector, oldFilter, fldPath.Child("deviceSelector"))...) var oldTaint *resource.DeviceTaint if oldSpec != nil { oldTaint = &oldSpec.Taint // +k8s:verify-mutation:reason=clone } allErrs = append(allErrs, validateDeviceTaint(spec.Taint, oldTaint, fldPath.Child("taint"))...) return allErrs } func validateDeviceTaintSelector(filter, oldFilter *resource.DeviceTaintSelector, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if filter == nil { return allErrs } if filter.Driver != nil { allErrs = append(allErrs, validateDriverName(*filter.Driver, fldPath.Child("driver"))...) } if filter.Pool != nil { allErrs = append(allErrs, validatePoolName(*filter.Pool, fldPath.Child("pool"))...) } if filter.Device != nil { allErrs = append(allErrs, validateDeviceName(*filter.Device, fldPath.Child("device"))...) } return allErrs } var validDeviceTolerationOperators = []resource.DeviceTolerationOperator{resource.DeviceTolerationOpEqual, resource.DeviceTolerationOpExists} var validDeviceTaintEffects = sets.New(resource.DeviceTaintEffectNoSchedule, resource.DeviceTaintEffectNoExecute, resource.DeviceTaintEffectNone) func validateDeviceTaint(taint resource.DeviceTaint, oldTaint *resource.DeviceTaint, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, metav1validation.ValidateLabelName(taint.Key, fldPath.Child("key"))...) // Includes checking for non-empty. if taint.Value != "" { allErrs = append(allErrs, validateLabelValue(taint.Value, fldPath.Child("value"))...) } if oldTaint == nil || oldTaint.Effect != taint.Effect { switch { case taint.Effect == "": allErrs = append(allErrs, field.Required(fldPath.Child("effect"), "").MarkCoveredByDeclarative()) // Required in a taint. case !validDeviceTaintEffects.Has(taint.Effect): allErrs = append(allErrs, field.NotSupported(fldPath.Child("effect"), taint.Effect, sets.List(validDeviceTaintEffects)).MarkCoveredByDeclarative()) } } return allErrs } func validateDeviceToleration(toleration resource.DeviceToleration, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if toleration.Key != "" { allErrs = append(allErrs, metav1validation.ValidateLabelName(toleration.Key, fldPath.Child("key")).MarkCoveredByDeclarative()...) } switch toleration.Operator { case resource.DeviceTolerationOpExists: if toleration.Value != "" { allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), toleration.Value, "must be empty for operator `Exists`")) } case resource.DeviceTolerationOpEqual: allErrs = append(allErrs, validateLabelValue(toleration.Value, fldPath.Child("value"))...) case "": allErrs = append(allErrs, field.Required(fldPath.Child("operator"), "").MarkCoveredByDeclarative()) default: allErrs = append(allErrs, field.NotSupported(fldPath.Child("operator"), toleration.Operator, validDeviceTolerationOperators).MarkCoveredByDeclarative()) } switch { case toleration.Effect == "": // Optional in a toleration. case !validDeviceTaintEffects.Has(toleration.Effect): allErrs = append(allErrs, field.NotSupported(fldPath.Child("effect"), toleration.Effect, sets.List(validDeviceTaintEffects)).MarkCoveredByDeclarative()) } return allErrs } func validateLabelValue(value string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList // There's no metav1validation.ValidateLabelValue. for _, msg := range validation.IsValidLabelValue(value) { allErrs = append(allErrs, field.Invalid(fldPath, value, msg)) } return allErrs } func validateDeviceBindingParameters(bindingConditions, bindingFailureConditions []string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateSlice(bindingConditions, resource.BindingConditionsMaxSize, metav1validation.ValidateLabelName, fldPath.Child("bindingConditions"), sizeCovered)...) allErrs = append(allErrs, validateSlice(bindingFailureConditions, resource.BindingFailureConditionsMaxSize, metav1validation.ValidateLabelName, fldPath.Child("bindingFailureConditions"), sizeCovered)...) // ensure BindingConditions and BindingFailureConditions contain no duplicate items and do not overlap with each other conditionsSet := sets.New[string]() for i, condition := range bindingConditions { if conditionsSet.Has(condition) { allErrs = append(allErrs, field.Duplicate(fldPath.Child("bindingConditions").Index(i), condition)) } else { conditionsSet.Insert(condition) } } failureConditionsSet := sets.New[string]() for i, condition := range bindingFailureConditions { if failureConditionsSet.Has(condition) { allErrs = append(allErrs, field.Duplicate(fldPath.Child("bindingFailureConditions").Index(i), condition)) } else { failureConditionsSet.Insert(condition) } if sets.New(bindingConditions...).Has(condition) { allErrs = append(allErrs, field.Invalid(fldPath.Child("bindingFailureConditions").Index(i), condition, "bindingFailureConditions must not overlap with bindingConditions")) } } if len(bindingConditions) == 0 && len(bindingFailureConditions) > 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("bindingConditions"), bindingConditions, "bindingConditions are required to use bindingFailureConditions")) } if len(bindingFailureConditions) == 0 && len(bindingConditions) > 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("bindingFailureConditions"), bindingFailureConditions, "bindingFailureConditions are required to use bindingConditions")) } return allErrs } // ValidateDeviceTaintRuleStatusUpdate tests if a DeviceTaintRule status update is valid. func ValidateDeviceTaintRuleStatusUpdate(rule, oldRule *resource.DeviceTaintRule) field.ErrorList { var allErrs field.ErrorList fldPath := field.NewPath("status") allErrs = corevalidation.ValidateObjectMetaUpdate(&rule.ObjectMeta, &oldRule.ObjectMeta, field.NewPath("metadata")) // Covers invalid name changes. allErrs = append(allErrs, metav1validation.ValidateConditions(rule.Status.Conditions, fldPath.Child("conditions"))...) if len(rule.Status.Conditions) > resource.DeviceTaintRuleStatusMaxConditions { allErrs = append(allErrs, field.TooMany(fldPath.Child("conditions"), len(rule.Status.Conditions), resource.DeviceTaintRuleStatusMaxConditions)) } return allErrs }