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