Skip to content

Commit 03e5cc4

Browse files
committed
pkg/cdi: add validation and application of edits.
Implement container edit validation, applying container edits to OCI Specs. Signed-off-by: Krisztian Litkey <[email protected]>
1 parent 57b060e commit 03e5cc4

File tree

2 files changed

+682
-0
lines changed

2 files changed

+682
-0
lines changed

pkg/cdi/container-edits.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
Copyright © 2021 The CDI Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cdi
18+
19+
import (
20+
"strings"
21+
22+
"github.com/pkg/errors"
23+
24+
"github.com/container-orchestrated-devices/container-device-interface/specs-go"
25+
oci "github.com/opencontainers/runtime-spec/specs-go"
26+
ocigen "github.com/opencontainers/runtime-tools/generate"
27+
)
28+
29+
const (
30+
// PrestartHook is the name of the OCI "prestart" hook.
31+
PrestartHook = "prestart"
32+
// CreateRuntimeHook is the name of the OCI "createRuntime" hook.
33+
CreateRuntimeHook = "createRuntime"
34+
// CreateContainerHook is the name of the OCI "createContainer" hook.
35+
CreateContainerHook = "createContainer"
36+
// StartContainerHook is the name of the OCI "startContainer" hook.
37+
StartContainerHook = "startContainer"
38+
// PoststartHook is the name of the OCI "poststart" hook.
39+
PoststartHook = "poststart"
40+
// PoststopHook is the name of the OCI "poststop" hook.
41+
PoststopHook = "poststop"
42+
)
43+
44+
var (
45+
// Names of recognized hooks.
46+
validHookNames = map[string]struct{}{
47+
PrestartHook: {},
48+
CreateRuntimeHook: {},
49+
CreateContainerHook: {},
50+
StartContainerHook: {},
51+
PoststartHook: {},
52+
PoststopHook: {},
53+
}
54+
)
55+
56+
// ContainerEdits represent updates to be applied to an OCI Spec.
57+
// These updates can be specific to a CDI device, or they can be
58+
// specific to a CDI Spec. In the former case these edits should
59+
// be applied to all OCI Specs where the corresponding CDI device
60+
// is injected. In the latter case, these edits should be applied
61+
// to all OCI Specs where at least one devices from the CDI Spec
62+
// is injected.
63+
type ContainerEdits struct {
64+
*specs.ContainerEdits
65+
}
66+
67+
// Apply edits to the given OCI Spec. Updates the OCI Spec in place.
68+
// Returns an error if the update fails.
69+
func (e *ContainerEdits) Apply(spec *oci.Spec) error {
70+
if spec == nil {
71+
return errors.New("can't edit nil OCI Spec")
72+
}
73+
if e == nil || e.ContainerEdits == nil {
74+
return nil
75+
}
76+
77+
specgen := ocigen.NewFromSpec(spec)
78+
if len(e.Env) > 0 {
79+
specgen.AddMultipleProcessEnv(e.Env)
80+
}
81+
for _, d := range e.DeviceNodes {
82+
specgen.AddDevice(d.ToOCI())
83+
}
84+
for _, m := range e.Mounts {
85+
specgen.AddMount(m.ToOCI())
86+
}
87+
for _, h := range e.Hooks {
88+
switch h.HookName {
89+
case PrestartHook:
90+
specgen.AddPreStartHook(h.ToOCI())
91+
case PoststartHook:
92+
specgen.AddPostStartHook(h.ToOCI())
93+
case PoststopHook:
94+
specgen.AddPostStopHook(h.ToOCI())
95+
// TODO: Maybe runtime-tools/generate should be updated with these...
96+
case CreateRuntimeHook:
97+
ensureOCIHooks(spec)
98+
spec.Hooks.CreateRuntime = append(spec.Hooks.CreateRuntime, h.ToOCI())
99+
case CreateContainerHook:
100+
ensureOCIHooks(spec)
101+
spec.Hooks.CreateContainer = append(spec.Hooks.CreateContainer, h.ToOCI())
102+
case StartContainerHook:
103+
ensureOCIHooks(spec)
104+
spec.Hooks.StartContainer = append(spec.Hooks.StartContainer, h.ToOCI())
105+
default:
106+
return errors.Errorf("unknown hook name %q", h.HookName)
107+
}
108+
}
109+
110+
return nil
111+
}
112+
113+
// Validate container edits.
114+
func (e *ContainerEdits) Validate() error {
115+
if e == nil || e.ContainerEdits == nil {
116+
return nil
117+
}
118+
119+
if err := ValidateEnv(e.Env); err != nil {
120+
return errors.Wrap(err, "invalid container edits")
121+
}
122+
for _, d := range e.DeviceNodes {
123+
if err := (&DeviceNode{d}).Validate(); err != nil {
124+
return err
125+
}
126+
}
127+
for _, h := range e.Hooks {
128+
if err := (&Hook{h}).Validate(); err != nil {
129+
return err
130+
}
131+
}
132+
for _, m := range e.Mounts {
133+
if err := (&Mount{m}).Validate(); err != nil {
134+
return err
135+
}
136+
}
137+
138+
return nil
139+
}
140+
141+
// isEmpty returns true if these edits are empty. This is valid in a
142+
// global Spec context but invalid in a Device context.
143+
func (e *ContainerEdits) isEmpty() bool {
144+
if e == nil {
145+
return false
146+
}
147+
return len(e.Env)+len(e.DeviceNodes)+len(e.Hooks)+len(e.Mounts) == 0
148+
}
149+
150+
// ValidateEnv validates the given environment variables.
151+
func ValidateEnv(env []string) error {
152+
for _, v := range env {
153+
if strings.IndexByte(v, byte('=')) <= 0 {
154+
return errors.Errorf("invalid environment variable %q", v)
155+
}
156+
}
157+
return nil
158+
}
159+
160+
// DeviceNode is a CDI Spec DeviceNode wrapper, used for validating DeviceNodes.
161+
type DeviceNode struct {
162+
*specs.DeviceNode
163+
}
164+
165+
// Validate a CDI Spec DeviceNode.
166+
func (d *DeviceNode) Validate() error {
167+
if d.Path == "" {
168+
return errors.New("invalid (empty) device path")
169+
}
170+
if d.Type != "" && d.Type != "b" && d.Type != "c" {
171+
return errors.Errorf("device %q: invalid type %q", d.Path, d.Type)
172+
}
173+
for _, bit := range d.Permissions {
174+
if bit != 'r' && bit != 'w' && bit != 'm' {
175+
return errors.Errorf("device %q: invalid persmissions %q",
176+
d.Path, d.Permissions)
177+
}
178+
}
179+
return nil
180+
}
181+
182+
// Hook is a CDI Spec Hook wrapper, used for validating hooks.
183+
type Hook struct {
184+
*specs.Hook
185+
}
186+
187+
// Validate a hook.
188+
func (h *Hook) Validate() error {
189+
if _, ok := validHookNames[h.HookName]; !ok {
190+
return errors.Errorf("invalid hook name %q", h.HookName)
191+
}
192+
if h.Path == "" {
193+
return errors.Errorf("invalid hook %q with empty path", h.HookName)
194+
}
195+
if err := ValidateEnv(h.Env); err != nil {
196+
return errors.Wrapf(err, "invalid hook %q", h.HookName)
197+
}
198+
return nil
199+
}
200+
201+
// Mount is a CDI Mount wrapper, used for validating mounts.
202+
type Mount struct {
203+
*specs.Mount
204+
}
205+
206+
// Validate a mount.
207+
func (m *Mount) Validate() error {
208+
if m.HostPath == "" {
209+
return errors.New("invalid mount, empty host path")
210+
}
211+
if m.ContainerPath == "" {
212+
return errors.New("invalid mount, empty container path")
213+
}
214+
return nil
215+
}
216+
217+
// Ensure OCI Spec hooks are not nil so we can add hooks.
218+
func ensureOCIHooks(spec *oci.Spec) {
219+
if spec.Hooks == nil {
220+
spec.Hooks = &oci.Hooks{}
221+
}
222+
}

0 commit comments

Comments
 (0)