Bump CDI go dependency to v0.3.0

This updates the CDI dependency to the v0.3.0 tagged version instead of
relying on a pseudo version. This also addresses the fact that cgroups
are not set correctly for devices using the previous dependency.

Signed-off-by: Evan Lezar <elezar@nvidia.com>
This commit is contained in:
Evan Lezar 2022-02-23 08:02:44 +02:00
parent ccb96a2791
commit 46b7c5bc63
9 changed files with 323 additions and 26 deletions

2
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/buger/goterm v1.0.4 github.com/buger/goterm v1.0.4
github.com/checkpoint-restore/checkpointctl v0.0.0-20211204171957-54b4ebfdb681 github.com/checkpoint-restore/checkpointctl v0.0.0-20211204171957-54b4ebfdb681
github.com/checkpoint-restore/go-criu/v5 v5.3.0 github.com/checkpoint-restore/go-criu/v5 v5.3.0
github.com/container-orchestrated-devices/container-device-interface v0.0.0-20220111162300-46367ec063fd github.com/container-orchestrated-devices/container-device-interface v0.3.0
github.com/containernetworking/cni v1.0.1 github.com/containernetworking/cni v1.0.1
github.com/containernetworking/plugins v1.0.1 github.com/containernetworking/plugins v1.0.1
github.com/containers/buildah v1.24.2 github.com/containers/buildah v1.24.2

4
go.sum
View File

@ -242,8 +242,8 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/container-orchestrated-devices/container-device-interface v0.0.0-20220111162300-46367ec063fd h1:Pzh64A349jzW89R73gwkxWoPvpkd8rz2X+KLUaMmBRY= github.com/container-orchestrated-devices/container-device-interface v0.3.0 h1:tM2zdVYZY8getsFaTc7Z+v+UqDXhk5alchOHVEADes0=
github.com/container-orchestrated-devices/container-device-interface v0.0.0-20220111162300-46367ec063fd/go.mod h1:YqXyXS/oVW3ix0IHVXitKBq3RZoCF8ccm5RPmRBraUI= github.com/container-orchestrated-devices/container-device-interface v0.3.0/go.mod h1:LGs3yHVe1wZn2XsWl4AxywYQ3NRZ6osTEZozCHQCRSM=
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=

View File

@ -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
if pluginName == "" {
return "", errors.New("invalid plugin name, empty")
}
if deviceID == "" {
return "", errors.New("invalid deviceID, empty")
}
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",
name, c)
}
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
}

View File

@ -153,10 +153,7 @@ func (c *Cache) Refresh() error {
// returns any unresolvable devices and an error if injection fails for // returns any unresolvable devices and an error if injection fails for
// any of the devices. // any of the devices.
func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, error) { func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, error) {
var ( var unresolved []string
unresolved []string
specs = map[*Spec]struct{}{}
)
if ociSpec == nil { if ociSpec == nil {
return devices, errors.Errorf("can't inject devices, nil OCI Spec") return devices, errors.Errorf("can't inject devices, nil OCI Spec")
@ -165,6 +162,9 @@ func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, e
c.Lock() c.Lock()
defer c.Unlock() defer c.Unlock()
edits := &ContainerEdits{}
specs := map[*Spec]struct{}{}
for _, device := range devices { for _, device := range devices {
d := c.devices[device] d := c.devices[device]
if d == nil { if d == nil {
@ -173,9 +173,9 @@ func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, e
} }
if _, ok := specs[d.GetSpec()]; !ok { if _, ok := specs[d.GetSpec()]; !ok {
specs[d.GetSpec()] = struct{}{} specs[d.GetSpec()] = struct{}{}
d.GetSpec().ApplyEdits(ociSpec) edits.Append(d.GetSpec().edits())
} }
d.ApplyEdits(ociSpec) edits.Append(d.edits())
} }
if unresolved != nil { if unresolved != nil {
@ -183,6 +183,10 @@ func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, e
strings.Join(devices, ", ")) strings.Join(devices, ", "))
} }
if err := edits.Apply(ociSpec); err != nil {
return nil, errors.Wrap(err, "failed to inject devices")
}
return nil, nil return nil, nil
} }

