Merge pull request #1239 from giuseppe/validate-cdi-devices

validate: ignore validation of CDI devices
This commit is contained in:
OpenShift Merge Robot 2022-11-23 04:55:47 -05:00 committed by GitHub
commit 32d0d9fc59
35 changed files with 4099 additions and 1087 deletions

View File

@ -4,6 +4,7 @@ go 1.17
require (
github.com/BurntSushi/toml v1.2.1
github.com/container-orchestrated-devices/container-device-interface v0.5.3
github.com/containerd/containerd v1.6.10
github.com/containernetworking/cni v1.1.2
github.com/containernetworking/plugins v1.1.1
@ -112,6 +113,7 @@ require (
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
retract (

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ import (
"strings"
"syscall"
"github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
units "github.com/docker/go-units"
)
@ -57,6 +58,9 @@ func (c *EngineConfig) validatePaths() error {
func (c *ContainersConfig) validateDevices() error {
for _, d := range c.Devices {
if cdi.IsQualifiedName(d) {
continue
}
_, _, _, err := Device(d)
if err != nil {
return err

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@ -0,0 +1,82 @@
/*
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 multierror
import (
"strings"
)
// New combines several errors into a single error. Parameters that are nil are
// ignored. If no errors are passed in or all parameters are nil, then the
// result is also nil.
func New(errors ...error) error {
// Filter out nil entries.
numErrors := 0
for _, err := range errors {
if err != nil {
errors[numErrors] = err
numErrors++
}
}
if numErrors == 0 {
return nil
}
return multiError(errors[0:numErrors])
}
// multiError is the underlying implementation used by New.
//
// Beware that a null multiError is not the same as a nil error.
type multiError []error
// multiError returns all individual error strings concatenated with "\n"
func (e multiError) Error() string {
var builder strings.Builder
for i, err := range e {
if i > 0 {
_, _ = builder.WriteString("\n")
}
_, _ = builder.WriteString(err.Error())
}
return builder.String()
}
// Append returns a new multi error all errors concatenated. Errors that are
// multi errors get flattened, nil is ignored.
func Append(err error, errors ...error) error {
var result multiError
if m, ok := err.(multiError); ok {
result = m
} else if err != nil {
result = append(result, err)
}
for _, e := range errors {
if e == nil {
continue
}
if m, ok := e.(multiError); ok {
result = append(result, m...)
} else {
result = append(result, e)
}
}
if len(result) == 0 {
return nil
}
return result
}

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

@ -0,0 +1,572 @@
/*
Copyright © 2021 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 (
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"sync"
stderr "errors"
"github.com/container-orchestrated-devices/container-device-interface/internal/multierror"
cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
"github.com/fsnotify/fsnotify"
oci "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
// Option is an option to change some aspect of default CDI behavior.
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
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{
autoRefresh: true,
watch: &watch{},
}
WithSpecDirs(DefaultSpecDirs...)(c)
c.Lock()
defer c.Unlock()
return c, c.configure(options...)
}
// 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
}
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 {
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 _, errors := range c.errors {
result = multierror.Append(result, errors...)
}
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{}
conflicts = map[string]struct{}{}
specErrors = map[string][]error{}
result []error
)
// collect errors per spec file path and once globally
collectError := func(err error, paths ...string) {
result = append(result, err)
for _, path := range paths {
specErrors[path] = append(specErrors[path], err)
}
}
// resolve conflicts based on device Spec priority (order of precedence)
resolveConflict := func(name string, dev *Device, old *Device) bool {
devSpec, oldSpec := dev.GetSpec(), old.GetSpec()
devPrio, oldPrio := devSpec.GetPriority(), oldSpec.GetPriority()
switch {
case devPrio > oldPrio:
return false
case devPrio == oldPrio:
devPath, oldPath := devSpec.GetPath(), oldSpec.GetPath()
collectError(errors.Errorf("conflicting device %q (specs %q, %q)",
name, devPath, oldPath), devPath, oldPath)
conflicts[name] = struct{}{}
}
return true
}
_ = scanSpecDirs(c.specDirs, func(path string, priority int, spec *Spec, err error) error {
path = filepath.Clean(path)
if err != nil {
collectError(errors.Wrapf(err, "failed to load CDI Spec"), path)
return nil
}
vendor := spec.GetVendor()
specs[vendor] = append(specs[vendor], spec)
for _, dev := range spec.devices {
qualified := dev.GetQualifiedName()
other, ok := devices[qualified]
if ok {
if resolveConflict(qualified, dev, other) {
continue
}
}
devices[qualified] = dev
}
return nil
})
for conflict := range conflicts {
delete(devices, conflict)
}
c.specs = specs
c.devices = devices
c.errors = specErrors
return multierror.New(result...)
}
// 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.
func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, error) {
var unresolved []string
if ociSpec == nil {
return devices, errors.Errorf("can't inject devices, nil OCI Spec")
}
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
edits := &ContainerEdits{}
specs := map[*Spec]struct{}{}
for _, device := range devices {
d := c.devices[device]
if d == nil {
unresolved = append(unresolved, device)
continue
}
if _, ok := specs[d.GetSpec()]; !ok {
specs[d.GetSpec()] = struct{}{}
edits.Append(d.GetSpec().edits())
}
edits.Append(d.edits())
}
if unresolved != nil {
return unresolved, errors.Errorf("unresolvable CDI devices %s",
strings.Join(devices, ", "))
}
if err := edits.Apply(ociSpec); err != nil {
return nil, errors.Wrap(err, "failed to inject devices")
}
return nil, nil
}
// highestPrioritySpecDir returns the Spec directory with highest priority
// and its priority.
func (c *Cache) highestPrioritySpecDir() (string, int) {
if len(c.specDirs) == 0 {
return "", -1
}
prio := len(c.specDirs) - 1
dir := c.specDirs[prio]
return dir, prio
}
// WriteSpec writes a Spec file with the given content into the highest
// priority Spec directory. If name has a "json" or "yaml" extension it
// choses the encoding. Otherwise the default YAML encoding is used.
func (c *Cache) WriteSpec(raw *cdi.Spec, name string) error {
var (
specDir string
path string
prio int
spec *Spec
err error
)
specDir, prio = c.highestPrioritySpecDir()
if specDir == "" {
return errors.New("no Spec directories to write to")
}
path = filepath.Join(specDir, name)
if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" {
path += defaultSpecExt
}
spec, err = newSpec(raw, path, prio)
if err != nil {
return err
}
return spec.write(true)
}
// RemoveSpec removes a Spec with the given name from the highest
// priority Spec directory. This function can be used to remove a
// Spec previously written by WriteSpec(). If the file exists and
// its removal fails RemoveSpec returns an error.
func (c *Cache) RemoveSpec(name string) error {
var (
specDir string
path string
err error
)
specDir, _ = c.highestPrioritySpecDir()
if specDir == "" {
return errors.New("no Spec directories to remove from")
}
path = filepath.Join(specDir, name)
if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" {
path += defaultSpecExt
}
err = os.Remove(path)
if err != nil && stderr.Is(err, fs.ErrNotExist) {
err = nil
}
return err
}
// 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]
}
// ListDevices lists all cached devices by qualified name.
func (c *Cache) ListDevices() []string {
var devices []string
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
for name := range c.devices {
devices = append(devices, name)
}
sort.Strings(devices)
return devices
}
// ListVendors lists all vendors known to the cache.
func (c *Cache) ListVendors() []string {
var vendors []string
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
for vendor := range c.specs {
vendors = append(vendors, vendor)
}
sort.Strings(vendors)
return vendors
}
// ListClasses lists all device classes known to the cache.
func (c *Cache) ListClasses() []string {
var (
cmap = map[string]struct{}{}
classes []string
)
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
for _, specs := range c.specs {
for _, spec := range specs {
cmap[spec.GetClass()] = struct{}{}
}
}
for class := range cmap {
classes = append(classes, class)
}
sort.Strings(classes)
return classes
}
// GetVendorSpecs returns all specs for the given vendor.
func (c *Cache) GetVendorSpecs(vendor string) []*Spec {
c.Lock()
defer c.Unlock()
c.refreshIfRequired(false)
return c.specs[vendor]
}
// GetSpecErrors returns all errors encountered for the spec during the
// last cache refresh.
func (c *Cache) GetSpecErrors(spec *Spec) []error {
return c.errors[spec.GetPath()]
}
// GetErrors returns all errors encountered during the last
// cache refresh.
func (c *Cache) GetErrors() map[string][]error {
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

@ -0,0 +1,26 @@
//go:build !windows
// +build !windows
/*
Copyright © 2021 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 "syscall"
func osSync() {
syscall.Sync()
}

View File

@ -0,0 +1,22 @@
//go:build windows
// +build windows
/*
Copyright © 2021 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
func osSync() {}

View File

@ -0,0 +1,331 @@
/*
Copyright © 2021 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"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/container-orchestrated-devices/container-device-interface/specs-go"
oci "github.com/opencontainers/runtime-spec/specs-go"
ocigen "github.com/opencontainers/runtime-tools/generate"
)
const (
// PrestartHook is the name of the OCI "prestart" hook.
PrestartHook = "prestart"
// CreateRuntimeHook is the name of the OCI "createRuntime" hook.
CreateRuntimeHook = "createRuntime"
// CreateContainerHook is the name of the OCI "createContainer" hook.
CreateContainerHook = "createContainer"
// StartContainerHook is the name of the OCI "startContainer" hook.
StartContainerHook = "startContainer"
// PoststartHook is the name of the OCI "poststart" hook.
PoststartHook = "poststart"
// PoststopHook is the name of the OCI "poststop" hook.
PoststopHook = "poststop"
)
var (
// Names of recognized hooks.
validHookNames = map[string]struct{}{
PrestartHook: {},
CreateRuntimeHook: {},
CreateContainerHook: {},
StartContainerHook: {},
PoststartHook: {},
PoststopHook: {},
}
)
// ContainerEdits represent updates to be applied to an OCI Spec.
// These updates can be specific to a CDI device, or they can be
// specific to a CDI Spec. In the former case these edits should
// be applied to all OCI Specs where the corresponding CDI device
// is injected. In the latter case, these edits should be applied
// to all OCI Specs where at least one devices from the CDI Spec
// is injected.
type ContainerEdits struct {
*specs.ContainerEdits
}
// Apply edits to the given OCI Spec. Updates the OCI Spec in place.
// Returns an error if the update fails.
func (e *ContainerEdits) Apply(spec *oci.Spec) error {
if spec == nil {
return errors.New("can't edit nil OCI Spec")
}
if e == nil || e.ContainerEdits == nil {
return nil
}
specgen := ocigen.NewFromSpec(spec)
if len(e.Env) > 0 {
specgen.AddMultipleProcessEnv(e.Env)
}
for _, d := range e.DeviceNodes {
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
}
}
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)
}
}
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 {
switch h.HookName {
case PrestartHook:
specgen.AddPreStartHook(h.ToOCI())
case PoststartHook:
specgen.AddPostStartHook(h.ToOCI())
case PoststopHook:
specgen.AddPostStopHook(h.ToOCI())
// TODO: Maybe runtime-tools/generate should be updated with these...
case CreateRuntimeHook:
ensureOCIHooks(spec)
spec.Hooks.CreateRuntime = append(spec.Hooks.CreateRuntime, h.ToOCI())
case CreateContainerHook:
ensureOCIHooks(spec)
spec.Hooks.CreateContainer = append(spec.Hooks.CreateContainer, h.ToOCI())
case StartContainerHook:
ensureOCIHooks(spec)
spec.Hooks.StartContainer = append(spec.Hooks.StartContainer, h.ToOCI())
default:
return errors.Errorf("unknown hook name %q", h.HookName)
}
}
return nil
}
// Validate container edits.
func (e *ContainerEdits) Validate() error {
if e == nil || e.ContainerEdits == nil {
return nil
}
if err := ValidateEnv(e.Env); err != nil {
return errors.Wrap(err, "invalid container edits")
}
for _, d := range e.DeviceNodes {
if err := (&DeviceNode{d}).Validate(); err != nil {
return err
}
}
for _, h := range e.Hooks {
if err := (&Hook{h}).Validate(); err != nil {
return err
}
}
for _, m := range e.Mounts {
if err := (&Mount{m}).Validate(); err != nil {
return err
}
}
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
// global Spec context but invalid in a Device context.
func (e *ContainerEdits) isEmpty() bool {
if e == nil {
return false
}
return len(e.Env)+len(e.DeviceNodes)+len(e.Hooks)+len(e.Mounts) == 0
}
// ValidateEnv validates the given environment variables.
func ValidateEnv(env []string) error {
for _, v := range env {
if strings.IndexByte(v, byte('=')) <= 0 {
return errors.Errorf("invalid environment variable %q", v)
}
}
return nil
}
// DeviceNode is a CDI Spec DeviceNode wrapper, used for validating DeviceNodes.
type DeviceNode struct {
*specs.DeviceNode
}
// Validate a CDI Spec DeviceNode.
func (d *DeviceNode) Validate() error {
validTypes := map[string]struct{}{
"": {},
"b": {},
"c": {},
"u": {},
"p": {},
}
if d.Path == "" {
return errors.New("invalid (empty) device path")
}
if _, ok := validTypes[d.Type]; !ok {
return errors.Errorf("device %q: invalid type %q", d.Path, d.Type)
}
for _, bit := range d.Permissions {
if bit != 'r' && bit != 'w' && bit != 'm' {
return errors.Errorf("device %q: invalid persmissions %q",
d.Path, d.Permissions)
}
}
return nil
}
// Hook is a CDI Spec Hook wrapper, used for validating hooks.
type Hook struct {
*specs.Hook
}
// Validate a hook.
func (h *Hook) Validate() error {
if _, ok := validHookNames[h.HookName]; !ok {
return errors.Errorf("invalid hook name %q", h.HookName)
}
if h.Path == "" {
return errors.Errorf("invalid hook %q with empty path", h.HookName)
}
if err := ValidateEnv(h.Env); err != nil {
return errors.Wrapf(err, "invalid hook %q", h.HookName)
}
return nil
}
// Mount is a CDI Mount wrapper, used for validating mounts.
type Mount struct {
*specs.Mount
}
// Validate a mount.
func (m *Mount) Validate() error {
if m.HostPath == "" {
return errors.New("invalid mount, empty host path")
}
if m.ContainerPath == "" {
return errors.New("invalid mount, empty container path")
}
return nil
}
// Ensure OCI Spec hooks are not nil so we can add hooks.
func ensureOCIHooks(spec *oci.Spec) {
if spec.Hooks == nil {
spec.Hooks = &oci.Hooks{}
}
}
// 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

@ -0,0 +1,56 @@
//go:build !windows
// +build !windows
/*
Copyright © 2021 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 (
runc "github.com/opencontainers/runc/libcontainer/devices"
"github.com/pkg/errors"
)
// fillMissingInfo fills in missing mandatory attributes from the host device.
func (d *DeviceNode) fillMissingInfo() error {
if d.HostPath == "" {
d.HostPath = d.Path
}
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 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 d.Major == 0 && d.Type != "p" {
d.Major = hostDev.Major
d.Minor = hostDev.Minor
}
return nil
}

View File

@ -0,0 +1,27 @@
//go:build windows
// +build windows
/*
Copyright © 2021 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 "fmt"
// fillMissingInfo fills in missing mandatory attributes from the host device.
func (d *DeviceNode) fillMissingInfo() error {
return fmt.Errorf("unimplemented")
}

View File

@ -0,0 +1,78 @@
/*
Copyright © 2021 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 (
cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
oci "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
// Device represents a CDI device of a Spec.
type Device struct {
*cdi.Device
spec *Spec
}
// Create a new Device, associate it with the given Spec.
func newDevice(spec *Spec, d cdi.Device) (*Device, error) {
dev := &Device{
Device: &d,
spec: spec,
}
if err := dev.validate(); err != nil {
return nil, err
}
return dev, nil
}
// GetSpec returns the Spec this device is defined in.
func (d *Device) GetSpec() *Spec {
return d.spec
}
// GetQualifiedName returns the qualified name for this device.
func (d *Device) GetQualifiedName() string {
return QualifiedName(d.spec.GetVendor(), d.spec.GetClass(), d.Name)
}
// ApplyEdits applies the device-speific container edits to an OCI Spec.
func (d *Device) ApplyEdits(ociSpec *oci.Spec) error {
return d.edits().Apply(ociSpec)
}
// edits returns the applicable container edits for this spec.
func (d *Device) edits() *ContainerEdits {
return &ContainerEdits{&d.ContainerEdits}
}
// Validate the device.
func (d *Device) validate() error {
if err := ValidateDeviceName(d.Name); err != nil {
return err
}
edits := d.edits()
if edits.isEmpty() {
return errors.Errorf("invalid device, empty device edits")
}
if err := edits.Validate(); err != nil {
return errors.Wrapf(err, "invalid device %q", d.Name)
}
return nil
}

View File

@ -0,0 +1,274 @@
// Package cdi has the primary purpose of providing an API for
// interacting with CDI and consuming CDI devices.
//
// For more information about Container Device Interface, please refer to
// https://github.com/container-orchestrated-devices/container-device-interface
//
// Container Device Interface
//
// Container Device Interface, or CDI for short, provides comprehensive
// third party device support for container runtimes. CDI uses vendor
// provided specification files, CDI Specs for short, to describe how a
// container's runtime environment should be modified when one or more
// of the vendor-specific devices is injected into the container. Beyond
// describing the low level platform-specific details of how to gain
// basic access to a device, CDI Specs allow more fine-grained device
// initialization, and the automatic injection of any necessary vendor-
// or device-specific software that might be required for a container
// to use a device or take full advantage of it.
//
// In the CDI device model containers request access to a device using
// fully qualified device names, qualified names for short, consisting of
// a vendor identifier, a device class and a device name or identifier.
// These pieces of information together uniquely identify a device among
// all device vendors, classes and device instances.
//
// This package implements an API for easy consumption of CDI. The API
// implements discovery, loading and caching of CDI Specs and injection
// of CDI devices into containers. This is the most common functionality
// the vast majority of CDI consumers need. The API should be usable both
// by OCI runtime clients and runtime implementations.
//
// CDI Registry
//
// The primary interface to interact with CDI devices is the Registry. It
// is essentially a cache of all Specs and devices discovered in standard
// CDI directories on the host. The registry has two main functionality,
// injecting devices into an OCI Spec and refreshing the cache of CDI
// Specs and devices.
//
// Device Injection
//
// Using the Registry one can inject CDI devices into a container with code
// similar to the following snippet:
//
// import (
// "fmt"
// "strings"
//
// "github.com/pkg/errors"
// log "github.com/sirupsen/logrus"
//
// "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
// oci "github.com/opencontainers/runtime-spec/specs-go"
// )
//
// func injectCDIDevices(spec *oci.Spec, devices []string) error {
// log.Debug("pristine OCI Spec: %s", dumpSpec(spec))
//
// unresolved, err := cdi.GetRegistry().InjectDevices(spec, devices)
// if err != nil {
// return errors.Wrap(err, "CDI device injection failed")
// }
//
// log.Debug("CDI-updated OCI Spec: %s", dumpSpec(spec))
// return nil
// }
//
// 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:
//
// import (
// "fmt"
// "strings"
//
// "github.com/pkg/errors"
// log "github.com/sirupsen/logrus"
//
// "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
// oci "github.com/opencontainers/runtime-spec/specs-go"
// )
//
// func injectCDIDevices(spec *oci.Spec, devices []string) error {
// registry := cdi.GetRegistry()
//
// if err := registry.Refresh(); err != nil {
// // Note:
// // It is up to the implementation to decide whether
// // to abort injection on errors. A failed Refresh()
// // does not necessarily render the registry unusable.
// // For instance, a parse error in a Spec file for
// // vendor A does not have any effect on devices of
// // vendor B...
// log.Warnf("pre-injection Refresh() failed: %v", err)
// }
//
// log.Debug("pristine OCI Spec: %s", dumpSpec(spec))
//
// unresolved, err := registry.InjectDevices(spec, devices)
// if err != nil {
// return errors.Wrap(err, "CDI device injection failed")
// }
//
// log.Debug("CDI-updated OCI Spec: %s", dumpSpec(spec))
// return nil
// }
//
// Generated Spec Files, Multiple Directories, Device Precedence
//
// It is often necessary to generate Spec files dynamically. On some
// systems the available or usable set of CDI devices might change
// dynamically which then needs to be reflected in CDI Specs. For
// some device classes it makes sense to enumerate the available
// devices at every boot and generate Spec file entries for each
// device found. Some CDI devices might need special client- or
// request-specific configuration which can only be fulfilled by
// dynamically generated client-specific entries in transient Spec
// files.
//
// CDI can collect Spec files from multiple directories. Spec files are
// automatically assigned priorities according to which directory they
// 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
// Spec file with the highest priority.
//
// The default CDI directory configuration is chosen to encourage
// separating dynamically generated CDI Spec files from static ones.
// The default directories are '/etc/cdi' and '/var/run/cdi'. By putting
// dynamically generated Spec files under '/var/run/cdi', those take
// precedence over static ones in '/etc/cdi'. With this scheme, static
// Spec files, typically installed by distro-specific packages, go into
// '/etc/cdi' while all the dynamically generated Spec files, transient
// or other, go into '/var/run/cdi'.
//
// Spec File Generation
//
// CDI offers two functions for writing and removing dynamically generated
// Specs from CDI Spec directories. These functions, WriteSpec() and
// RemoveSpec() implicitly follow the principle of separating dynamic Specs
// from the rest and therefore always write to and remove Specs from the
// last configured directory.
//
// Corresponding functions are also provided for generating names for Spec
// files. These functions follow a simple naming convention to ensure that
// multiple entities generating Spec files simultaneously on the same host
// do not end up using conflicting Spec file names. GenerateSpecName(),
// GenerateNameForSpec(), GenerateTransientSpecName(), and
// GenerateTransientNameForSpec() all generate names which can be passed
// as such to WriteSpec() and subsequently to RemoveSpec().
//
// Generating a Spec file for a vendor/device class can be done with a
// code snippet similar to the following:
//
// import (
// "fmt"
// ...
// "github.com/container-orchestrated-devices/container-device-interface/specs-go"
// "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
// )
//
// func generateDeviceSpecs() error {
// registry := cdi.GetRegistry()
// spec := &specs.Spec{
// Version: specs.CurrentVersion,
// Kind: vendor+"/"+class,
// }
//
// for _, dev := range enumerateDevices() {
// spec.Devices = append(spec.Devices, specs.Device{
// Name: dev.Name,
// ContainerEdits: getContainerEditsForDevice(dev),
// })
// }
//
// specName, err := cdi.GenerateNameForSpec(spec)
// if err != nil {
// return fmt.Errorf("failed to generate Spec name: %w", err)
// }
//
// return registry.WriteSpec(spec, specName)
// }
//
// Similary, 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
// and removed once that container is removed.
//
// import (
// "fmt"
// ...
// "github.com/container-orchestrated-devices/container-device-interface/specs-go"
// "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
// )
//
// func generateTransientSpec(ctr Container) error {
// registry := cdi.GetRegistry()
// devices := getContainerDevs(ctr, vendor, class)
// spec := &specs.Spec{
// Version: specs.CurrentVersion,
// Kind: vendor+"/"+class,
// }
//
// for _, dev := range devices {
// spec.Devices = append(spec.Devices, specs.Device{
// // the generated name needs to be unique within the
// // vendor/class domain on the host/node.
// Name: generateUniqueDevName(dev, ctr),
// ContainerEdits: getEditsForContainer(dev),
// })
// }
//
// // transientID is expected to guarantee that the Spec file name
// // generated using <vendor, class, transientID> is unique within
// // the host/node. If more than one device is allocated with the
// // same vendor/class domain, either all generated Spec entries
// // should go to a single Spec file (like in this sample snippet),
// // or transientID should be unique for each generated Spec file.
// transientID := getSomeSufficientlyUniqueIDForContainer(ctr)
// specName, err := cdi.GenerateNameForTransientSpec(vendor, class, transientID)
// if err != nil {
// return fmt.Errorf("failed to generate Spec name: %w", err)
// }
//
// return registry.WriteSpec(spec, specName)
// }
//
// func removeTransientSpec(ctr Container) error {
// registry := cdi.GetRegistry()
// transientID := getSomeSufficientlyUniqueIDForContainer(ctr)
// specName := cdi.GenerateNameForTransientSpec(vendor, class, transientID)
//
// return registry.RemoveSpec(specName)
// }
//
// CDI Spec Validation
//
// This package performs both syntactic and semantic validation of CDI
// Spec file data when a Spec file is loaded via the registry or using
// the ReadSpec API function. As part of the semantic verification, the
// Spec file is verified against the CDI Spec JSON validation schema.
//
// If a valid externally provided JSON validation schema is found in
// the filesystem at /etc/cdi/schema/schema.json it is loaded and used
// as the default validation schema. If such a file is not found or
// fails to load, an embedded no-op schema is used.
//
// The used validation schema can also be changed programmatically using
// the SetSchema API convenience function. This function also accepts
// the special "builtin" (BuiltinSchemaName) and "none" (NoneSchemaName)
// 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.
package cdi

View File

@ -0,0 +1,206 @@
/*
Copyright © 2021 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"
)
// QualifiedName returns the qualified name for a device.
// The syntax for a qualified device names is
// "<vendor>/<class>=<name>".
// 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', '-', '_', '.', ':'
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, errors.Errorf("unqualified device %q, missing vendor", device)
}
if class == "" {
return "", "", device, errors.Errorf("unqualified device %q, missing class", device)
}
if name == "" {
return "", "", device, errors.Errorf("unqualified device %q, missing device name", device)
}
if err := ValidateVendorName(vendor); err != nil {
return "", "", device, errors.Wrapf(err, "invalid device %q", device)
}
if err := ValidateClassName(class); err != nil {
return "", "", device, errors.Wrapf(err, "invalid device %q", device)
}
if err := ValidateDeviceName(name); err != nil {
return "", "", device, errors.Wrapf(err, "invalid device %q", device)
}
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
// "<vendor>/<class>"
// 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 {
if vendor == "" {
return errors.Errorf("invalid (empty) vendor name")
}
if !isLetter(rune(vendor[0])) {
return errors.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 errors.Errorf("invalid character '%c' in vendor name %q",
c, vendor)
}
}
if !isAlphaNumeric(rune(vendor[len(vendor)-1])) {
return errors.Errorf("invalid vendor %q, should end with a letter or digit", vendor)
}
return nil
}
// 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 ('_', '-')
func ValidateClassName(class string) error {
if class == "" {
return errors.Errorf("invalid (empty) device class")
}
if !isLetter(rune(class[0])) {
return errors.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 errors.Errorf("invalid character '%c' in device class %q",
c, class)
}
}
if !isAlphaNumeric(rune(class[len(class)-1])) {
return errors.Errorf("invalid class %q, should end with a letter or digit", class)
}
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 errors.Errorf("invalid (empty) device 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 {
case isAlphaNumeric(c):
case c == '_' || c == '-' || c == '.' || c == ':':
default:
return errors.Errorf("invalid character '%c' in device name %q",
c, name)
}
}
if !isAlphaNumeric(rune(name[len(name)-1])) {
return errors.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)
}

View File

@ -0,0 +1,152 @@
/*
Copyright © 2021 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 (
"sync"
cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
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
DeviceDB() RegistryDeviceDB
SpecDB() RegistrySpecDB
}
// 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.
//
// GetErrors returns all errors encountered for any of the scanned
// Spec files during the last cache refresh.
//
// 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
// devices into an OCI Spec.
//
// InjectDevices takes an OCI Spec and injects into it a set of
// CDI devices given by qualified name. It returns the names of
// any unresolved devices and an error if injection fails.
type RegistryResolver interface {
InjectDevices(spec *oci.Spec, device ...string) (unresolved []string, err error)
}
// RegistryDeviceDB is the registry interface for querying devices.
//
// GetDevice returns the CDI device for the given qualified name. If
// the device is not GetDevice returns nil.
//
// ListDevices returns a slice with the names of qualified device
// known. The returned slice is sorted.
type RegistryDeviceDB interface {
GetDevice(device string) *Device
ListDevices() []string
}
// RegistrySpecDB is the registry interface for querying CDI Specs.
//
// ListVendors returns a slice with all vendors known. The returned
// slice is sorted.
//
// ListClasses returns a slice with all classes known. The returned
// slice is sorted.
//
// GetVendorSpecs returns a slice of all Specs for the vendor.
//
// 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
RemoveSpec(name string) error
}
type registry struct {
*Cache
}
var _ Registry = &registry{}
var (
reg *registry
initOnce sync.Once
)
// GetRegistry returns the CDI registry. If any options are given, those
// are applied to the registry.
func GetRegistry(options ...Option) Registry {
var new bool
initOnce.Do(func() {
reg, _ = getRegistry(options...)
new = true
})
if !new && len(options) > 0 {
reg.Configure(options...)
reg.Refresh()
}
return reg
}
// DeviceDB returns the registry interface for querying devices.
func (r *registry) DeviceDB() RegistryDeviceDB {
return r
}
// SpecDB returns the registry interface for querying Specs.
func (r *registry) SpecDB() RegistrySpecDB {
return r
}
func getRegistry(options ...Option) (*registry, error) {
c, err := NewCache(options...)
return &registry{c}, err
}

View File

@ -0,0 +1,114 @@
/*
Copyright © 2021 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 (
"errors"
"io/fs"
"os"
"path/filepath"
)
const (
// DefaultStaticDir is the default directory for static CDI Specs.
DefaultStaticDir = "/etc/cdi"
// DefaultDynamicDir is the default directory for generated CDI Specs
DefaultDynamicDir = "/var/run/cdi"
)
var (
// DefaultSpecDirs is the default Spec directory configuration.
// While altering this variable changes the package defaults,
// the preferred way of overriding the default directories is
// to use a WithSpecDirs options. Otherwise the change is only
// effective if it takes place before creating the Registry or
// other Cache instances.
DefaultSpecDirs = []string{DefaultStaticDir, DefaultDynamicDir}
// ErrStopScan can be returned from a ScanSpecFunc to stop the scan.
ErrStopScan = errors.New("stop Spec scan")
)
// WithSpecDirs returns an option to override the CDI Spec directories.
func WithSpecDirs(dirs ...string) Option {
return func(c *Cache) error {
specDirs := make([]string, len(dirs))
for i, dir := range dirs {
specDirs[i] = filepath.Clean(dir)
}
c.specDirs = specDirs
return nil
}
}
// scanSpecFunc is a function for processing CDI Spec files.
type scanSpecFunc func(string, int, *Spec, error) error
// ScanSpecDirs scans the given directories looking for CDI Spec files,
// which are all files with a '.json' or '.yaml' suffix. For every Spec
// file discovered, ScanSpecDirs loads a Spec from the file then calls
// the scan function passing it the path to the file, the priority (the
// index of the directory in the slice of directories given), the Spec
// itself, and any error encountered while loading the Spec.
//
// Scanning stops once all files have been processed or when the scan
// function returns an error. The result of ScanSpecDirs is the error
// returned by the scan function, if any. The special error ErrStopScan
// can be used to terminate the scan gracefully without ScanSpecDirs
// returning an error. ScanSpecDirs silently skips any subdirectories.
func scanSpecDirs(dirs []string, scanFn scanSpecFunc) error {
var (
spec *Spec
err error
)
for priority, dir := range dirs {
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
if info.IsDir() {
if path == dir {
return nil
}
return filepath.SkipDir
}
// ignore obviously non-Spec files
if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" {
return nil
}
if err != nil {
return scanFn(path, priority, nil, err)
}
spec, err = ReadSpec(path, priority)
return scanFn(path, priority, spec, err)
})
if err != nil && err != ErrStopScan {
return err
}
}
return nil
}

View File

@ -0,0 +1,350 @@
/*
Copyright © 2021 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 (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
oci "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
)
const (
// CurrentVersion is the current vesion of the CDI Spec.
CurrentVersion = cdi.CurrentVersion
// defaultSpecExt is the file extension for the default encoding.
defaultSpecExt = ".yaml"
)
var (
// Valid CDI Spec versions.
validSpecVersions = map[string]struct{}{
"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
validatorLock sync.RWMutex
)
// Spec represents a single CDI Spec. It is usually loaded from a
// file and stored in a cache. The Spec has an associated priority.
// This priority is inherited from the associated priority of the
// CDI Spec directory that contains the CDI Spec file and is used
// to resolve conflicts if multiple CDI Spec files contain entries
// for the same fully qualified device.
type Spec struct {
*cdi.Spec
vendor string
class string
path string
priority int
devices map[string]*Device
}
// ReadSpec reads the given CDI Spec file. The resulting Spec is
// assigned the given priority. If reading or parsing the Spec
// data fails ReadSpec returns a nil Spec and an error.
func ReadSpec(path string, priority int) (*Spec, error) {
data, err := ioutil.ReadFile(path)
switch {
case os.IsNotExist(err):
return nil, err
case err != nil:
return nil, errors.Wrapf(err, "failed to read CDI Spec %q", path)
}
raw, err := ParseSpec(data)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse CDI Spec %q", path)
}
if raw == nil {
return nil, errors.Errorf("failed to parse CDI Spec %q, no Spec data", path)
}
spec, err := newSpec(raw, path, priority)
if err != nil {
return nil, err
}
return spec, nil
}
// newSpec creates a new Spec from the given CDI Spec data. The
// Spec is marked as loaded from the given path with the given
// 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 := validateSpec(raw)
if err != nil {
return nil, err
}
spec := &Spec{
Spec: raw,
path: filepath.Clean(path),
priority: priority,
}
if ext := filepath.Ext(spec.path); ext != ".yaml" && ext != ".json" {
spec.path += defaultSpecExt
}
spec.vendor, spec.class = ParseQualifier(spec.Kind)
if spec.devices, err = spec.validate(); err != nil {
return nil, errors.Wrap(err, "invalid CDI Spec")
}
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
}
// GetClass returns the device class of this Spec.
func (s *Spec) GetClass() string {
return s.class
}
// GetDevice returns the device for the given unqualified name.
func (s *Spec) GetDevice(name string) *Device {
return s.devices[name]
}
// GetPath returns the filesystem path of this Spec.
func (s *Spec) GetPath() string {
return s.path
}
// GetPriority returns the priority of this Spec.
func (s *Spec) GetPriority() int {
return s.priority
}
// ApplyEdits applies the Spec's global-scope container edits to an OCI Spec.
func (s *Spec) ApplyEdits(ociSpec *oci.Spec) error {
return s.edits().Apply(ociSpec)
}
// edits returns the applicable global container edits for this spec.
func (s *Spec) edits() *ContainerEdits {
return &ContainerEdits{&s.ContainerEdits}
}
// Validate the Spec.
func (s *Spec) validate() (map[string]*Device, error) {
if err := validateVersion(s.Version); err != nil {
return nil, err
}
if err := ValidateVendorName(s.vendor); err != nil {
return nil, err
}
if err := ValidateClassName(s.class); err != nil {
return nil, err
}
if err := s.edits().Validate(); err != nil {
return nil, err
}
devices := make(map[string]*Device)
for _, d := range s.Devices {
dev, err := newDevice(s, d)
if err != nil {
return nil, errors.Wrapf(err, "failed add device %q", d.Name)
}
if _, conflict := devices[d.Name]; conflict {
return nil, errors.Errorf("invalid spec, multiple device %q", d.Name)
}
devices[d.Name] = dev
}
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
}
// 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 {
return nil, errors.Wrap(err, "failed to unmarshal CDI Spec")
}
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 written (using WriteSpec()).
func SetSpecValidator(fn func(*cdi.Spec) error) {
validatorLock.Lock()
defer validatorLock.Unlock()
specValidator = fn
}
// validateSpec validates the Spec using the extneral validator.
func validateSpec(raw *cdi.Spec) error {
validatorLock.RLock()
defer validatorLock.RUnlock()
if specValidator == nil {
return nil
}
err := specValidator(raw)
if err != nil {
return errors.Wrap(err, "Spec validation failed")
}
return nil
}
// GenerateSpecName generates a vendor+class scoped Spec file name. The
// name can be passed to WriteSpec() to write a Spec file to the file
// system.
//
// vendor and class should match the vendor and class of the CDI Spec.
// The file name is generated without a ".json" or ".yaml" extension.
// The caller can append the desired extension to choose a particular
// encoding. Otherwise WriteSpec() will use its default encoding.
//
// This function always returns the same name for the same vendor/class
// combination. Therefore it cannot be used as such to generate multiple
// Spec file names for a single vendor and class.
func GenerateSpecName(vendor, class string) string {
return vendor + "-" + class
}
// GenerateTransientSpecName generates a vendor+class scoped transient
// Spec file name. The name can be passed to WriteSpec() to write a Spec
// file to the file system.
//
// Transient Specs are those whose lifecycle is tied to that of some
// external entity, for instance a container. vendor and class should
// 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
// is tied to has a unique ID of its own, then this is usually a
// good choice for transientID.
//
// The file name is generated without a ".json" or ".yaml" extension.
// The caller can append the desired extension to choose a particular
// encoding. Otherwise WriteSpec() will use its default encoding.
func GenerateTransientSpecName(vendor, class, transientID string) string {
transientID = strings.ReplaceAll(transientID, "/", "_")
return GenerateSpecName(vendor, class) + "_" + transientID
}
// GenerateNameForSpec generates a name for the given Spec using
// GenerateSpecName with the vendor and class taken from the Spec.
// On success it returns the generated name and a nil error. If
// the Spec does not contain a valid vendor or class, it returns
// an empty name and a non-nil error.
func GenerateNameForSpec(raw *cdi.Spec) (string, error) {
vendor, class := ParseQualifier(raw.Kind)
if vendor == "" {
return "", errors.Errorf("invalid vendor/class %q in Spec", raw.Kind)
}
return GenerateSpecName(vendor, class), nil
}
// GenerateNameForTransientSpec generates a name for the given transient
// Spec using GenerateTransientSpecName with the vendor and class taken
// from the Spec. On success it returns the generated name and a nil error.
// If the Spec does not contain a valid vendor or class, it returns an
// an empty name and a non-nil error.
func GenerateNameForTransientSpec(raw *cdi.Spec, transientID string) (string, error) {
vendor, class := ParseQualifier(raw.Kind)
if vendor == "" {
return "", errors.Errorf("invalid vendor/class %q in Spec", raw.Kind)
}
return GenerateTransientSpecName(vendor, class, transientID), nil
}

View File

@ -0,0 +1,48 @@
/*
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"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
// 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
dirf, err := os.Open(dir)
if err != nil {
return errors.Wrap(err, "rename failed")
}
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
}

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

@ -0,0 +1,59 @@
package specs
import "os"
// CurrentVersion is the current version of the Spec.
const CurrentVersion = "0.5.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"`
}
// Device is a "Device" a container runtime can add to a container
type Device struct {
Name string `json:"name"`
ContainerEdits ContainerEdits `json:"containerEdits"`
}
// ContainerEdits are edits a container runtime must make to the OCI spec to expose the device.
type ContainerEdits struct {
Env []string `json:"env,omitempty"`
DeviceNodes []*DeviceNode `json:"deviceNodes,omitempty"`
Hooks []*Hook `json:"hooks,omitempty"`
Mounts []*Mount `json:"mounts,omitempty"`
}
// 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"`
FileMode *os.FileMode `json:"fileMode,omitempty"`
Permissions string `json:"permissions,omitempty"`
UID *uint32 `json:"uid,omitempty"`
GID *uint32 `json:"gid,omitempty"`
}
// Mount represents a mount that needs to be added to the OCI spec.
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.
type Hook struct {
HookName string `json:"hookName"`
Path string `json:"path"`
Args []string `json:"args,omitempty"`
Env []string `json:"env,omitempty"`
Timeout *int `json:"timeout,omitempty"`
}

View File

@ -0,0 +1,113 @@
package specs
import (
"errors"
"fmt"
spec "github.com/opencontainers/runtime-spec/specs-go"
)
// ApplyOCIEditsForDevice applies devices OCI edits, in other words
// it finds the device in the CDI spec and applies the OCI patches that device
// requires to the OCI specification.
func ApplyOCIEditsForDevice(config *spec.Spec, cdi *Spec, dev string) error {
for _, d := range cdi.Devices {
if d.Name != dev {
continue
}
return ApplyEditsToOCISpec(config, &d.ContainerEdits)
}
return fmt.Errorf("CDI: device %q not found for spec %q", dev, cdi.Kind)
}
// ApplyOCIEdits applies the OCI edits the CDI spec declares globablly
func ApplyOCIEdits(config *spec.Spec, cdi *Spec) error {
return ApplyEditsToOCISpec(config, &cdi.ContainerEdits)
}
// ApplyEditsToOCISpec applies the specified edits to the OCI spec.
func ApplyEditsToOCISpec(config *spec.Spec, edits *ContainerEdits) error {
if config == nil {
return errors.New("spec is nil")
}
if edits == nil {
return nil
}
if len(edits.Env) > 0 {
if config.Process == nil {
config.Process = &spec.Process{}
}
config.Process.Env = append(config.Process.Env, edits.Env...)
}
for _, d := range edits.DeviceNodes {
if config.Linux == nil {
config.Linux = &spec.Linux{}
}
config.Linux.Devices = append(config.Linux.Devices, d.ToOCI())
}
for _, m := range edits.Mounts {
config.Mounts = append(config.Mounts, m.ToOCI())
}
for _, h := range edits.Hooks {
if config.Hooks == nil {
config.Hooks = &spec.Hooks{}
}
switch h.HookName {
case "prestart":
config.Hooks.Prestart = append(config.Hooks.Prestart, h.ToOCI())
case "createRuntime":
config.Hooks.CreateRuntime = append(config.Hooks.CreateRuntime, h.ToOCI())
case "createContainer":
config.Hooks.CreateContainer = append(config.Hooks.CreateContainer, h.ToOCI())
case "startContainer":
config.Hooks.StartContainer = append(config.Hooks.StartContainer, h.ToOCI())
case "poststart":
config.Hooks.Poststart = append(config.Hooks.Poststart, h.ToOCI())
case "poststop":
config.Hooks.Poststop = append(config.Hooks.Poststop, h.ToOCI())
default:
fmt.Printf("CDI: Unknown hook %q\n", h.HookName)
}
}
return nil
}
// ToOCI returns the opencontainers runtime Spec Hook for this Hook.
func (h *Hook) ToOCI() spec.Hook {
return spec.Hook{
Path: h.Path,
Args: h.Args,
Env: h.Env,
Timeout: h.Timeout,
}
}
// ToOCI returns the opencontainers runtime Spec Mount for this Mount.
func (m *Mount) ToOCI() spec.Mount {
return spec.Mount{
Source: m.HostPath,
Destination: m.ContainerPath,
Options: m.Options,
Type: m.Type,
}
}
// ToOCI returns the opencontainers runtime Spec LinuxDevice for this DeviceNode.
func (d *DeviceNode) ToOCI() spec.LinuxDevice {
return spec.LinuxDevice{
Path: d.Path,
Type: d.Type,
Major: d.Major,
Minor: d.Minor,
FileMode: d.FileMode,
UID: d.UID,
GID: d.GID,
}
}

View File

@ -46,6 +46,11 @@ github.com/blang/semver/v4
# github.com/chzyer/readline v1.5.1
## explicit; go 1.15
github.com/chzyer/readline
# github.com/container-orchestrated-devices/container-device-interface v0.5.3
## explicit; go 1.17
github.com/container-orchestrated-devices/container-device-interface/internal/multierror
github.com/container-orchestrated-devices/container-device-interface/pkg/cdi
github.com/container-orchestrated-devices/container-device-interface/specs-go
# github.com/containerd/cgroups v1.0.4
## explicit; go 1.17
github.com/containerd/cgroups/stats/v1
@ -730,4 +735,7 @@ gopkg.in/yaml.v2
# gopkg.in/yaml.v3 v3.0.1
## explicit
gopkg.in/yaml.v3
# sigs.k8s.io/yaml v1.3.0
## explicit; go 1.12
sigs.k8s.io/yaml
# github.com/opencontainers/runc => github.com/opencontainers/runc v1.1.1-0.20220617142545-8b9452f75cbc

24
common/vendor/sigs.k8s.io/yaml/.gitignore generated vendored Normal file
View File

@ -0,0 +1,24 @@
# OSX leaves these everywhere on SMB shares
._*
# Eclipse files
.classpath
.project
.settings/**
# Idea files
.idea/**
.idea/
# Emacs save files
*~
# Vim-related files
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
# Go test binaries
*.test

12
common/vendor/sigs.k8s.io/yaml/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,12 @@
language: go
arch: arm64
dist: focal
go: 1.15.x
script:
- diff -u <(echo -n) <(gofmt -d *.go)
- diff -u <(echo -n) <(golint $(go list -e ./...) | grep -v YAMLToJSON)
- GO111MODULE=on go vet .
- GO111MODULE=on go test -v -race ./...
- git diff --exit-code
install:
- GO111MODULE=off go get golang.org/x/lint/golint

31
common/vendor/sigs.k8s.io/yaml/CONTRIBUTING.md generated vendored Normal file
View File

@ -0,0 +1,31 @@
# Contributing Guidelines
Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://github.com/kubernetes/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt:
_As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._
## Getting Started
We have full documentation on how to get started contributing here:
<!---
If your repo has certain guidelines for contribution, put them here ahead of the general k8s resources
-->
- [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests
- [Kubernetes Contributor Guide](http://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](http://git.k8s.io/community/contributors/guide#contributing)
- [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet.md) - Common resources for existing developers
## Mentorship
- [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers!
<!---
Custom Information - if you're copying this template for the first time you can add custom content here, for example:
## Contact Information
- [Slack channel](https://kubernetes.slack.com/messages/kubernetes-users) - Replace `kubernetes-users` with your slack channel string, this will send users directly to your channel.
- [Mailing list](URL)
-->

50
common/vendor/sigs.k8s.io/yaml/LICENSE generated vendored Normal file
View File

@ -0,0 +1,50 @@
The MIT License (MIT)
Copyright (c) 2014 Sam Ghods
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

27
common/vendor/sigs.k8s.io/yaml/OWNERS generated vendored Normal file
View File

@ -0,0 +1,27 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- dims
- lavalamp
- smarterclayton
- deads2k
- sttts
- liggitt
- caesarxuchao
reviewers:
- dims
- thockin
- lavalamp
- smarterclayton
- wojtek-t
- deads2k
- derekwaynecarr
- caesarxuchao
- mikedanese
- liggitt
- gmarek
- sttts
- ncdc
- tallclair
labels:
- sig/api-machinery

123
common/vendor/sigs.k8s.io/yaml/README.md generated vendored Normal file
View File

@ -0,0 +1,123 @@
# YAML marshaling and unmarshaling support for Go
[![Build Status](https://travis-ci.org/kubernetes-sigs/yaml.svg)](https://travis-ci.org/kubernetes-sigs/yaml)
kubernetes-sigs/yaml is a permanent fork of [ghodss/yaml](https://github.com/ghodss/yaml).
## Introduction
A wrapper around [go-yaml](https://github.com/go-yaml/yaml) designed to enable a better way of handling YAML when marshaling to and from structs.
In short, this library first converts YAML to JSON using go-yaml and then uses `json.Marshal` and `json.Unmarshal` to convert to or from the struct. This means that it effectively reuses the JSON struct tags as well as the custom JSON methods `MarshalJSON` and `UnmarshalJSON` unlike go-yaml. For a detailed overview of the rationale behind this method, [see this blog post](http://web.archive.org/web/20190603050330/http://ghodss.com/2014/the-right-way-to-handle-yaml-in-golang/).
## Compatibility
This package uses [go-yaml](https://github.com/go-yaml/yaml) and therefore supports [everything go-yaml supports](https://github.com/go-yaml/yaml#compatibility).
## Caveats
**Caveat #1:** When using `yaml.Marshal` and `yaml.Unmarshal`, binary data should NOT be preceded with the `!!binary` YAML tag. If you do, go-yaml will convert the binary data from base64 to native binary data, which is not compatible with JSON. You can still use binary in your YAML files though - just store them without the `!!binary` tag and decode the base64 in your code (e.g. in the custom JSON methods `MarshalJSON` and `UnmarshalJSON`). This also has the benefit that your YAML and your JSON binary data will be decoded exactly the same way. As an example:
```
BAD:
exampleKey: !!binary gIGC
GOOD:
exampleKey: gIGC
... and decode the base64 data in your code.
```
**Caveat #2:** When using `YAMLToJSON` directly, maps with keys that are maps will result in an error since this is not supported by JSON. This error will occur in `Unmarshal` as well since you can't unmarshal map keys anyways since struct fields can't be keys.
## Installation and usage
To install, run:
```
$ go get sigs.k8s.io/yaml
```
And import using:
```
import "sigs.k8s.io/yaml"
```
Usage is very similar to the JSON library:
```go
package main
import (
"fmt"
"sigs.k8s.io/yaml"
)
type Person struct {
Name string `json:"name"` // Affects YAML field names too.
Age int `json:"age"`
}
func main() {
// Marshal a Person struct to YAML.
p := Person{"John", 30}
y, err := yaml.Marshal(p)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
fmt.Println(string(y))
/* Output:
age: 30
name: John
*/
// Unmarshal the YAML back into a Person struct.
var p2 Person
err = yaml.Unmarshal(y, &p2)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
fmt.Println(p2)
/* Output:
{John 30}
*/
}
```
`yaml.YAMLToJSON` and `yaml.JSONToYAML` methods are also available:
```go
package main
import (
"fmt"
"sigs.k8s.io/yaml"
)
func main() {
j := []byte(`{"name": "John", "age": 30}`)
y, err := yaml.JSONToYAML(j)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
fmt.Println(string(y))
/* Output:
age: 30
name: John
*/
j2, err := yaml.YAMLToJSON(y)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
fmt.Println(string(j2))
/* Output:
{"age":30,"name":"John"}
*/
}
```

9
common/vendor/sigs.k8s.io/yaml/RELEASE.md generated vendored Normal file
View File

@ -0,0 +1,9 @@
# Release Process
The `yaml` Project is released on an as-needed basis. The process is as follows:
1. An issue is proposing a new release with a changelog since the last release
1. All [OWNERS](OWNERS) must LGTM this release
1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION`
1. The release issue is closed
1. An announcement email is sent to `kubernetes-dev@googlegroups.com` with the subject `[ANNOUNCE] kubernetes-template-project $VERSION is released`

17
common/vendor/sigs.k8s.io/yaml/SECURITY_CONTACTS generated vendored Normal file
View File

@ -0,0 +1,17 @@
# Defined below are the security contacts for this repo.
#
# They are the contact point for the Product Security Team to reach out
# to for triaging and handling of incoming issues.
#
# The below names agree to abide by the
# [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy)
# and will be removed and replaced if they violate that agreement.
#
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://kubernetes.io/security/
cjcullen
jessfraz
liggitt
philips
tallclair

3
common/vendor/sigs.k8s.io/yaml/code-of-conduct.md generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Kubernetes Community Code of Conduct
Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md)

502
common/vendor/sigs.k8s.io/yaml/fields.go generated vendored Normal file
View File

@ -0,0 +1,502 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package yaml
import (
"bytes"
"encoding"
"encoding/json"
"reflect"
"sort"
"strings"
"sync"
"unicode"
"unicode/utf8"
)
// indirect walks down v allocating pointers as needed,
// until it gets to a non-pointer.
// if it encounters an Unmarshaler, indirect stops and returns that.
// if decodingNull is true, indirect stops at the last pointer so it can be set to nil.
func indirect(v reflect.Value, decodingNull bool) (json.Unmarshaler, encoding.TextUnmarshaler, reflect.Value) {
// If v is a named type and is addressable,
// start with its address, so that if the type has pointer methods,
// we find them.
if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() {
v = v.Addr()
}
for {
// Load value from interface, but only if the result will be
// usefully addressable.
if v.Kind() == reflect.Interface && !v.IsNil() {
e := v.Elem()
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
v = e
continue
}
}
if v.Kind() != reflect.Ptr {
break
}
if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() {
break
}
if v.IsNil() {
if v.CanSet() {
v.Set(reflect.New(v.Type().Elem()))
} else {
v = reflect.New(v.Type().Elem())
}
}
if v.Type().NumMethod() > 0 {
if u, ok := v.Interface().(json.Unmarshaler); ok {
return u, nil, reflect.Value{}
}
if u, ok := v.Interface().(encoding.TextUnmarshaler); ok {
return nil, u, reflect.Value{}
}
}
v = v.Elem()
}
return nil, nil, v
}
// A field represents a single field found in a struct.
type field struct {
name string
nameBytes []byte // []byte(name)
equalFold func(s, t []byte) bool // bytes.EqualFold or equivalent
tag bool
index []int
typ reflect.Type
omitEmpty bool
quoted bool
}
func fillField(f field) field {
f.nameBytes = []byte(f.name)
f.equalFold = foldFunc(f.nameBytes)
return f
}
// byName sorts field by name, breaking ties with depth,
// then breaking ties with "name came from json tag", then
// breaking ties with index sequence.
type byName []field
func (x byName) Len() int { return len(x) }
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byName) Less(i, j int) bool {
if x[i].name != x[j].name {
return x[i].name < x[j].name
}
if len(x[i].index) != len(x[j].index) {
return len(x[i].index) < len(x[j].index)
}
if x[i].tag != x[j].tag {
return x[i].tag
}
return byIndex(x).Less(i, j)
}
// byIndex sorts field by index sequence.
type byIndex []field
func (x byIndex) Len() int { return len(x) }
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byIndex) Less(i, j int) bool {
for k, xik := range x[i].index {
if k >= len(x[j].index) {
return false
}
if xik != x[j].index[k] {
return xik < x[j].index[k]
}
}
return len(x[i].index) < len(x[j].index)
}
// typeFields returns a list of fields that JSON should recognize for the given type.
// The algorithm is breadth-first search over the set of structs to include - the top struct
// and then any reachable anonymous structs.
func typeFields(t reflect.Type) []field {
// Anonymous fields to explore at the current level and the next.
current := []field{}
next := []field{{typ: t}}
// Count of queued names for current level and the next.
count := map[reflect.Type]int{}
nextCount := map[reflect.Type]int{}
// Types already visited at an earlier level.
visited := map[reflect.Type]bool{}
// Fields found.
var fields []field
for len(next) > 0 {
current, next = next, current[:0]
count, nextCount = nextCount, map[reflect.Type]int{}
for _, f := range current {
if visited[f.typ] {
continue
}
visited[f.typ] = true
// Scan f.typ for fields to include.
for i := 0; i < f.typ.NumField(); i++ {
sf := f.typ.Field(i)
if sf.PkgPath != "" { // unexported
continue
}
tag := sf.Tag.Get("json")
if tag == "-" {
continue
}
name, opts := parseTag(tag)
if !isValidTag(name) {
name = ""
}
index := make([]int, len(f.index)+1)
copy(index, f.index)
index[len(f.index)] = i
ft := sf.Type
if ft.Name() == "" && ft.Kind() == reflect.Ptr {
// Follow pointer.
ft = ft.Elem()
}
// Record found field and index sequence.
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := name != ""
if name == "" {
name = sf.Name
}
fields = append(fields, fillField(field{
name: name,
tag: tagged,
index: index,
typ: ft,
omitEmpty: opts.Contains("omitempty"),
quoted: opts.Contains("string"),
}))
if count[f.typ] > 1 {
// If there were multiple instances, add a second,
// so that the annihilation code will see a duplicate.
// It only cares about the distinction between 1 or 2,
// so don't bother generating any more copies.
fields = append(fields, fields[len(fields)-1])
}
continue
}
// Record new anonymous struct to explore in next round.
nextCount[ft]++
if nextCount[ft] == 1 {
next = append(next, fillField(field{name: ft.Name(), index: index, typ: ft}))
}
}
}
}
sort.Sort(byName(fields))
// Delete all fields that are hidden by the Go rules for embedded fields,
// except that fields with JSON tags are promoted.
// The fields are sorted in primary order of name, secondary order
// of field index length. Loop over names; for each name, delete
// hidden fields by choosing the one dominant field that survives.
out := fields[:0]
for advance, i := 0, 0; i < len(fields); i += advance {
// One iteration per name.
// Find the sequence of fields with the name of this first field.
fi := fields[i]
name := fi.name
for advance = 1; i+advance < len(fields); advance++ {
fj := fields[i+advance]
if fj.name != name {
break
}
}
if advance == 1 { // Only one field with this name
out = append(out, fi)
continue
}
dominant, ok := dominantField(fields[i : i+advance])
if ok {
out = append(out, dominant)
}
}
fields = out
sort.Sort(byIndex(fields))
return fields
}
// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// JSON tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
// The fields are sorted in increasing index-length order. The winner
// must therefore be one with the shortest index length. Drop all
// longer entries, which is easy: just truncate the slice.
length := len(fields[0].index)
tagged := -1 // Index of first tagged field.
for i, f := range fields {
if len(f.index) > length {
fields = fields[:i]
break
}
if f.tag {
if tagged >= 0 {
// Multiple tagged fields at the same level: conflict.
// Return no field.
return field{}, false
}
tagged = i
}
}
if tagged >= 0 {
return fields[tagged], true
}
// All remaining fields have the same length. If there's more than one,
// we have a conflict (two fields named "X" at the same level) and we
// return no field.
if len(fields) > 1 {
return field{}, false
}
return fields[0], true
}
var fieldCache struct {
sync.RWMutex
m map[reflect.Type][]field
}
// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) []field {
fieldCache.RLock()
f := fieldCache.m[t]
fieldCache.RUnlock()
if f != nil {
return f
}
// Compute fields without lock.
// Might duplicate effort but won't hold other computations back.
f = typeFields(t)
if f == nil {
f = []field{}
}
fieldCache.Lock()
if fieldCache.m == nil {
fieldCache.m = map[reflect.Type][]field{}
}
fieldCache.m[t] = f
fieldCache.Unlock()
return f
}
func isValidTag(s string) bool {
if s == "" {
return false
}
for _, c := range s {
switch {
case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
// Backslash and quote chars are reserved, but
// otherwise any punctuation chars are allowed
// in a tag name.
default:
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
return false
}
}
}
return true
}
const (
caseMask = ^byte(0x20) // Mask to ignore case in ASCII.
kelvin = '\u212a'
smallLongEss = '\u017f'
)
// foldFunc returns one of four different case folding equivalence
// functions, from most general (and slow) to fastest:
//
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
// 3) asciiEqualFold, no special, but includes non-letters (including _)
// 4) simpleLetterEqualFold, no specials, no non-letters.
//
// The letters S and K are special because they map to 3 runes, not just 2:
// * S maps to s and to U+017F 'ſ' Latin small letter long s
// * k maps to K and to U+212A '' Kelvin sign
// See http://play.golang.org/p/tTxjOc0OGo
//
// The returned function is specialized for matching against s and
// should only be given s. It's not curried for performance reasons.
func foldFunc(s []byte) func(s, t []byte) bool {
nonLetter := false
special := false // special letter
for _, b := range s {
if b >= utf8.RuneSelf {
return bytes.EqualFold
}
upper := b & caseMask
if upper < 'A' || upper > 'Z' {
nonLetter = true
} else if upper == 'K' || upper == 'S' {
// See above for why these letters are special.
special = true
}
}
if special {
return equalFoldRight
}
if nonLetter {
return asciiEqualFold
}
return simpleLetterEqualFold
}
// equalFoldRight is a specialization of bytes.EqualFold when s is
// known to be all ASCII (including punctuation), but contains an 's',
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
// See comments on foldFunc.
func equalFoldRight(s, t []byte) bool {
for _, sb := range s {
if len(t) == 0 {
return false
}
tb := t[0]
if tb < utf8.RuneSelf {
if sb != tb {
sbUpper := sb & caseMask
if 'A' <= sbUpper && sbUpper <= 'Z' {
if sbUpper != tb&caseMask {
return false
}
} else {
return false
}
}
t = t[1:]
continue
}
// sb is ASCII and t is not. t must be either kelvin
// sign or long s; sb must be s, S, k, or K.
tr, size := utf8.DecodeRune(t)
switch sb {
case 's', 'S':
if tr != smallLongEss {
return false
}
case 'k', 'K':
if tr != kelvin {
return false
}
default:
return false
}
t = t[size:]
}
if len(t) > 0 {
return false
}
return true
}
// asciiEqualFold is a specialization of bytes.EqualFold for use when
// s is all ASCII (but may contain non-letters) and contains no
// special-folding letters.
// See comments on foldFunc.
func asciiEqualFold(s, t []byte) bool {
if len(s) != len(t) {
return false
}
for i, sb := range s {
tb := t[i]
if sb == tb {
continue
}
if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
if sb&caseMask != tb&caseMask {
return false
}
} else {
return false
}
}
return true
}
// simpleLetterEqualFold is a specialization of bytes.EqualFold for
// use when s is all ASCII letters (no underscores, etc) and also
// doesn't contain 'k', 'K', 's', or 'S'.
// See comments on foldFunc.
func simpleLetterEqualFold(s, t []byte) bool {
if len(s) != len(t) {
return false
}
for i, b := range s {
if b&caseMask != t[i]&caseMask {
return false
}
}
return true
}
// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string
// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx], tagOptions(tag[idx+1:])
}
return tag, tagOptions("")
}
// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i+1:]
}
if s == optionName {
return true
}
s = next
}
return false
}

