k8s.io/apiserver@v0.31.1/pkg/cel/library/format.go (about) 1 /* 2 Copyright 2024 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 library 18 19 import ( 20 "fmt" 21 "net/url" 22 23 "github.com/asaskevich/govalidator" 24 "github.com/google/cel-go/cel" 25 "github.com/google/cel-go/common/decls" 26 "github.com/google/cel-go/common/types" 27 "github.com/google/cel-go/common/types/ref" 28 apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" 29 "k8s.io/apimachinery/pkg/util/validation" 30 apiservercel "k8s.io/apiserver/pkg/cel" 31 "k8s.io/kube-openapi/pkg/validation/strfmt" 32 ) 33 34 // Format provides a CEL library exposing common named Kubernetes string 35 // validations. Can be used in CRD ValidationRules messageExpression. 36 // 37 // Example: 38 // 39 // rule: format.dns1123label.validate(object.metadata.name).hasValue() 40 // messageExpression: format.dns1123label.validate(object.metadata.name).value().join("\n") 41 // 42 // format.named(name: string) -> ?Format 43 // 44 // Returns the Format with the given name, if it exists. Otherwise, optional.none 45 // Allowed names are: 46 // - `dns1123Label` 47 // - `dns1123Subdomain` 48 // - `dns1035Label` 49 // - `qualifiedName` 50 // - `dns1123LabelPrefix` 51 // - `dns1123SubdomainPrefix` 52 // - `dns1035LabelPrefix` 53 // - `labelValue` 54 // - `uri` 55 // - `uuid` 56 // - `byte` 57 // - `date` 58 // - `datetime` 59 // 60 // format.<formatName>() -> Format 61 // 62 // Convenience functions for all the named formats are also available 63 // 64 // Examples: 65 // format.dns1123Label().validate("my-label-name") 66 // format.dns1123Subdomain().validate("apiextensions.k8s.io") 67 // format.dns1035Label().validate("my-label-name") 68 // format.qualifiedName().validate("apiextensions.k8s.io/v1beta1") 69 // format.dns1123LabelPrefix().validate("my-label-prefix-") 70 // format.dns1123SubdomainPrefix().validate("mysubdomain.prefix.-") 71 // format.dns1035LabelPrefix().validate("my-label-prefix-") 72 // format.uri().validate("http://example.com") 73 // Uses same pattern as isURL, but returns an error 74 // format.uuid().validate("123e4567-e89b-12d3-a456-426614174000") 75 // format.byte().validate("aGVsbG8=") 76 // format.date().validate("2021-01-01") 77 // format.datetime().validate("2021-01-01T00:00:00Z") 78 // 79 80 // <Format>.validate(str: string) -> ?list<string> 81 // 82 // Validates the given string against the given format. Returns optional.none 83 // if the string is valid, otherwise a list of validation error strings. 84 func Format() cel.EnvOption { 85 return cel.Lib(formatLib) 86 } 87 88 var formatLib = &format{} 89 90 type format struct{} 91 92 func (*format) LibraryName() string { 93 return "format" 94 } 95 96 func ZeroArgumentFunctionBinding(binding func() ref.Val) decls.OverloadOpt { 97 return func(o *decls.OverloadDecl) (*decls.OverloadDecl, error) { 98 wrapped, err := decls.FunctionBinding(func(values ...ref.Val) ref.Val { return binding() })(o) 99 if err != nil { 100 return nil, err 101 } 102 if len(wrapped.ArgTypes()) != 0 { 103 return nil, fmt.Errorf("function binding must have 0 arguments") 104 } 105 return o, nil 106 } 107 } 108 109 func (*format) CompileOptions() []cel.EnvOption { 110 options := make([]cel.EnvOption, 0, len(formatLibraryDecls)) 111 for name, overloads := range formatLibraryDecls { 112 options = append(options, cel.Function(name, overloads...)) 113 } 114 for name, constantValue := range ConstantFormats { 115 prefixedName := "format." + name 116 options = append(options, cel.Function(prefixedName, cel.Overload(prefixedName, []*cel.Type{}, apiservercel.FormatType, ZeroArgumentFunctionBinding(func() ref.Val { 117 return constantValue 118 })))) 119 } 120 return options 121 } 122 123 func (*format) ProgramOptions() []cel.ProgramOption { 124 return []cel.ProgramOption{} 125 } 126 127 var ConstantFormats map[string]*apiservercel.Format = map[string]*apiservercel.Format{ 128 "dns1123Label": { 129 Name: "DNS1123Label", 130 ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, false) }, 131 MaxRegexSize: 30, 132 }, 133 "dns1123Subdomain": { 134 Name: "DNS1123Subdomain", 135 ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSSubdomain(s, false) }, 136 MaxRegexSize: 60, 137 }, 138 "dns1035Label": { 139 Name: "DNS1035Label", 140 ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNS1035Label(s, false) }, 141 MaxRegexSize: 30, 142 }, 143 "qualifiedName": { 144 Name: "QualifiedName", 145 ValidateFunc: validation.IsQualifiedName, 146 MaxRegexSize: 60, // uses subdomain regex 147 }, 148 149 "dns1123LabelPrefix": { 150 Name: "DNS1123LabelPrefix", 151 ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, true) }, 152 MaxRegexSize: 30, 153 }, 154 "dns1123SubdomainPrefix": { 155 Name: "DNS1123SubdomainPrefix", 156 ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSSubdomain(s, true) }, 157 MaxRegexSize: 60, 158 }, 159 "dns1035LabelPrefix": { 160 Name: "DNS1035LabelPrefix", 161 ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNS1035Label(s, true) }, 162 MaxRegexSize: 30, 163 }, 164 "labelValue": { 165 Name: "LabelValue", 166 ValidateFunc: validation.IsValidLabelValue, 167 MaxRegexSize: 40, 168 }, 169 170 // CRD formats 171 // Implementations sourced from strfmt, which kube-openapi uses as its 172 // format library. There are other CRD formats supported, but they are 173 // covered by other portions of the CEL library (like IP/CIDR), or their 174 // use is discouraged (like bsonobjectid, email, etc) 175 "uri": { 176 Name: "URI", 177 ValidateFunc: func(s string) []string { 178 // Directly call ParseRequestURI since we can get a better error message 179 _, err := url.ParseRequestURI(s) 180 if err != nil { 181 return []string{err.Error()} 182 } 183 return nil 184 }, 185 // Use govalidator url regex to estimate, since ParseRequestURI 186 // doesnt use regex 187 MaxRegexSize: len(govalidator.URL), 188 }, 189 "uuid": { 190 Name: "uuid", 191 ValidateFunc: func(s string) []string { 192 if !strfmt.Default.Validates("uuid", s) { 193 return []string{"does not match the UUID format"} 194 } 195 return nil 196 }, 197 MaxRegexSize: len(strfmt.UUIDPattern), 198 }, 199 "byte": { 200 Name: "byte", 201 ValidateFunc: func(s string) []string { 202 if !strfmt.Default.Validates("byte", s) { 203 return []string{"invalid base64"} 204 } 205 return nil 206 }, 207 MaxRegexSize: len(govalidator.Base64), 208 }, 209 "date": { 210 Name: "date", 211 ValidateFunc: func(s string) []string { 212 if !strfmt.Default.Validates("date", s) { 213 return []string{"invalid date"} 214 } 215 return nil 216 }, 217 // Estimated regex size for RFC3339FullDate which is 218 // a date format. Assume a date-time pattern is longer 219 // so use that to conservatively estimate this 220 MaxRegexSize: len(strfmt.DateTimePattern), 221 }, 222 "datetime": { 223 Name: "datetime", 224 ValidateFunc: func(s string) []string { 225 if !strfmt.Default.Validates("datetime", s) { 226 return []string{"invalid datetime"} 227 } 228 return nil 229 }, 230 MaxRegexSize: len(strfmt.DateTimePattern), 231 }, 232 } 233 234 var formatLibraryDecls = map[string][]cel.FunctionOpt{ 235 "validate": { 236 cel.MemberOverload("format-validate", []*cel.Type{apiservercel.FormatType, cel.StringType}, cel.OptionalType(cel.ListType(cel.StringType)), cel.BinaryBinding(formatValidate)), 237 }, 238 "format.named": { 239 cel.Overload("format-named", []*cel.Type{cel.StringType}, cel.OptionalType(apiservercel.FormatType), cel.UnaryBinding(func(name ref.Val) ref.Val { 240 nameString, ok := name.Value().(string) 241 if !ok { 242 return types.MaybeNoSuchOverloadErr(name) 243 } 244 245 f, ok := ConstantFormats[nameString] 246 if !ok { 247 return types.OptionalNone 248 } 249 return types.OptionalOf(f) 250 })), 251 }, 252 } 253 254 func formatValidate(arg1, arg2 ref.Val) ref.Val { 255 f, ok := arg1.Value().(*apiservercel.Format) 256 if !ok { 257 return types.MaybeNoSuchOverloadErr(arg1) 258 } 259 260 str, ok := arg2.Value().(string) 261 if !ok { 262 return types.MaybeNoSuchOverloadErr(arg2) 263 } 264 265 res := f.ValidateFunc(str) 266 if len(res) == 0 { 267 return types.OptionalNone 268 } 269 return types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, res)) 270 }