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