Merge pull request #7374 from soulseen/update-cdi-version

This commit is contained in:
Fu Wei 2022-09-07 13:37:41 +08:00 committed by GitHub
commit 99ee82d0b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 497 additions and 527 deletions

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20220723121359-e03a0069662f
github.com/Microsoft/go-winio v0.5.2
github.com/Microsoft/hcsshim v0.10.0-rc.1
github.com/container-orchestrated-devices/container-device-interface v0.3.1
github.com/container-orchestrated-devices/container-device-interface v0.5.1
github.com/containerd/aufs v1.0.0
github.com/containerd/btrfs v1.0.0
github.com/containerd/cgroups v1.0.5-0.20220816231112-7083cd60b721

4
go.sum
View File

@ -178,8 +178,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/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/container-orchestrated-devices/container-device-interface v0.3.1 h1:AvASKHHm6w3qMU49iPYyp8GhwbacvqjfGHUZEgvA/mQ=
github.com/container-orchestrated-devices/container-device-interface v0.3.1/go.mod h1:E1zcucIkq9P3eyNmY+68dBQsTcsXJh9cgRo2IVNScKQ=
github.com/container-orchestrated-devices/container-device-interface v0.5.1 h1:nXIUTrlEgGcA/n2geY3J7yyaGGhkocSlMkKPS4Qp4c0=
github.com/container-orchestrated-devices/container-device-interface v0.5.1/go.mod h1:ZToWfSyUH5l9Rk7/bjkUUkNLz4b1mE+CVUVafuikDPY=
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-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=

View File

