k8s.io/apimachinery@v0.29.2/pkg/util/validation/validation.go (about) 1 /* 2 Copyright 2014 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 "math" 22 "net" 23 "regexp" 24 "strconv" 25 "strings" 26 27 "k8s.io/apimachinery/pkg/util/validation/field" 28 netutils "k8s.io/utils/net" 29 ) 30 31 const qnameCharFmt string = "[A-Za-z0-9]" 32 const qnameExtCharFmt string = "[-A-Za-z0-9_.]" 33 const qualifiedNameFmt string = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt 34 const qualifiedNameErrMsg string = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character" 35 const qualifiedNameMaxLength int = 63 36 37 var qualifiedNameRegexp = regexp.MustCompile("^" + qualifiedNameFmt + "$") 38 39 // IsQualifiedName tests whether the value passed is what Kubernetes calls a 40 // "qualified name". This is a format used in various places throughout the 41 // system. If the value is not valid, a list of error strings is returned. 42 // Otherwise an empty list (or nil) is returned. 43 func IsQualifiedName(value string) []string { 44 var errs []string 45 parts := strings.Split(value, "/") 46 var name string 47 switch len(parts) { 48 case 1: 49 name = parts[0] 50 case 2: 51 var prefix string 52 prefix, name = parts[0], parts[1] 53 if len(prefix) == 0 { 54 errs = append(errs, "prefix part "+EmptyError()) 55 } else if msgs := IsDNS1123Subdomain(prefix); len(msgs) != 0 { 56 errs = append(errs, prefixEach(msgs, "prefix part ")...) 57 } 58 default: 59 return append(errs, "a qualified name "+RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc")+ 60 " with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')") 61 } 62 63 if len(name) == 0 { 64 errs = append(errs, "name part "+EmptyError()) 65 } else if len(name) > qualifiedNameMaxLength { 66 errs = append(errs, "name part "+MaxLenError(qualifiedNameMaxLength)) 67 } 68 if !qualifiedNameRegexp.MatchString(name) { 69 errs = append(errs, "name part "+RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc")) 70 } 71 return errs 72 } 73 74 // IsFullyQualifiedName checks if the name is fully qualified. This is similar 75 // to IsFullyQualifiedDomainName but requires a minimum of 3 segments instead of 76 // 2 and does not accept a trailing . as valid. 77 // TODO: This function is deprecated and preserved until all callers migrate to 78 // IsFullyQualifiedDomainName; please don't add new callers. 79 func IsFullyQualifiedName(fldPath *field.Path, name string) field.ErrorList { 80 var allErrors field.ErrorList 81 if len(name) == 0 { 82 return append(allErrors, field.Required(fldPath, "")) 83 } 84 if errs := IsDNS1123Subdomain(name); len(errs) > 0 { 85 return append(allErrors, field.Invalid(fldPath, name, strings.Join(errs, ","))) 86 } 87 if len(strings.Split(name, ".")) < 3 { 88 return append(allErrors, field.Invalid(fldPath, name, "should be a domain with at least three segments separated by dots")) 89 } 90 return allErrors 91 } 92 93 // IsFullyQualifiedDomainName checks if the domain name is fully qualified. This 94 // is similar to IsFullyQualifiedName but only requires a minimum of 2 segments 95 // instead of 3 and accepts a trailing . as valid. 96 func IsFullyQualifiedDomainName(fldPath *field.Path, name string) field.ErrorList { 97 var allErrors field.ErrorList 98 if len(name) == 0 { 99 return append(allErrors, field.Required(fldPath, "")) 100 } 101 if strings.HasSuffix(name, ".") { 102 name = name[:len(name)-1] 103 } 104 if errs := IsDNS1123Subdomain(name); len(errs) > 0 { 105 return append(allErrors, field.Invalid(fldPath, name, strings.Join(errs, ","))) 106 } 107 if len(strings.Split(name, ".")) < 2 { 108 return append(allErrors, field.Invalid(fldPath, name, "should be a domain with at least two segments separated by dots")) 109 } 110 for _, label := range strings.Split(name, ".") { 111 if errs := IsDNS1123Label(label); len(errs) > 0 { 112 return append(allErrors, field.Invalid(fldPath, label, strings.Join(errs, ","))) 113 } 114 } 115 return allErrors 116 } 117 118 // Allowed characters in an HTTP Path as defined by RFC 3986. A HTTP path may 119 // contain: 120 // * unreserved characters (alphanumeric, '-', '.', '_', '~') 121 // * percent-encoded octets 122 // * sub-delims ("!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=") 123 // * a colon character (":") 124 const httpPathFmt string = `[A-Za-z0-9/\-._~%!$&'()*+,;=:]+` 125 126 var httpPathRegexp = regexp.MustCompile("^" + httpPathFmt + "$") 127 128 // IsDomainPrefixedPath checks if the given string is a domain-prefixed path 129 // (e.g. acme.io/foo). All characters before the first "/" must be a valid 130 // subdomain as defined by RFC 1123. All characters trailing the first "/" must 131 // be valid HTTP Path characters as defined by RFC 3986. 132 func IsDomainPrefixedPath(fldPath *field.Path, dpPath string) field.ErrorList { 133 var allErrs field.ErrorList 134 if len(dpPath) == 0 { 135 return append(allErrs, field.Required(fldPath, "")) 136 } 137 138 segments := strings.SplitN(dpPath, "/", 2) 139 if len(segments) != 2 || len(segments[0]) == 0 || len(segments[1]) == 0 { 140 return append(allErrs, field.Invalid(fldPath, dpPath, "must be a domain-prefixed path (such as \"acme.io/foo\")")) 141 } 142 143 host := segments[0] 144 for _, err := range IsDNS1123Subdomain(host) { 145 allErrs = append(allErrs, field.Invalid(fldPath, host, err)) 146 } 147 148 path := segments[1] 149 if !httpPathRegexp.MatchString(path) { 150 return append(allErrs, field.Invalid(fldPath, path, RegexError("Invalid path", httpPathFmt))) 151 } 152 153 return allErrs 154 } 155 156 const labelValueFmt string = "(" + qualifiedNameFmt + ")?" 157 const labelValueErrMsg string = "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character" 158 159 // LabelValueMaxLength is a label's max length 160 const LabelValueMaxLength int = 63 161 162 var labelValueRegexp = regexp.MustCompile("^" + labelValueFmt + "$") 163 164 // IsValidLabelValue tests whether the value passed is a valid label value. If 165 // the value is not valid, a list of error strings is returned. Otherwise an 166 // empty list (or nil) is returned. 167 func IsValidLabelValue(value string) []string { 168 var errs []string 169 if len(value) > LabelValueMaxLength { 170 errs = append(errs, MaxLenError(LabelValueMaxLength)) 171 } 172 if !labelValueRegexp.MatchString(value) { 173 errs = append(errs, RegexError(labelValueErrMsg, labelValueFmt, "MyValue", "my_value", "12345")) 174 } 175 return errs 176 } 177 178 const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" 179 const dns1123LabelErrMsg string = "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character" 180 181 // DNS1123LabelMaxLength is a label's max length in DNS (RFC 1123) 182 const DNS1123LabelMaxLength int = 63 183 184 var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$") 185 186 // IsDNS1123Label tests for a string that conforms to the definition of a label in 187 // DNS (RFC 1123). 188 func IsDNS1123Label(value string) []string { 189 var errs []string 190 if len(value) > DNS1123LabelMaxLength { 191 errs = append(errs, MaxLenError(DNS1123LabelMaxLength)) 192 } 193 if !dns1123LabelRegexp.MatchString(value) { 194 if dns1123SubdomainRegexp.MatchString(value) { 195 // It was a valid subdomain and not a valid label. Since we 196 // already checked length, it must be dots. 197 errs = append(errs, "must not contain dots") 198 } else { 199 errs = append(errs, RegexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc")) 200 } 201 } 202 return errs 203 } 204 205 const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*" 206 const dns1123SubdomainErrorMsg string = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character" 207 208 // DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123) 209 const DNS1123SubdomainMaxLength int = 253 210 211 var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$") 212 213 // IsDNS1123Subdomain tests for a string that conforms to the definition of a 214 // subdomain in DNS (RFC 1123). 215 func IsDNS1123Subdomain(value string) []string { 216 var errs []string 217 if len(value) > DNS1123SubdomainMaxLength { 218 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength)) 219 } 220 if !dns1123SubdomainRegexp.MatchString(value) { 221 errs = append(errs, RegexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com")) 222 } 223 return errs 224 } 225 226 const dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?" 227 const dns1035LabelErrMsg string = "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character" 228 229 // DNS1035LabelMaxLength is a label's max length in DNS (RFC 1035) 230 const DNS1035LabelMaxLength int = 63 231 232 var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFmt + "$") 233 234 // IsDNS1035Label tests for a string that conforms to the definition of a label in 235 // DNS (RFC 1035). 236 func IsDNS1035Label(value string) []string { 237 var errs []string 238 if len(value) > DNS1035LabelMaxLength { 239 errs = append(errs, MaxLenError(DNS1035LabelMaxLength)) 240 } 241 if !dns1035LabelRegexp.MatchString(value) { 242 errs = append(errs, RegexError(dns1035LabelErrMsg, dns1035LabelFmt, "my-name", "abc-123")) 243 } 244 return errs 245 } 246 247 // wildcard definition - RFC 1034 section 4.3.3. 248 // examples: 249 // - valid: *.bar.com, *.foo.bar.com 250 // - invalid: *.*.bar.com, *.foo.*.com, *bar.com, f*.bar.com, * 251 const wildcardDNS1123SubdomainFmt = "\\*\\." + dns1123SubdomainFmt 252 const wildcardDNS1123SubdomainErrMsg = "a wildcard DNS-1123 subdomain must start with '*.', followed by a valid DNS subdomain, which must consist of lower case alphanumeric characters, '-' or '.' and end with an alphanumeric character" 253 254 // IsWildcardDNS1123Subdomain tests for a string that conforms to the definition of a 255 // wildcard subdomain in DNS (RFC 1034 section 4.3.3). 256 func IsWildcardDNS1123Subdomain(value string) []string { 257 wildcardDNS1123SubdomainRegexp := regexp.MustCompile("^" + wildcardDNS1123SubdomainFmt + "$") 258 259 var errs []string 260 if len(value) > DNS1123SubdomainMaxLength { 261 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength)) 262 } 263 if !wildcardDNS1123SubdomainRegexp.MatchString(value) { 264 errs = append(errs, RegexError(wildcardDNS1123SubdomainErrMsg, wildcardDNS1123SubdomainFmt, "*.example.com")) 265 } 266 return errs 267 } 268 269 const cIdentifierFmt string = "[A-Za-z_][A-Za-z0-9_]*" 270 const identifierErrMsg string = "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_'" 271 272 var cIdentifierRegexp = regexp.MustCompile("^" + cIdentifierFmt + "$") 273 274 // IsCIdentifier tests for a string that conforms the definition of an identifier 275 // in C. This checks the format, but not the length. 276 func IsCIdentifier(value string) []string { 277 if !cIdentifierRegexp.MatchString(value) { 278 return []string{RegexError(identifierErrMsg, cIdentifierFmt, "my_name", "MY_NAME", "MyName")} 279 } 280 return nil 281 } 282 283 // IsValidPortNum tests that the argument is a valid, non-zero port number. 284 func IsValidPortNum(port int) []string { 285 if 1 <= port && port <= 65535 { 286 return nil 287 } 288 return []string{InclusiveRangeError(1, 65535)} 289 } 290 291 // IsInRange tests that the argument is in an inclusive range. 292 func IsInRange(value int, min int, max int) []string { 293 if value >= min && value <= max { 294 return nil 295 } 296 return []string{InclusiveRangeError(min, max)} 297 } 298 299 // Now in libcontainer UID/GID limits is 0 ~ 1<<31 - 1 300 // TODO: once we have a type for UID/GID we should make these that type. 301 const ( 302 minUserID = 0 303 maxUserID = math.MaxInt32 304 minGroupID = 0 305 maxGroupID = math.MaxInt32 306 ) 307 308 // IsValidGroupID tests that the argument is a valid Unix GID. 309 func IsValidGroupID(gid int64) []string { 310 if minGroupID <= gid && gid <= maxGroupID { 311 return nil 312 } 313 return []string{InclusiveRangeError(minGroupID, maxGroupID)} 314 } 315 316 // IsValidUserID tests that the argument is a valid Unix UID. 317 func IsValidUserID(uid int64) []string { 318 if minUserID <= uid && uid <= maxUserID { 319 return nil 320 } 321 return []string{InclusiveRangeError(minUserID, maxUserID)} 322 } 323 324 var portNameCharsetRegex = regexp.MustCompile("^[-a-z0-9]+$") 325 var portNameOneLetterRegexp = regexp.MustCompile("[a-z]") 326 327 // IsValidPortName check that the argument is valid syntax. It must be 328 // non-empty and no more than 15 characters long. It may contain only [-a-z0-9] 329 // and must contain at least one letter [a-z]. It must not start or end with a 330 // hyphen, nor contain adjacent hyphens. 331 // 332 // Note: We only allow lower-case characters, even though RFC 6335 is case 333 // insensitive. 334 func IsValidPortName(port string) []string { 335 var errs []string 336 if len(port) > 15 { 337 errs = append(errs, MaxLenError(15)) 338 } 339 if !portNameCharsetRegex.MatchString(port) { 340 errs = append(errs, "must contain only alpha-numeric characters (a-z, 0-9), and hyphens (-)") 341 } 342 if !portNameOneLetterRegexp.MatchString(port) { 343 errs = append(errs, "must contain at least one letter (a-z)") 344 } 345 if strings.Contains(port, "--") { 346 errs = append(errs, "must not contain consecutive hyphens") 347 } 348 if len(port) > 0 && (port[0] == '-' || port[len(port)-1] == '-') { 349 errs = append(errs, "must not begin or end with a hyphen") 350 } 351 return errs 352 } 353 354 // IsValidIP tests that the argument is a valid IP address. 355 func IsValidIP(value string) []string { 356 if netutils.ParseIPSloppy(value) == nil { 357 return []string{"must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)"} 358 } 359 return nil 360 } 361 362 // IsValidIPv4Address tests that the argument is a valid IPv4 address. 363 func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList { 364 var allErrors field.ErrorList 365 ip := netutils.ParseIPSloppy(value) 366 if ip == nil || ip.To4() == nil { 367 allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address")) 368 } 369 return allErrors 370 } 371 372 // IsValidIPv6Address tests that the argument is a valid IPv6 address. 373 func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList { 374 var allErrors field.ErrorList 375 ip := netutils.ParseIPSloppy(value) 376 if ip == nil || ip.To4() != nil { 377 allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address")) 378 } 379 return allErrors 380 } 381 382 const percentFmt string = "[0-9]+%" 383 const percentErrMsg string = "a valid percent string must be a numeric string followed by an ending '%'" 384 385 var percentRegexp = regexp.MustCompile("^" + percentFmt + "$") 386 387 // IsValidPercent checks that string is in the form of a percentage 388 func IsValidPercent(percent string) []string { 389 if !percentRegexp.MatchString(percent) { 390 return []string{RegexError(percentErrMsg, percentFmt, "1%", "93%")} 391 } 392 return nil 393 } 394 395 const httpHeaderNameFmt string = "[-A-Za-z0-9]+" 396 const httpHeaderNameErrMsg string = "a valid HTTP header must consist of alphanumeric characters or '-'" 397 398 var httpHeaderNameRegexp = regexp.MustCompile("^" + httpHeaderNameFmt + "$") 399 400 // IsHTTPHeaderName checks that a string conforms to the Go HTTP library's 401 // definition of a valid header field name (a stricter subset than RFC7230). 402 func IsHTTPHeaderName(value string) []string { 403 if !httpHeaderNameRegexp.MatchString(value) { 404 return []string{RegexError(httpHeaderNameErrMsg, httpHeaderNameFmt, "X-Header-Name")} 405 } 406 return nil 407 } 408 409 const envVarNameFmt = "[-._a-zA-Z][-._a-zA-Z0-9]*" 410 const envVarNameFmtErrMsg string = "a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit" 411 412 var envVarNameRegexp = regexp.MustCompile("^" + envVarNameFmt + "$") 413 414 // IsEnvVarName tests if a string is a valid environment variable name. 415 func IsEnvVarName(value string) []string { 416 var errs []string 417 if !envVarNameRegexp.MatchString(value) { 418 errs = append(errs, RegexError(envVarNameFmtErrMsg, envVarNameFmt, "my.env-name", "MY_ENV.NAME", "MyEnvName1")) 419 } 420 421 errs = append(errs, hasChDirPrefix(value)...) 422 return errs 423 } 424 425 const configMapKeyFmt = `[-._a-zA-Z0-9]+` 426 const configMapKeyErrMsg string = "a valid config key must consist of alphanumeric characters, '-', '_' or '.'" 427 428 var configMapKeyRegexp = regexp.MustCompile("^" + configMapKeyFmt + "$") 429 430 // IsConfigMapKey tests for a string that is a valid key for a ConfigMap or Secret 431 func IsConfigMapKey(value string) []string { 432 var errs []string 433 if len(value) > DNS1123SubdomainMaxLength { 434 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength)) 435 } 436 if !configMapKeyRegexp.MatchString(value) { 437 errs = append(errs, RegexError(configMapKeyErrMsg, configMapKeyFmt, "key.name", "KEY_NAME", "key-name")) 438 } 439 errs = append(errs, hasChDirPrefix(value)...) 440 return errs 441 } 442 443 // MaxLenError returns a string explanation of a "string too long" validation 444 // failure. 445 func MaxLenError(length int) string { 446 return fmt.Sprintf("must be no more than %d characters", length) 447 } 448 449 // RegexError returns a string explanation of a regex validation failure. 450 func RegexError(msg string, fmt string, examples ...string) string { 451 if len(examples) == 0 { 452 return msg + " (regex used for validation is '" + fmt + "')" 453 } 454 msg += " (e.g. " 455 for i := range examples { 456 if i > 0 { 457 msg += " or " 458 } 459 msg += "'" + examples[i] + "', " 460 } 461 msg += "regex used for validation is '" + fmt + "')" 462 return msg 463 } 464 465 // EmptyError returns a string explanation of a "must not be empty" validation 466 // failure. 467 func EmptyError() string { 468 return "must be non-empty" 469 } 470 471 func prefixEach(msgs []string, prefix string) []string { 472 for i := range msgs { 473 msgs[i] = prefix + msgs[i] 474 } 475 return msgs 476 } 477 478 // InclusiveRangeError returns a string explanation of a numeric "must be 479 // between" validation failure. 480 func InclusiveRangeError(lo, hi int) string { 481 return fmt.Sprintf(`must be between %d and %d, inclusive`, lo, hi) 482 } 483 484 func hasChDirPrefix(value string) []string { 485 var errs []string 486 switch { 487 case value == ".": 488 errs = append(errs, `must not be '.'`) 489 case value == "..": 490 errs = append(errs, `must not be '..'`) 491 case strings.HasPrefix(value, ".."): 492 errs = append(errs, `must not start with '..'`) 493 } 494 return errs 495 } 496 497 // IsValidSocketAddr checks that string represents a valid socket address 498 // as defined in RFC 789. (e.g 0.0.0.0:10254 or [::]:10254)) 499 func IsValidSocketAddr(value string) []string { 500 var errs []string 501 ip, port, err := net.SplitHostPort(value) 502 if err != nil { 503 errs = append(errs, "must be a valid socket address format, (e.g. 0.0.0.0:10254 or [::]:10254)") 504 return errs 505 } 506 portInt, _ := strconv.Atoi(port) 507 errs = append(errs, IsValidPortNum(portInt)...) 508 errs = append(errs, IsValidIP(ip)...) 509 return errs 510 }