mirror of
https://github.com/NVIDIA/nvidia-container-toolkit
synced 2024-11-24 13:05:17 +00:00
bc4bfb94a2
Signed-off-by: Evan Lezar <elezar@nvidia.com>
333 lines
8.5 KiB
Go
333 lines
8.5 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 (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"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 fmt.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 fmt.Errorf("invalid container edits: %w", err)
|
|
}
|
|
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 fmt.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 fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
|
|
}
|
|
for _, bit := range d.Permissions {
|
|
if bit != 'r' && bit != 'w' && bit != 'm' {
|
|
return fmt.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 fmt.Errorf("invalid hook name %q", h.HookName)
|
|
}
|
|
if h.Path == "" {
|
|
return fmt.Errorf("invalid hook %q with empty path", h.HookName)
|
|
}
|
|
if err := ValidateEnv(h.Env); err != nil {
|
|
return fmt.Errorf("invalid hook %q: %w", h.HookName, err)
|
|
}
|
|
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))
|
|
}
|