Skip to content

Commit 1282f64

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

File tree

10 files changed

+366
-158
lines changed

10 files changed

+366
-158
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/validator"
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 validator.Default.ValidateAny(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 validator.Default.ValidateAny(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 validator.Default.ValidateAny(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 validator.Default.ValidateAny(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 validator.Default.ValidateAny(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/validator"
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 validator.Default.ValidateAny(d.Device)
8869
}

pkg/cdi/producer/api.go

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package producer
1818

19+
import cdi "tags.cncf.io/container-device-interface/specs-go"
20+
1921
type specFormat string
2022

2123
const (
@@ -27,3 +29,8 @@ const (
2729
// SpecFormatYAML defines a CDI spec formatted as YAML.
2830
SpecFormatYAML = specFormat(".yaml")
2931
)
32+
33+
// a specValidator is used to validate a CDI spec.
34+
type specValidator interface {
35+
Validate(*cdi.Spec) error
36+
}

pkg/cdi/producer/options.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
package producer
1818

19-
import "fmt"
19+
import (
20+
"fmt"
21+
22+
"tags.cncf.io/container-device-interface/pkg/cdi/producer/validator"
23+
)
2024

2125
// An Option defines a functional option for constructing a producer.
2226
type Option func(*Producer) error
@@ -34,6 +38,17 @@ func WithSpecFormat(format specFormat) Option {
3438
}
3539
}
3640

41+
// WithSpecValidator sets a validator to be used when writing an output spec.
42+
func WithSpecValidator(v specValidator) Option {
43+
return func(p *Producer) error {
44+
if v == nil {
45+
v = validator.Disabled
46+
}
47+
p.validator = v
48+
return nil
49+
}
50+
}
51+
3752
// WithOverwrite specifies whether a producer should overwrite a CDI spec when
3853
// saving to file.
3954
func WithOverwrite(overwrite bool) Option {

pkg/cdi/producer/producer.go

+18-2
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,25 @@
1717
package producer
1818

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

23+
"tags.cncf.io/container-device-interface/pkg/cdi/producer/validator"
2224
cdi "tags.cncf.io/container-device-interface/specs-go"
2325
)
2426

2527
// A Producer defines a structure for outputting CDI specifications.
2628
type Producer struct {
2729
format specFormat
2830
failIfExists bool
31+
validator specValidator
2932
}
3033

3134
// New creates a new producer with the supplied options.
3235
func New(opts ...Option) (*Producer, error) {
3336
p := &Producer{
34-
format: DefaultSpecFormat,
37+
format: DefaultSpecFormat,
38+
validator: validator.Default,
3539
}
3640
for _, opt := range opts {
3741
err := opt(p)
@@ -47,8 +51,11 @@ func New(opts ...Option) (*Producer, error) {
4751
// extension takes precedence over the format with which the Producer was
4852
// configured.
4953
func (p *Producer) SaveSpec(s *cdi.Spec, filename string) (string, error) {
50-
filename = p.normalizeFilename(filename)
54+
if err := p.Validate(s); err != nil {
55+
return "", fmt.Errorf("spec validation failed: %w", err)
56+
}
5157

58+
filename = p.normalizeFilename(filename)
5259
sp := spec{
5360
Spec: s,
5461
format: p.specFormatFromFilename(filename),
@@ -61,6 +68,15 @@ func (p *Producer) SaveSpec(s *cdi.Spec, filename string) (string, error) {
6168
return filename, nil
6269
}
6370

71+
// Validate performs a validation on a CDI spec using the configured validator.
72+
// If no validator is configured, the spec is considered unconditionaly valid.
73+
func (p *Producer) Validate(s *cdi.Spec) error {
74+
if p == nil || p.validator == nil {
75+
return nil
76+
}
77+
return p.validator.Validate(s)
78+
}
79+
6480
// specFormatFromFilename determines the CDI spec format for the given filename.
6581
func (p *Producer) specFormatFromFilename(filename string) specFormat {
6682
switch filepath.Ext(filename) {

pkg/cdi/producer/validator/api.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
Copyright © 2024 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 validator
18+
19+
// Validators as constants.
20+
const (
21+
Default = defaultValidator("default")
22+
Disabled = disabledValidator("disabled")
23+
)

0 commit comments

Comments
 (0)