mirror of
https://github.com/NVIDIA/nvidia-container-toolkit
synced 2024-11-23 08:46:52 +00:00
a34a571d2e
Signed-off-by: Evan Lezar <elezar@nvidia.com>
273 lines
6.7 KiB
Go
273 lines
6.7 KiB
Go
/*
|
|
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"
|
|
"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"
|
|
)
|
|
|
|
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,
|
|
}
|
|
|
|
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 NewSpec()) or written (Spec.Write()).
|
|
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
|
|
}
|