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  }