1
16
17 package validation
18
19 import (
20 "fmt"
21 "math"
22 "regexp"
23 "strings"
24 "unicode"
25
26 "k8s.io/apimachinery/pkg/util/validation/field"
27 netutils "k8s.io/utils/net"
28 )
29
30 const qnameCharFmt string = "[A-Za-z0-9]"
31 const qnameExtCharFmt string = "[-A-Za-z0-9_.]"
32 const qualifiedNameFmt string = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt
33 const qualifiedNameErrMsg string = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character"
34 const qualifiedNameMaxLength int = 63
35
36 var qualifiedNameRegexp = regexp.MustCompile("^" + qualifiedNameFmt + "$")
37
38
39
40
41
42 func 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
74
75
76
77
78 func 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
93
94
95 func 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
118
119
120
121
122
123 const httpPathFmt string = `[A-Za-z0-9/\-._~%!$&'()*+,;=:]+`
124
125 var httpPathRegexp = regexp.MustCompile("^" + httpPathFmt + "$")
126
127
128
129
130
131 func 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
155 const labelValueFmt string = "(" + qualifiedNameFmt + ")?"
156 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"
157
158
159 const LabelValueMaxLength int = 63
160
161 var labelValueRegexp = regexp.MustCompile("^" + labelValueFmt + "$")
162
163
164
165
166 func 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
177 const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
178 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"
179
180
181 const DNS1123LabelMaxLength int = 63
182
183 var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$")
184
185
186
187 func 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 if dns1123SubdomainRegexp.MatchString(value) {
194
195
196 errs = append(errs, "must not contain dots")
197 } else {
198 errs = append(errs, RegexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc"))
199 }
200 }
201 return errs
202 }
203
204 const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*"
205 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"
206
207
208 const DNS1123SubdomainMaxLength int = 253
209
210 var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$")
211
212
213
214 func IsDNS1123Subdomain(value string) []string {
215 var errs []string
216 if len(value) > DNS1123SubdomainMaxLength {
217 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
218 }
219 if !dns1123SubdomainRegexp.MatchString(value) {
220 errs = append(errs, RegexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com"))
221 }
222 return errs
223 }
224
225 const dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?"
226 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"
227
228
229 const DNS1035LabelMaxLength int = 63
230
231 var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFmt + "$")
232
233
234
235 func IsDNS1035Label(value string) []string {
236 var errs []string
237 if len(value) > DNS1035LabelMaxLength {
238 errs = append(errs, MaxLenError(DNS1035LabelMaxLength))
239 }
240 if !dns1035LabelRegexp.MatchString(value) {
241 errs = append(errs, RegexError(dns1035LabelErrMsg, dns1035LabelFmt, "my-name", "abc-123"))
242 }
243 return errs
244 }
245
246
247
248
249
250 const wildcardDNS1123SubdomainFmt = "\\*\\." + dns1123SubdomainFmt
251 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"
252
253
254
255 func IsWildcardDNS1123Subdomain(value string) []string {
256 wildcardDNS1123SubdomainRegexp := regexp.MustCompile("^" + wildcardDNS1123SubdomainFmt + "$")
257
258 var errs []string
259 if len(value) > DNS1123SubdomainMaxLength {
260 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
261 }
262 if !wildcardDNS1123SubdomainRegexp.MatchString(value) {
263 errs = append(errs, RegexError(wildcardDNS1123SubdomainErrMsg, wildcardDNS1123SubdomainFmt, "*.example.com"))
264 }
265 return errs
266 }
267
268 const cIdentifierFmt string = "[A-Za-z_][A-Za-z0-9_]*"
269 const identifierErrMsg string = "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_'"
270
271 var cIdentifierRegexp = regexp.MustCompile("^" + cIdentifierFmt + "$")
272
273
274
275 func IsCIdentifier(value string) []string {
276 if !cIdentifierRegexp.MatchString(value) {
277 return []string{RegexError(identifierErrMsg, cIdentifierFmt, "my_name", "MY_NAME", "MyName")}
278 }
279 return nil
280 }
281
282
283 func IsValidPortNum(port int) []string {
284 if 1 <= port && port <= 65535 {
285 return nil
286 }
287 return []string{InclusiveRangeError(1, 65535)}
288 }
289
290
291 func IsInRange(value int, min int, max int) []string {
292 if value >= min && value <= max {
293 return nil
294 }
295 return []string{InclusiveRangeError(min, max)}
296 }
297
298
299
300 const (
301 minUserID = 0
302 maxUserID = math.MaxInt32
303 minGroupID = 0
304 maxGroupID = math.MaxInt32
305 )
306
307
308 func IsValidGroupID(gid int64) []string {
309 if minGroupID <= gid && gid <= maxGroupID {
310 return nil
311 }
312 return []string{InclusiveRangeError(minGroupID, maxGroupID)}
313 }
314
315
316 func IsValidUserID(uid int64) []string {
317 if minUserID <= uid && uid <= maxUserID {
318 return nil
319 }
320 return []string{InclusiveRangeError(minUserID, maxUserID)}
321 }
322
323 var portNameCharsetRegex = regexp.MustCompile("^[-a-z0-9]+$")
324 var portNameOneLetterRegexp = regexp.MustCompile("[a-z]")
325
326
327
328
329
330
331
332
333 func IsValidPortName(port string) []string {
334 var errs []string
335 if len(port) > 15 {
336 errs = append(errs, MaxLenError(15))
337 }
338 if !portNameCharsetRegex.MatchString(port) {
339 errs = append(errs, "must contain only alpha-numeric characters (a-z, 0-9), and hyphens (-)")
340 }
341 if !portNameOneLetterRegexp.MatchString(port) {
342 errs = append(errs, "must contain at least one letter (a-z)")
343 }
344 if strings.Contains(port, "--") {
345 errs = append(errs, "must not contain consecutive hyphens")
346 }
347 if len(port) > 0 && (port[0] == '-' || port[len(port)-1] == '-') {
348 errs = append(errs, "must not begin or end with a hyphen")
349 }
350 return errs
351 }
352
353
354 func IsValidIP(fldPath *field.Path, value string) field.ErrorList {
355 var allErrors field.ErrorList
356 if netutils.ParseIPSloppy(value) == nil {
357 allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)"))
358 }
359 return allErrors
360 }
361
362
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
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
383 func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList {
384 var allErrors field.ErrorList
385 _, _, err := netutils.ParseCIDRSloppy(value)
386 if err != nil {
387 allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"))
388 }
389 return allErrors
390 }
391
392 const percentFmt string = "[0-9]+%"
393 const percentErrMsg string = "a valid percent string must be a numeric string followed by an ending '%'"
394
395 var percentRegexp = regexp.MustCompile("^" + percentFmt + "$")
396
397
398 func IsValidPercent(percent string) []string {
399 if !percentRegexp.MatchString(percent) {
400 return []string{RegexError(percentErrMsg, percentFmt, "1%", "93%")}
401 }
402 return nil
403 }
404
405 const httpHeaderNameFmt string = "[-A-Za-z0-9]+"
406 const httpHeaderNameErrMsg string = "a valid HTTP header must consist of alphanumeric characters or '-'"
407
408 var httpHeaderNameRegexp = regexp.MustCompile("^" + httpHeaderNameFmt + "$")
409
410
411
412 func IsHTTPHeaderName(value string) []string {
413 if !httpHeaderNameRegexp.MatchString(value) {
414 return []string{RegexError(httpHeaderNameErrMsg, httpHeaderNameFmt, "X-Header-Name")}
415 }
416 return nil
417 }
418
419 const envVarNameFmt = "[-._a-zA-Z][-._a-zA-Z0-9]*"
420 const envVarNameFmtErrMsg string = "a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit"
421
422
423 const relaxedEnvVarNameFmtErrMsg string = "a valid environment variable name must consist only of printable ASCII characters other than '='"
424
425 var envVarNameRegexp = regexp.MustCompile("^" + envVarNameFmt + "$")
426
427
428 func IsEnvVarName(value string) []string {
429 var errs []string
430 if !envVarNameRegexp.MatchString(value) {
431 errs = append(errs, RegexError(envVarNameFmtErrMsg, envVarNameFmt, "my.env-name", "MY_ENV.NAME", "MyEnvName1"))
432 }
433
434 errs = append(errs, hasChDirPrefix(value)...)
435 return errs
436 }
437
438
439 func IsRelaxedEnvVarName(value string) []string {
440 var errs []string
441
442 if len(value) == 0 {
443 errs = append(errs, "environment variable name "+EmptyError())
444 }
445
446 for _, r := range value {
447 if r > unicode.MaxASCII || !unicode.IsPrint(r) || r == '=' {
448 errs = append(errs, relaxedEnvVarNameFmtErrMsg)
449 break
450 }
451 }
452
453 return errs
454 }
455
456 const configMapKeyFmt = `[-._a-zA-Z0-9]+`
457 const configMapKeyErrMsg string = "a valid config key must consist of alphanumeric characters, '-', '_' or '.'"
458
459 var configMapKeyRegexp = regexp.MustCompile("^" + configMapKeyFmt + "$")
460
461
462 func IsConfigMapKey(value string) []string {
463 var errs []string
464 if len(value) > DNS1123SubdomainMaxLength {
465 errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
466 }
467 if !configMapKeyRegexp.MatchString(value) {
468 errs = append(errs, RegexError(configMapKeyErrMsg, configMapKeyFmt, "key.name", "KEY_NAME", "key-name"))
469 }
470 errs = append(errs, hasChDirPrefix(value)...)
471 return errs
472 }
473
474
475
476 func MaxLenError(length int) string {
477 return fmt.Sprintf("must be no more than %d characters", length)
478 }
479
480
481 func RegexError(msg string, fmt string, examples ...string) string {
482 if len(examples) == 0 {
483 return msg + " (regex used for validation is '" + fmt + "')"
484 }
485 msg += " (e.g. "
486 for i := range examples {
487 if i > 0 {
488 msg += " or "
489 }
490 msg += "'" + examples[i] + "', "
491 }
492 msg += "regex used for validation is '" + fmt + "')"
493 return msg
494 }
495
496
497
498 func EmptyError() string {
499 return "must be non-empty"
500 }
501
502 func prefixEach(msgs []string, prefix string) []string {
503 for i := range msgs {
504 msgs[i] = prefix + msgs[i]
505 }
506 return msgs
507 }
508
509
510
511 func InclusiveRangeError(lo, hi int) string {
512 return fmt.Sprintf(`must be between %d and %d, inclusive`, lo, hi)
513 }
514
515 func hasChDirPrefix(value string) []string {
516 var errs []string
517 switch {
518 case value == ".":
519 errs = append(errs, `must not be '.'`)
520 case value == "..":
521 errs = append(errs, `must not be '..'`)
522 case strings.HasPrefix(value, ".."):
523 errs = append(errs, `must not start with '..'`)
524 }
525 return errs
526 }
527
View as plain text