github.com/spotmaxtech/k8s-apimachinery-v0260@v0.0.1/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 "github.com/spotmaxtech/k8s-apimachinery-v0260/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 errs = append(errs, RegexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc")) 195 } 196 return errs 197 } 198 199 const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*" 200 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" 201 202 // DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123) 203 const DNS1123SubdomainMaxLength int = 253 204 205 var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$") 206 207 // IsDNS1123Subdomain tests for a string that conforms to the definition of a 208 // subdomain in DNS (RFC 1123). 209 func IsDNS1123Subdomain(value string) []string { 210 var errs []string 211 if len(value) > DNS1123SubdomainMaxLength { 212 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength)) 213 } 214 if !dns1123SubdomainRegexp.MatchString(value) { 215 errs = append(errs, RegexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com")) 216 } 217 return errs 218 } 219 220 const dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?" 221 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" 222 223 // DNS1035LabelMaxLength is a label's max length in DNS (RFC 1035) 224 const DNS1035LabelMaxLength int = 63 225 226 var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFmt + "$") 227 228 // IsDNS1035Label tests for a string that conforms to the definition of a label in 229 // DNS (RFC 1035). 230 func IsDNS1035Label(value string) []string { 231 var errs []string 232 if len(value) > DNS1035LabelMaxLength { 233 errs = append(errs, MaxLenError(DNS1035LabelMaxLength)) 234 } 235 if !dns1035LabelRegexp.MatchString(value) { 236 errs = append(errs, RegexError(dns1035LabelErrMsg, dns1035LabelFmt, "my-name", "abc-123")) 237 } 238 return errs 239 } 240 241 // wildcard definition - RFC 1034 section 4.3.3. 242 // examples: 243 // - valid: *.bar.com, *.foo.bar.com 244 // - invalid: *.*.bar.com, *.foo.*.com, *bar.com, f*.bar.com, * 245 const wildcardDNS1123SubdomainFmt = "\\*\\." + dns1123SubdomainFmt 246 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" 247 248 // IsWildcardDNS1123Subdomain tests for a string that conforms to the definition of a 249 // wildcard subdomain in DNS (RFC 1034 section 4.3.3). 250 func IsWildcardDNS1123Subdomain(value string) []string { 251 wildcardDNS1123SubdomainRegexp := regexp.MustCompile("^" + wildcardDNS1123SubdomainFmt + "$") 252 253 var errs []string 254 if len(value) > DNS1123SubdomainMaxLength { 255 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength)) 256 } 257 if !wildcardDNS1123SubdomainRegexp.MatchString(value) { 258 errs = append(errs, RegexError(wildcardDNS1123SubdomainErrMsg, wildcardDNS1123SubdomainFmt, "*.example.com")) 259 } 260 return errs 261 } 262 263 const cIdentifierFmt string = "[A-Za-z_][A-Za-z0-9_]*" 264 const identifierErrMsg string = "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_'" 265 266 var cIdentifierRegexp = regexp.MustCompile("^" + cIdentifierFmt + "$") 267 268 // IsCIdentifier tests for a string that conforms the definition of an identifier 269 // in C. This checks the format, but not the length. 270 func IsCIdentifier(value string) []string { 271 if !cIdentifierRegexp.MatchString(value) { 272 return []string{RegexError(identifierErrMsg, cIdentifierFmt, "my_name", "MY_NAME", "MyName")} 273 } 274 return nil 275 } 276 277 // IsValidPortNum tests that the argument is a valid, non-zero port number. 278 func IsValidPortNum(port int) []string { 279 if 1 <= port && port <= 65535 { 280 return nil 281 } 282 return []string{InclusiveRangeError(1, 65535)} 283 } 284 285 // IsInRange tests that the argument is in an inclusive range. 286 func IsInRange(value int, min int, max int) []string { 287 if value >= min && value <= max { 288 return nil 289 } 290 return []string{InclusiveRangeError(min, max)} 291 } 292 293 // Now in libcontainer UID/GID limits is 0 ~ 1<<31 - 1 294 // TODO: once we have a type for UID/GID we should make these that type. 295 const ( 296 minUserID = 0 297 maxUserID = math.MaxInt32 298 minGroupID = 0 299 maxGroupID = math.MaxInt32 300 ) 301 302 // IsValidGroupID tests that the argument is a valid Unix GID. 303 func IsValidGroupID(gid int64) []string { 304 if minGroupID <= gid && gid <= maxGroupID { 305 return nil 306 } 307 return []string{InclusiveRangeError(minGroupID, maxGroupID)} 308 } 309 310 // IsValidUserID tests that the argument is a valid Unix UID. 311 func IsValidUserID(uid int64) []string { 312 if minUserID <= uid && uid <= maxUserID { 313 return nil 314 } 315 return []string{InclusiveRangeError(minUserID, maxUserID)} 316 } 317 318 var portNameCharsetRegex = regexp.MustCompile("^[-a-z0-9]+$") 319 var portNameOneLetterRegexp = regexp.MustCompile("[a-z]") 320 321 // IsValidPortName check that the argument is valid syntax. It must be 322 // non-empty and no more than 15 characters long. It may contain only [-a-z0-9] 323 // and must contain at least one letter [a-z]. It must not start or end with a 324 // hyphen, nor contain adjacent hyphens. 325 // 326 // Note: We only allow lower-case characters, even though RFC 6335 is case 327 // insensitive. 328 func IsValidPortName(port string) []string { 329 var errs []string 330 if len(port) > 15 { 331 errs = append(errs, MaxLenError(15)) 332 } 333 if !portNameCharsetRegex.MatchString(port) { 334 errs = append(errs, "must contain only alpha-numeric characters (a-z, 0-9), and hyphens (-)") 335 } 336 if !portNameOneLetterRegexp.MatchString(port) { 337 errs = append(errs, "must contain at least one letter (a-z)") 338 } 339 if strings.Contains(port, "--") { 340 errs = append(errs, "must not contain consecutive hyphens") 341 } 342 if len(port) > 0 && (port[0] == '-' || port[len(port)-1] == '-') { 343 errs = append(errs, "must not begin or end with a hyphen") 344 } 345 return errs 346 } 347 348 // IsValidIP tests that the argument is a valid IP address. 349 func IsValidIP(value string) []string { 350 if netutils.ParseIPSloppy(value) == nil { 351 return []string{"must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)"} 352 } 353 return nil 354 } 355 356 // IsValidIPv4Address tests that the argument is a valid IPv4 address. 357 func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList { 358 var allErrors field.ErrorList 359 ip := netutils.ParseIPSloppy(value) 360 if ip == nil || ip.To4() == nil { 361 allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address")) 362 } 363 return allErrors 364 } 365 366 // IsValidIPv6Address tests that the argument is a valid IPv6 address. 367 func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList { 368 var allErrors field.ErrorList 369 ip := netutils.ParseIPSloppy(value) 370 if ip == nil || ip.To4() != nil { 371 allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address")) 372 } 373 return allErrors 374 } 375 376 const percentFmt string = "[0-9]+%" 377 const percentErrMsg string = "a valid percent string must be a numeric string followed by an ending '%'" 378 379 var percentRegexp = regexp.MustCompile("^" + percentFmt + "$") 380 381 // IsValidPercent checks that string is in the form of a percentage 382 func IsValidPercent(percent string) []string { 383 if !percentRegexp.MatchString(percent) { 384 return []string{RegexError(percentErrMsg, percentFmt, "1%", "93%")} 385 } 386 return nil 387 } 388 389 const httpHeaderNameFmt string = "[-A-Za-z0-9]+" 390 const httpHeaderNameErrMsg string = "a valid HTTP header must consist of alphanumeric characters or '-'" 391 392 var httpHeaderNameRegexp = regexp.MustCompile("^" + httpHeaderNameFmt + "$") 393 394 // IsHTTPHeaderName checks that a string conforms to the Go HTTP library's 395 // definition of a valid header field name (a stricter subset than RFC7230). 396 func IsHTTPHeaderName(value string) []string { 397 if !httpHeaderNameRegexp.MatchString(value) { 398 return []string{RegexError(httpHeaderNameErrMsg, httpHeaderNameFmt, "X-Header-Name")} 399 } 400 return nil 401 } 402 403 const envVarNameFmt = "[-._a-zA-Z][-._a-zA-Z0-9]*" 404 const envVarNameFmtErrMsg string = "a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit" 405 406 var envVarNameRegexp = regexp.MustCompile("^" + envVarNameFmt + "$") 407 408 // IsEnvVarName tests if a string is a valid environment variable name. 409 func IsEnvVarName(value string) []string { 410 var errs []string 411 if !envVarNameRegexp.MatchString(value) { 412 errs = append(errs, RegexError(envVarNameFmtErrMsg, envVarNameFmt, "my.env-name", "MY_ENV.NAME", "MyEnvName1")) 413 } 414 415 errs = append(errs, hasChDirPrefix(value)...) 416 return errs 417 } 418 419 const configMapKeyFmt = `[-._a-zA-Z0-9]+` 420 const configMapKeyErrMsg string = "a valid config key must consist of alphanumeric characters, '-', '_' or '.'" 421 422 var configMapKeyRegexp = regexp.MustCompile("^" + configMapKeyFmt + "$") 423 424 // IsConfigMapKey tests for a string that is a valid key for a ConfigMap or Secret 425 func IsConfigMapKey(value string) []string { 426 var errs []string 427 if len(value) > DNS1123SubdomainMaxLength { 428 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength)) 429 } 430 if !configMapKeyRegexp.MatchString(value) { 431 errs = append(errs, RegexError(configMapKeyErrMsg, configMapKeyFmt, "key.name", "KEY_NAME", "key-name")) 432 } 433 errs = append(errs, hasChDirPrefix(value)...) 434 return errs 435 } 436 437 // MaxLenError returns a string explanation of a "string too long" validation 438 // failure. 439 func MaxLenError(length int) string { 440 return fmt.Sprintf("must be no more than %d characters", length) 441 } 442 443 // RegexError returns a string explanation of a regex validation failure. 444 func RegexError(msg string, fmt string, examples ...string) string { 445 if len(examples) == 0 { 446 return msg + " (regex used for validation is '" + fmt + "')" 447 } 448 msg += " (e.g. " 449 for i := range examples { 450 if i > 0 { 451 msg += " or " 452 } 453 msg += "'" + examples[i] + "', " 454 } 455 msg += "regex used for validation is '" + fmt + "')" 456 return msg 457 } 458 459 // EmptyError returns a string explanation of a "must not be empty" validation 460 // failure. 461 func EmptyError() string { 462 return "must be non-empty" 463 } 464 465 func prefixEach(msgs []string, prefix string) []string { 466 for i := range msgs { 467 msgs[i] = prefix + msgs[i] 468 } 469 return msgs 470 } 471 472 // InclusiveRangeError returns a string explanation of a numeric "must be 473 // between" validation failure. 474 func InclusiveRangeError(lo, hi int) string { 475 return fmt.Sprintf(`must be between %d and %d, inclusive`, lo, hi) 476 } 477 478 func hasChDirPrefix(value string) []string { 479 var errs []string 480 switch { 481 case value == ".": 482 errs = append(errs, `must not be '.'`) 483 case value == "..": 484 errs = append(errs, `must not be '..'`) 485 case strings.HasPrefix(value, ".."): 486 errs = append(errs, `must not start with '..'`) 487 } 488 return errs 489 } 490 491 // IsValidSocketAddr checks that string represents a valid socket address 492 // as defined in RFC 789. (e.g 0.0.0.0:10254 or [::]:10254)) 493 func IsValidSocketAddr(value string) []string { 494 var errs []string 495 ip, port, err := net.SplitHostPort(value) 496 if err != nil { 497 errs = append(errs, "must be a valid socket address format, (e.g. 0.0.0.0:10254 or [::]:10254)") 498 return errs 499 } 500 portInt, _ := strconv.Atoi(port) 501 errs = append(errs, IsValidPortNum(portInt)...) 502 errs = append(errs, IsValidIP(ip)...) 503 return errs 504 }