Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

devel: better support for runtime integration #43

Merged
merged 3 commits into from
Jan 31, 2022
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/"
elezar marked this conversation as resolved.
Show resolved Hide resolved
)

// 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.
klihub marked this conversation as resolved.
Show resolved Hide resolved
// 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