Skip to content

Commit 320d82b

Browse files
committed
Add internal validation package
This change adds an internal validation package for spec annotation validation. This includes code imported from k8s.io/apimachinery/pkg/api and k8s.io/apimachinery/pkg/util to allow for basic validation of the annotations without importing additional dependencies. Signed-off-by: Evan Lezar <[email protected]>
1 parent 84d2793 commit 320d82b

File tree

3 files changed

+329
-0
lines changed

3 files changed

+329
-0
lines changed

internal/validation/k8s/objectmeta.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright 2014 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Adapted from k8s.io/apimachinery/pkg/api/validation:
18+
// https://github.com/kubernetes/apimachinery/blob/7687996c715ee7d5c8cf1e3215e607eb065a4221/pkg/api/validation/objectmeta.go
19+
20+
package k8s
21+
22+
import (
23+
"fmt"
24+
"strings"
25+
26+
"github.com/container-orchestrated-devices/container-device-interface/internal/multierror"
27+
)
28+
29+
// TotalAnnotationSizeLimitB defines the maximum size of all annotations in characters.
30+
const TotalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB
31+
32+
// ValidateAnnotations validates that a set of annotations are correctly defined.
33+
func ValidateAnnotations(annotations map[string]string, path string) error {
34+
errors := multierror.New()
35+
for k := range annotations {
36+
// The rule is QualifiedName except that case doesn't matter, so convert to lowercase before checking.
37+
for _, msg := range IsQualifiedName(strings.ToLower(k)) {
38+
errors = multierror.Append(errors, fmt.Errorf("%v.%v is invalid: %v", path, k, msg))
39+
}
40+
}
41+
if err := ValidateAnnotationsSize(annotations); err != nil {
42+
errors = multierror.Append(errors, fmt.Errorf("%v is too long: %v", path, err))
43+
}
44+
return errors
45+
}
46+
47+
// ValidateAnnotationsSize validates that a set of annotations is not too large.
48+
func ValidateAnnotationsSize(annotations map[string]string) error {
49+
var totalSize int64
50+
for k, v := range annotations {
51+
totalSize += (int64)(len(k)) + (int64)(len(v))
52+
}
53+
if totalSize > (int64)(TotalAnnotationSizeLimitB) {
54+
return fmt.Errorf("annotations size %d is larger than limit %d", totalSize, TotalAnnotationSizeLimitB)
55+
}
56+
return nil
57+
}

