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  }