Files
kubernetes/pkg/apis/resource/validation/validation.go

913 lines
40 KiB
Go

/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
apiequality "k8s.io/apimachinery/pkg/api/equality"
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"
dracel "k8s.io/dynamic-resource-allocation/cel"
"k8s.io/dynamic-resource-allocation/structured"
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/apis/resource"
netutils "k8s.io/utils/net"
)
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
)
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))
}
parts := strings.Split(name, "/")
for _, part := range parts {
allErrs = append(allErrs, corevalidation.ValidateDNS1123Subdomain(part, fldPath)...)
}
}
return allErrs
}
// 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.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldClaim.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(resourceClaim.Spec, oldClaim.Spec, field.NewPath("spec"))...)
// Because the spec is immutable, all CEL expressions in it must have been stored.
// If the user tries an update, this is not true and checking is less strict, but
// as there are errors, it doesn't matter.
allErrs = append(allErrs, validateResourceClaimSpec(&resourceClaim.Spec, field.NewPath("spec"), true)...)
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
}
func validateDeviceClaim(deviceClaim *resource.DeviceClaim, fldPath *field.Path, stored bool) field.ErrorList {
allErrs := field.ErrorList{}
requestNames := gatherRequestNames(deviceClaim)
allErrs = append(allErrs, validateSet(deviceClaim.Requests, resource.DeviceRequestsMaxSize,
func(request resource.DeviceRequest, fldPath *field.Path) field.ErrorList {
return validateDeviceRequest(request, fldPath, stored)
},
func(request resource.DeviceRequest) (string, string) {
return request.Name, "name"
},
fldPath.Child("requests"))...)
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"))...)
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"))...)
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.DeviceID] {
allocatedDevices := sets.New[structured.DeviceID]()
for _, result := range allocationResult.Results {
deviceID := structured.MakeDeviceID(result.Driver, result.Pool, result.Device)
allocatedDevices.Insert(deviceID)
}
return allocatedDevices
}
func validateDeviceRequest(request resource.DeviceRequest, fldPath *field.Path, stored bool) field.ErrorList {
allErrs := validateRequestName(request.Name, fldPath.Child("name"))
if len(request.FirstAvailable) > 0 {
if request.DeviceClassName != "" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("deviceClassName"), request.DeviceClassName, "must not be specified when firstAvailable is set"))
}
if request.Selectors != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("selectors"), request.Selectors, "must not be specified when firstAvailable is set"))
}
if request.AllocationMode != "" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("allocationMode"), request.AllocationMode, "must not be specified when firstAvailable is set"))
}
if request.Count != 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("count"), request.Count, "must not be specified when firstAvailable is set"))
}
if request.AdminAccess != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("adminAccess"), request.AdminAccess, "must not be specified when firstAvailable is set"))
}
allErrs = append(allErrs, validateSet(request.FirstAvailable, resource.FirstAvailableDeviceRequestMaxSize,
func(subRequest resource.DeviceSubRequest, fldPath *field.Path) field.ErrorList {
return validateDeviceSubRequest(subRequest, fldPath, stored)
},
func(subRequest resource.DeviceSubRequest) (string, string) {
return subRequest.Name, "name"
},
fldPath.Child("firstAvailable"))...)
return allErrs
}
allErrs = append(allErrs, validateDeviceClass(request.DeviceClassName, fldPath.Child("deviceClassName"))...)
allErrs = append(allErrs, validateSelectorSlice(request.Selectors, fldPath.Child("selectors"), stored)...)
allErrs = append(allErrs, validateDeviceAllocationMode(request.AllocationMode, request.Count, fldPath.Child("allocationMode"), fldPath.Child("count"))...)
return allErrs
}
func validateDeviceSubRequest(subRequest resource.DeviceSubRequest, fldPath *field.Path, stored bool) field.ErrorList {
allErrs := validateRequestName(subRequest.Name, fldPath.Child("name"))
allErrs = append(allErrs, validateDeviceClass(subRequest.DeviceClassName, fldPath.Child("deviceClassName"))...)
allErrs = append(allErrs, validateSelectorSlice(subRequest.Selectors, fldPath.Child("selectors"), stored)...)
allErrs = append(allErrs, validateDeviceAllocationMode(subRequest.AllocationMode, subRequest.Count, fldPath.Child("allocationMode"), fldPath.Child("count"))...)
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:
allErrs = append(allErrs, field.NotSupported(allocModeFldPath, deviceAllocationMode, []resource.DeviceAllocationMode{resource.DeviceAllocationModeAll, resource.DeviceAllocationModeExactCount}))
}
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, stored bool) field.ErrorList {
return validateSlice(selectors, resource.DeviceSelectorsMaxSize,
func(selector resource.DeviceSelector, fldPath *field.Path) field.ErrorList {
return validateSelector(selector, fldPath, stored)
},
fldPath)
}
func validateSelector(selector resource.DeviceSelector, fldPath *field.Path, stored bool) field.ErrorList {
var allErrs field.ErrorList
if selector.CEL == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("cel"), ""))
} else {
allErrs = append(allErrs, validateCELSelector(*selector.CEL, fldPath.Child("cel"), stored)...)
}
return allErrs
}
func validateCELSelector(celSelector resource.CELDeviceSelector, fldPath *field.Path, stored bool) field.ErrorList {
var allErrs field.ErrorList
envType := environment.NewExpressions
if stored {
envType = environment.StoredExpressions
}
if len(celSelector.Expression) > resource.CELSelectorExpressionMaxLength {
allErrs = append(allErrs, field.TooLong(fldPath.Child("expression"), "" /*unused*/, resource.CELSelectorExpressionMaxLength))
// Don't bother compiling too long expressions.
return allErrs
}
result := dracel.GetCompiler().CompileCELExpression(celSelector.Expression, dracel.Options{EnvType: &envType})
if result.Error != nil {
allErrs = append(allErrs, convertCELErrorToValidationError(fldPath.Child("expression"), celSelector.Expression, result.Error))
} else if result.MaxCost > resource.CELSelectorExpressionMaxCost {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("expression"), "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 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"))...)
if constraint.MatchAttribute == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("matchAttribute"), ""))
} else {
allErrs = append(allErrs, validateFullyQualifiedName(*constraint.MatchAttribute, 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"))...)
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"))...)
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, string) { return consumer.UID, "uid" },
fldPath.Child("reservedFor"))...)
var allocatedDevices sets.Set[structured.DeviceID]
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.DeviceID, string) {
return structured.MakeDeviceID(device.Driver, device.Pool, device.Device), "deviceID"
},
fldPath.Child("devices"))...)
// 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"))...)
} 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"))...)
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"))...)
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"))...)
allErrs = append(allErrs, validatePoolName(result.Pool, fldPath.Child("pool"))...)
allErrs = append(allErrs, validateDeviceName(result.Device, fldPath.Child("device"))...)
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"))...)
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, ""))
case resource.AllocationConfigSourceClaim, resource.AllocationConfigSourceClass:
default:
allErrs = append(allErrs, field.NotSupported(fldPath, source, []resource.AllocationConfigSource{resource.AllocationConfigSourceClaim, resource.AllocationConfigSourceClass}))
}
return allErrs
}
// ValidateClass validates a DeviceClass.
func ValidateDeviceClass(class *resource.DeviceClass) field.ErrorList {
allErrs := corevalidation.ValidateObjectMeta(&class.ObjectMeta, false, corevalidation.ValidateClassName, field.NewPath("metadata"))
allErrs = append(allErrs, validateDeviceClassSpec(&class.Spec, nil, field.NewPath("spec"))...)
return allErrs
}
// ValidateClassUpdate tests if an update to DeviceClass is valid.
func ValidateDeviceClassUpdate(class, oldClass *resource.DeviceClass) field.ErrorList {
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, stored)
},
fldPath.Child("selectors"))...)
// Same logic as above for configs.
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"))...)
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"))
allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(template.Spec, oldTemplate.Spec, field.NewPath("spec"))...)
// Because the spec is immutable, all CEL expressions in it must have been stored.
// If the user tries an update, this is not true and checking is less strict, but
// as there are errors, it doesn't matter.
allErrs = append(allErrs, validateResourceClaimTemplateSpec(&template.Spec, field.NewPath("spec"), true)...)
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
}
// ValidateResourceSlice 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"))...)
}
numNodeSelectionFields := 0
if spec.NodeName != "" {
numNodeSelectionFields++
allErrs = append(allErrs, validateNodeName(spec.NodeName, fldPath.Child("nodeName"))...)
}
if spec.NodeSelector != nil {
numNodeSelectionFields++
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 {
numNodeSelectionFields++
}
switch numNodeSelectionFields {
case 0:
allErrs = append(allErrs, field.Required(fldPath, "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required"))
case 1:
default:
allErrs = append(allErrs, field.Invalid(fldPath, nil, "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required"))
}
allErrs = append(allErrs, validateSet(spec.Devices, resource.ResourceSliceMaxDevices, validateDevice,
func(device resource.Device) (string, string) {
return device.Name, "name"
}, fldPath.Child("devices"))...)
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, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateDeviceName(device.Name, fldPath.Child("name"))...)
if device.Basic == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("basic"), ""))
} else {
allErrs = append(allErrs, validateBasicDevice(*device.Basic, fldPath.Child("basic"))...)
}
return allErrs
}
func validateBasicDevice(device resource.BasicDevice, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
// Warn about exceeding the maximum length only once. If any individual
// field is too large, then so is the combination.
maxKeyLen := resource.DeviceMaxDomainLength + 1 + resource.DeviceMaxIDLength
allErrs = append(allErrs, validateMap(device.Attributes, -1, maxKeyLen, validateQualifiedName, validateDeviceAttribute, fldPath.Child("attributes"))...)
allErrs = append(allErrs, validateMap(device.Capacity, -1, maxKeyLen, validateQualifiedName, validateDeviceCapacity, fldPath.Child("capacity"))...)
if combinedLen, max := len(device.Attributes)+len(device.Capacity), resource.ResourceSliceMaxAttributesAndCapacitiesPerDevice; combinedLen > max {
allErrs = append(allErrs, field.Invalid(fldPath, combinedLen, fmt.Sprintf("the total number of attributes and capacities must not exceed %d", max)))
}
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 {
if len(*attribute.StringValue) > resource.DeviceAttributeMaxValueLength {
allErrs = append(allErrs, field.TooLong(fldPath.Child("string"), "" /*unused*/, resource.DeviceAttributeMaxValueLength))
}
numFields++
}
if attribute.VersionValue != nil {
numFields++
if !semverRe.MatchString(*attribute.VersionValue) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), *attribute.VersionValue, "must be a string compatible with semver.org spec 2.0.0"))
}
if len(*attribute.VersionValue) > resource.DeviceAttributeMaxValueLength {
allErrs = append(allErrs, field.TooLong(fldPath.Child("version"), "" /*unused*/, resource.DeviceAttributeMaxValueLength))
}
}
switch numFields {
case 0:
allErrs = append(allErrs, field.Required(fldPath, "exactly one value must be specified"))
case 1:
// Okay.
default:
allErrs = append(allErrs, field.Invalid(fldPath, attribute, "exactly one value must be specified"))
}
return allErrs
}
func validateDeviceCapacity(capacity resource.DeviceCapacity, 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
if name == "" {
allErrs = append(allErrs, field.Required(fldPath, "name required"))
return allErrs
}
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.Required(fldPath, "the domain must not be empty"))
} else {
allErrs = append(allErrs, validateDriverName(parts[0], fldPath)...)
}
if len(parts[1]) == 0 {
allErrs = append(allErrs, field.Required(fldPath, "the name must not be empty"))
} else {
allErrs = append(allErrs, validateCIdentifier(parts[1], fldPath)...)
}
}
return allErrs
}
func validateFullyQualifiedName(name resource.FullyQualifiedName, fldPath *field.Path) field.ErrorList {
allErrs := validateQualifiedName(resource.QualifiedName(name), fldPath)
// validateQualifiedName checks that the name isn't empty and both parts are valid.
// What we need to enforce here is that there really is a domain.
if name != "" && !strings.Contains(string(name), "/") {
allErrs = append(allErrs, field.Invalid(fldPath, name, "must include a domain"))
}
return allErrs
}
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 validation.IsCIdentifier(id) {
allErrs = append(allErrs, field.TypeInvalid(fldPath, id, msg))
}
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) field.ErrorList {
var allErrs field.ErrorList
for i, item := range slice {
idxPath := fldPath.Index(i)
allErrs = append(allErrs, validateItem(item, idxPath)...)
}
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.
allErrs = append(allErrs, field.TooMany(fldPath, len(slice), maxSize))
}
return allErrs
}
// 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, string), fldPath *field.Path) field.ErrorList {
allErrs := validateSlice(slice, maxSize, validateItem, fldPath)
allItems := sets.New[K]()
for i, item := range slice {
idxPath := fldPath.Index(i)
key, fieldName := itemKey(item)
childPath := idxPath
if fieldName != "" {
childPath = childPath.Child(fieldName)
}
if allItems.Has(key) {
allErrs = append(allErrs, field.Duplicate(childPath, key))
} else {
allItems.Insert(key)
}
}
return allErrs
}
// stringKey uses the item itself as a key for validateSet.
func stringKey(item string) (string, string) {
return item, ""
}
// 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) field.ErrorList {
var allErrs field.ErrorList
if maxSize >= 0 && len(m) > maxSize {
allErrs = append(allErrs, field.TooMany(fldPath, len(m), maxSize))
}
for key, item := range m {
keyPath := fldPath.Key(truncateIfTooLong(string(key), truncateKeyLen))
allErrs = append(allErrs, validateKey(key, keyPath)...)
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.DeviceID]) 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"))...)
deviceID := structured.MakeDeviceID(device.Driver, device.Pool, device.Device)
if !allocatedDevices.Has(deviceID) {
allErrs = append(allErrs, field.Invalid(fldPath, deviceID, "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>", "parameters 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))
}
if len(networkDeviceData.HardwareAddress) > resource.NetworkDeviceDataHardwareAddressMaxLength {
allErrs = append(allErrs, field.TooLong(fldPath.Child("hardwareAddress"), "" /* unused */, resource.NetworkDeviceDataHardwareAddressMaxLength))
}
allErrs = append(allErrs, validateSet(networkDeviceData.IPs, resource.NetworkDeviceDataMaxIPs,
func(address string, fldPath *field.Path) field.ErrorList {
// reformat CIDR to handle different ways IPs can be written
// (e.g. 2001:db8::1/64 == 2001:0db8::1/64)
ip, ipNet, err := netutils.ParseCIDRSloppy(address)
if err != nil {
// must fail
return validation.IsValidCIDR(fldPath, address)
}
maskSize, _ := ipNet.Mask.Size()
canonical := fmt.Sprintf("%s/%d", ip.String(), maskSize)
if address != canonical {
return field.ErrorList{
field.Invalid(fldPath, address, fmt.Sprintf("must be in canonical form (%s)", canonical)),
}
}
return nil
},
func(address string) (string, string) {
return address, ""
},
fldPath.Child("ips"))...)
return allErrs
}