Skip to content

Commit 39d1b55

Browse files
committed
Add spec validation on save to producer API
Signed-off-by: Evan Lezar <[email protected]>
1 parent b67d046 commit 39d1b55

File tree

8 files changed

+300
-157
lines changed

8 files changed

+300
-157
lines changed

pkg/cdi/cache.go

+16-14
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
"github.com/fsnotify/fsnotify"
3030
oci "github.com/opencontainers/runtime-spec/specs-go"
31+
"tags.cncf.io/container-device-interface/pkg/cdi/producer"
3132
cdi "tags.cncf.io/container-device-interface/specs-go"
3233
)
3334

@@ -280,30 +281,31 @@ func (c *Cache) highestPrioritySpecDir() (string, int) {
280281
// priority Spec directory. If name has a "json" or "yaml" extension it
281282
// choses the encoding. Otherwise the default YAML encoding is used.
282283
func (c *Cache) WriteSpec(raw *cdi.Spec, name string) error {
283-
var (
284-
specDir string
285-
path string
286-
prio int
287-
spec *Spec
288-
err error
289-
)
290-
291-
specDir, prio = c.highestPrioritySpecDir()
284+
specDir, _ := c.highestPrioritySpecDir()
292285
if specDir == "" {
293286
return errors.New("no Spec directories to write to")
294287
}
295288

296-
path = filepath.Join(specDir, name)
297-
if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" {
298-
path += defaultSpecExt
289+
// Ideally we would like to pass the configured spec validator to the
290+
// producer, but we would need to handle the synchronisation.
291+
// Instead we call `validateSpec` here which is a no-op if no validator is
292+
// configured.
293+
if err := validateSpec(raw); err != nil {
294+
return err
299295
}
300296

301-
spec, err = newSpec(raw, path, prio)
297+
p, err := producer.New(
298+
producer.WithOverwrite(true),
299+
)
302300
if err != nil {
303301
return err
304302
}
305303

306-
return spec.write(true)
304+
path := filepath.Join(specDir, name)
305+
if _, err := p.SaveSpec(raw, path); err != nil {
306+
return err
307+
}
308+
return nil
307309
}
308310

309311
// RemoveSpec removes a Spec with the given name from the highest

pkg/cdi/container-edits.go

+6-106
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
oci "github.com/opencontainers/runtime-spec/specs-go"
2828
ocigen "github.com/opencontainers/runtime-tools/generate"
29+
"tags.cncf.io/container-device-interface/pkg/cdi/producer"
2930
cdi "tags.cncf.io/container-device-interface/specs-go"
3031
)
3132

@@ -167,32 +168,7 @@ func (e *ContainerEdits) Validate() error {
167168
if e == nil || e.ContainerEdits == nil {
168169
return nil
169170
}
170-
171-
if err := ValidateEnv(e.Env); err != nil {
172-
return fmt.Errorf("invalid container edits: %w", err)
173-
}
174-
for _, d := range e.DeviceNodes {
175-
if err := (&DeviceNode{d}).Validate(); err != nil {
176-
return err
177-
}
178-
}
179-
for _, h := range e.Hooks {
180-
if err := (&Hook{h}).Validate(); err != nil {
181-
return err
182-
}
183-
}
184-
for _, m := range e.Mounts {
185-
if err := (&Mount{m}).Validate(); err != nil {
186-
return err
187-
}
188-
}
189-
if e.IntelRdt != nil {
190-
if err := (&IntelRdt{e.IntelRdt}).Validate(); err != nil {
191-
return err
192-
}
193-
}
194-
195-
return nil
171+
return producer.DefaultValidator.Validate(e.ContainerEdits)
196172
}
197173

198174
// Append other edits into this one. If called with a nil receiver,
@@ -220,71 +196,14 @@ func (e *ContainerEdits) Append(o *ContainerEdits) *ContainerEdits {
220196
return e
221197
}
222198

