diff --git a/go.mod b/go.mod index 64dc695b..caae806a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/BurntSushi/toml v1.2.1 github.com/NVIDIA/go-nvml v0.12.0-1 - github.com/container-orchestrated-devices/container-device-interface v0.5.4-0.20230111111500-5b3b5d81179a + github.com/container-orchestrated-devices/container-device-interface v0.6.0 github.com/fsnotify/fsnotify v1.5.4 github.com/opencontainers/runtime-spec v1.1.0-rc.2 github.com/pelletier/go-toml v1.9.4 diff --git a/go.sum b/go.sum index d385f677..c4bcb615 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/container-orchestrated-devices/container-device-interface v0.5.4-0.20230111111500-5b3b5d81179a h1:sP3PcgyIkRlHqfF3Jfpe/7G8kf/qpzG4C8r94y9hLbE= github.com/container-orchestrated-devices/container-device-interface v0.5.4-0.20230111111500-5b3b5d81179a/go.mod h1:xMRa4fJgXzSDFUCURSimOUgoSc+odohvO3uXT9xjqH0= +github.com/container-orchestrated-devices/container-device-interface v0.6.0 h1:aWwcz/Ep0Fd7ZuBjQGjU/jdPloM7ydhMW13h85jZNvk= +github.com/container-orchestrated-devices/container-device-interface v0.6.0/go.mod h1:OQlgtJtDrOxSQ1BWODC8OZK1tzi9W69wek+Jy17ndzo= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/internal/validation/k8s/objectmeta.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/internal/validation/k8s/objectmeta.go new file mode 100644 index 00000000..b8a6487f --- /dev/null +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/internal/validation/k8s/objectmeta.go @@ -0,0 +1,57 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Adapted from k8s.io/apimachinery/pkg/api/validation: +// https://github.com/kubernetes/apimachinery/blob/7687996c715ee7d5c8cf1e3215e607eb065a4221/pkg/api/validation/objectmeta.go + +package k8s + +import ( + "fmt" + "strings" + + "github.com/container-orchestrated-devices/container-device-interface/internal/multierror" +) + +// TotalAnnotationSizeLimitB defines the maximum size of all annotations in characters. +const TotalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB + +// ValidateAnnotations validates that a set of annotations are correctly defined. +func ValidateAnnotations(annotations map[string]string, path string) error { + errors := multierror.New() + for k := range annotations { + // The rule is QualifiedName except that case doesn't matter, so convert to lowercase before checking. + for _, msg := range IsQualifiedName(strings.ToLower(k)) { + errors = multierror.Append(errors, fmt.Errorf("%v.%v is invalid: %v", path, k, msg)) + } + } + if err := ValidateAnnotationsSize(annotations); err != nil { + errors = multierror.Append(errors, fmt.Errorf("%v is too long: %v", path, err)) + } + return errors +} + +// ValidateAnnotationsSize validates that a set of annotations is not too large. +func ValidateAnnotationsSize(annotations map[string]string) error { + var totalSize int64 + for k, v := range annotations { + totalSize += (int64)(len(k)) + (int64)(len(v)) + } + if totalSize > (int64)(TotalAnnotationSizeLimitB) { + return fmt.Errorf("annotations size %d is larger than limit %d", totalSize, TotalAnnotationSizeLimitB) + } + return nil +} diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/internal/validation/k8s/validation.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/internal/validation/k8s/validation.go new file mode 100644 index 00000000..5ad6ce27 --- /dev/null +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/internal/validation/k8s/validation.go @@ -0,0 +1,217 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Adapted from k8s.io/apimachinery/pkg/util/validation: +// https://github.com/kubernetes/apimachinery/blob/7687996c715ee7d5c8cf1e3215e607eb065a4221/pkg/util/validation/validation.go + +package k8s + +import ( + "fmt" + "regexp" + "strings" +) + +const qnameCharFmt string = "[A-Za-z0-9]" +const qnameExtCharFmt string = "[-A-Za-z0-9_.]" +const qualifiedNameFmt string = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt +const qualifiedNameErrMsg string = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character" +const qualifiedNameMaxLength int = 63 + +var qualifiedNameRegexp = regexp.MustCompile("^" + qualifiedNameFmt + "$") + +// IsQualifiedName tests whether the value passed is what Kubernetes calls a +// "qualified name". This is a format used in various places throughout the +// system. If the value is not valid, a list of error strings is returned. +// Otherwise an empty list (or nil) is returned. +func IsQualifiedName(value string) []string { + var errs []string + parts := strings.Split(value, "/") + var name string + switch len(parts) { + case 1: + name = parts[0] + case 2: + var prefix string + prefix, name = parts[0], parts[1] + if len(prefix) == 0 { + errs = append(errs, "prefix part "+EmptyError()) + } else if msgs := IsDNS1123Subdomain(prefix); len(msgs) != 0 { + errs = append(errs, prefixEach(msgs, "prefix part ")...) + } + default: + return append(errs, "a qualified name "+RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc")+ + " with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')") + } + + if len(name) == 0 { + errs = append(errs, "name part "+EmptyError()) + } else if len(name) > qualifiedNameMaxLength { + errs = append(errs, "name part "+MaxLenError(qualifiedNameMaxLength)) + } + if !qualifiedNameRegexp.MatchString(name) { + errs = append(errs, "name part "+RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc")) + } + return errs +} + +const labelValueFmt string = "(" + qualifiedNameFmt + ")?" +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" + +// LabelValueMaxLength is a label's max length +const LabelValueMaxLength int = 63 + +var labelValueRegexp = regexp.MustCompile("^" + labelValueFmt + "$") + +// IsValidLabelValue tests whether the value passed is a valid label value. If +// the value is not valid, a list of error strings is returned. Otherwise an +// empty list (or nil) is returned. +func IsValidLabelValue(value string) []string { + var errs []string + if len(value) > LabelValueMaxLength { + errs = append(errs, MaxLenError(LabelValueMaxLength)) + } + if !labelValueRegexp.MatchString(value) { + errs = append(errs, RegexError(labelValueErrMsg, labelValueFmt, "MyValue", "my_value", "12345")) + } + return errs +} + +const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" +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" + +// DNS1123LabelMaxLength is a label's max length in DNS (RFC 1123) +const DNS1123LabelMaxLength int = 63 + +var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$") + +// IsDNS1123Label tests for a string that conforms to the definition of a label in +// DNS (RFC 1123). +func IsDNS1123Label(value string) []string { + var errs []string + if len(value) > DNS1123LabelMaxLength { + errs = append(errs, MaxLenError(DNS1123LabelMaxLength)) + } + if !dns1123LabelRegexp.MatchString(value) { + errs = append(errs, RegexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc")) + } + return errs +} + +const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*" +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" + +// DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123) +const DNS1123SubdomainMaxLength int = 253 + +var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$") + +// IsDNS1123Subdomain tests for a string that conforms to the definition of a +// subdomain in DNS (RFC 1123). +func IsDNS1123Subdomain(value string) []string { + var errs []string + if len(value) > DNS1123SubdomainMaxLength { + errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength)) + } + if !dns1123SubdomainRegexp.MatchString(value) { + errs = append(errs, RegexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com")) + } + return errs +} + +const dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?" +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" + +// DNS1035LabelMaxLength is a label's max length in DNS (RFC 1035) +const DNS1035LabelMaxLength int = 63 + +var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFmt + "$") + +// IsDNS1035Label tests for a string that conforms to the definition of a label in +// DNS (RFC 1035). +func IsDNS1035Label(value string) []string { + var errs []string + if len(value) > DNS1035LabelMaxLength { + errs = append(errs, MaxLenError(DNS1035LabelMaxLength)) + } + if !dns1035LabelRegexp.MatchString(value) { + errs = append(errs, RegexError(dns1035LabelErrMsg, dns1035LabelFmt, "my-name", "abc-123")) + } + return errs +} + +// wildcard definition - RFC 1034 section 4.3.3. +// examples: +// - valid: *.bar.com, *.foo.bar.com +// - invalid: *.*.bar.com, *.foo.*.com, *bar.com, f*.bar.com, * +const wildcardDNS1123SubdomainFmt = "\\*\\." + dns1123SubdomainFmt +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" + +// IsWildcardDNS1123Subdomain tests for a string that conforms to the definition of a +// wildcard subdomain in DNS (RFC 1034 section 4.3.3). +func IsWildcardDNS1123Subdomain(value string) []string { + wildcardDNS1123SubdomainRegexp := regexp.MustCompile("^" + wildcardDNS1123SubdomainFmt + "$") + + var errs []string + if len(value) > DNS1123SubdomainMaxLength { + errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength)) + } + if !wildcardDNS1123SubdomainRegexp.MatchString(value) { + errs = append(errs, RegexError(wildcardDNS1123SubdomainErrMsg, wildcardDNS1123SubdomainFmt, "*.example.com")) + } + return errs +} + +// MaxLenError returns a string explanation of a "string too long" validation +// failure. +func MaxLenError(length int) string { + return fmt.Sprintf("must be no more than %d characters", length) +} + +// RegexError returns a string explanation of a regex validation failure. +func RegexError(msg string, fmt string, examples ...string) string { + if len(examples) == 0 { + return msg + " (regex used for validation is '" + fmt + "')" + } + msg += " (e.g. " + for i := range examples { + if i > 0 { + msg += " or " + } + msg += "'" + examples[i] + "', " + } + msg += "regex used for validation is '" + fmt + "')" + return msg +} + +// EmptyError returns a string explanation of a "must not be empty" validation +// failure. +func EmptyError() string { + return "must be non-empty" +} + +func prefixEach(msgs []string, prefix string) []string { + for i := range msgs { + msgs[i] = prefix + msgs[i] + } + return msgs +} + +// InclusiveRangeError returns a string explanation of a numeric "must be +// between" validation failure. +func InclusiveRangeError(lo, hi int) string { + return fmt.Sprintf(`must be between %d and %d, inclusive`, lo, hi) +} diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/internal/validation/validate.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/internal/validation/validate.go new file mode 100644 index 00000000..59c14c20 --- /dev/null +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/internal/validation/validate.go @@ -0,0 +1,56 @@ +/* + Copyright © The CDI Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package validation + +import ( + "fmt" + "strings" + + "github.com/container-orchestrated-devices/container-device-interface/internal/validation/k8s" +) + +// ValidateSpecAnnotations checks whether spec annotations are valid. +func ValidateSpecAnnotations(name string, any interface{}) error { + if any == nil { + return nil + } + + switch v := any.(type) { + case map[string]interface{}: + annotations := make(map[string]string) + for k, v := range v { + if s, ok := v.(string); ok { + annotations[k] = s + } else { + return fmt.Errorf("invalid annotation %v.%v; %v is not a string", name, k, any) + } + } + return validateSpecAnnotations(name, annotations) + } + + return nil +} + +// validateSpecAnnotations checks whether spec annotations are valid. +func validateSpecAnnotations(name string, annotations map[string]string) error { + path := "annotations" + if name != "" { + path = strings.Join([]string{name, path}, ".") + } + + return k8s.ValidateAnnotations(annotations, path) +} diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/annotations.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/annotations.go index c512ea09..69b69663 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/annotations.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/annotations.go @@ -20,6 +20,8 @@ import ( "errors" "fmt" "strings" + + "github.com/container-orchestrated-devices/container-device-interface/pkg/parser" ) const ( @@ -101,22 +103,22 @@ func AnnotationKey(pluginName, deviceID string) (string, error) { return "", fmt.Errorf("invalid plugin+deviceID %q, too long", name) } - if c := rune(name[0]); !isAlphaNumeric(c) { + if c := rune(name[0]); !parser.IsAlphaNumeric(c) { return "", fmt.Errorf("invalid name %q, first '%c' should be alphanumeric", name, c) } if len(name) > 2 { for _, c := range name[1 : len(name)-1] { switch { - case isAlphaNumeric(c): + case parser.IsAlphaNumeric(c): case c == '_' || c == '-' || c == '.': default: - return "", fmt.Errorf("invalid name %q, invalid charcter '%c'", + return "", fmt.Errorf("invalid name %q, invalid character '%c'", name, c) } } } - if c := rune(name[len(name)-1]); !isAlphaNumeric(c) { + if c := rune(name[len(name)-1]); !parser.IsAlphaNumeric(c) { return "", fmt.Errorf("invalid name %q, last '%c' should be alphanumeric", name, c) } diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/container-edits.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/container-edits.go index 3e0d24ed..55c748fc 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/container-edits.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/container-edits.go @@ -238,7 +238,7 @@ func (d *DeviceNode) Validate() error { } for _, bit := range d.Permissions { if bit != 'r' && bit != 'w' && bit != 'm' { - return fmt.Errorf("device %q: invalid persmissions %q", + return fmt.Errorf("device %q: invalid permissions %q", d.Path, d.Permissions) } } diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/device.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/device.go index 7b16a313..d93ddd02 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/device.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/device.go @@ -19,6 +19,8 @@ package cdi import ( "fmt" + "github.com/container-orchestrated-devices/container-device-interface/internal/validation" + "github.com/container-orchestrated-devices/container-device-interface/pkg/parser" cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go" oci "github.com/opencontainers/runtime-spec/specs-go" ) @@ -50,7 +52,7 @@ func (d *Device) GetSpec() *Spec { // GetQualifiedName returns the qualified name for this device. func (d *Device) GetQualifiedName() string { - return QualifiedName(d.spec.GetVendor(), d.spec.GetClass(), d.Name) + return parser.QualifiedName(d.spec.GetVendor(), d.spec.GetClass(), d.Name) } // ApplyEdits applies the device-speific container edits to an OCI Spec. @@ -68,6 +70,13 @@ func (d *Device) validate() error { if err := ValidateDeviceName(d.Name); err != nil { return err } + name := d.Name + if d.spec != nil { + name = d.GetQualifiedName() + } + if err := validation.ValidateSpecAnnotations(name, d.Annotations); err != nil { + return err + } edits := d.edits() if edits.isEmpty() { return fmt.Errorf("invalid device, empty device edits") diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/doc.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/doc.go index ab063a13..c5cce0c8 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/doc.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/doc.go @@ -137,7 +137,7 @@ // were loaded from. The later a directory occurs in the list of CDI // directories to scan, the higher priority Spec files loaded from that // directory are assigned to. When two or more Spec files define the -// same device, conflict is resolved by chosing the definition from the +// same device, conflict is resolved by choosing the definition from the // Spec file with the highest priority. // // The default CDI directory configuration is chosen to encourage @@ -197,7 +197,7 @@ // return registry.SpecDB().WriteSpec(spec, specName) // } // -// Similary, generating and later cleaning up transient Spec files can be +// Similarly, generating and later cleaning up transient Spec files can be // done with code fragments similar to the following. These transient Spec // files are temporary Spec files with container-specific parametrization. // They are typically created before the associated container is created diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/qualified-device.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/qualified-device.go index 4fe3cfe4..16e889a7 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/qualified-device.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/qualified-device.go @@ -17,27 +17,32 @@ package cdi import ( - "fmt" - "strings" + "github.com/container-orchestrated-devices/container-device-interface/pkg/parser" ) // QualifiedName returns the qualified name for a device. // The syntax for a qualified device names is -// "/=". -// A valid vendor name may contain the following runes: -// 'A'-'Z', 'a'-'z', '0'-'9', '.', '-', '_'. -// A valid class name may contain the following runes: -// 'A'-'Z', 'a'-'z', '0'-'9', '-', '_'. -// A valid device name may containe the following runes: -// 'A'-'Z', 'a'-'z', '0'-'9', '-', '_', '.', ':' +// +// "/=". +// +// A valid vendor and class name may contain the following runes: +// +// 'A'-'Z', 'a'-'z', '0'-'9', '.', '-', '_'. +// +// A valid device name may contain the following runes: +// +// 'A'-'Z', 'a'-'z', '0'-'9', '-', '_', '.', ':' +// +// Deprecated: use parser.QualifiedName instead func QualifiedName(vendor, class, name string) string { - return vendor + "/" + class + "=" + name + return parser.QualifiedName(vendor, class, name) } // IsQualifiedName tests if a device name is qualified. +// +// Deprecated: use parser.IsQualifiedName instead func IsQualifiedName(device string) bool { - _, _, _, err := ParseQualifiedName(device) - return err == nil + return parser.IsQualifiedName(device) } // ParseQualifiedName splits a qualified name into device vendor, class, @@ -45,66 +50,33 @@ func IsQualifiedName(device string) bool { // of the split components fail to pass syntax validation, vendor and // class are returned as empty, together with the verbatim input as the // name and an error describing the reason for failure. +// +// Deprecated: use parser.ParseQualifiedName instead func ParseQualifiedName(device string) (string, string, string, error) { - vendor, class, name := ParseDevice(device) - - if vendor == "" { - return "", "", device, fmt.Errorf("unqualified device %q, missing vendor", device) - } - if class == "" { - return "", "", device, fmt.Errorf("unqualified device %q, missing class", device) - } - if name == "" { - return "", "", device, fmt.Errorf("unqualified device %q, missing device name", device) - } - - if err := ValidateVendorName(vendor); err != nil { - return "", "", device, fmt.Errorf("invalid device %q: %w", device, err) - } - if err := ValidateClassName(class); err != nil { - return "", "", device, fmt.Errorf("invalid device %q: %w", device, err) - } - if err := ValidateDeviceName(name); err != nil { - return "", "", device, fmt.Errorf("invalid device %q: %w", device, err) - } - - return vendor, class, name, nil + return parser.ParseQualifiedName(device) } // ParseDevice tries to split a device name into vendor, class, and name. // If this fails, for instance in the case of unqualified device names, // ParseDevice returns an empty vendor and class together with name set // to the verbatim input. +// +// Deprecated: use parser.ParseDevice instead func ParseDevice(device string) (string, string, string) { - if device == "" || device[0] == '/' { - return "", "", device - } - - parts := strings.SplitN(device, "=", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return "", "", device - } - - name := parts[1] - vendor, class := ParseQualifier(parts[0]) - if vendor == "" { - return "", "", device - } - - return vendor, class, name + return parser.ParseDevice(device) } // ParseQualifier splits a device qualifier into vendor and class. // The syntax for a device qualifier is -// "/" +// +// "/" +// // If parsing fails, an empty vendor and the class set to the // verbatim input is returned. +// +// Deprecated: use parser.ParseQualifier instead func ParseQualifier(kind string) (string, string) { - parts := strings.SplitN(kind, "/", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return "", kind - } - return parts[0], parts[1] + return parser.ParseQualifier(kind) } // ValidateVendorName checks the validity of a vendor name. @@ -112,54 +84,21 @@ func ParseQualifier(kind string) (string, string) { // - upper- and lowercase letters ('A'-'Z', 'a'-'z') // - digits ('0'-'9') // - underscore, dash, and dot ('_', '-', and '.') +// +// Deprecated: use parser.ValidateVendorName instead func ValidateVendorName(vendor string) error { - if vendor == "" { - return fmt.Errorf("invalid (empty) vendor name") - } - if !isLetter(rune(vendor[0])) { - return fmt.Errorf("invalid vendor %q, should start with letter", vendor) - } - for _, c := range string(vendor[1 : len(vendor)-1]) { - switch { - case isAlphaNumeric(c): - case c == '_' || c == '-' || c == '.': - default: - return fmt.Errorf("invalid character '%c' in vendor name %q", - c, vendor) - } - } - if !isAlphaNumeric(rune(vendor[len(vendor)-1])) { - return fmt.Errorf("invalid vendor %q, should end with a letter or digit", vendor) - } - - return nil + return parser.ValidateVendorName(vendor) } // ValidateClassName checks the validity of class name. // A class name may contain the following ASCII characters: // - upper- and lowercase letters ('A'-'Z', 'a'-'z') // - digits ('0'-'9') -// - underscore and dash ('_', '-') +// - underscore, dash, and dot ('_', '-', and '.') +// +// Deprecated: use parser.ValidateClassName instead func ValidateClassName(class string) error { - if class == "" { - return fmt.Errorf("invalid (empty) device class") - } - if !isLetter(rune(class[0])) { - return fmt.Errorf("invalid class %q, should start with letter", class) - } - for _, c := range string(class[1 : len(class)-1]) { - switch { - case isAlphaNumeric(c): - case c == '_' || c == '-': - default: - return fmt.Errorf("invalid character '%c' in device class %q", - c, class) - } - } - if !isAlphaNumeric(rune(class[len(class)-1])) { - return fmt.Errorf("invalid class %q, should end with a letter or digit", class) - } - return nil + return parser.ValidateClassName(class) } // ValidateDeviceName checks the validity of a device name. @@ -167,39 +106,8 @@ func ValidateClassName(class string) error { // - upper- and lowercase letters ('A'-'Z', 'a'-'z') // - digits ('0'-'9') // - underscore, dash, dot, colon ('_', '-', '.', ':') +// +// Deprecated: use parser.ValidateDeviceName instead func ValidateDeviceName(name string) error { - if name == "" { - return fmt.Errorf("invalid (empty) device name") - } - if !isAlphaNumeric(rune(name[0])) { - return fmt.Errorf("invalid class %q, should start with a letter or digit", name) - } - if len(name) == 1 { - return nil - } - for _, c := range string(name[1 : len(name)-1]) { - switch { - case isAlphaNumeric(c): - case c == '_' || c == '-' || c == '.' || c == ':': - default: - return fmt.Errorf("invalid character '%c' in device name %q", - c, name) - } - } - if !isAlphaNumeric(rune(name[len(name)-1])) { - return fmt.Errorf("invalid name %q, should end with a letter or digit", name) - } - return nil -} - -func isLetter(c rune) bool { - return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') -} - -func isDigit(c rune) bool { - return '0' <= c && c <= '9' -} - -func isAlphaNumeric(c rune) bool { - return isLetter(c) || isDigit(c) + return parser.ValidateDeviceName(name) } diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/registry.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/registry.go index 10fab899..e13ce60b 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/registry.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/registry.go @@ -23,14 +23,12 @@ import ( oci "github.com/opencontainers/runtime-spec/specs-go" ) -// // Registry keeps a cache of all CDI Specs installed or generated on // the host. Registry is the primary interface clients should use to // interact with CDI. // // The most commonly used Registry functions are for refreshing the // registry and injecting CDI devices into an OCI Spec. -// type Registry interface { RegistryResolver RegistryRefresher diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/spec.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/spec.go index fe20e6dd..62693c1b 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/spec.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/spec.go @@ -28,6 +28,7 @@ import ( oci "github.com/opencontainers/runtime-spec/specs-go" "sigs.k8s.io/yaml" + "github.com/container-orchestrated-devices/container-device-interface/internal/validation" cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go" ) @@ -131,6 +132,7 @@ func (s *Spec) write(overwrite bool) error { if filepath.Ext(s.path) == ".yaml" { data, err = yaml.Marshal(s.Spec) + data = append([]byte("---\n"), data...) } else { data, err = json.Marshal(s.Spec) } @@ -207,7 +209,7 @@ func (s *Spec) validate() (map[string]*Device, error) { minVersion, err := MinimumRequiredVersion(s.Spec) if err != nil { - return nil, fmt.Errorf("could not determine minumum required version: %v", err) + return nil, fmt.Errorf("could not determine minimum required version: %v", err) } if newVersion(minVersion).IsGreaterThan(newVersion(s.Version)) { return nil, fmt.Errorf("the spec version must be at least v%v", minVersion) @@ -219,6 +221,9 @@ func (s *Spec) validate() (map[string]*Device, error) { if err := ValidateClassName(s.class); err != nil { return nil, err } + if err := validation.ValidateSpecAnnotations(s.Kind, s.Annotations); err != nil { + return nil, err + } if err := s.edits().Validate(); err != nil { return nil, err } @@ -306,7 +311,7 @@ func GenerateSpecName(vendor, class string) string { // match the vendor and class of the CDI Spec. transientID should be // unique among all CDI users on the same host that might generate // transient Spec files using the same vendor/class combination. If -// the external entity to which the lifecycle of the tranient Spec +// the external entity to which the lifecycle of the transient Spec // is tied to has a unique ID of its own, then this is usually a // good choice for transientID. // diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/version.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/version.go index 08b36a32..22534d92 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/version.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/version.go @@ -21,6 +21,7 @@ import ( "golang.org/x/mod/semver" + "github.com/container-orchestrated-devices/container-device-interface/pkg/parser" cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go" ) @@ -37,6 +38,7 @@ const ( v030 version = "v0.3.0" v040 version = "v0.4.0" v050 version = "v0.5.0" + v060 version = "v0.6.0" // vEarliest is the earliest supported version of the CDI specification vEarliest version = v030 @@ -51,9 +53,10 @@ var validSpecVersions = requiredVersionMap{ v030: nil, v040: requiresV040, v050: requiresV050, + v060: requiresV060, } -// MinimumRequiredVersion determines the minumum spec version for the input spec. +// MinimumRequiredVersion determines the minimum spec version for the input spec. func MinimumRequiredVersion(spec *cdi.Spec) (string, error) { minVersion := validSpecVersions.requiredVersion(spec) return minVersion.String(), nil @@ -115,13 +118,38 @@ func (r requiredVersionMap) requiredVersion(spec *cdi.Spec) version { return minVersion } +// requiresV060 returns true if the spec uses v0.6.0 features +func requiresV060(spec *cdi.Spec) bool { + // The v0.6.0 spec allows annotations to be specified at a spec level + for range spec.Annotations { + return true + } + + // The v0.6.0 spec allows annotations to be specified at a device level + for _, d := range spec.Devices { + for range d.Annotations { + return true + } + } + + // The v0.6.0 spec allows dots "." in Kind name label (class) + vendor, class := parser.ParseQualifier(spec.Kind) + if vendor != "" { + if strings.ContainsRune(class, '.') { + return true + } + } + + return false +} + // requiresV050 returns true if the spec uses v0.5.0 features func requiresV050(spec *cdi.Spec) bool { var edits []*cdi.ContainerEdits for _, d := range spec.Devices { // The v0.5.0 spec allowed device names to start with a digit instead of requiring a letter - if len(d.Name) > 0 && !isLetter(rune(d.Name[0])) { + if len(d.Name) > 0 && !parser.IsLetter(rune(d.Name[0])) { return true } edits = append(edits, &d.ContainerEdits) diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/parser/parser.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/parser/parser.go new file mode 100644 index 00000000..53259895 --- /dev/null +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/parser/parser.go @@ -0,0 +1,212 @@ +/* + Copyright © The CDI Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package parser + +import ( + "fmt" + "strings" +) + +// QualifiedName returns the qualified name for a device. +// The syntax for a qualified device names is +// +// "/=". +// +// A valid vendor and class name may contain the following runes: +// +// 'A'-'Z', 'a'-'z', '0'-'9', '.', '-', '_'. +// +// A valid device name may contain the following runes: +// +// 'A'-'Z', 'a'-'z', '0'-'9', '-', '_', '.', ':' +func QualifiedName(vendor, class, name string) string { + return vendor + "/" + class + "=" + name +} + +// IsQualifiedName tests if a device name is qualified. +func IsQualifiedName(device string) bool { + _, _, _, err := ParseQualifiedName(device) + return err == nil +} + +// ParseQualifiedName splits a qualified name into device vendor, class, +// and name. If the device fails to parse as a qualified name, or if any +// of the split components fail to pass syntax validation, vendor and +// class are returned as empty, together with the verbatim input as the +// name and an error describing the reason for failure. +func ParseQualifiedName(device string) (string, string, string, error) { + vendor, class, name := ParseDevice(device) + + if vendor == "" { + return "", "", device, fmt.Errorf("unqualified device %q, missing vendor", device) + } + if class == "" { + return "", "", device, fmt.Errorf("unqualified device %q, missing class", device) + } + if name == "" { + return "", "", device, fmt.Errorf("unqualified device %q, missing device name", device) + } + + if err := ValidateVendorName(vendor); err != nil { + return "", "", device, fmt.Errorf("invalid device %q: %w", device, err) + } + if err := ValidateClassName(class); err != nil { + return "", "", device, fmt.Errorf("invalid device %q: %w", device, err) + } + if err := ValidateDeviceName(name); err != nil { + return "", "", device, fmt.Errorf("invalid device %q: %w", device, err) + } + + return vendor, class, name, nil +} + +// ParseDevice tries to split a device name into vendor, class, and name. +// If this fails, for instance in the case of unqualified device names, +// ParseDevice returns an empty vendor and class together with name set +// to the verbatim input. +func ParseDevice(device string) (string, string, string) { + if device == "" || device[0] == '/' { + return "", "", device + } + + parts := strings.SplitN(device, "=", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", device + } + + name := parts[1] + vendor, class := ParseQualifier(parts[0]) + if vendor == "" { + return "", "", device + } + + return vendor, class, name +} + +// ParseQualifier splits a device qualifier into vendor and class. +// The syntax for a device qualifier is +// +// "/" +// +// If parsing fails, an empty vendor and the class set to the +// verbatim input is returned. +func ParseQualifier(kind string) (string, string) { + parts := strings.SplitN(kind, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", kind + } + return parts[0], parts[1] +} + +// ValidateVendorName checks the validity of a vendor name. +// A vendor name may contain the following ASCII characters: +// - upper- and lowercase letters ('A'-'Z', 'a'-'z') +// - digits ('0'-'9') +// - underscore, dash, and dot ('_', '-', and '.') +func ValidateVendorName(vendor string) error { + err := validateVendorOrClassName(vendor) + if err != nil { + err = fmt.Errorf("invalid vendor. %w", err) + } + return err +} + +// ValidateClassName checks the validity of class name. +// A class name may contain the following ASCII characters: +// - upper- and lowercase letters ('A'-'Z', 'a'-'z') +// - digits ('0'-'9') +// - underscore, dash, and dot ('_', '-', and '.') +func ValidateClassName(class string) error { + err := validateVendorOrClassName(class) + if err != nil { + err = fmt.Errorf("invalid class. %w", err) + } + return err +} + +// validateVendorOrClassName checks the validity of vendor or class name. +// A name may contain the following ASCII characters: +// - upper- and lowercase letters ('A'-'Z', 'a'-'z') +// - digits ('0'-'9') +// - underscore, dash, and dot ('_', '-', and '.') +func validateVendorOrClassName(name string) error { + if name == "" { + return fmt.Errorf("empty name") + } + if !IsLetter(rune(name[0])) { + return fmt.Errorf("%q, should start with letter", name) + } + for _, c := range string(name[1 : len(name)-1]) { + switch { + case IsAlphaNumeric(c): + case c == '_' || c == '-' || c == '.': + default: + return fmt.Errorf("invalid character '%c' in name %q", + c, name) + } + } + if !IsAlphaNumeric(rune(name[len(name)-1])) { + return fmt.Errorf("%q, should end with a letter or digit", name) + } + + return nil +} + +// ValidateDeviceName checks the validity of a device name. +// A device name may contain the following ASCII characters: +// - upper- and lowercase letters ('A'-'Z', 'a'-'z') +// - digits ('0'-'9') +// - underscore, dash, dot, colon ('_', '-', '.', ':') +func ValidateDeviceName(name string) error { + if name == "" { + return fmt.Errorf("invalid (empty) device name") + } + if !IsAlphaNumeric(rune(name[0])) { + return fmt.Errorf("invalid class %q, should start with a letter or digit", name) + } + if len(name) == 1 { + return nil + } + for _, c := range string(name[1 : len(name)-1]) { + switch { + case IsAlphaNumeric(c): + case c == '_' || c == '-' || c == '.' || c == ':': + default: + return fmt.Errorf("invalid character '%c' in device name %q", + c, name) + } + } + if !IsAlphaNumeric(rune(name[len(name)-1])) { + return fmt.Errorf("invalid name %q, should end with a letter or digit", name) + } + return nil +} + +// IsLetter reports whether the rune is a letter. +func IsLetter(c rune) bool { + return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') +} + +// IsDigit reports whether the rune is a digit. +func IsDigit(c rune) bool { + return '0' <= c && c <= '9' +} + +// IsAlphaNumeric reports whether the rune is a letter or digit. +func IsAlphaNumeric(c rune) bool { + return IsLetter(c) || IsDigit(c) +} diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/config.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/config.go index 3fa2e814..4043b858 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/config.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/config.go @@ -3,21 +3,24 @@ package specs import "os" // CurrentVersion is the current version of the Spec. -const CurrentVersion = "0.5.0" +const CurrentVersion = "0.6.0" // Spec is the base configuration for CDI type Spec struct { Version string `json:"cdiVersion"` Kind string `json:"kind"` - - Devices []Device `json:"devices"` - ContainerEdits ContainerEdits `json:"containerEdits,omitempty"` + // Annotations add meta information per CDI spec. Note these are CDI-specific and do not affect container metadata. + Annotations map[string]string `json:"annotations,omitempty"` + Devices []Device `json:"devices"` + ContainerEdits ContainerEdits `json:"containerEdits,omitempty"` } // Device is a "Device" a container runtime can add to a container type Device struct { - Name string `json:"name"` - ContainerEdits ContainerEdits `json:"containerEdits"` + Name string `json:"name"` + // Annotations add meta information per device. Note these are CDI-specific and do not affect container metadata. + Annotations map[string]string `json:"annotations,omitempty"` + ContainerEdits ContainerEdits `json:"containerEdits"` } // ContainerEdits are edits a container runtime must make to the OCI spec to expose the device. diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/oci.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/oci.go index 14a0f6a0..d709ecbc 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/oci.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/oci.go @@ -22,7 +22,7 @@ func ApplyOCIEditsForDevice(config *spec.Spec, cdi *Spec, dev string) error { return fmt.Errorf("CDI: device %q not found for spec %q", dev, cdi.Kind) } -// ApplyOCIEdits applies the OCI edits the CDI spec declares globablly +// ApplyOCIEdits applies the OCI edits the CDI spec declares globally func ApplyOCIEdits(config *spec.Spec, cdi *Spec) error { return ApplyEditsToOCISpec(config, &cdi.ContainerEdits) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 42402ce8..b34d4f8d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -6,10 +6,13 @@ github.com/BurntSushi/toml/internal ## explicit; go 1.15 github.com/NVIDIA/go-nvml/pkg/dl github.com/NVIDIA/go-nvml/pkg/nvml -# github.com/container-orchestrated-devices/container-device-interface v0.5.4-0.20230111111500-5b3b5d81179a +# github.com/container-orchestrated-devices/container-device-interface v0.6.0 ## explicit; go 1.17 github.com/container-orchestrated-devices/container-device-interface/internal/multierror +github.com/container-orchestrated-devices/container-device-interface/internal/validation +github.com/container-orchestrated-devices/container-device-interface/internal/validation/k8s github.com/container-orchestrated-devices/container-device-interface/pkg/cdi +github.com/container-orchestrated-devices/container-device-interface/pkg/parser github.com/container-orchestrated-devices/container-device-interface/specs-go # github.com/cpuguy83/go-md2man/v2 v2.0.2 ## explicit; go 1.11