From 3b02a924126ac2df74b1fc53c983709d9709920a Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 18 Feb 2026 18:13:45 +0100 Subject: [PATCH 1/4] test: Fix tests on darwin Signed-off-by: Evan Lezar --- pkg/cdi/container-edits_test.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/pkg/cdi/container-edits_test.go b/pkg/cdi/container-edits_test.go index 2240f3d..509cf93 100644 --- a/pkg/cdi/container-edits_test.go +++ b/pkg/cdi/container-edits_test.go @@ -438,10 +438,10 @@ func TestApplyContainerEdits(t *testing.T) { edits: &cdi.ContainerEdits{ DeviceNodes: []*cdi.DeviceNode{ { - Path: "/dev/nil", + Path: "/dev/null", Type: "c", - Major: 1, - Minor: 3, + Major: nullDeviceMajor, + Minor: nullDeviceMinor, Permissions: NoPermissions, }, }, @@ -450,10 +450,11 @@ func TestApplyContainerEdits(t *testing.T) { Linux: &oci.Linux{ Devices: []oci.LinuxDevice{ { - Path: "/dev/nil", - Type: "c", - Major: nullDeviceMajor, - Minor: nullDeviceMinor, + Path: "/dev/null", + Type: "c", + Major: nullDeviceMajor, + Minor: nullDeviceMinor, + FileMode: nullDeviceFileMode, }, }, Resources: &oci.LinuxResources{ @@ -476,10 +477,10 @@ func TestApplyContainerEdits(t *testing.T) { edits: &cdi.ContainerEdits{ DeviceNodes: []*cdi.DeviceNode{ { - Path: "/dev/nil", + Path: "/dev/null", Type: "c", - Major: 1, - Minor: 3, + Major: nullDeviceMajor, + Minor: nullDeviceMinor, Permissions: "", }, }, @@ -488,10 +489,11 @@ func TestApplyContainerEdits(t *testing.T) { Linux: &oci.Linux{ Devices: []oci.LinuxDevice{ { - Path: "/dev/nil", - Type: "c", - Major: nullDeviceMajor, - Minor: nullDeviceMinor, + Path: "/dev/null", + Type: "c", + Major: nullDeviceMajor, + Minor: nullDeviceMinor, + FileMode: nullDeviceFileMode, }, }, Resources: &oci.LinuxResources{ From 07ab0e44aeb911b683f5e44b9a296e9f956326bf Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 18 Feb 2026 17:11:01 +0100 Subject: [PATCH 2/4] Add cdi/producer package This change addes a new producer subpackage to cdi. The purpose of this package is to handle use-cases that do not require a CDI cache and are only concerned with generating and outputing CDI specifications. Signed-off-by: Evan Lezar --- pkg/cdi/cache.go | 34 +-- .../renamein_linux.go} | 2 +- .../renamein_other.go} | 2 +- pkg/cdi/producer/save.go | 245 ++++++++++++++++++ pkg/cdi/producer/save_test.go | 164 ++++++++++++ pkg/cdi/spec.go | 58 +---- 6 files changed, 427 insertions(+), 78 deletions(-) rename pkg/cdi/{spec_linux.go => producer/renamein_linux.go} (98%) rename pkg/cdi/{spec_other.go => producer/renamein_other.go} (98%) create mode 100644 pkg/cdi/producer/save.go create mode 100644 pkg/cdi/producer/save_test.go diff --git a/pkg/cdi/cache.go b/pkg/cdi/cache.go index ba817a5..73a42af 100644 --- a/pkg/cdi/cache.go +++ b/pkg/cdi/cache.go @@ -29,6 +29,7 @@ import ( "github.com/fsnotify/fsnotify" oci "github.com/opencontainers/runtime-spec/specs-go" + "tags.cncf.io/container-device-interface/pkg/cdi/producer" cdi "tags.cncf.io/container-device-interface/specs-go" ) @@ -282,25 +283,13 @@ func (c *Cache) highestPrioritySpecDir() (string, int) { // priority Spec directory. If name has a "json" or "yaml" extension it // choses the encoding. Otherwise the default YAML encoding is used. func (c *Cache) WriteSpec(raw *cdi.Spec, name string) error { - var ( - specDir string - path string - prio int - spec *Spec - err error - ) - - specDir, prio = c.highestPrioritySpecDir() + specDir, prio := c.highestPrioritySpecDir() if specDir == "" { return errors.New("no Spec directories to write to") } - path = filepath.Join(specDir, name) - if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" { - path += defaultSpecExt - } - - spec, err = newSpec(raw, path, prio) + path := producer.EnsureExtension(filepath.Join(specDir, name)) + spec, err := newSpec(raw, path, prio) if err != nil { return err } @@ -313,23 +302,14 @@ func (c *Cache) WriteSpec(raw *cdi.Spec, name string) error { // Spec previously written by WriteSpec(). If the file exists and // its removal fails RemoveSpec returns an error. func (c *Cache) RemoveSpec(name string) error { - var ( - specDir string - path string - err error - ) - - specDir, _ = c.highestPrioritySpecDir() + specDir, _ := c.highestPrioritySpecDir() if specDir == "" { return errors.New("no Spec directories to remove from") } - path = filepath.Join(specDir, name) - if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" { - path += defaultSpecExt - } + path := producer.EnsureExtension(filepath.Join(specDir, name)) - err = os.Remove(path) + err := os.Remove(path) if err != nil && errors.Is(err, fs.ErrNotExist) { err = nil } diff --git a/pkg/cdi/spec_linux.go b/pkg/cdi/producer/renamein_linux.go similarity index 98% rename from pkg/cdi/spec_linux.go rename to pkg/cdi/producer/renamein_linux.go index 88fd9bb..275a2f5 100644 --- a/pkg/cdi/spec_linux.go +++ b/pkg/cdi/producer/renamein_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package cdi +package producer import ( "fmt" diff --git a/pkg/cdi/spec_other.go b/pkg/cdi/producer/renamein_other.go similarity index 98% rename from pkg/cdi/spec_other.go rename to pkg/cdi/producer/renamein_other.go index f102c46..5f60e62 100644 --- a/pkg/cdi/spec_other.go +++ b/pkg/cdi/producer/renamein_other.go @@ -16,7 +16,7 @@ limitations under the License. */ -package cdi +package producer import ( "os" diff --git a/pkg/cdi/producer/save.go b/pkg/cdi/producer/save.go new file mode 100644 index 0000000..7f06769 --- /dev/null +++ b/pkg/cdi/producer/save.go @@ -0,0 +1,245 @@ +/* + Copyright © 2026 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 producer + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + orderedyaml "gopkg.in/yaml.v3" + + cdi "tags.cncf.io/container-device-interface/specs-go" +) + +type options struct { + outputFormat string + validator validator + // The following options are only applicable when calling the Save function. + overwrite bool + permissions os.FileMode +} + +// Save a CDI specification to the requested path. +// The file is created at the target path as an atomic operation by first +// creating a temporary file in the target directory. If the path consists of +// only a filename, the current working directory is used as the target +// directory. +// +// A set of options control how the file is to be saved. +// +// The output format (e.g. YAML or JSON) of the spec is determined from the +// provided path based on the file extension and overrides any caller-specified +// format. +// +// Note that it is up to the caller to ensure that the specified path has an +// extension that is consistent with the format. The EnsureExtension helper +// function can be used to do this. +func Save(raw *cdi.Spec, path string, opts ...Option) error { + if strings.TrimSpace(path) == "" { + return fmt.Errorf("a path is required") + } + dir, filename, err := splitPath(path) + if err != nil { + return err + } + if filename == "" { + return fmt.Errorf("unexpected empty filename") + } + if dir == "" { + return fmt.Errorf("unexpected empty directory name") + } + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("failed to create Spec dir: %w", err) + } + + o := populateOptions(opts...) + + tmpFile, err := os.CreateTemp(dir, "spec.*.tmp") + if err != nil { + return fmt.Errorf("failed to create Spec file: %w", err) + } + defer func() { + _ = os.Remove(tmpFile.Name()) + }() + + if o.permissions != 0 { + err = tmpFile.Chmod(o.permissions) + if err != nil { + return fmt.Errorf("failed to set file permissions: %w", err) + } + } + + format := o.formatFromFilename(filename) + _, err = WriteTo(raw, tmpFile, append(opts, WithOutputFormat(format))...) + _ = tmpFile.Close() + if err != nil { + return fmt.Errorf("failed to write Spec file: %w", err) + } + + return renameIn(dir, filepath.Base(tmpFile.Name()), filename, o.overwrite) +} + +// WriteTo writes a CDI specification to a writer with the supplied options. +// The specification is marshalled based on the configured output format (YAML +// or JSON) before being writtent to the writer. +// If a validator is supplied, this is applied to the output specification +// before writing. +func WriteTo(raw *cdi.Spec, w io.Writer, opts ...Option) (int64, error) { + o := populateOptions(opts...) + + data, err := o.marshal(raw) + if err != nil { + return 0, fmt.Errorf("failed to marshal spec: %w", err) + } + + n, err := w.Write(data) + if err != nil { + return 0, err + } + return int64(n), nil +} + +// EnsureExtension returns the specified path with the extension specified by +// the options appended if required. +func EnsureExtension(path string, opts ...Option) string { + o := populateOptions(opts...) + ext := filepath.Ext(path) + switch ext { + case ".yaml", ".json": + return path + default: + return path + "." + o.outputFormat + } +} + +func populateOptions(opts ...Option) *options { + o := &options{ + outputFormat: "yaml", + overwrite: false, + permissions: 0644, + } + for _, opt := range opts { + opt(o) + } + return o +} + +// An Option is used to supply additional configuration to the functions of the +// producer API. +type Option func(*options) + +// WithOutputFormat sets the format (e.g. YAML or JSON) to use when outputting a +// CDI specification. +func WithOutputFormat(format string) Option { + return func(o *options) { + o.outputFormat = format + } +} + +// WithValidator sets a validator to apply to a CDI specification before +// outputting it. +func WithValidator(validator validator) Option { + return func(o *options) { + o.validator = validator + } +} + +// WithOverwrite defines whether an existing CDI specification file should be +// overwritten if it exists at the specified path. +func WithOverwrite(overwrite bool) Option { + return func(o *options) { + o.overwrite = overwrite + } +} + +// WithPermissions sets the file permissions for the file created when +// outputting a CDI specification. +func WithPermissions(permissions os.FileMode) Option { + return func(o *options) { + o.permissions = permissions + } +} + +func (o *options) formatFromFilename(path string) string { + ext := filepath.Ext(path) + switch ext { + case ".yaml", ".yml": + return "yaml" + case ".json": + return "json" + default: + return o.outputFormat + } +} + +// splitPath separates a path into a directory and a filename. +// If the directory is unspecified or '.' the current working directory is +// returned instead. +func splitPath(path string) (string, string, error) { + path = filepath.Clean(path) + if err := assertNotDirectory(path); err != nil { + return "", "", err + } + dir, filename := filepath.Split(path) + if dir != "." { + return dir, filename, nil + } + + cwd, err := os.Getwd() + if err != nil { + return "", "", fmt.Errorf("error getting current directory: %w", err) + } + return cwd, filename, nil +} + +func assertNotDirectory(path string) error { + info, err := os.Stat(path) + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return err + } + if info.IsDir() { + return fmt.Errorf("specified path is a directory") + } + return nil +} + +// marshal the specified content based on the configured output format. +func (o *options) marshal(v any) ([]byte, error) { + switch o.outputFormat { + case "yaml": + data, err := orderedyaml.Marshal(v) + if err != nil { + return nil, err + } + // We prepend a YAML separator to the output to allow multiple YAML + // files to be concatenated together if required. + data = append([]byte("---\n"), data...) + return data, err + case "json": + return json.Marshal(v) + default: + return nil, fmt.Errorf("invalid output format: %q", o.outputFormat) + } +} diff --git a/pkg/cdi/producer/save_test.go b/pkg/cdi/producer/save_test.go new file mode 100644 index 0000000..513b820 --- /dev/null +++ b/pkg/cdi/producer/save_test.go @@ -0,0 +1,164 @@ +/* + Copyright © 2026 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 producer + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + cdi "tags.cncf.io/container-device-interface/specs-go" +) + +func TestSave(t *testing.T) { + testCases := []struct { + description string + spec *cdi.Spec + filename string + options []Option + expectedError string + assert func(*testing.T, string) + }{ + { + description: "empty filename returns directory error", + filename: "", + expectedError: "specified path is a directory", + }, + { + description: "spec is written with default permissions", + spec: &cdi.Spec{ + Version: "v1.1.0", + }, + filename: "test.yaml", + assert: func(t *testing.T, fullpath string) { + require.FileExists(t, fullpath) + info, err := os.Stat(fullpath) + require.NoError(t, err) + require.EqualValues(t, os.FileMode(0644), info.Mode().Perm()) + contents, err := os.ReadFile(fullpath) + require.NoError(t, err) + expectedContents := `--- +cdiVersion: v1.1.0 +kind: "" +devices: [] +` + require.EqualValues(t, expectedContents, string(contents)) + }, + }, + { + description: "spec is written as json with default permissions", + spec: &cdi.Spec{ + Version: "v1.1.0", + }, + filename: "test.json", + assert: func(t *testing.T, fullpath string) { + require.FileExists(t, fullpath) + info, err := os.Stat(fullpath) + require.NoError(t, err) + require.EqualValues(t, os.FileMode(0644), info.Mode().Perm()) + contents, err := os.ReadFile(fullpath) + require.NoError(t, err) + expectedContents := `{"cdiVersion":"v1.1.0","kind":"","devices":null,"containerEdits":{}}` + require.EqualValues(t, expectedContents, string(contents)) + }, + }, + { + description: "spec is written with format specified as json", + spec: &cdi.Spec{ + Version: "v1.1.0", + }, + filename: "test", + options: []Option{WithOutputFormat("json")}, + assert: func(t *testing.T, fullpath string) { + require.FileExists(t, fullpath) + info, err := os.Stat(fullpath) + require.NoError(t, err) + require.EqualValues(t, os.FileMode(0644), info.Mode().Perm()) + contents, err := os.ReadFile(fullpath) + require.NoError(t, err) + expectedContents := `{"cdiVersion":"v1.1.0","kind":"","devices":null,"containerEdits":{}}` + require.EqualValues(t, expectedContents, string(contents)) + }, + }, + { + description: "spec is written with format specified as yaml", + spec: &cdi.Spec{ + Version: "v1.1.0", + }, + filename: "test", + options: []Option{WithOutputFormat("yaml")}, + assert: func(t *testing.T, fullpath string) { + require.FileExists(t, fullpath) + info, err := os.Stat(fullpath) + require.NoError(t, err) + require.EqualValues(t, os.FileMode(0644), info.Mode().Perm()) + contents, err := os.ReadFile(fullpath) + require.NoError(t, err) + expectedContents := `--- +cdiVersion: v1.1.0 +kind: "" +devices: [] +` + require.EqualValues(t, expectedContents, string(contents)) + }, + }, + { + description: "spec is written with specified permissions", + spec: &cdi.Spec{ + Version: "v1.1.0", + }, + filename: "test.yaml", + options: []Option{ + WithPermissions(os.FileMode(0666)), + }, + assert: func(t *testing.T, fullpath string) { + require.FileExists(t, fullpath) + info, err := os.Stat(fullpath) + require.NoError(t, err) + require.EqualValues(t, os.FileMode(0666), info.Mode().Perm()) + contents, err := os.ReadFile(fullpath) + require.NoError(t, err) + expectedContents := `--- +cdiVersion: v1.1.0 +kind: "" +devices: [] +` + require.EqualValues(t, expectedContents, string(contents)) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + dir := t.TempDir() + + fullpath := filepath.Join(dir, tc.filename) + err := Save(tc.spec, fullpath, tc.options...) + + if tc.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedError) + } + if tc.assert != nil { + tc.assert(t, fullpath) + } + }) + } + +} diff --git a/pkg/cdi/spec.go b/pkg/cdi/spec.go index fdaa268..6ea56a0 100644 --- a/pkg/cdi/spec.go +++ b/pkg/cdi/spec.go @@ -17,7 +17,6 @@ package cdi import ( - "encoding/json" "fmt" "os" "path/filepath" @@ -25,19 +24,14 @@ import ( "sync" oci "github.com/opencontainers/runtime-spec/specs-go" - orderedyaml "gopkg.in/yaml.v3" "sigs.k8s.io/yaml" "tags.cncf.io/container-device-interface/internal/validation" + "tags.cncf.io/container-device-interface/pkg/cdi/producer" "tags.cncf.io/container-device-interface/pkg/parser" cdi "tags.cncf.io/container-device-interface/specs-go" ) -const ( - // defaultSpecExt is the file extension for the default encoding. - defaultSpecExt = ".yaml" -) - type validator interface { Validate(*cdi.Spec) error } @@ -107,10 +101,6 @@ func newSpec(raw *cdi.Spec, path string, priority int) (*Spec, error) { priority: priority, } - if ext := filepath.Ext(spec.path); ext != ".yaml" && ext != ".json" { - spec.path += defaultSpecExt - } - spec.vendor, spec.class = parser.ParseQualifier(spec.Kind) if spec.devices, err = spec.validate(); err != nil { @@ -124,10 +114,7 @@ func newSpec(raw *cdi.Spec, path string, priority int) (*Spec, error) { // by newSpec() or ReadSpec(). func (s *Spec) write(overwrite bool) error { var ( - data []byte - dir string - tmp *os.File - err error + err error ) err = validateSpec(s.Spec) @@ -135,40 +122,13 @@ func (s *Spec) write(overwrite bool) error { return err } - if filepath.Ext(s.path) == ".yaml" { - data, err = orderedyaml.Marshal(s.Spec) - data = append([]byte("---\n"), data...) - } else { - data, err = json.Marshal(s.Spec) - } - if err != nil { - return fmt.Errorf("failed to marshal Spec file: %w", err) - } - - dir = filepath.Dir(s.path) - err = os.MkdirAll(dir, 0o755) - if err != nil { - return fmt.Errorf("failed to create Spec dir: %w", err) - } - - tmp, err = os.CreateTemp(dir, "spec.*.tmp") - if err != nil { - return fmt.Errorf("failed to create Spec file: %w", err) - } - _, err = tmp.Write(data) - _ = tmp.Close() - if err != nil { - return fmt.Errorf("failed to write Spec file: %w", err) - } - - err = renameIn(dir, filepath.Base(tmp.Name()), filepath.Base(s.path), overwrite) - - if err != nil { - _ = os.Remove(tmp.Name()) - err = fmt.Errorf("failed to write Spec file: %w", err) - } - - return err + return producer.Save(s.Spec, s.path, + // If we cannot determine the file format as yaml from the extension, + // we assume a json format. + producer.WithOutputFormat("json"), + producer.WithOverwrite(overwrite), + producer.WithPermissions(0), + ) } // GetVendor returns the vendor of this Spec. From dd8a82cf32db5e91c35c3ddd4118b93b0db1a90f Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 18 Feb 2026 17:42:10 +0100 Subject: [PATCH 3/4] Add WithValidator option to producer API Signed-off-by: Evan Lezar --- pkg/cdi/producer/save.go | 26 ++++++++++++++++++++++++++ pkg/cdi/producer/save_test.go | 16 ++++++++++++++++ pkg/cdi/spec.go | 10 +--------- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/pkg/cdi/producer/save.go b/pkg/cdi/producer/save.go index 7f06769..5c1a29b 100644 --- a/pkg/cdi/producer/save.go +++ b/pkg/cdi/producer/save.go @@ -30,6 +30,10 @@ import ( cdi "tags.cncf.io/container-device-interface/specs-go" ) +type validator interface { + Validate(*cdi.Spec) error +} + type options struct { outputFormat string validator validator @@ -106,6 +110,10 @@ func Save(raw *cdi.Spec, path string, opts ...Option) error { func WriteTo(raw *cdi.Spec, w io.Writer, opts ...Option) (int64, error) { o := populateOptions(opts...) + if err := o.Validate(raw); err != nil { + return 0, fmt.Errorf("spec validation failed: %w", err) + } + data, err := o.marshal(raw) if err != nil { return 0, fmt.Errorf("failed to marshal spec: %w", err) @@ -243,3 +251,21 @@ func (o *options) marshal(v any) ([]byte, error) { return nil, fmt.Errorf("invalid output format: %q", o.outputFormat) } } + +// Validate a CDI specification using the supplied options. +// If no validator is specified, validation always succeeds. +func (o *options) Validate(raw *cdi.Spec) error { + if o == nil || o.validator == nil { + return nil + } + return o.validator.Validate(raw) +} + +type validatorFunction func(*cdi.Spec) error + +func (v validatorFunction) Validate(raw *cdi.Spec) error { + if v == nil { + return nil + } + return v(raw) +} diff --git a/pkg/cdi/producer/save_test.go b/pkg/cdi/producer/save_test.go index 513b820..840e501 100644 --- a/pkg/cdi/producer/save_test.go +++ b/pkg/cdi/producer/save_test.go @@ -17,6 +17,7 @@ package producer import ( + "fmt" "os" "path/filepath" "testing" @@ -141,6 +142,21 @@ devices: [] require.EqualValues(t, expectedContents, string(contents)) }, }, + { + description: "validator is called before save", + spec: &cdi.Spec{ + Version: "v1.1.0", + }, + filename: "test.yaml", + options: []Option{ + WithValidator(validatorFunction(func(s *cdi.Spec) error { return fmt.Errorf("invalid spec") })), + WithPermissions(os.FileMode(0666)), + }, + expectedError: "failed to write Spec file: spec validation failed: invalid spec", + assert: func(t *testing.T, fullpath string) { + require.NoFileExists(t, fullpath) + }, + }, } for _, tc := range testCases { diff --git a/pkg/cdi/spec.go b/pkg/cdi/spec.go index 6ea56a0..d8f1a18 100644 --- a/pkg/cdi/spec.go +++ b/pkg/cdi/spec.go @@ -113,21 +113,13 @@ func newSpec(raw *cdi.Spec, path string, priority int) (*Spec, error) { // Write the CDI Spec to the file associated with it during instantiation // by newSpec() or ReadSpec(). func (s *Spec) write(overwrite bool) error { - var ( - err error - ) - - err = validateSpec(s.Spec) - if err != nil { - return err - } - return producer.Save(s.Spec, s.path, // If we cannot determine the file format as yaml from the extension, // we assume a json format. producer.WithOutputFormat("json"), producer.WithOverwrite(overwrite), producer.WithPermissions(0), + producer.WithValidatorFunction(validateSpec), ) } From 3f95ba49e1c1a36f5016f643d3f16cded659c54c Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 18 Feb 2026 17:53:30 +0100 Subject: [PATCH 4/4] Add a content-based spec validator Signed-off-by: Evan Lezar --- pkg/cdi/spec.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/cdi/spec.go b/pkg/cdi/spec.go index d8f1a18..152d063 100644 --- a/pkg/cdi/spec.go +++ b/pkg/cdi/spec.go @@ -119,7 +119,7 @@ func (s *Spec) write(overwrite bool) error { producer.WithOutputFormat("json"), producer.WithOverwrite(overwrite), producer.WithPermissions(0), - producer.WithValidatorFunction(validateSpec), + producer.WithValidator(getSpecValidator()), ) } @@ -219,6 +219,13 @@ func SetSpecValidator(v validator) { specValidator = v } +// getSpecValidator returns a reference to the current validator. +func getSpecValidator() validator { + validatorLock.Lock() + defer validatorLock.Unlock() + return specValidator +} + // validateSpec validates the Spec using the external validator. func validateSpec(raw *cdi.Spec) error { validatorLock.RLock() @@ -298,3 +305,23 @@ func GenerateNameForTransientSpec(raw *cdi.Spec, transientID string) (string, er return GenerateTransientSpecName(vendor, class, transientID), nil } + +type contentSpecValidator string + +const ( + // SpecContentValidator validates the CDI spec based on the content and + // not the JSON schema. + SpecContentValidator = contentSpecValidator("default") +) + +func (v contentSpecValidator) Validate(raw *cdi.Spec) error { + spec := &Spec{ + Spec: raw, + } + spec.vendor, spec.class = parser.ParseQualifier(spec.Kind) + _, err := spec.validate() + if err != nil { + return fmt.Errorf("invalid CDI Spec: %w", err) + } + return nil +}