internal/validation/k8s/validation.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
Copyright 2014 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Adapted from k8s.io/apimachinery/pkg/util/validation:
18+
// https://github.com/kubernetes/apimachinery/blob/7687996c715ee7d5c8cf1e3215e607eb065a4221/pkg/util/validation/validation.go
19+
20+
package k8s
21+
22+
import (
23+
"fmt"
24+
"regexp"
25+
"strings"
26+
)
27+
28+
const qnameCharFmt string = "[A-Za-z0-9]"
29+
const qnameExtCharFmt string = "[-A-Za-z0-9_.]"
30+
const qualifiedNameFmt string = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt
31+
const qualifiedNameErrMsg string = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character"
32+
const qualifiedNameMaxLength int = 63
33+
34+
var qualifiedNameRegexp = regexp.MustCompile("^" + qualifiedNameFmt + "$")
35+
36+
// IsQualifiedName tests whether the value passed is what Kubernetes calls a
37+
// "qualified name". This is a format used in various places throughout the
38+
// system. If the value is not valid, a list of error strings is returned.
39+
// Otherwise an empty list (or nil) is returned.
40+
func IsQualifiedName(value string) []string {
41+
var errs []string
42+
parts := strings.Split(value, "/")
43+
var name string
44+
switch len(parts) {
45+
case 1:
46+
name = parts[0]
47+
case 2:
48+
var prefix string
49+
prefix, name = parts[0], parts[1]
50+
if len(prefix) == 0 {
51+
errs = append(errs, "prefix part "+EmptyError())
52+
} else if msgs := IsDNS1123Subdomain(prefix); len(msgs) != 0 {
53+
errs = append(errs, prefixEach(msgs, "prefix part ")...)
54+
}
55+
default:
56+
return append(errs, "a qualified name "+RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc")+
57+
" with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')")
58+
}
59+
60+
if len(name) == 0 {
61+
errs = append(errs, "name part "+EmptyError())
62+
} else if len(name) > qualifiedNameMaxLength {
63+
errs = append(errs, "name part "+MaxLenError(qualifiedNameMaxLength))
64+
}
65+
if !qualifiedNameRegexp.MatchString(name) {
66+
errs = append(errs, "name part "+RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc"))
67+
}
68+
return errs
69+
}
70+
71+
const labelValueFmt string = "(" + qualifiedNameFmt + ")?"
72+
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"
73+
74+
// LabelValueMaxLength is a label's max length
75+
const LabelValueMaxLength int = 63
76+
77+
var labelValueRegexp = regexp.MustCompile("^" + labelValueFmt + "$")
78+
79+
// IsValidLabelValue tests whether the value passed is a valid label value. If
80+
// the value is not valid, a list of error strings is returned. Otherwise an
81+
// empty list (or nil) is returned.
82+
func IsValidLabelValue(value string) []string {
83+
var errs []string
84+
if len(value) > LabelValueMaxLength {
85+
errs = append(errs, MaxLenError(LabelValueMaxLength))
86+
}
87+
if !labelValueRegexp.MatchString(value) {
88+
errs = append(errs, RegexError(labelValueErrMsg, labelValueFmt, "MyValue", "my_value", "12345"))
89+
}
90+
return errs
91+
}
92+
93+
const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
94+
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"
95+
96+
// DNS1123LabelMaxLength is a label's max length in DNS (RFC 1123)
97+
const DNS1123LabelMaxLength int = 63
98+
99+
var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$")
100+
101+
// IsDNS1123Label tests for a string that conforms to the definition of a label in
102+
// DNS (RFC 1123).
103+
func IsDNS1123Label(value string) []string {
104+
var errs []string
105+
if len(value) > DNS1123LabelMaxLength {
106+
errs = append(errs, MaxLenError(DNS1123LabelMaxLength))
107+
}
108+
if !dns1123LabelRegexp.MatchString(value) {
109+
errs = append(errs, RegexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc"))
110+
}
111+
return errs
112+
}
113+
114+
const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*"
115+
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"
116+
117+
// DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123)
118+
const DNS1123SubdomainMaxLength int = 253
119+
120+
var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$")
121+
122+
// IsDNS1123Subdomain tests for a string that conforms to the definition of a
123+
// subdomain in DNS (RFC 1123).
124+
func IsDNS1123Subdomain(value string) []string {
125+
var errs []string
126+
if len(value) > DNS1123SubdomainMaxLength {
127+
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
128+
}
129+
if !dns1123SubdomainRegexp.MatchString(value) {
130+
errs = append(errs, RegexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com"))
131+
}
132+
return errs
133+
}
134+
135+
const dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?"
136+
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"
137+
138+
// DNS1035LabelMaxLength is a label's max length in DNS (RFC 1035)
139+
const DNS1035LabelMaxLength int = 63
140+
141+
var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFmt + "$")
142+
143+
// IsDNS1035Label tests for a string that conforms to the definition of a label in
144+
// DNS (RFC 1035).
145+
func IsDNS1035Label(value string) []string {
146+
var errs []string
147+
if len(value) > DNS1035LabelMaxLength {
148+
errs = append(errs, MaxLenError(DNS1035LabelMaxLength))
149+
}
150+
if !dns1035LabelRegexp.MatchString(value) {
151+
errs = append(errs, RegexError(dns1035LabelErrMsg, dns1035LabelFmt, "my-name", "abc-123"))
152+
}
153+
return errs
154+
}
155+
156+
// wildcard definition - RFC 1034 section 4.3.3.
157+
// examples:
158+
// - valid: *.bar.com, *.foo.bar.com
159+
// - invalid: *.*.bar.com, *.foo.*.com, *bar.com, f*.bar.com, *
160+
const wildcardDNS1123SubdomainFmt = "\\*\\." + dns1123SubdomainFmt
161+
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"
162+
163+
// IsWildcardDNS1123Subdomain tests for a string that conforms to the definition of a
164+
// wildcard subdomain in DNS (RFC 1034 section 4.3.3).
165+
func IsWildcardDNS1123Subdomain(value string) []string {
166+
wildcardDNS1123SubdomainRegexp := regexp.MustCompile("^" + wildcardDNS1123SubdomainFmt + "$")
167+
168+
var errs []string
169+
if len(value) > DNS1123SubdomainMaxLength {
170+
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
171+
}
172+
if !wildcardDNS1123SubdomainRegexp.MatchString(value) {
173+
errs = append(errs, RegexError(wildcardDNS1123SubdomainErrMsg, wildcardDNS1123SubdomainFmt, "*.example.com"))
174+
}
175+
return errs
176+
}
177+
178+
// MaxLenError returns a string explanation of a "string too long" validation
179+
// failure.
180+
func MaxLenError(length int) string {
181+
return fmt.Sprintf("must be no more than %d characters", length)
182+
}
183+
184+
// RegexError returns a string explanation of a regex validation failure.
185+
func RegexError(msg string, fmt string, examples ...string) string {
186+
if len(examples) == 0 {
187+
return msg + " (regex used for validation is '" + fmt + "')"
188+
}
189+
msg += " (e.g. "
190+
for i := range examples {
191+
if i > 0 {
192+
msg += " or "
193+
}
194+
msg += "'" + examples[i] + "', "
195+
}
196+
msg += "regex used for validation is '" + fmt + "')"
197+
return msg
198+
}
199+
200+
// EmptyError returns a string explanation of a "must not be empty" validation
201+
// failure.
202+
func EmptyError() string {
203+
return "must be non-empty"
204+
}
205+
206+
func prefixEach(msgs []string, prefix string) []string {
207+
for i := range msgs {
208+
msgs[i] = prefix + msgs[i]
209+
}
210+
return msgs
211+
}
212+
213+
// InclusiveRangeError returns a string explanation of a numeric "must be
214+
// between" validation failure.
215+
func InclusiveRangeError(lo, hi int) string {
216+
return fmt.Sprintf(`must be between %d and %d, inclusive`, lo, hi)
217+
}

internal/validation/validate.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
Copyright © The CDI Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package validation
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
"github.com/container-orchestrated-devices/container-device-interface/internal/validation/k8s"
24+
)
25+
26+
// ValidateSpecAnnotations checks whether spec annotations are valid.
27+
func ValidateSpecAnnotations(name string, any interface{}) error {
28+
if any == nil {
29+
return nil
30+
}
31+
switch v := any.(type) {
32+
case map[string]interface{}:
33+
annotations := make(map[string]string)
34+
for k, v := range v {
35+
if s, ok := v.(string); ok {
36+
annotations[k] = s
37+
} else {
38+
return fmt.Errorf("invalid annotation %v.%v; %v is not a string", name, k, any)
39+
}
40+
}
41+
return validateSpecAnnotations(name, annotations)
42+
}
43+
44+
return fmt.Errorf("invalid spec annotations %v: %v", name, any)
45+
}
46+
47+
// validateSpecAnnotations checks whether spec annotations are valid.
48+
func validateSpecAnnotations(name string, annotations map[string]string) error {
49+
path := "annotations"
50+
if name != "" {
51+
path = strings.Join([]string{name, path}, ".")
52+
}
53+
54+
return k8s.ValidateAnnotations(annotations, path)
55+
}

0 commit comments

Comments
 (0)