223-
// isEmpty returns true if these edits are empty. This is valid in a
224-
// global Spec context but invalid in a Device context.
225-
func (e *ContainerEdits) isEmpty() bool {
226-
if e == nil {
227-
return false
228-
}
229-
if len(e.Env) > 0 {
230-
return false
231-
}
232-
if len(e.DeviceNodes) > 0 {
233-
return false
234-
}
235-
if len(e.Hooks) > 0 {
236-
return false
237-
}
238-
if len(e.Mounts) > 0 {
239-
return false
240-
}
241-
if len(e.AdditionalGIDs) > 0 {
242-
return false
243-
}
244-
if e.IntelRdt != nil {
245-
return false
246-
}
247-
return true
248-
}
249-
250-
// ValidateEnv validates the given environment variables.
251-
func ValidateEnv(env []string) error {
252-
for _, v := range env {
253-
if strings.IndexByte(v, byte('=')) <= 0 {
254-
return fmt.Errorf("invalid environment variable %q", v)
255-
}
256-
}
257-
return nil
258-
}
259-
260199
// DeviceNode is a CDI Spec DeviceNode wrapper, used for validating DeviceNodes.
261200
type DeviceNode struct {
262201
*cdi.DeviceNode
263202
}
264203

265204
// Validate a CDI Spec DeviceNode.
266205
func (d *DeviceNode) Validate() error {
267-
validTypes := map[string]struct{}{
268-
"": {},
269-
"b": {},
270-
"c": {},
271-
"u": {},
272-
"p": {},
273-
}
274-
275-
if d.Path == "" {
276-
return errors.New("invalid (empty) device path")
277-
}
278-
if _, ok := validTypes[d.Type]; !ok {
279-
return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
280-
}
281-
for _, bit := range d.Permissions {
282-
if bit != 'r' && bit != 'w' && bit != 'm' {
283-
return fmt.Errorf("device %q: invalid permissions %q",
284-
d.Path, d.Permissions)
285-
}
286-
}
287-
return nil
206+
return producer.DefaultValidator.Validate(d.DeviceNode)
288207
}
289208

