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