k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/resource/structured/namedresources/validation/validation.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package validation 18 19 import ( 20 "fmt" 21 "regexp" 22 23 "k8s.io/apimachinery/pkg/util/sets" 24 "k8s.io/apimachinery/pkg/util/validation/field" 25 "k8s.io/apiserver/pkg/cel" 26 "k8s.io/apiserver/pkg/cel/environment" 27 namedresourcescel "k8s.io/dynamic-resource-allocation/structured/namedresources/cel" 28 corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" 29 "k8s.io/kubernetes/pkg/apis/resource" 30 ) 31 32 var ( 33 validateInstanceName = corevalidation.ValidateDNS1123Subdomain 34 validateAttributeName = corevalidation.ValidateDNS1123Subdomain 35 ) 36 37 type Options struct { 38 // StoredExpressions must be true if and only if validating CEL 39 // expressions that were already stored persistently. This makes 40 // validation more permissive by enabling CEL definitions that are not 41 // valid yet for new expressions. 42 StoredExpressions bool 43 } 44 45 func ValidateResources(resources *resource.NamedResourcesResources, fldPath *field.Path) field.ErrorList { 46 allErrs := validateInstances(resources.Instances, fldPath.Child("instances")) 47 return allErrs 48 } 49 50 func validateInstances(instances []resource.NamedResourcesInstance, fldPath *field.Path) field.ErrorList { 51 var allErrs field.ErrorList 52 instanceNames := sets.New[string]() 53 for i, instance := range instances { 54 idxPath := fldPath.Index(i) 55 instanceName := instance.Name 56 allErrs = append(allErrs, validateInstanceName(instanceName, idxPath.Child("name"))...) 57 if instanceNames.Has(instanceName) { 58 allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), instanceName)) 59 } else { 60 instanceNames.Insert(instanceName) 61 } 62 allErrs = append(allErrs, validateAttributes(instance.Attributes, idxPath.Child("attributes"))...) 63 } 64 return allErrs 65 } 66 67 var ( 68 numericIdentifier = `(0|[1-9]\d*)` 69 70 preReleaseIdentifier = `(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)` 71 72 buildIdentifier = `[0-9a-zA-Z-]+` 73 74 semverRe = regexp.MustCompile(`^` + 75 76 // dot-separated version segments (e.g. 1.2.3) 77 numericIdentifier + `\.` + numericIdentifier + `\.` + numericIdentifier + 78 79 // optional dot-separated prerelease segments (e.g. -alpha.PRERELEASE.1) 80 `(-` + preReleaseIdentifier + `(\.` + preReleaseIdentifier + `)*)?` + 81 82 // optional dot-separated build identifier segments (e.g. +build.id.20240305) 83 `(\+` + buildIdentifier + `(\.` + buildIdentifier + `)*)?` + 84 85 `$`) 86 ) 87 88 func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *field.Path) field.ErrorList { 89 var allErrs field.ErrorList 90 attributeNames := sets.New[string]() 91 for i, attribute := range attributes { 92 idxPath := fldPath.Index(i) 93 attributeName := attribute.Name 94 allErrs = append(allErrs, validateAttributeName(attributeName, idxPath.Child("name"))...) 95 if attributeNames.Has(attributeName) { 96 allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), attributeName)) 97 } else { 98 attributeNames.Insert(attributeName) 99 } 100 101 entries := sets.New[string]() 102 if attribute.QuantityValue != nil { 103 entries.Insert("quantity") 104 } 105 if attribute.BoolValue != nil { 106 entries.Insert("bool") 107 } 108 if attribute.IntValue != nil { 109 entries.Insert("int") 110 } 111 if attribute.IntSliceValue != nil { 112 entries.Insert("intSlice") 113 } 114 if attribute.StringValue != nil { 115 entries.Insert("string") 116 } 117 if attribute.StringSliceValue != nil { 118 entries.Insert("stringSlice") 119 } 120 if attribute.VersionValue != nil { 121 entries.Insert("version") 122 if !semverRe.MatchString(*attribute.VersionValue) { 123 allErrs = append(allErrs, field.Invalid(idxPath.Child("version"), *attribute.VersionValue, "must be a string compatible with semver.org spec 2.0.0")) 124 } 125 } 126 127 switch len(entries) { 128 case 0: 129 allErrs = append(allErrs, field.Required(idxPath, "exactly one value must be set")) 130 case 1: 131 // Okay. 132 default: 133 allErrs = append(allErrs, field.Invalid(idxPath, sets.List(entries), "exactly one field must be set, not several")) 134 } 135 } 136 return allErrs 137 } 138 139 func ValidateRequest(opts Options, request *resource.NamedResourcesRequest, fldPath *field.Path) field.ErrorList { 140 return validateSelector(opts, request.Selector, fldPath.Child("selector")) 141 } 142 143 func ValidateFilter(opts Options, filter *resource.NamedResourcesFilter, fldPath *field.Path) field.ErrorList { 144 return validateSelector(opts, filter.Selector, fldPath.Child("selector")) 145 } 146 147 func validateSelector(opts Options, selector string, fldPath *field.Path) field.ErrorList { 148 var allErrs field.ErrorList 149 if selector == "" { 150 allErrs = append(allErrs, field.Required(fldPath, "")) 151 } else { 152 envType := environment.NewExpressions 153 if opts.StoredExpressions { 154 envType = environment.StoredExpressions 155 } 156 result := namedresourcescel.Compiler.CompileCELExpression(selector, envType) 157 if result.Error != nil { 158 allErrs = append(allErrs, convertCELErrorToValidationError(fldPath, selector, result.Error)) 159 } 160 } 161 return allErrs 162 } 163 164 func convertCELErrorToValidationError(fldPath *field.Path, expression string, err *cel.Error) *field.Error { 165 switch err.Type { 166 case cel.ErrorTypeRequired: 167 return field.Required(fldPath, err.Detail) 168 case cel.ErrorTypeInvalid: 169 return field.Invalid(fldPath, expression, err.Detail) 170 case cel.ErrorTypeInternal: 171 return field.InternalError(fldPath, err) 172 } 173 return field.InternalError(fldPath, fmt.Errorf("unsupported error type: %w", err)) 174 } 175 176 func ValidateAllocationResult(result *resource.NamedResourcesAllocationResult, fldPath *field.Path) field.ErrorList { 177 return validateInstanceName(result.Name, fldPath.Child("name")) 178 }