290209
// Hook is a CDI Spec Hook wrapper, used for validating hooks.
@@ -294,16 +213,7 @@ type Hook struct {
294213

295214
// Validate a hook.
296215
func (h *Hook) Validate() error {
297-
if _, ok := validHookNames[h.HookName]; !ok {
298-
return fmt.Errorf("invalid hook name %q", h.HookName)
299-
}
300-
if h.Path == "" {
301-
return fmt.Errorf("invalid hook %q with empty path", h.HookName)
302-
}
303-
if err := ValidateEnv(h.Env); err != nil {
304-
return fmt.Errorf("invalid hook %q: %w", h.HookName, err)
305-
}
306-
return nil
216+
return producer.DefaultValidator.Validate(h.Hook)
307217
}
308218

309219
// Mount is a CDI Mount wrapper, used for validating mounts.
@@ -313,13 +223,7 @@ type Mount struct {
313223

314224
// Validate a mount.
315225
func (m *Mount) Validate() error {
316-
if m.HostPath == "" {
317-
return errors.New("invalid mount, empty host path")
318-
}
319-
if m.ContainerPath == "" {
320-
return errors.New("invalid mount, empty container path")
321-
}
322-
return nil
226+
return producer.DefaultValidator.Validate(m.Mount)
323227
}
324228

325229
// IntelRdt is a CDI IntelRdt wrapper.
@@ -337,11 +241,7 @@ func ValidateIntelRdt(i *cdi.IntelRdt) error {
337241

338242
// Validate validates the IntelRdt configuration.
339243
func (i *IntelRdt) Validate() error {
340-
// ClosID must be a valid Linux filename
341-
if len(i.ClosID) >= 4096 || i.ClosID == "." || i.ClosID == ".." || strings.ContainsAny(i.ClosID, "/\n") {
342-
return errors.New("invalid ClosID")
343-
}
344-
return nil
244+
return producer.DefaultValidator.Validate(i.IntelRdt)
345245
}
346246

347247
// Ensure OCI Spec hooks are not nil so we can add hooks.

pkg/cdi/device.go

+2-21
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@
1717
package cdi
1818

1919
import (
20-
"fmt"
21-
2220
oci "github.com/opencontainers/runtime-spec/specs-go"
23-
"tags.cncf.io/container-device-interface/internal/validation"
21+
"tags.cncf.io/container-device-interface/pkg/cdi/producer"
2422
"tags.cncf.io/container-device-interface/pkg/parser"
2523
cdi "tags.cncf.io/container-device-interface/specs-go"
2624
)
@@ -67,22 +65,5 @@ func (d *Device) edits() *ContainerEdits {
6765

6866
// Validate the device.
6967
func (d *Device) validate() error {
70-
if err := parser.ValidateDeviceName(d.Name); err != nil {
71-
return err
72-
}
73-
name := d.Name
74-
if d.spec != nil {
75-
name = d.GetQualifiedName()
76-
}
77-
if err := validation.ValidateSpecAnnotations(name, d.Annotations); err != nil {
78-
return err
79-
}
80-
edits := d.edits()
81-
if edits.isEmpty() {
82-
return fmt.Errorf("invalid device, empty device edits")
83-
}
84-
if err := edits.Validate(); err != nil {
85-
return fmt.Errorf("invalid device %q: %w", d.Name, err)
86-
}
87-
return nil
68+
return producer.DefaultValidator.Validate(d.Device)
8869
}

pkg/cdi/producer/api.go

+6
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,9 @@ const (
2727
// SpecFormatYAML defines a CDI spec formatted as YAML.
2828
SpecFormatYAML = specFormat(".yaml")
2929
)
30+
31+
// Validators as constants.
32+
const (
33+
DefaultValidator = defaultValidator("default")
34+
DisabledValidator = disabledValidator("disabled")
35+
)

pkg/cdi/producer/options.go

+11
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ func WithSpecFormat(format specFormat) Option {
3434
}
3535
}
3636

37+
// WithSpecValidator sets a validator to be used when writing an output spec.
38+
func WithSpecValidator(validator Validator) Option {
39+
return func(p *Producer) error {
40+
if validator == nil {
41+
validator = DisabledValidator
42+
}
43+
p.validator = validator
44+
return nil
45+
}
46+
}
47+
3748
// WithOverwrite specifies whether a producer should overwrite a CDI spec when
3849
// saving to file.
3950
func WithOverwrite(overwrite bool) Option {

pkg/cdi/producer/producer.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package producer
1818

1919
import (
20+
"fmt"
2021
"path/filepath"
2122

2223
cdi "tags.cncf.io/container-device-interface/specs-go"
@@ -26,12 +27,14 @@ import (
2627
type Producer struct {
2728
format specFormat
2829
failIfExists bool
30+
validator Validator
2931
}
3032

3133
// New creates a new producer with the supplied options.
3234
func New(opts ...Option) (*Producer, error) {
3335
p := &Producer{
34-
format: DefaultSpecFormat,
36+
format: DefaultSpecFormat,
37+
validator: DefaultValidator,
3538
}
3639
for _, opt := range opts {
3740
err := opt(p)
@@ -47,8 +50,11 @@ func New(opts ...Option) (*Producer, error) {
4750
// extension takes precedence over the format with which the Producer was
4851
// configured.
4952
func (p *Producer) SaveSpec(s *cdi.Spec, filename string) (string, error) {
50-
filename = p.normalizeFilename(filename)
53+
if err := p.validator.Validate(s); err != nil {
54+
return "", fmt.Errorf("spec validation failed: %w", err)
55+
}
5156

57+
filename = p.normalizeFilename(filename)
5258
sp := spec{
5359
Spec: s,
5460
format: p.specFormatFromFilename(filename),

0 commit comments

Comments
 (0)