Skip to content

Commit 8438418

Browse files
committed
Add validation by default to producer
Signed-off-by: Evan Lezar <[email protected]>
1 parent b0797a6 commit 8438418

File tree

10 files changed

+1149
-5
lines changed

10 files changed

+1149
-5
lines changed

api/producer/annotations.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 producer
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
"tags.cncf.io/container-device-interface/api/producer/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+
32+
switch v := any.(type) {
33+
case map[string]interface{}:
34+
annotations := make(map[string]string)
35+
for k, v := range v {
36+
if s, ok := v.(string); ok {
37+
annotations[k] = s
38+
} else {
39+
return fmt.Errorf("invalid annotation %v.%v; %v is not a string", name, k, any)
40+
}
41+
}
42+
return validateSpecAnnotations(name, annotations)
43+
}
44+
45+
return nil
46+
}
47+
48+
// validateSpecAnnotations checks whether spec annotations are valid.
49+
func validateSpecAnnotations(name string, annotations map[string]string) error {
50+
path := "annotations"
51+
if name != "" {
52+
path = strings.Join([]string{name, path}, ".")
53+
}
54+
55+
return k8s.ValidateAnnotations(annotations, path)
56+
}

api/producer/identifiers.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
Copyright © 2024 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 producer
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
)
23+
24+
// ValidateKind checks the validity of a CDI kind.
25+
// The syntax for a device kind“ is
26+
//
27+
// "<vendor>/<class>"
28+
func ValidateKind(kind string) error {
29+
parts := strings.SplitN(kind, "/", 2)
30+
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
31+
return fmt.Errorf("kind %s does not contain a / %w", kind, errInvalid)
32+
}
33+
if err := ValidateVendorName(parts[0]); err != nil {
34+
return err
35+
}
36+
if err := ValidateClassName(parts[1]); err != nil {
37+
return err
38+
}
39+
return nil
40+
}
41+
42+
// ValidateVendorName checks the validity of a vendor name.
43+
// A vendor name may contain the following ASCII characters:
44+
// - upper- and lowercase letters ('A'-'Z', 'a'-'z')
45+
// - digits ('0'-'9')
46+
// - underscore, dash, and dot ('_', '-', and '.')
47+
func ValidateVendorName(vendor string) error {
48+
err := validateVendorOrClassName(vendor)
49+
if err != nil {
50+
err = fmt.Errorf("invalid vendor. %w", err)
51+
}
52+
return err
53+
}
54+
55+
// ValidateClassName checks the validity of class name.
56+
// A class name may contain the following ASCII characters:
57+
// - upper- and lowercase letters ('A'-'Z', 'a'-'z')
58+
// - digits ('0'-'9')
59+
// - underscore, dash, and dot ('_', '-', and '.')
60+
func ValidateClassName(class string) error {
61+
err := validateVendorOrClassName(class)
62+
if err != nil {
63+
err = fmt.Errorf("invalid class. %w", err)
64+
}
65+
return err
66+
}
67+
68+
// validateVendorOrClassName checks the validity of vendor or class name.
69+
// A name may contain the following ASCII characters:
70+
// - upper- and lowercase letters ('A'-'Z', 'a'-'z')
71+
// - digits ('0'-'9')
72+
// - underscore, dash, and dot ('_', '-', and '.')
73+
func validateVendorOrClassName(name string) error {
74+
if name == "" {
75+
return fmt.Errorf("empty name")
76+
}
77+
if !IsLetter(rune(name[0])) {
78+
return fmt.Errorf("%q, should start with letter", name)
79+
}
80+
for _, c := range string(name[1 : len(name)-1]) {
81+
switch {
82+
case IsAlphaNumeric(c):
83+
case c == '_' || c == '-' || c == '.':
84+
default:
85+
return fmt.Errorf("invalid character '%c' in name %q",
86+
c, name)
87+
}
88+
}
89+
if !IsAlphaNumeric(rune(name[len(name)-1])) {
90+
return fmt.Errorf("%q, should end with a letter or digit", name)
91+
}
92+
93+
return nil
94+
}
95+
96+
// ValidateDeviceName checks the validity of a device name.
97+
// A device name may contain the following ASCII characters:
98+
// - upper- and lowercase letters ('A'-'Z', 'a'-'z')
99+
// - digits ('0'-'9')
100+
// - underscore, dash, dot, colon ('_', '-', '.', ':')
101+
func ValidateDeviceName(name string) error {
102+
if name == "" {
103+
return fmt.Errorf("invalid (empty) device name")
104+
}
105+
if !IsAlphaNumeric(rune(name[0])) {
106+
return fmt.Errorf("invalid class %q, should start with a letter or digit", name)
107+
}
108+
if len(name) == 1 {
109+
return nil
110+
}
111+
for _, c := range string(name[1 : len(name)-1]) {
112+
switch {
113+
case IsAlphaNumeric(c):
114+
case c == '_' || c == '-' || c == '.' || c == ':':
115+
default:
116+
return fmt.Errorf("invalid character '%c' in device name %q",
117+
c, name)
118+
}
119+
}
120+
if !IsAlphaNumeric(rune(name[len(name)-1])) {
121+
return fmt.Errorf("invalid name %q, should end with a letter or digit", name)
122+
}
123+
return nil
124+
}
125+
126+
// IsLetter reports whether the rune is a letter.
127+
func IsLetter(c rune) bool {
128+
return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')
129+
}
130+
131+
// IsDigit reports whether the rune is a digit.
132+
func IsDigit(c rune) bool {
133+
return '0' <= c && c <= '9'
134+
}
135+
136+
// IsAlphaNumeric reports whether the rune is a letter or digit.
137+
func IsAlphaNumeric(c rune) bool {
138+
return IsLetter(c) || IsDigit(c)
139+
}

api/producer/k8s/objectmeta.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
"errors"
24+
"fmt"
25+
"strings"
26+
)
27+
28+
// TotalAnnotationSizeLimitB defines the maximum size of all annotations in characters.
29+
const TotalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB
30+
31+
// ValidateAnnotations validates that a set of annotations are correctly defined.
32+
func ValidateAnnotations(annotations map[string]string, path string) error {
33+
errs := []error{}
34+
for k := range annotations {
35+
// The rule is QualifiedName except that case doesn't matter, so convert to lowercase before checking.
36+
for _, msg := range IsQualifiedName(strings.ToLower(k)) {
37+
errs = append(errs, fmt.Errorf("%v.%v is invalid: %v", path, k, msg))
38+
}
39+
}
40+
if err := ValidateAnnotationsSize(annotations); err != nil {
41+
errs = append(errs, fmt.Errorf("%v is too long: %v", path, err))
42+
}
43+
return errors.Join(errs...)
44+
}
45+
46+
// ValidateAnnotationsSize validates that a set of annotations is not too large.
47+
func ValidateAnnotationsSize(annotations map[string]string) error {
48+
var totalSize int64
49+
for k, v := range annotations {
50+
totalSize += (int64)(len(k)) + (int64)(len(v))
51+
}
52+
if totalSize > (int64)(TotalAnnotationSizeLimitB) {
53+
return fmt.Errorf("annotations size %d is larger than limit %d", totalSize, TotalAnnotationSizeLimitB)
54+
}
55+
return nil
56+
}

0 commit comments

Comments
 (0)