View File

@ -17,6 +17,9 @@
package cdi package cdi
import ( import (
"os"
"path/filepath"
"sort"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -24,6 +27,8 @@ import (
"github.com/container-orchestrated-devices/container-device-interface/specs-go" "github.com/container-orchestrated-devices/container-device-interface/specs-go"
oci "github.com/opencontainers/runtime-spec/specs-go" oci "github.com/opencontainers/runtime-spec/specs-go"
ocigen "github.com/opencontainers/runtime-tools/generate" ocigen "github.com/opencontainers/runtime-tools/generate"
runc "github.com/opencontainers/runc/libcontainer/devices"
) )
const ( const (
@ -78,12 +83,44 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error {
if len(e.Env) > 0 { if len(e.Env) > 0 {
specgen.AddMultipleProcessEnv(e.Env) specgen.AddMultipleProcessEnv(e.Env)
} }
for _, d := range e.DeviceNodes { for _, d := range e.DeviceNodes {
specgen.AddDevice(d.ToOCI()) dev := d.ToOCI()
if err := fillMissingInfo(&dev); err != nil {
return err
}
if dev.UID == nil && spec.Process != nil {
if uid := spec.Process.User.UID; uid > 0 {
dev.UID = &uid
}
}
if dev.GID == nil && spec.Process != nil {
if gid := spec.Process.User.GID; gid > 0 {
dev.GID = &gid
}
}
specgen.RemoveDevice(dev.Path)
specgen.AddDevice(dev)
if dev.Type == "b" || dev.Type == "c" {
access := d.Permissions
if access == "" {
access = "rwm"
}
specgen.AddLinuxResourcesDevice(true, dev.Type, &dev.Major, &dev.Minor, access)
}
} }
for _, m := range e.Mounts {
specgen.AddMount(m.ToOCI()) if len(e.Mounts) > 0 {
for _, m := range e.Mounts {
specgen.RemoveMount(m.ContainerPath)
specgen.AddMount(m.ToOCI())
}
sortMounts(&specgen)
} }
for _, h := range e.Hooks { for _, h := range e.Hooks {
switch h.HookName { switch h.HookName {
case PrestartHook: case PrestartHook:
@ -138,6 +175,27 @@ func (e *ContainerEdits) Validate() error {
return nil return nil
} }
// Append other edits into this one. If called with a nil receiver,
// allocates and returns newly allocated edits.
func (e *ContainerEdits) Append(o *ContainerEdits) *ContainerEdits {
if o == nil || o.ContainerEdits == nil {
return e
}
if e == nil {
e = &ContainerEdits{}
}
if e.ContainerEdits == nil {
e.ContainerEdits = &specs.ContainerEdits{}
}
e.Env = append(e.Env, o.Env...)
e.DeviceNodes = append(e.DeviceNodes, o.DeviceNodes...)
e.Hooks = append(e.Hooks, o.Hooks...)
e.Mounts = append(e.Mounts, o.Mounts...)
return e
}
// isEmpty returns true if these edits are empty. This is valid in a // isEmpty returns true if these edits are empty. This is valid in a
// global Spec context but invalid in a Device context. // global Spec context but invalid in a Device context.
func (e *ContainerEdits) isEmpty() bool { func (e *ContainerEdits) isEmpty() bool {
@ -164,10 +222,18 @@ type DeviceNode struct {
// Validate a CDI Spec DeviceNode. // Validate a CDI Spec DeviceNode.
func (d *DeviceNode) Validate() error { func (d *DeviceNode) Validate() error {
validTypes := map[string]struct{}{
"": {},
"b": {},
"c": {},
"u": {},
"p": {},
}
if d.Path == "" { if d.Path == "" {
return errors.New("invalid (empty) device path") return errors.New("invalid (empty) device path")
} }
if d.Type != "" && d.Type != "b" && d.Type != "c" { if _, ok := validTypes[d.Type]; !ok {
return errors.Errorf("device %q: invalid type %q", d.Path, d.Type) return errors.Errorf("device %q: invalid type %q", d.Path, d.Type)
} }
for _, bit := range d.Permissions { for _, bit := range d.Permissions {
@ -220,3 +286,72 @@ func ensureOCIHooks(spec *oci.Spec) {
spec.Hooks = &oci.Hooks{} spec.Hooks = &oci.Hooks{}
} }
} }
// fillMissingInfo fills in missing mandatory attributes from the host device.
func fillMissingInfo(dev *oci.LinuxDevice) error {
if dev.Type != "" && (dev.Major != 0 || dev.Type == "p") {
return nil
}
hostDev, err := runc.DeviceFromPath(dev.Path, "rwm")
if err != nil {
return errors.Wrapf(err, "failed to stat CDI host device %q", dev.Path)
}
if dev.Type == "" {
dev.Type = string(hostDev.Type)
} else {
if dev.Type != string(hostDev.Type) {
return errors.Errorf("CDI device %q, host type mismatch (%s, %s)",
dev.Path, dev.Type, string(hostDev.Type))
}
}
if dev.Major == 0 && dev.Type != "p" {
dev.Major = hostDev.Major
dev.Minor = hostDev.Minor
}
return nil
}
// sortMounts sorts the mounts in the given OCI Spec.
func sortMounts(specgen *ocigen.Generator) {
mounts := specgen.Mounts()
specgen.ClearMounts()
sort.Sort(orderedMounts(mounts))
specgen.Config.Mounts = mounts
}
// orderedMounts defines how to sort an OCI Spec Mount slice.
// This is the almost the same implementation sa used by CRI-O and Docker,
// with a minor tweak for stable sorting order (easier to test):
// https://github.com/moby/moby/blob/17.05.x/daemon/volumes.go#L26
type orderedMounts []oci.Mount
// Len returns the number of mounts. Used in sorting.
func (m orderedMounts) Len() int {
return len(m)
}
// Less returns true if the number of parts (a/b/c would be 3 parts) in the
// mount indexed by parameter 1 is less than that of the mount indexed by
// parameter 2. Used in sorting.
func (m orderedMounts) Less(i, j int) bool {
ip, jp := m.parts(i), m.parts(j)
if ip < jp {
return true
}
if jp < ip {
return false
}
return m[i].Destination < m[j].Destination
}
// Swap swaps two items in an array of mounts. Used in sorting
func (m orderedMounts) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
// parts returns the number of parts in the destination of a mount. Used in sorting.
func (m orderedMounts) parts(i int) int {
return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator))
}

View File

@ -54,8 +54,12 @@ func (d *Device) GetQualifiedName() string {
// ApplyEdits applies the device-speific container edits to an OCI Spec. // ApplyEdits applies the device-speific container edits to an OCI Spec.
func (d *Device) ApplyEdits(ociSpec *oci.Spec) error { func (d *Device) ApplyEdits(ociSpec *oci.Spec) error {
e := ContainerEdits{&d.ContainerEdits} return d.edits().Apply(ociSpec)
return e.Apply(ociSpec) }
// edits returns the applicable container edits for this spec.
func (d *Device) edits() *ContainerEdits {
return &ContainerEdits{&d.ContainerEdits}
} }
// Validate the device. // Validate the device.
@ -63,7 +67,7 @@ func (d *Device) validate() error {
if err := ValidateDeviceName(d.Name); err != nil { if err := ValidateDeviceName(d.Name); err != nil {
return err return err
} }
edits := ContainerEdits{&d.ContainerEdits} edits := d.edits()
if edits.isEmpty() { if edits.isEmpty() {
return errors.Errorf("invalid device, empty device edits") return errors.Errorf("invalid device, empty device edits")
} }

View File

@ -33,6 +33,7 @@ var (
validSpecVersions = map[string]struct{}{ validSpecVersions = map[string]struct{}{
"0.1.0": {}, "0.1.0": {},
"0.2.0": {}, "0.2.0": {},
"0.3.0": {},
} }
) )
@ -120,14 +121,18 @@ func (s *Spec) GetPriority() int {
// ApplyEdits applies the Spec's global-scope container edits to an OCI Spec. // ApplyEdits applies the Spec's global-scope container edits to an OCI Spec.
func (s *Spec) ApplyEdits(ociSpec *oci.Spec) error { func (s *Spec) ApplyEdits(ociSpec *oci.Spec) error {
e := ContainerEdits{&s.ContainerEdits} return s.edits().Apply(ociSpec)
return e.Apply(ociSpec) }
// edits returns the applicable global container edits for this spec.
func (s *Spec) edits() *ContainerEdits {
return &ContainerEdits{&s.ContainerEdits}
} }
// Validate the Spec. // Validate the Spec.
func (s *Spec) validate() (map[string]*Device, error) { func (s *Spec) validate() (map[string]*Device, error) {
if _, ok := validSpecVersions[s.Version]; !ok { if err := validateVersion(s.Version); err != nil {
return nil, errors.Errorf("invalid version %q", s.Version) return nil, err
} }
if err := ValidateVendorName(s.vendor); err != nil { if err := ValidateVendorName(s.vendor); err != nil {
return nil, err return nil, err
@ -135,8 +140,7 @@ func (s *Spec) validate() (map[string]*Device, error) {
if err := ValidateClassName(s.class); err != nil { if err := ValidateClassName(s.class); err != nil {
return nil, err return nil, err
} }
edits := &ContainerEdits{&s.ContainerEdits} if err := s.edits().Validate(); err != nil {
if err := edits.Validate(); err != nil {
return nil, err return nil, err
} }
@ -155,6 +159,15 @@ func (s *Spec) validate() (map[string]*Device, error) {
return devices, nil return devices, nil
} }
// validateVersion checks whether the specified spec version is supported.
func validateVersion(version string) error {
if _, ok := validSpecVersions[version]; !ok {
return errors.Errorf("invalid version %q", version)
}
return nil
}
// Parse raw CDI Spec file data. // Parse raw CDI Spec file data.
func parseSpec(data []byte) (*cdi.Spec, error) { func parseSpec(data []byte) (*cdi.Spec, error) {
raw := &cdi.Spec{} raw := &cdi.Spec{}

View File

@ -2,11 +2,13 @@ package specs
import "os" import "os"
// CurrentVersion is the current version of the Spec.
const CurrentVersion = "0.3.0"
// Spec is the base configuration for CDI // Spec is the base configuration for CDI
type Spec struct { type Spec struct {
Version string `json:"cdiVersion"` Version string `json:"cdiVersion"`
Kind string `json:"kind"` Kind string `json:"kind"`
ContainerRuntime []string `json:"containerRuntime,omitempty"`
Devices []Device `json:"devices"` Devices []Device `json:"devices"`
ContainerEdits ContainerEdits `json:"containerEdits,omitempty"` ContainerEdits ContainerEdits `json:"containerEdits,omitempty"`

2
vendor/modules.txt vendored
View File

@ -57,7 +57,7 @@ github.com/checkpoint-restore/go-criu/v5/rpc
github.com/checkpoint-restore/go-criu/v5/stats github.com/checkpoint-restore/go-criu/v5/stats
# github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e # github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/chzyer/readline github.com/chzyer/readline
# github.com/container-orchestrated-devices/container-device-interface v0.0.0-20220111162300-46367ec063fd # github.com/container-orchestrated-devices/container-device-interface v0.3.0
## explicit ## explicit
github.com/container-orchestrated-devices/container-device-interface/pkg/cdi github.com/container-orchestrated-devices/container-device-interface/pkg/cdi
github.com/container-orchestrated-devices/container-device-interface/specs-go github.com/container-orchestrated-devices/container-device-interface/specs-go