380
common/vendor/sigs.k8s.io/yaml/yaml.go generated vendored Normal file
View File

@ -0,0 +1,380 @@
package yaml
import (
"bytes"
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
"gopkg.in/yaml.v2"
)
// Marshal marshals the object into JSON then converts JSON to YAML and returns the
// YAML.
func Marshal(o interface{}) ([]byte, error) {
j, err := json.Marshal(o)
if err != nil {
return nil, fmt.Errorf("error marshaling into JSON: %v", err)
}
y, err := JSONToYAML(j)
if err != nil {
return nil, fmt.Errorf("error converting JSON to YAML: %v", err)
}
return y, nil
}
// JSONOpt is a decoding option for decoding from JSON format.
type JSONOpt func(*json.Decoder) *json.Decoder
// Unmarshal converts YAML to JSON then uses JSON to unmarshal into an object,
// optionally configuring the behavior of the JSON unmarshal.
func Unmarshal(y []byte, o interface{}, opts ...JSONOpt) error {
return yamlUnmarshal(y, o, false, opts...)
}
// UnmarshalStrict strictly converts YAML to JSON then uses JSON to unmarshal
// into an object, optionally configuring the behavior of the JSON unmarshal.
func UnmarshalStrict(y []byte, o interface{}, opts ...JSONOpt) error {
return yamlUnmarshal(y, o, true, append(opts, DisallowUnknownFields)...)
}
// yamlUnmarshal unmarshals the given YAML byte stream into the given interface,
// optionally performing the unmarshalling strictly
func yamlUnmarshal(y []byte, o interface{}, strict bool, opts ...JSONOpt) error {
vo := reflect.ValueOf(o)
unmarshalFn := yaml.Unmarshal
if strict {
unmarshalFn = yaml.UnmarshalStrict
}
j, err := yamlToJSON(y, &vo, unmarshalFn)
if err != nil {
return fmt.Errorf("error converting YAML to JSON: %v", err)
}
err = jsonUnmarshal(bytes.NewReader(j), o, opts...)
if err != nil {
return fmt.Errorf("error unmarshaling JSON: %v", err)
}
return nil
}
// jsonUnmarshal unmarshals the JSON byte stream from the given reader into the
// object, optionally applying decoder options prior to decoding. We are not
// using json.Unmarshal directly as we want the chance to pass in non-default
// options.
func jsonUnmarshal(r io.Reader, o interface{}, opts ...JSONOpt) error {
d := json.NewDecoder(r)
for _, opt := range opts {
d = opt(d)
}
if err := d.Decode(&o); err != nil {
return fmt.Errorf("while decoding JSON: %v", err)
}
return nil
}
// JSONToYAML Converts JSON to YAML.
func JSONToYAML(j []byte) ([]byte, error) {
// Convert the JSON to an object.
var jsonObj interface{}
// We are using yaml.Unmarshal here (instead of json.Unmarshal) because the
// Go JSON library doesn't try to pick the right number type (int, float,
// etc.) when unmarshalling to interface{}, it just picks float64
// universally. go-yaml does go through the effort of picking the right
// number type, so we can preserve number type throughout this process.
err := yaml.Unmarshal(j, &jsonObj)
if err != nil {
return nil, err
}
// Marshal this object into YAML.
return yaml.Marshal(jsonObj)
}
// YAMLToJSON converts YAML to JSON. Since JSON is a subset of YAML,
// passing JSON through this method should be a no-op.
//
// Things YAML can do that are not supported by JSON:
// * In YAML you can have binary and null keys in your maps. These are invalid
// in JSON. (int and float keys are converted to strings.)
// * Binary data in YAML with the !!binary tag is not supported. If you want to
// use binary data with this library, encode the data as base64 as usual but do
// not use the !!binary tag in your YAML. This will ensure the original base64
// encoded data makes it all the way through to the JSON.
//
// For strict decoding of YAML, use YAMLToJSONStrict.
func YAMLToJSON(y []byte) ([]byte, error) {
return yamlToJSON(y, nil, yaml.Unmarshal)
}
// YAMLToJSONStrict is like YAMLToJSON but enables strict YAML decoding,
// returning an error on any duplicate field names.
func YAMLToJSONStrict(y []byte) ([]byte, error) {
return yamlToJSON(y, nil, yaml.UnmarshalStrict)
}
func yamlToJSON(y []byte, jsonTarget *reflect.Value, yamlUnmarshal func([]byte, interface{}) error) ([]byte, error) {
// Convert the YAML to an object.
var yamlObj interface{}
err := yamlUnmarshal(y, &yamlObj)
if err != nil {
return nil, err
}
// YAML objects are not completely compatible with JSON objects (e.g. you
// can have non-string keys in YAML). So, convert the YAML-compatible object
// to a JSON-compatible object, failing with an error if irrecoverable
// incompatibilties happen along the way.
jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget)
if err != nil {
return nil, err
}
// Convert this object to JSON and return the data.
return json.Marshal(jsonObj)
}
func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (interface{}, error) {
var err error
// Resolve jsonTarget to a concrete value (i.e. not a pointer or an
// interface). We pass decodingNull as false because we're not actually
// decoding into the value, we're just checking if the ultimate target is a
// string.
if jsonTarget != nil {
ju, tu, pv := indirect(*jsonTarget, false)
// We have a JSON or Text Umarshaler at this level, so we can't be trying
// to decode into a string.
if ju != nil || tu != nil {
jsonTarget = nil
} else {
jsonTarget = &pv
}
}
// If yamlObj is a number or a boolean, check if jsonTarget is a string -
// if so, coerce. Else return normal.
// If yamlObj is a map or array, find the field that each key is
// unmarshaling to, and when you recurse pass the reflect.Value for that
// field back into this function.
switch typedYAMLObj := yamlObj.(type) {
case map[interface{}]interface{}:
// JSON does not support arbitrary keys in a map, so we must convert
// these keys to strings.
//
// From my reading of go-yaml v2 (specifically the resolve function),
// keys can only have the types string, int, int64, float64, binary
// (unsupported), or null (unsupported).
strMap := make(map[string]interface{})
for k, v := range typedYAMLObj {
// Resolve the key to a string first.
var keyString string
switch typedKey := k.(type) {
case string:
keyString = typedKey
case int:
keyString = strconv.Itoa(typedKey)
case int64:
// go-yaml will only return an int64 as a key if the system
// architecture is 32-bit and the key's value is between 32-bit
// and 64-bit. Otherwise the key type will simply be int.
keyString = strconv.FormatInt(typedKey, 10)
case float64:
// Stolen from go-yaml to use the same conversion to string as
// the go-yaml library uses to convert float to string when
// Marshaling.
s := strconv.FormatFloat(typedKey, 'g', -1, 32)
switch s {
case "+Inf":
s = ".inf"
case "-Inf":
s = "-.inf"
case "NaN":
s = ".nan"
}
keyString = s
case bool:
if typedKey {
keyString = "true"
} else {
keyString = "false"
}
default:
return nil, fmt.Errorf("Unsupported map key of type: %s, key: %+#v, value: %+#v",
reflect.TypeOf(k), k, v)
}
// jsonTarget should be a struct or a map. If it's a struct, find
// the field it's going to map to and pass its reflect.Value. If
// it's a map, find the element type of the map and pass the
// reflect.Value created from that type. If it's neither, just pass
// nil - JSON conversion will error for us if it's a real issue.
if jsonTarget != nil {
t := *jsonTarget
if t.Kind() == reflect.Struct {
keyBytes := []byte(keyString)
// Find the field that the JSON library would use.
var f *field
fields := cachedTypeFields(t.Type())
for i := range fields {
ff := &fields[i]
if bytes.Equal(ff.nameBytes, keyBytes) {
f = ff
break
}
// Do case-insensitive comparison.
if f == nil && ff.equalFold(ff.nameBytes, keyBytes) {
f = ff
}
}
if f != nil {
// Find the reflect.Value of the most preferential
// struct field.
jtf := t.Field(f.index[0])
strMap[keyString], err = convertToJSONableObject(v, &jtf)
if err != nil {
return nil, err
}
continue
}
} else if t.Kind() == reflect.Map {
// Create a zero value of the map's element type to use as
// the JSON target.
jtv := reflect.Zero(t.Type().Elem())
strMap[keyString], err = convertToJSONableObject(v, &jtv)
if err != nil {
return nil, err
}
continue
}
}
strMap[keyString], err = convertToJSONableObject(v, nil)
if err != nil {
return nil, err
}
}
return strMap, nil
case []interface{}:
// We need to recurse into arrays in case there are any
// map[interface{}]interface{}'s inside and to convert any
// numbers to strings.
// If jsonTarget is a slice (which it really should be), find the
// thing it's going to map to. If it's not a slice, just pass nil
// - JSON conversion will error for us if it's a real issue.
var jsonSliceElemValue *reflect.Value
if jsonTarget != nil {
t := *jsonTarget
if t.Kind() == reflect.Slice {
// By default slices point to nil, but we need a reflect.Value
// pointing to a value of the slice type, so we create one here.
ev := reflect.Indirect(reflect.New(t.Type().Elem()))
jsonSliceElemValue = &ev
}
}
// Make and use a new array.
arr := make([]interface{}, len(typedYAMLObj))
for i, v := range typedYAMLObj {
arr[i], err = convertToJSONableObject(v, jsonSliceElemValue)
if err != nil {
return nil, err
}
}
return arr, nil
default:
// If the target type is a string and the YAML type is a number,
// convert the YAML type to a string.
if jsonTarget != nil && (*jsonTarget).Kind() == reflect.String {
// Based on my reading of go-yaml, it may return int, int64,
// float64, or uint64.
var s string
switch typedVal := typedYAMLObj.(type) {
case int:
s = strconv.FormatInt(int64(typedVal), 10)
case int64:
s = strconv.FormatInt(typedVal, 10)
case float64:
s = strconv.FormatFloat(typedVal, 'g', -1, 32)
case uint64:
s = strconv.FormatUint(typedVal, 10)
case bool:
if typedVal {
s = "true"
} else {
s = "false"
}
}
if len(s) > 0 {
yamlObj = interface{}(s)
}
}
return yamlObj, nil
}
}
// JSONObjectToYAMLObject converts an in-memory JSON object into a YAML in-memory MapSlice,
// without going through a byte representation. A nil or empty map[string]interface{} input is
// converted to an empty map, i.e. yaml.MapSlice(nil).
//
// interface{} slices stay interface{} slices. map[string]interface{} becomes yaml.MapSlice.
//
// int64 and float64 are down casted following the logic of github.com/go-yaml/yaml:
// - float64s are down-casted as far as possible without data-loss to int, int64, uint64.
// - int64s are down-casted to int if possible without data-loss.
//
// Big int/int64/uint64 do not lose precision as in the json-yaml roundtripping case.
//
// string, bool and any other types are unchanged.
func JSONObjectToYAMLObject(j map[string]interface{}) yaml.MapSlice {
if len(j) == 0 {
return nil
}
ret := make(yaml.MapSlice, 0, len(j))
for k, v := range j {
ret = append(ret, yaml.MapItem{Key: k, Value: jsonToYAMLValue(v)})
}
return ret
}
func jsonToYAMLValue(j interface{}) interface{} {
switch j := j.(type) {
case map[string]interface{}:
if j == nil {
return interface{}(nil)
}
return JSONObjectToYAMLObject(j)
case []interface{}:
if j == nil {
return interface{}(nil)
}
ret := make([]interface{}, len(j))
for i := range j {
ret[i] = jsonToYAMLValue(j[i])
}
return ret
case float64:
// replicate the logic in https://github.com/go-yaml/yaml/blob/51d6538a90f86fe93ac480b35f37b2be17fef232/resolve.go#L151
if i64 := int64(j); j == float64(i64) {
if i := int(i64); i64 == int64(i) {
return i
}
return i64
}
if ui64 := uint64(j); j == float64(ui64) {
return ui64
}
return j
case int64:
if i := int(j); j == int64(i) {
return i
}
return j
}
return j
}

14
common/vendor/sigs.k8s.io/yaml/yaml_go110.go generated vendored Normal file
View File

@ -0,0 +1,14 @@
// This file contains changes that are only compatible with go 1.10 and onwards.
// +build go1.10
package yaml
import "encoding/json"
// DisallowUnknownFields configures the JSON decoder to error out if unknown
// fields come along, instead of dropping them by default.
func DisallowUnknownFields(d *json.Decoder) *json.Decoder {
d.DisallowUnknownFields()
return d
}