@ -26,7 +26,7 @@ require (
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cilium/ebpf v0.9.1 // indirect
github.com/container-orchestrated-devices/container-device-interface v0.3.1 // indirect
github.com/container-orchestrated-devices/container-device-interface v0.5.1 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/containerd/fifo v1.0.0 // indirect
@ -35,6 +35,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect

View File

@ -182,8 +182,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/container-orchestrated-devices/container-device-interface v0.3.1 h1:AvASKHHm6w3qMU49iPYyp8GhwbacvqjfGHUZEgvA/mQ=
github.com/container-orchestrated-devices/container-device-interface v0.3.1/go.mod h1:E1zcucIkq9P3eyNmY+68dBQsTcsXJh9cgRo2IVNScKQ=
github.com/container-orchestrated-devices/container-device-interface v0.5.1 h1:nXIUTrlEgGcA/n2geY3J7yyaGGhkocSlMkKPS4Qp4c0=
github.com/container-orchestrated-devices/container-device-interface v0.5.1/go.mod h1:ZToWfSyUH5l9Rk7/bjkUUkNLz4b1mE+CVUVafuikDPY=
github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
@ -309,6 +309,7 @@ github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzP
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
@ -722,6 +723,7 @@ github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1 h1:9iF
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1/go.mod h1:K/JAU0m27RFhDRX4PcFdIKntROP6y5Ed6O91aZYDQfs=
github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
github.com/opencontainers/runc v1.0.3/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc=
github.com/opencontainers/runc v1.1.4 h1:nRCz/8sKg6K6jgYAFLDlXzPeITBZJyX28DBVhWD+5dg=
github.com/opencontainers/runc v1.1.4/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
@ -733,7 +735,6 @@ github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mo
github.com/opencontainers/runtime-tools v0.0.0-20190417131837-cd1349b7c47e h1:2Tg49TNXSTIsX8AAtmo1aQ1IbfnoUFzkOp7p2iWygtc=
github.com/opencontainers/runtime-tools v0.0.0-20190417131837-cd1349b7c47e/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/opencontainers/selinux v1.10.1 h1:09LIPVRP3uuZGQvgR+SgMSNBd1Eb3vlRbGqQpoHsF8w=
github.com/opencontainers/selinux v1.10.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
@ -815,6 +816,7 @@ github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZ
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=

View File

@ -22,6 +22,8 @@ import (
"strings"
"sync"
cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
"github.com/fsnotify/fsnotify"
"github.com/hashicorp/go-multierror"
oci "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
@ -33,30 +35,46 @@ type Option func(*Cache) error
// Cache stores CDI Specs loaded from Spec directories.
type Cache struct {
sync.Mutex
specDirs []string
specs map[string][]*Spec
devices map[string]*Device
errors map[string][]error
specDirs []string
specs map[string][]*Spec
devices map[string]*Device
errors map[string][]error
dirErrors map[string]error
autoRefresh bool
watch *watch
}
// WithAutoRefresh returns an option to control automatic Cache refresh.
// By default auto-refresh is enabled, the list of Spec directories are
// monitored and the Cache is automatically refreshed whenever a change
// is detected. This option can be used to disable this behavior when a
// manually refreshed mode is preferable.
func WithAutoRefresh(autoRefresh bool) Option {
return func(c *Cache) error {
c.autoRefresh = autoRefresh
return nil
}
}
// NewCache creates a new CDI Cache. The cache is populated from a set
// of CDI Spec directories. These can be specified using a WithSpecDirs
// option. The default set of directories is exposed in DefaultSpecDirs.
func NewCache(options ...Option) (*Cache, error) {
c := &Cache{}
if err := c.Configure(options...); err != nil {
return nil, err
}
if len(c.specDirs) == 0 {
c.Configure(WithSpecDirs(DefaultSpecDirs...))
c := &Cache{
autoRefresh: true,
watch: &watch{},
}
return c, c.Refresh()
WithSpecDirs(DefaultSpecDirs...)(c)
c.Lock()
defer c.Unlock()
return c, c.configure(options...)
}
// Configure applies options to the cache. Updates the cache if options have
// changed.
// Configure applies options to the Cache. Updates and refreshes the
// Cache if options have changed.
func (c *Cache) Configure(options ...Option) error {
if len(options) == 0 {
return nil
@ -65,17 +83,54 @@ func (c *Cache) Configure(options ...Option) error {
c.Lock()
defer c.Unlock()
return c.configure(options...)
}
// Configure the Cache. Start/stop CDI Spec directory watch, refresh
// the Cache if necessary.
func (c *Cache) configure(options ...Option) error {
var err error
for _, o := range options {
if err := o(c); err != nil {
if err = o(c); err != nil {
return errors.Wrapf(err, "failed to apply cache options")
}
}
c.dirErrors = make(map[string]error)
c.watch.stop()
if c.autoRefresh {
c.watch.setup(c.specDirs, c.dirErrors)
c.watch.start(&c.Mutex, c.refresh, c.dirErrors)
}
c.refresh()
return nil
}
// Refresh rescans the CDI Spec directories and refreshes the Cache.
// In manual refresh mode the cache is always refreshed. In auto-
// refresh mode the cache is only refreshed if it is out of date.
func (c *Cache) Refresh() error {
c.Lock()
defer c.Unlock()
// force a refresh in manual mode
if refreshed, err := c.refreshIfRequired(!c.autoRefresh); refreshed {
return err
}
// collect and return cached errors, much like refresh() does it
var result error
for _, err := range c.errors {
result = multierror.Append(result, err...)
}
return result
}
// Refresh the Cache by rescanning CDI Spec directories and files.
func (c *Cache) refresh() error {
var (
specs = map[string][]*Spec{}
devices = map[string]*Device{}
@ -135,9 +190,6 @@ func (c *Cache) Refresh() error {
delete(devices, conflict)
}
c.Lock()
defer c.Unlock()
c.specs = specs
c.devices = devices
c.errors = specErrors
@ -149,6 +201,17 @@ func (c *Cache) Refresh() error {
return nil
}
// RefreshIfRequired triggers a refresh if necessary.
func (c *Cache) refreshIfRequired(force bool) (bool, error) {
// We need to refresh if
// - it's forced by an explicitly call to Refresh() in manual mode
// - a missing Spec dir appears (added to watch) in auto-refresh mode
if force || (c.autoRefresh && c.watch.update(c.dirErrors)) {
return true, c.refresh()
}
return false, nil
}
// InjectDevices injects the given qualified devices to an OCI Spec. It
// returns any unresolvable devices and an error if injection fails for
// any of the devices.
@ -162,6 +225,8 @@ func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, e
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
edits := &ContainerEdits{}
specs := map[*Spec]struct{}{}
@ -190,11 +255,46 @@ func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, e
return nil, nil
}
// WriteSpec writes a Spec file with the given content. Priority is used
// as an index into the list of Spec directories to pick a directory for
// the file, adjusting for any under- or overflows. If name has a "json"
// or "yaml" extension it choses the encoding. Otherwise JSON encoding
// is used with a "json" extension.
func (c *Cache) WriteSpec(raw *cdi.Spec, name string) error {
var (
specDir string
path string
prio int
spec *Spec
err error
)
if len(c.specDirs) == 0 {
return errors.New("no Spec directories to write to")
}
prio = len(c.specDirs) - 1
specDir = c.specDirs[prio]
path = filepath.Join(specDir, name)
if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" {
path += ".json"
}
spec, err = NewSpec(raw, path, prio)
if err != nil {
return err
}
return spec.Write(true)
}
// GetDevice returns the cached device for the given qualified name.
func (c *Cache) GetDevice(device string) *Device {
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
return c.devices[device]
}
@ -205,6 +305,8 @@ func (c *Cache) ListDevices() []string {
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
for name := range c.devices {
devices = append(devices, name)
}
@ -220,6 +322,8 @@ func (c *Cache) ListVendors() []string {
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
for vendor := range c.specs {
vendors = append(vendors, vendor)
}
@ -238,6 +342,8 @@ func (c *Cache) ListClasses() []string {
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
for _, specs := range c.specs {
for _, spec := range specs {
cmap[spec.GetClass()] = struct{}{}
@ -256,6 +362,8 @@ func (c *Cache) GetVendorSpecs(vendor string) []*Spec {
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
return c.specs[vendor]
}
@ -268,12 +376,158 @@ func (c *Cache) GetSpecErrors(spec *Spec) []error {
// GetErrors returns all errors encountered during the last
// cache refresh.
func (c *Cache) GetErrors() map[string][]error {
return c.errors
c.Lock()
defer c.Unlock()
errors := map[string][]error{}
for path, errs := range c.errors {
errors[path] = errs
}
for path, err := range c.dirErrors {
errors[path] = []error{err}
}
return errors
}
// GetSpecDirectories returns the CDI Spec directories currently in use.
func (c *Cache) GetSpecDirectories() []string {
c.Lock()
defer c.Unlock()
dirs := make([]string, len(c.specDirs))
copy(dirs, c.specDirs)
return dirs
}
// GetSpecDirErrors returns any errors related to configured Spec directories.
func (c *Cache) GetSpecDirErrors() map[string]error {
if c.dirErrors == nil {
return nil
}
c.Lock()
defer c.Unlock()
errors := make(map[string]error)
for dir, err := range c.dirErrors {
errors[dir] = err
}
return errors
}
// Our fsnotify helper wrapper.
type watch struct {
watcher *fsnotify.Watcher
tracked map[string]bool
}
// Setup monitoring for the given Spec directories.
func (w *watch) setup(dirs []string, dirErrors map[string]error) {
var (
dir string
err error
)
w.tracked = make(map[string]bool)
for _, dir = range dirs {
w.tracked[dir] = false
}
w.watcher, err = fsnotify.NewWatcher()
if err != nil {
for _, dir := range dirs {
dirErrors[dir] = errors.Wrap(err, "failed to create watcher")
}
return
}
w.update(dirErrors)
}
// Start watching Spec directories for relevant changes.
func (w *watch) start(m *sync.Mutex, refresh func() error, dirErrors map[string]error) {
go w.watch(w.watcher, m, refresh, dirErrors)
}
// Stop watching directories.
func (w *watch) stop() {
if w.watcher == nil {
return
}
w.watcher.Close()
w.tracked = nil
}
// Watch Spec directory changes, triggering a refresh if necessary.
func (w *watch) watch(fsw *fsnotify.Watcher, m *sync.Mutex, refresh func() error, dirErrors map[string]error) {
watch := fsw
if watch == nil {
return
}
for {
select {
case event, ok := <-watch.Events:
if !ok {
return
}
if (event.Op & (fsnotify.Rename | fsnotify.Remove | fsnotify.Write)) == 0 {
continue
}
if event.Op == fsnotify.Write {
if ext := filepath.Ext(event.Name); ext != ".json" && ext != ".yaml" {
continue
}
}
m.Lock()
if event.Op == fsnotify.Remove && w.tracked[event.Name] {
w.update(dirErrors, event.Name)
} else {
w.update(dirErrors)
}
refresh()
m.Unlock()
case _, ok := <-watch.Errors:
if !ok {
return
}
}
}
}
// Update watch with pending/missing or removed directories.
func (w *watch) update(dirErrors map[string]error, removed ...string) bool {
var (
dir string
ok bool
err error
update bool
)
for dir, ok = range w.tracked {
if ok {
continue
}
err = w.watcher.Add(dir)
if err == nil {
w.tracked[dir] = true
delete(dirErrors, dir)
update = true
} else {
w.tracked[dir] = false
dirErrors[dir] = errors.Wrap(err, "failed to monitor for changes")
}
}
for _, dir = range removed {
w.tracked[dir] = false
dirErrors[dir] = errors.New("directory removed")
update = true
}
return update
}

View File

@ -85,11 +85,13 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error {
}
for _, d := range e.DeviceNodes {
dev := d.ToOCI()
if err := fillMissingInfo(&dev); err != nil {
dn := DeviceNode{d}
err := dn.fillMissingInfo()
if err != nil {
return err
}
dev := d.ToOCI()
if dev.UID == nil && spec.Process != nil {
if uid := spec.Process.User.UID; uid > 0 {
dev.UID = &uid
@ -288,26 +290,31 @@ func ensureOCIHooks(spec *oci.Spec) {
}
// 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)
func (d *DeviceNode) fillMissingInfo() error {
if d.HostPath == "" {
d.HostPath = d.Path
}
if dev.Type == "" {
dev.Type = string(hostDev.Type)
if d.Type != "" && (d.Major != 0 || d.Type == "p") {
return nil
}
hostDev, err := runc.DeviceFromPath(d.HostPath, "rwm")
if err != nil {
return errors.Wrapf(err, "failed to stat CDI host device %q", d.HostPath)
}
if d.Type == "" {
d.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 d.Type != string(hostDev.Type) {
return errors.Errorf("CDI device (%q, %q), host type mismatch (%s, %s)",
d.Path, d.HostPath, d.Type, string(hostDev.Type))
}
}
if dev.Major == 0 && dev.Type != "p" {
dev.Major = hostDev.Major
dev.Minor = hostDev.Minor
if d.Major == 0 && d.Type != "p" {
d.Major = hostDev.Major
d.Minor = hostDev.Minor
}
return nil

View File

@ -67,6 +67,21 @@
//
// Cache Refresh
//
// By default the CDI Spec cache monitors the configured Spec directories
// and automatically refreshes itself when necessary. This behavior can be
// disabled using the WithAutoRefresh(false) option.
//
// Failure to set up monitoring for a Spec directory causes the directory to
// get ignored and an error to be recorded among the Spec directory errors.
// These errors can be queried using the GetSpecDirErrors() function. If the
// error condition is transient, for instance a missing directory which later
// gets created, the corresponding error will be removed once the condition
// is over.
//
// With auto-refresh enabled injecting any CDI devices can be done without
// an explicit call to Refresh(), using a code snippet similar to the
// following:
//
// In a runtime implementation one typically wants to make sure the
// CDI Spec cache is up to date before performing device injection.
// A code snippet similar to the following accmplishes that:
@ -146,5 +161,5 @@
// schema names which switch the used schema to the in-repo validation
// schema embedded into the binary or the now default no-op schema
// correspondingly. Other names are interpreted as the path to the actual
/// validation schema to load and use.
// validation schema to load and use.
package cdi

View File

@ -130,7 +130,7 @@ func ValidateVendorName(vendor string) error {
}
}
if !isAlphaNumeric(rune(vendor[len(vendor)-1])) {
return errors.Errorf("invalid vendor %q, should end with letter", vendor)
return errors.Errorf("invalid vendor %q, should end with a letter or digit", vendor)
}
return nil
@ -158,7 +158,7 @@ func ValidateClassName(class string) error {
}
}
if !isAlphaNumeric(rune(class[len(class)-1])) {
return errors.Errorf("invalid class %q, should end with letter", class)
return errors.Errorf("invalid class %q, should end with a letter or digit", class)
}
return nil
}
@ -172,8 +172,11 @@ func ValidateDeviceName(name string) error {
if name == "" {
return errors.Errorf("invalid (empty) device name")
}
if !isLetter(rune(name[0])) {
return errors.Errorf("invalid name %q, should start with letter", name)
if !isAlphaNumeric(rune(name[0])) {
return errors.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 {
@ -185,7 +188,7 @@ func ValidateDeviceName(name string) error {
}
}
if !isAlphaNumeric(rune(name[len(name)-1])) {
return errors.Errorf("invalid name %q, should start with letter", name)
return errors.Errorf("invalid name %q, should end with a letter or digit", name)
}
return nil
}

View File

@ -19,6 +19,7 @@ package cdi
import (
"sync"
cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
oci "github.com/opencontainers/runtime-spec/specs-go"
)
@ -40,6 +41,8 @@ type Registry interface {
// RegistryRefresher is the registry interface for refreshing the
// cache of CDI Specs and devices.
//
// Configure reconfigures the registry with the given options.
//
// Refresh rescans all CDI Spec directories and updates the
// state of the cache to reflect any changes. It returns any
// errors encountered during the refresh.
@ -50,10 +53,15 @@ type Registry interface {
// GetSpecDirectories returns the set up CDI Spec directories
// currently in use. The directories are returned in the scan
// order of Refresh().
//
// GetSpecDirErrors returns any errors related to the configured
// Spec directories.
type RegistryRefresher interface {
Configure(...Option) error
Refresh() error
GetErrors() map[string][]error
GetSpecDirectories() []string
GetSpecDirErrors() map[string]error
}
// RegistryResolver is the registry interface for injecting CDI
@ -90,11 +98,15 @@ type RegistryDeviceDB interface {
//
// GetSpecErrors returns any errors for the Spec encountered during
// the last cache refresh.
//
// WriteSpec writes the Spec with the given content and name to the
// last Spec directory.
type RegistrySpecDB interface {
ListVendors() []string
ListClasses() []string
GetVendorSpecs(vendor string) []*Spec
GetSpecErrors(*Spec) []error
WriteSpec(raw *cdi.Spec, name string) error
}
type registry struct {

View File

@ -17,10 +17,10 @@
package cdi
import (
"errors"
"io/fs"
"os"
"path/filepath"
"github.com/pkg/errors"
)
const (
@ -45,10 +45,11 @@ var (
// WithSpecDirs returns an option to override the CDI Spec directories.
func WithSpecDirs(dirs ...string) Option {
return func(c *Cache) error {
c.specDirs = make([]string, len(dirs))
specDirs := make([]string, len(dirs))
for i, dir := range dirs {
c.specDirs[i] = filepath.Clean(dir)
specDirs[i] = filepath.Clean(dir)
}
c.specDirs = specDirs
return nil
}
}
@ -78,6 +79,9 @@ func scanSpecDirs(dirs []string, scanFn scanSpecFunc) error {
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
// for initial stat failure Walk calls us with nil info
if info == nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
}
return err
}
// first call from Walk is for dir itself, others we skip

View File

@ -17,6 +17,7 @@
package cdi
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
@ -34,7 +35,12 @@ var (
"0.1.0": {},
"0.2.0": {},
"0.3.0": {},
"0.4.0": {},
"0.5.0": {},
}
// Externally set CDI Spec validation function.
specValidator func(*cdi.Spec) error
)
// Spec represents a single CDI Spec. It is usually loaded from a
@ -64,7 +70,7 @@ func ReadSpec(path string, priority int) (*Spec, error) {
return nil, errors.Wrapf(err, "failed to read CDI Spec %q", path)
}
raw, err := parseSpec(data)
raw, err := ParseSpec(data)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse CDI Spec %q", path)
}
@ -85,7 +91,7 @@ func ReadSpec(path string, priority int) (*Spec, error) {
// priority. If Spec data validation fails NewSpec returns a nil
// Spec and an error.
func NewSpec(raw *cdi.Spec, path string, priority int) (*Spec, error) {
err := validateWithSchema(raw)
err := validateSpec(raw)
if err != nil {
return nil, err
}
@ -105,6 +111,56 @@ func NewSpec(raw *cdi.Spec, path string, priority int) (*Spec, error) {
return spec, nil
}
// Write the CDI Spec to the file associated with it during instantiation
// by NewSpec() or ReadSpec().
func (s *Spec) Write(overwrite bool) error {
var (
data []byte
dir string
tmp *os.File
err error
)
err = validateSpec(s.Spec)
if err != nil {
return err
}
if filepath.Ext(s.path) == ".yaml" {
data, err = yaml.Marshal(s.Spec)
} else {
data, err = json.Marshal(s.Spec)
}
if err != nil {
return errors.Wrap(err, "failed to marshal Spec file")
}
dir = filepath.Dir(s.path)
err = os.MkdirAll(dir, 0o755)
if err != nil {
return errors.Wrap(err, "failed to create Spec dir")
}
tmp, err = os.CreateTemp(dir, "spec.*.tmp")
if err != nil {
return errors.Wrap(err, "failed to create Spec file")
}
_, err = tmp.Write(data)
tmp.Close()
if err != nil {
return errors.Wrap(err, "failed to write Spec file")
}
err = renameIn(dir, filepath.Base(tmp.Name()), filepath.Base(s.path), overwrite)
if err != nil {
os.Remove(tmp.Name())
err = errors.Wrap(err, "failed to write Spec file")
}
return err
}
// GetVendor returns the vendor of this Spec.
func (s *Spec) GetVendor() string {
return s.vendor
@ -179,8 +235,8 @@ func validateVersion(version string) error {
return nil
}
// Parse raw CDI Spec file data.
func parseSpec(data []byte) (*cdi.Spec, error) {
// ParseSpec parses CDI Spec data into a raw CDI Spec.
func ParseSpec(data []byte) (*cdi.Spec, error) {
var raw *cdi.Spec
err := yaml.UnmarshalStrict(data, &raw)
if err != nil {
@ -188,3 +244,22 @@ func parseSpec(data []byte) (*cdi.Spec, error) {
}
return raw, nil
}
// SetSpecValidator sets a CDI Spec validator function. This function
// is used for extra CDI Spec content validation whenever a Spec file
// loaded (using ReadSpec() or NewSpec()) or written (Spec.Write()).
func SetSpecValidator(fn func(*cdi.Spec) error) {
specValidator = fn
}
// validateSpec validates the Spec using the extneral validator.
func validateSpec(raw *cdi.Spec) error {
if specValidator == nil {
return nil
}
err := specValidator(raw)
if err != nil {
return errors.Wrap(err, "Spec validation failed")
}
return nil
}

View File

@ -17,30 +17,32 @@
package cdi
import (
"github.com/container-orchestrated-devices/container-device-interface/schema"
cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
"os"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
const (
// DefaultExternalSchema is the JSON schema to load if found.
DefaultExternalSchema = "/etc/cdi/schema/schema.json"
)
// Rename src to dst, both relative to the directory dir. If dst already exists
// refuse renaming with an error unless overwrite is explicitly asked for.
func renameIn(dir, src, dst string, overwrite bool) error {
var flags uint
// SetSchema sets the Spec JSON validation schema to use.
func SetSchema(name string) error {
s, err := schema.Load(name)
dirf, err := os.Open(dir)
if err != nil {
return err
return errors.Wrap(err, "rename failed")
}
schema.Set(s)
defer dirf.Close()
if !overwrite {
flags = unix.RENAME_NOREPLACE
}
dirFd := int(dirf.Fd())
err = unix.Renameat2(dirFd, src, dirFd, dst, flags)
if err != nil {
return errors.Wrap(err, "rename failed")
}
return nil
}
// Validate CDI Spec against JSON Schema.
func validateWithSchema(raw *cdi.Spec) error {
return schema.ValidateType(raw)
}
func init() {
SetSchema(DefaultExternalSchema)
}

View File

@ -0,0 +1,39 @@
//go:build !linux
// +build !linux
/*
Copyright © 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 (
"os"
"path/filepath"
)
// Rename src to dst, both relative to the directory dir. If dst already exists
// refuse renaming with an error unless overwrite is explicitly asked for.
func renameIn(dir, src, dst string, overwrite bool) error {
src = filepath.Join(dir, src)
dst = filepath.Join(dir, dst)
_, err := os.Stat(dst)
if err == nil && !overwrite {
return os.ErrExist
}
return os.Rename(src, dst)
}

View File

@ -1,27 +0,0 @@
VALIDATE ?= ../bin/validate
SCHEMA ?= schema.json
test:
@FMT_RED=$$(tput setaf 1); \
FMT_BLUE=$$(tput setaf 12); \
FMT_CLEAR=$$(tput sgr0); \
echo "Running Good Tests"; \
for FILE in $$(ls "testdata/good"); do \
FILE_PATH="testdata/good/$${FILE}"; \
if $(VALIDATE) --schema "$(SCHEMA)" "$${FILE_PATH}" > /dev/null ; then \
printf '%s[OK]%s %s\n' "$${FMT_BLUE}" "$${FMT_CLEAR}" "$${FILE_PATH}"; \
else \
printf '%s[KO]%s %s\n' "$${FMT_RED}" "$${FMT_CLEAR}" "$${FILE_PATH}"; \
exit 1; \
fi \
done; \
echo "Running Bad Tests"; \
for FILE in $$(ls "testdata/bad"); do \
FILE_PATH="testdata/bad/$${FILE}"; \
if $(VALIDATE) --schema "$(SCHEMA)" "$${FILE_PATH}" > /dev/null ; then \
printf '%s[KO]%s %s\n' "$${FMT_RED}" "$${FMT_CLEAR}" "$${FILE_PATH}"; \
exit 1; \
else \
printf '%s[OK]%s %s\n' "$${FMT_BLUE}" "$${FMT_CLEAR}" "$${FILE_PATH}"; \
fi \
done

View File

@ -1,127 +0,0 @@
{
"description": "Definitions used throughout the Container Device Interface Specification",
"definitions": {
"uint32": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"int64": {
"type": "integer",
"minimum": -9223372036854775808,
"maximum": 9223372036854775807
},
"ArrayOfStrings": {
"type": "array",
"items": {
"type": "string"
}
},
"FilePath": {
"type": "string"
},
"Env": {
"$ref": "#/definitions/ArrayOfStrings"
},
"DeviceNode": {
"type": "object",
"properties": {
"path": {
"$ref": "#/definitions/FilePath"
},
"permissions": {
"type": "string"
},
"type": {
"type": "string"
},
"major": {
"$ref": "#/definitions/int64"
},
"minor": {
"$ref": "#/definitions/int64"
},
"uid": {
"$ref": "#/definitions/uint32"
},
"gid": {
"$ref": "#/definitions/uint32"
}
},
"required": [
"path"
]
},
"Mount": {
"type": "object",
"properties": {
"hostPath": {
"$ref": "#/definitions/FilePath"
},
"containerPath": {
"$ref": "#/definitions/FilePath"
},
"options": {
"type": "string"
}
},
"required": [
"hostPath",
"containerPath"
]
},
"Hook": {
"type": "object",
"properties": {
"hookName": {
"type": "string"
},
"path": {
"$ref": "#/definitions/FilePath"
},
"args": {
"$ref": "#/definitions/ArrayOfStrings"
},
"env": {
"$ref": "#/definitions/ArrayOfStrings"
},
"timeout": {
"$ref": "#/definitions/uint32"
}
},
"required": [
"hookName",
"path"
]
},
"containerEdits": {
"type": "object",
"properties": {
"env": {
"type": "array",
"items": {
"ref": "#definitions/Env"
}
},
"deviceNodes": {
"type": "array",
"items": {
"$ref": "#/definitions/DeviceNode"
}
},
"mounts": {
"type": "array",
"items": {
"$ref": "#/definitions/Mount"
}
},
"hooks": {
"type": "array",
"items": {
"$ref": "#/definitions/Hook"
}
}
}
}
}
}

View File

@ -1,253 +0,0 @@
/*
Copyright © 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 schema
import (
"bytes"
"embed"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"path/filepath"
"strings"
"sigs.k8s.io/yaml"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
schema "github.com/xeipuuv/gojsonschema"
)
const (
// BuiltinSchemaName references the builtin schema for Load()/Set().
BuiltinSchemaName = "builtin"
// NoneSchemaName references a disabled/NOP schema for Load()/Set().
NoneSchemaName = "none"
// DefaultSchemaName is the none schema.
DefaultSchemaName = NoneSchemaName
// builtinSchemaFile is the builtin schema URI in our embedded FS.
builtinSchemaFile = "file:///schema.json"
)
// Schema is a JSON validation schema.
type Schema struct {
schema *schema.Schema
}
// Error wraps a JSON validation result.
type Error struct {
Result *schema.Result
}
// Set sets the default validating JSON schema.
func Set(s *Schema) {
current = s
}
// Get returns the active validating JSON schema.
func Get() *Schema {
return current
}
// BuiltinSchema returns the builtin validating JSON Schema.
func BuiltinSchema() *Schema {
return builtin
}
// NopSchema returns an validating JSON Schema that does no real validation.
func NopSchema() *Schema {
return &Schema{}
}
// ReadAndValidate all data from the given reader, using the active schema for validation.
func ReadAndValidate(r io.Reader) ([]byte, error) {
return current.ReadAndValidate(r)
}
// Validate validates the data read from an io.Reader against the active schema.
func Validate(r io.Reader) error {
return current.Validate(r)
}
// ValidateData validates the given JSON document against the active schema.
func ValidateData(data []byte) error {
return current.ValidateData(data)
}
// ValidateFile validates the given JSON file against the active schema.
func ValidateFile(path string) error {
return current.ValidateFile(path)
}
// ValidateType validates a go object against the schema.
func ValidateType(obj interface{}) error {
return current.ValidateType(obj)
}
// Load the given JSON Schema.
func Load(source string) (*Schema, error) {
var (
loader schema.JSONLoader
err error
s *schema.Schema
)
source = strings.TrimSpace(source)
switch {
case source == BuiltinSchemaName:
return BuiltinSchema(), nil
case source == NoneSchemaName, source == "":
return NopSchema(), nil
case strings.HasPrefix(source, "file://"):
case strings.HasPrefix(source, "http://"):
case strings.HasPrefix(source, "https://"):
default:
if strings.Index(source, "://") < 0 {
source, err = filepath.Abs(source)
if err != nil {
return nil, errors.Wrapf(err,
"failed to get JSON schema absolute path for %s", source)
}
source = "file://" + source
}
}
loader = schema.NewReferenceLoader(source)
s, err = schema.NewSchema(loader)
if err != nil {
return nil, errors.Wrap(err, "failed to load JSON schema")
}
return &Schema{schema: s}, nil
}
// ReadAndValidate all data from the given reader, using the schema for validation.
func (s *Schema) ReadAndValidate(r io.Reader) ([]byte, error) {
loader, reader := schema.NewReaderLoader(r)
data, err := ioutil.ReadAll(reader)
if err != nil {
return nil, errors.Wrap(err, "failed to read data for validation")
}
return data, s.validate(loader)
}
// Validate validates the data read from an io.Reader against the schema.
func (s *Schema) Validate(r io.Reader) error {
_, err := s.ReadAndValidate(r)
return err
}
// ValidateData validates the given JSON data against the schema.
func (s *Schema) ValidateData(data []byte) error {
var (
any interface{}
err error
)
if !bytes.HasPrefix(bytes.TrimSpace(data), []byte{'{'}) {
err = yaml.Unmarshal(data, &any)
if err != nil {
return errors.Wrap(err, "failed to YAML unmarshal data for validation")
}
data, err = json.Marshal(any)
if err != nil {
return errors.Wrap(err, "failed to JSON remarshal data for validation")
}
}
return s.validate(schema.NewBytesLoader(data))
}
// ValidateFile validates the given JSON file against the schema.
func (s *Schema) ValidateFile(path string) error {
if filepath.Ext(path) == ".json" {
return s.validate(schema.NewReferenceLoader("file://" + path))
}
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
return s.ValidateData(data)
}
// ValidateType validates a go object against the schema.
func (s *Schema) ValidateType(obj interface{}) error {
l := schema.NewGoLoader(obj)
return s.validate(l)
}
// Validate the (to be) loaded doc against the schema.
func (s *Schema) validate(doc schema.JSONLoader) error {
if s == nil || s.schema == nil {
return nil
}
docErr, jsonErr := s.schema.Validate(doc)
if jsonErr != nil {
return errors.Wrap(jsonErr, "failed to load JSON data for validation")
}
if docErr.Valid() {
return nil
}
return &Error{Result: docErr}
}
// Error returns the given Result's error as a multierror(.Error()).
func (e *Error) Error() string {
if e == nil || e.Result == nil || e.Result.Valid() {
return ""
}
var multi error
for _, err := range e.Result.Errors() {
multi = multierror.Append(multi, errors.Errorf("%v", err))
}
return strings.TrimRight(multi.Error(), "\n")
}
var (
// our builtin schema
builtin *Schema
// currently loaded schema, builtin by default
current *Schema
)
//go:embed *.json
var builtinFS embed.FS
func init() {
s, err := schema.NewSchema(
schema.NewReferenceLoaderFileSystem(
builtinSchemaFile,
http.FS(builtinFS),
),
)
if err != nil {
builtin = NopSchema()
} else {
builtin = &Schema{schema: s}
}
current = builtin
}

View File

@ -1,39 +0,0 @@
{
"description": "Configuration Schema for the Container Device Interface",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"cdiVersion": {
"description": "The version of the Container Device Interface Specification that the document complies with",
"type": "string"
},
"kind": {
"description": "The kind of the device usually of the form 'vendor.com/device'",
"type": "string"
},
"devices": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "The name of the device",
"type": "string"
},
"containerEdits": {
"$ref": "defs.json#/definitions/containerEdits"
}
},
"required": [
"name",
"containerEdits"
]
}
}
},
"required": [
"cdiVersion",
"kind",
"devices"
]
}

View File

@ -3,7 +3,7 @@ package specs
import "os"
// CurrentVersion is the current version of the Spec.
const CurrentVersion = "0.3.0"
const CurrentVersion = "0.5.0"
// Spec is the base configuration for CDI
type Spec struct {
@ -31,6 +31,7 @@ type ContainerEdits struct {
// DeviceNode represents a device node that needs to be added to the OCI spec.
type DeviceNode struct {
Path string `json:"path"`
HostPath string `json:"hostPath,omitempty"`
Type string `json:"type,omitempty"`
Major int64 `json:"major,omitempty"`
Minor int64 `json:"minor,omitempty"`
@ -45,6 +46,7 @@ type Mount struct {
HostPath string `json:"hostPath"`
ContainerPath string `json:"containerPath"`
Options []string `json:"options,omitempty"`
Type string `json:"type,omitempty"`
}
// Hook represents a hook that needs to be added to the OCI spec.

View File

@ -95,6 +95,7 @@ func (m *Mount) ToOCI() spec.Mount {
Source: m.HostPath,
Destination: m.ContainerPath,
Options: m.Options,
Type: m.Type,
}
}

3
vendor/modules.txt vendored
View File

@ -77,10 +77,9 @@ github.com/cilium/ebpf/internal
github.com/cilium/ebpf/internal/sys
github.com/cilium/ebpf/internal/unix
github.com/cilium/ebpf/link
# github.com/container-orchestrated-devices/container-device-interface v0.3.1
# github.com/container-orchestrated-devices/container-device-interface v0.5.1
## explicit; go 1.17
github.com/container-orchestrated-devices/container-device-interface/pkg/cdi
github.com/container-orchestrated-devices/container-device-interface/schema
github.com/container-orchestrated-devices/container-device-interface/specs-go
# github.com/containerd/aufs v1.0.0
## explicit; go 1.13