Skip to content

Commit 3205f41

Browse files
committed
Add producer API to write specs
This change adds a SpecProducer that can be used by clients that are only concerned with outputing specs. A spec producer is configured on construction to allow for the default output format, file permissions, and spec validation to be specified. Signed-off-by: Evan Lezar <[email protected]>
1 parent 44fef1d commit 3205f41

13 files changed

+802
-210
lines changed

pkg/cdi/cache.go

+17-15
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

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

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

302-
spec, err = newSpec(raw, path, prio)
298+
path := filepath.Join(specDir, name)
299+
300+
p, err := producer.New(raw,
301+
producer.WithOverwrite(true),
302+
)
303303
if err != nil {
304304
return err
305305
}
306-
307-
return spec.write(true)
306+
if _, err := p.Save(path); err != nil {
307+
return err
308+
}
309+
return nil
308310
}
309311

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

pkg/cdi/container-edits.go

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

2727
oci "github.com/opencontainers/runtime-spec/specs-go"
2828
ocigen "github.com/opencontainers/runtime-tools/generate"
29+
30+
"tags.cncf.io/container-device-interface/pkg/cdi/producer/validator"
2931
cdi "tags.cncf.io/container-device-interface/specs-go"
3032
)
3133

@@ -44,18 +46,6 @@ const (
4446
PoststopHook = "poststop"
4547
)
4648

47-
var (
48-
// Names of recognized hooks.
49-
validHookNames = map[string]struct{}{
50-
PrestartHook: {},
51-
CreateRuntimeHook: {},
52-
CreateContainerHook: {},
53-
StartContainerHook: {},
54-
PoststartHook: {},
55-
PoststopHook: {},
56-
}
57-
)
58-
5949
// ContainerEdits represent updates to be applied to an OCI Spec.
6050
// These updates can be specific to a CDI device, or they can be
6151
// specific to a CDI Spec. In the former case these edits should
@@ -167,32 +157,7 @@ func (e *ContainerEdits) Validate() error {
167157
if e == nil || e.ContainerEdits == nil {
168158
return nil
169159
}
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
160+
return validator.Default.ValidateAny(e.ContainerEdits)
196161
}
197162

198163
// Append other edits into this one. If called with a nil receiver,
@@ -220,71 +185,14 @@ func (e *ContainerEdits) Append(o *ContainerEdits) *ContainerEdits {
220185
return e
221186
}
222187

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-
260188
// DeviceNode is a CDI Spec DeviceNode wrapper, used for validating DeviceNodes.
261189
type DeviceNode struct {
262190
*cdi.DeviceNode
263191
}
264192

265193
// Validate a CDI Spec DeviceNode.
266194
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
195+
return validator.Default.ValidateAny(d.DeviceNode)
288196
}
289197

290198
// Hook is a CDI Spec Hook wrapper, used for validating hooks.
@@ -294,16 +202,7 @@ type Hook struct {
294202

295203
// Validate a hook.
296204
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
205+
return validator.Default.ValidateAny(h.Hook)
307206
}
308207

309208
// Mount is a CDI Mount wrapper, used for validating mounts.
@@ -313,13 +212,7 @@ type Mount struct {
313212

314213
// Validate a mount.
315214
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
215+
return validator.Default.ValidateAny(m.Mount)
323216
}
324217

325218
// IntelRdt is a CDI IntelRdt wrapper.
@@ -337,11 +230,7 @@ func ValidateIntelRdt(i *cdi.IntelRdt) error {
337230

338231
// Validate validates the IntelRdt configuration.
339232
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
233+
return validator.Default.ValidateAny(i.IntelRdt)
345234
}
346235

347236
// 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

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 producer
18+
19+
import cdi "tags.cncf.io/container-device-interface/specs-go"
20+
21+
type SpecFormat string
22+
23+
const (
24+
// DefaultSpecFormat defines the default encoding used to write CDI specs.
25+
DefaultSpecFormat = SpecFormatYAML
26+
27+
// SpecFormatJSON defines a CDI spec formatted as JSON.
28+
SpecFormatJSON = SpecFormat(".json")
29+
// SpecFormatYAML defines a CDI spec formatted as YAML.
30+
SpecFormatYAML = SpecFormat(".yaml")
31+
)
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

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 producer
18+
19+
import (
20+
"fmt"
21+
"io/fs"
22+
23+
"tags.cncf.io/container-device-interface/pkg/cdi/producer/validator"
24+
)
25+
26+
// An Option defines a functional option for constructing a producer.
27+
type Option func(*options) error
28+
29+
type options struct {
30+
specFormat SpecFormat
31+
specValidator SpecValidator
32+
overwrite bool
33+
permissions fs.FileMode
34+
}
35+
36+
// WithSpecFormat sets the output format of a CDI specification.
37+
func WithSpecFormat(format SpecFormat) Option {
38+
return func(o *options) error {
39+
switch format {
40+
case SpecFormatJSON, SpecFormatYAML:
41+
o.specFormat = format
42+
default:
43+
return fmt.Errorf("invalid CDI spec format %v", format)
44+
}
45+
return nil
46+
}
47+
}
48+
49+
// WithSpecValidator sets a validator to be used when writing an output spec.
50+
func WithSpecValidator(specValidator SpecValidator) Option {
51+
return func(o *options) error {
52+
if specValidator == nil {
53+
specValidator = validator.Disabled
54+
}
55+
o.specValidator = specValidator
56+
return nil
57+
}
58+
}
59+
60+
// WithOverwrite specifies whether a producer should overwrite a CDI spec when
61+
// saving to file.
62+
func WithOverwrite(overwrite bool) Option {
63+
return func(o *options) error {
64+
o.overwrite = overwrite
65+
return nil
66+
}
67+
}
68+
69+
// WithPermissions sets the file mode to be used for a saved CDI spec.
70+
func WithPermissions(permissions fs.FileMode) Option {
71+
return func(o *options) error {
72+
o.permissions = permissions
73+
return nil
74+
}
75+
}

0 commit comments

Comments
 (0)