blob: 2dd99992dcad653c3b88489e66b53c8ee8f4154a [file] [log] [blame]
Scott Bakere7144bc2019-10-01 14:16:47 -07001/*
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.
74func IsFullyQualifiedName(fldPath *field.Path, name string) field.ErrorList {
75 var allErrors field.ErrorList
76 if len(name) == 0 {
77 return append(allErrors, field.Required(fldPath, ""))
78 }
79 if errs := IsDNS1123Subdomain(name); len(errs) > 0 {
80 return append(allErrors, field.Invalid(fldPath, name, strings.Join(errs, ",")))
81 }
82 if len(strings.Split(name, ".")) < 3 {
83 return append(allErrors, field.Invalid(fldPath, name, "should be a domain with at least three segments separated by dots"))
84 }
85 return allErrors
86}
87
88const labelValueFmt string = "(" + qualifiedNameFmt + ")?"
89const 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"
90
91// LabelValueMaxLength is a label's max length
92const LabelValueMaxLength int = 63
93
94var labelValueRegexp = regexp.MustCompile("^" + labelValueFmt + "$")
95
96// IsValidLabelValue tests whether the value passed is a valid label value. If
97// the value is not valid, a list of error strings is returned. Otherwise an
98// empty list (or nil) is returned.
99func IsValidLabelValue(value string) []string {
100 var errs []string
101 if len(value) > LabelValueMaxLength {
102 errs = append(errs, MaxLenError(LabelValueMaxLength))
103 }
104 if !labelValueRegexp.MatchString(value) {
105 errs = append(errs, RegexError(labelValueErrMsg, labelValueFmt, "MyValue", "my_value", "12345"))
106 }
107 return errs
108}
109
110const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
111const dns1123LabelErrMsg string = "a DNS-1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character"
112
113// DNS1123LabelMaxLength is a label's max length in DNS (RFC 1123)
114const DNS1123LabelMaxLength int = 63
115
116var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$")
117
118// IsDNS1123Label tests for a string that conforms to the definition of a label in
119// DNS (RFC 1123).
120func IsDNS1123Label(value string) []string {
121 var errs []string
122 if len(value) > DNS1123LabelMaxLength {
123 errs = append(errs, MaxLenError(DNS1123LabelMaxLength))
124 }
125 if !dns1123LabelRegexp.MatchString(value) {
126 errs = append(errs, RegexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc"))
127 }
128 return errs
129}
130
131const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*"
132const dns1123SubdomainErrorMsg string = "a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character"
133
134// DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123)
135const DNS1123SubdomainMaxLength int = 253
136
137var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$")
138
139// IsDNS1123Subdomain tests for a string that conforms to the definition of a
140// subdomain in DNS (RFC 1123).
141func IsDNS1123Subdomain(value string) []string {
142 var errs []string
143 if len(value) > DNS1123SubdomainMaxLength {
144 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
145 }
146 if !dns1123SubdomainRegexp.MatchString(value) {
147 errs = append(errs, RegexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com"))
148 }
149 return errs
150}
151
152const dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?"
153const 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"
154
155// DNS1035LabelMaxLength is a label's max length in DNS (RFC 1035)
156const DNS1035LabelMaxLength int = 63
157
158var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFmt + "$")
159
160// IsDNS1035Label tests for a string that conforms to the definition of a label in
161// DNS (RFC 1035).
162func IsDNS1035Label(value string) []string {
163 var errs []string
164 if len(value) > DNS1035LabelMaxLength {
165 errs = append(errs, MaxLenError(DNS1035LabelMaxLength))
166 }
167 if !dns1035LabelRegexp.MatchString(value) {
168 errs = append(errs, RegexError(dns1035LabelErrMsg, dns1035LabelFmt, "my-name", "abc-123"))
169 }
170 return errs
171}
172
173// wildcard definition - RFC 1034 section 4.3.3.
174// examples:
175// - valid: *.bar.com, *.foo.bar.com
176// - invalid: *.*.bar.com, *.foo.*.com, *bar.com, f*.bar.com, *
177const wildcardDNS1123SubdomainFmt = "\\*\\." + dns1123SubdomainFmt
178const 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"
179
180// IsWildcardDNS1123Subdomain tests for a string that conforms to the definition of a
181// wildcard subdomain in DNS (RFC 1034 section 4.3.3).
182func IsWildcardDNS1123Subdomain(value string) []string {
183 wildcardDNS1123SubdomainRegexp := regexp.MustCompile("^" + wildcardDNS1123SubdomainFmt + "$")
184
185 var errs []string
186 if len(value) > DNS1123SubdomainMaxLength {
187 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
188 }
189 if !wildcardDNS1123SubdomainRegexp.MatchString(value) {
190 errs = append(errs, RegexError(wildcardDNS1123SubdomainErrMsg, wildcardDNS1123SubdomainFmt, "*.example.com"))
191 }
192 return errs
193}
194
195const cIdentifierFmt string = "[A-Za-z_][A-Za-z0-9_]*"
196const identifierErrMsg string = "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_'"
197
198var cIdentifierRegexp = regexp.MustCompile("^" + cIdentifierFmt + "$")
199
200// IsCIdentifier tests for a string that conforms the definition of an identifier
201// in C. This checks the format, but not the length.
202func IsCIdentifier(value string) []string {
203 if !cIdentifierRegexp.MatchString(value) {
204 return []string{RegexError(identifierErrMsg, cIdentifierFmt, "my_name", "MY_NAME", "MyName")}
205 }
206 return nil
207}
208
209// IsValidPortNum tests that the argument is a valid, non-zero port number.
210func IsValidPortNum(port int) []string {
211 if 1 <= port && port <= 65535 {
212 return nil
213 }
214 return []string{InclusiveRangeError(1, 65535)}
215}
216
217// IsInRange tests that the argument is in an inclusive range.
218func IsInRange(value int, min int, max int) []string {
219 if value >= min && value <= max {
220 return nil
221 }
222 return []string{InclusiveRangeError(min, max)}
223}
224
225// Now in libcontainer UID/GID limits is 0 ~ 1<<31 - 1
226// TODO: once we have a type for UID/GID we should make these that type.
227const (
228 minUserID = 0
229 maxUserID = math.MaxInt32
230 minGroupID = 0
231 maxGroupID = math.MaxInt32
232)
233
234// IsValidGroupID tests that the argument is a valid Unix GID.
235func IsValidGroupID(gid int64) []string {
236 if minGroupID <= gid && gid <= maxGroupID {
237 return nil
238 }
239 return []string{InclusiveRangeError(minGroupID, maxGroupID)}
240}
241
242// IsValidUserID tests that the argument is a valid Unix UID.
243func IsValidUserID(uid int64) []string {
244 if minUserID <= uid && uid <= maxUserID {
245 return nil
246 }
247 return []string{InclusiveRangeError(minUserID, maxUserID)}
248}
249
250var portNameCharsetRegex = regexp.MustCompile("^[-a-z0-9]+$")
251var portNameOneLetterRegexp = regexp.MustCompile("[a-z]")
252
253// IsValidPortName check that the argument is valid syntax. It must be
254// non-empty and no more than 15 characters long. It may contain only [-a-z0-9]
255// and must contain at least one letter [a-z]. It must not start or end with a
256// hyphen, nor contain adjacent hyphens.
257//
258// Note: We only allow lower-case characters, even though RFC 6335 is case
259// insensitive.
260func IsValidPortName(port string) []string {
261 var errs []string
262 if len(port) > 15 {
263 errs = append(errs, MaxLenError(15))
264 }
265 if !portNameCharsetRegex.MatchString(port) {
266 errs = append(errs, "must contain only alpha-numeric characters (a-z, 0-9), and hyphens (-)")
267 }
268 if !portNameOneLetterRegexp.MatchString(port) {
269 errs = append(errs, "must contain at least one letter or number (a-z, 0-9)")
270 }
271 if strings.Contains(port, "--") {
272 errs = append(errs, "must not contain consecutive hyphens")
273 }
274 if len(port) > 0 && (port[0] == '-' || port[len(port)-1] == '-') {
275 errs = append(errs, "must not begin or end with a hyphen")
276 }
277 return errs
278}
279
280// IsValidIP tests that the argument is a valid IP address.
281func IsValidIP(value string) []string {
282 if net.ParseIP(value) == nil {
283 return []string{"must be a valid IP address, (e.g. 10.9.8.7)"}
284 }
285 return nil
286}
287
288const percentFmt string = "[0-9]+%"
289const percentErrMsg string = "a valid percent string must be a numeric string followed by an ending '%'"
290
291var percentRegexp = regexp.MustCompile("^" + percentFmt + "$")
292
293// IsValidPercent checks that string is in the form of a percentage
294func IsValidPercent(percent string) []string {
295 if !percentRegexp.MatchString(percent) {
296 return []string{RegexError(percentErrMsg, percentFmt, "1%", "93%")}
297 }
298 return nil
299}
300
301const httpHeaderNameFmt string = "[-A-Za-z0-9]+"
302const httpHeaderNameErrMsg string = "a valid HTTP header must consist of alphanumeric characters or '-'"
303
304var httpHeaderNameRegexp = regexp.MustCompile("^" + httpHeaderNameFmt + "$")
305
306// IsHTTPHeaderName checks that a string conforms to the Go HTTP library's
307// definition of a valid header field name (a stricter subset than RFC7230).
308func IsHTTPHeaderName(value string) []string {
309 if !httpHeaderNameRegexp.MatchString(value) {
310 return []string{RegexError(httpHeaderNameErrMsg, httpHeaderNameFmt, "X-Header-Name")}
311 }
312 return nil
313}
314
315const envVarNameFmt = "[-._a-zA-Z][-._a-zA-Z0-9]*"
316const envVarNameFmtErrMsg string = "a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit"
317
318var envVarNameRegexp = regexp.MustCompile("^" + envVarNameFmt + "$")
319
320// IsEnvVarName tests if a string is a valid environment variable name.
321func IsEnvVarName(value string) []string {
322 var errs []string
323 if !envVarNameRegexp.MatchString(value) {
324 errs = append(errs, RegexError(envVarNameFmtErrMsg, envVarNameFmt, "my.env-name", "MY_ENV.NAME", "MyEnvName1"))
325 }
326
327 errs = append(errs, hasChDirPrefix(value)...)
328 return errs
329}
330
331const configMapKeyFmt = `[-._a-zA-Z0-9]+`
332const configMapKeyErrMsg string = "a valid config key must consist of alphanumeric characters, '-', '_' or '.'"
333
334var configMapKeyRegexp = regexp.MustCompile("^" + configMapKeyFmt + "$")
335
336// IsConfigMapKey tests for a string that is a valid key for a ConfigMap or Secret
337func IsConfigMapKey(value string) []string {
338 var errs []string
339 if len(value) > DNS1123SubdomainMaxLength {
340 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
341 }
342 if !configMapKeyRegexp.MatchString(value) {
343 errs = append(errs, RegexError(configMapKeyErrMsg, configMapKeyFmt, "key.name", "KEY_NAME", "key-name"))
344 }
345 errs = append(errs, hasChDirPrefix(value)...)
346 return errs
347}
348
349// MaxLenError returns a string explanation of a "string too long" validation
350// failure.
351func MaxLenError(length int) string {
352 return fmt.Sprintf("must be no more than %d characters", length)
353}
354
355// RegexError returns a string explanation of a regex validation failure.
356func RegexError(msg string, fmt string, examples ...string) string {
357 if len(examples) == 0 {
358 return msg + " (regex used for validation is '" + fmt + "')"
359 }
360 msg += " (e.g. "
361 for i := range examples {
362 if i > 0 {
363 msg += " or "
364 }
365 msg += "'" + examples[i] + "', "
366 }
367 msg += "regex used for validation is '" + fmt + "')"
368 return msg
369}
370
371// EmptyError returns a string explanation of a "must not be empty" validation
372// failure.
373func EmptyError() string {
374 return "must be non-empty"
375}
376
377func prefixEach(msgs []string, prefix string) []string {
378 for i := range msgs {
379 msgs[i] = prefix + msgs[i]
380 }
381 return msgs
382}
383
384// InclusiveRangeError returns a string explanation of a numeric "must be
385// between" validation failure.
386func InclusiveRangeError(lo, hi int) string {
387 return fmt.Sprintf(`must be between %d and %d, inclusive`, lo, hi)
388}
389
390func hasChDirPrefix(value string) []string {
391 var errs []string
392 switch {
393 case value == ".":
394 errs = append(errs, `must not be '.'`)
395 case value == "..":
396 errs = append(errs, `must not be '..'`)
397 case strings.HasPrefix(value, ".."):
398 errs = append(errs, `must not start with '..'`)
399 }
400 return errs
401}
402
403// IsValidSocketAddr checks that string represents a valid socket address
404// as defined in RFC 789. (e.g 0.0.0.0:10254 or [::]:10254))
405func IsValidSocketAddr(value string) []string {
406 var errs []string
407 ip, port, err := net.SplitHostPort(value)
408 if err != nil {
409 errs = append(errs, "must be a valid socket address format, (e.g. 0.0.0.0:10254 or [::]:10254)")
410 return errs
411 }
412 portInt, _ := strconv.Atoi(port)
413 errs = append(errs, IsValidPortNum(portInt)...)
414 errs = append(errs, IsValidIP(ip)...)
415 return errs
416}