Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 16 additions & 19 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,25 @@ module github.com/container-orchestrated-devices/container-device-interface
go 1.14

require (
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/coreos/etcd v3.3.10+incompatible // indirect
github.com/coreos/go-etcd v2.0.0+incompatible // indirect
github.com/cpuguy83/go-md2man v1.0.10 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/checkpoint-restore/go-criu/v5 v5.3.0 // indirect
github.com/cilium/ebpf v0.7.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/opencontainers/runtime-spec v1.0.2
github.com/opencontainers/runtime-tools v0.0.0-20190417131837-cd1349b7c47e // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/hashicorp/go-multierror v1.1.1
github.com/moby/sys/mountinfo v0.5.0 // indirect
github.com/opencontainers/runc v1.0.3
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417
github.com/opencontainers/runtime-tools v0.0.0-20190417131837-cd1349b7c47e
github.com/opencontainers/selinux v1.10.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/cobra v1.2.1 // indirect
github.com/spf13/viper v1.8.1 // indirect
github.com/pkg/errors v0.9.1
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921 // indirect
github.com/spf13/cobra v1.2.1
github.com/stretchr/testify v1.7.0
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect
github.com/xeipuuv/gojsonschema v1.2.0
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c // indirect
gopkg.in/fsnotify.v1 v1.4.7
sigs.k8s.io/yaml v1.3.0
)
89 changes: 61 additions & 28 deletions go.sum

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions pkg/cdi/annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
Copyright © 2021-2022 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 cdi

import (
"strings"

"github.com/pkg/errors"
)

const (
// AnnotationPrefix is the prefix for CDI container annotation keys.
AnnotationPrefix = "cdi.k8s.io/"
)

// UpdateAnnotations updates annotations with a plugin-specific CDI device
// injection request for the given devices. Upon any error a non-nil error
// is returned and annotations are left intact. By convention plugin should
// be in the format of "vendor.device-type".
func UpdateAnnotations(annotations map[string]string, plugin string, deviceID string, devices []string) (map[string]string, error) {
key, err := AnnotationKey(plugin, deviceID)
if err != nil {
return annotations, errors.Wrap(err, "CDI annotation failed")
}
if _, ok := annotations[key]; ok {
return annotations, errors.Errorf("CDI annotation failed, key %q used", key)
}
value, err := AnnotationValue(devices)
if err != nil {
return annotations, errors.Wrap(err, "CDI annotation failed")
}

if annotations == nil {
annotations = make(map[string]string)
}
annotations[key] = value

return annotations, nil
}

// ParseAnnotations parses annotations for CDI device injection requests.
// The keys and devices from all such requests are collected into slices
// which are returned as the result. All devices are expected to be fully
// qualified CDI device names. If any device fails this check empty slices
// are returned along with a non-nil error. The annotations are expected
// to be formatted by, or in a compatible fashion to UpdateAnnotations().
func ParseAnnotations(annotations map[string]string) ([]string, []string, error) {
var (
keys []string
devices []string
)

for key, value := range annotations {
if !strings.HasPrefix(key, AnnotationPrefix) {
continue
}
for _, d := range strings.Split(value, ",") {
if !IsQualifiedName(d) {
return nil, nil, errors.Errorf("invalid CDI device name %q", d)
}
devices = append(devices, d)
}
keys = append(keys, key)
}

return keys, devices, nil
}

// AnnotationKey returns a unique annotation key for an device allocation
// by a K8s device plugin. pluginName should be in the format of
// "vendor.device-type". deviceID is the ID of the device the plugin is
// allocating. It is used to make sure that the generated key is unique
// even if multiple allocations by a single plugin needs to be annotated.
func AnnotationKey(pluginName, deviceID string) (string, error) {
const maxNameLen = 63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Where does this limit come from?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, it comes from k8s annotation/label name limits.


if pluginName == "" {
return "", errors.New("invalid plugin name, empty")
}
if deviceID == "" {
return "", errors.New("invalid deviceID, empty")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: `"invailid device ID"

}

name := pluginName + "_" + strings.ReplaceAll(deviceID, "/", "_")

if len(name) > maxNameLen {
return "", errors.Errorf("invalid plugin+deviceID %q, too long", name)
}

if c := rune(name[0]); !isAlphaNumeric(c) {
return "", errors.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 c == '_' || c == '-' || c == '.':
default:
return "", errors.Errorf("invalid name %q, invalid charcter '%c'",
name, c)
}
}
}
if c := rune(name[len(name)-1]); !isAlphaNumeric(c) {
return "", errors.Errorf("invalid name %q, last '%c' should be alphanumeric",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a requirement? Is the only hard requirement not that the string cannot end on (or contain) = since this is used for the key-value representation of the annotations?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that we need to be as unrestrictive as possible here. The motivation being that we are looking to support multiple plugin vendors / implementations which may have vastly different ways of representing device IDs and / or plugin names. Requiring that these change or that different "variants" be tracked seems like unnecessary additional overhead.

name, c)
}
Comment on lines +104 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not just the same as ValidateVendorName (minus the check for an empty string)? Does it make sense to factor this out into validateAnnotationKeySuffix or something (note that it's private)?


return AnnotationPrefix + name, nil
}

// AnnotationValue returns an annotation value for the given devices.
func AnnotationValue(devices []string) (string, error) {
value, sep := "", ""
for _, d := range devices {
if _, _, _, err := ParseQualifiedName(d); err != nil {
return "", err
}
value += sep + d
sep = ","
}

return value, nil
Comment on lines +129 to +138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about:

        var valid []string
	for _, d := range devices {
		if _, _, _, err := ParseQualifiedName(d); err != nil {
			return "", err
		}
		valid = append(valid, d)
	}

	return strings.Join(valid, ","), nil

Note, we could also just call IsQualifiedName if we don't think that the additional information in the error is useful.

}
Loading