From 27986128829bc24a5a06fb10bbe61d6c3d68f271 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 18 Feb 2026 21:35:11 +0100 Subject: [PATCH 1/2] TOFIX: Use producer API Signed-off-by: Evan Lezar --- go.mod | 2 + go.sum | 2 - pkg/nvcdi/spec/spec.go | 139 +++++----- vendor/modules.txt | 4 +- .../renamein_linux.go} | 2 +- .../renamein_other.go} | 2 +- .../pkg/cdi-producer/save.go | 249 ++++++++++++++++++ .../pkg/cdi/cache.go | 34 +-- .../pkg/cdi/container-edits.go | 42 ++- .../container-device-interface/pkg/cdi/oci.go | 36 ++- .../pkg/cdi/spec.go | 91 +++---- 11 files changed, 432 insertions(+), 171 deletions(-) rename vendor/tags.cncf.io/container-device-interface/pkg/{cdi/spec_linux.go => cdi-producer/renamein_linux.go} (98%) rename vendor/tags.cncf.io/container-device-interface/pkg/{cdi/spec_other.go => cdi-producer/renamein_other.go} (98%) create mode 100644 vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/save.go diff --git a/go.mod b/go.mod index a2118bcdb..4335642b1 100644 --- a/go.mod +++ b/go.mod @@ -48,3 +48,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace tags.cncf.io/container-device-interface => ../container-device-interface diff --git a/go.sum b/go.sum index 7249964f6..8097b5ba0 100644 --- a/go.sum +++ b/go.sum @@ -116,7 +116,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY= -tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q= tags.cncf.io/container-device-interface/specs-go v1.1.0 h1:QRZVeAceQM+zTZe12eyfuJuuzp524EKYwhmvLd+h+yQ= tags.cncf.io/container-device-interface/specs-go v1.1.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= diff --git a/pkg/nvcdi/spec/spec.go b/pkg/nvcdi/spec/spec.go index 2efdb0d8b..b55cc6724 100644 --- a/pkg/nvcdi/spec/spec.go +++ b/pkg/nvcdi/spec/spec.go @@ -21,8 +21,10 @@ import ( "io" "os" "path/filepath" + "strings" "tags.cncf.io/container-device-interface/pkg/cdi" + producer "tags.cncf.io/container-device-interface/pkg/cdi-producer" "tags.cncf.io/container-device-interface/specs-go" "github.com/NVIDIA/nvidia-container-toolkit/pkg/nvcdi/transform" @@ -42,73 +44,75 @@ func New(opts ...Option) (Interface, error) { return newBuilder(opts...).Build() } -// Save writes the spec to the specified path and overwrites the file if it exists. -func (s *spec) Save(path string) error { - if s.transformOnSave != nil { - err := s.transformOnSave.Transform(s.Raw()) +type Validator interface { + Validate(*specs.Spec) error +} + +type Validators []Validator + +func (v Validators) Validate(s *specs.Spec) error { + for _, vv := range v { + if vv == nil { + continue + } + err := vv.Validate(s) if err != nil { - return fmt.Errorf("error applying transform: %w", err) + return err } } - path, err := s.normalizePath(path) - if err != nil { - return fmt.Errorf("failed to normalize path: %w", err) - } + return nil +} - specDir, filename := filepath.Split(path) - cache, _ := cdi.NewCache( - cdi.WithAutoRefresh(false), - cdi.WithSpecDirs(specDir), - ) - if err := cache.WriteSpec(s.Raw(), filename); err != nil { - return fmt.Errorf("failed to write spec: %w", err) - } +type transfromAsValidator struct { + transform.Transformer +} - specDirAsRoot, err := os.OpenRoot(specDir) - if err != nil { - return fmt.Errorf("failed to open root: %w", err) +func fromTransform(t transform.Transformer) Validator { + if t == nil { + return nil } - defer specDirAsRoot.Close() + return &transfromAsValidator{t} +} - if err := specDirAsRoot.Chmod(filename, s.permissions); err != nil { - return fmt.Errorf("failed to set permissions on spec file: %w", err) +func (t *transfromAsValidator) Validate(s *specs.Spec) error { + if t == nil || t.Transformer == nil { + return nil + } + if err := t.Transform(s); err != nil { + return fmt.Errorf("error applying transform: %w", err) } - return nil } -// WriteTo writes the spec to the specified writer. -func (s *spec) WriteTo(w io.Writer) (int64, error) { - tmpFile, err := os.CreateTemp("", "nvcdi-spec-*"+s.extension()) - if err != nil { - return 0, err - } - tmpDir, tmpFileName := filepath.Split(tmpFile.Name()) +// Save writes the spec to the specified path and overwrites the file if it exists. +func (s *spec) Save(path string) error { + pathWithExtension := s.ensureExtension(path) - tmpDirRoot, err := os.OpenRoot(tmpDir) - if err != nil { - return 0, err - } - defer tmpDirRoot.Close() - defer func() { - _ = tmpDirRoot.Remove(tmpFileName) - }() + return producer.Save(s.Raw(), pathWithExtension, + s.producerOptions()..., + ) +} - if err := s.Save(tmpFile.Name()); err != nil { - return 0, err - } +// WriteTo writes the spec to the specified writer. +func (s *spec) WriteTo(w io.Writer) (int64, error) { + return producer.WriteTo(s.Raw(), w, + s.producerOptions()..., + ) +} - if err := tmpFile.Close(); err != nil { - return 0, fmt.Errorf("failed to close temporary file: %w", err) +func (s *spec) producerOptions() []producer.Option { + var validators Validators + if s.transformOnSave != nil { + validators = append(validators, fromTransform(s.transformOnSave)) } + validators = append(validators, cdi.SpecContentValidator) - savedFile, err := tmpDirRoot.Open(tmpFileName) - if err != nil { - return 0, fmt.Errorf("failed to open temporary file: %w", err) + return []producer.Option{ + producer.WithOutputFormat(s.format), + producer.WithOverwrite(true), + producer.WithPermissions(s.permissions), + producer.WithValidator(validators), } - defer savedFile.Close() - - return savedFile.WriteTo(w) } // Raw returns a pointer to the raw spec. @@ -116,30 +120,17 @@ func (s *spec) Raw() *specs.Spec { return s.Spec } -// normalizePath ensures that the specified path has a supported extension -func (s *spec) normalizePath(path string) (string, error) { - if ext := filepath.Ext(path); ext != ".yaml" && ext != ".json" { - path += s.extension() +func (s *spec) ensureExtension(filename string) string { + if filename == "" { + return "" } - - if filepath.Clean(filepath.Dir(path)) == "." { - pwd, err := os.Getwd() - if err != nil { - return path, fmt.Errorf("failed to get current working directory: %v", err) - } - path = filepath.Join(pwd, path) + ext := filepath.Ext(filename) + switch ext { + case ".yaml", ".json": + return filename + case ".yml": + return strings.TrimSuffix(filename, ".yml") + ".yaml" + default: + return filename + "." + s.format } - - return path, nil -} - -func (s *spec) extension() string { - switch s.format { - case FormatJSON: - return ".json" - case FormatYAML: - return ".yaml" - } - - return ".yaml" } diff --git a/vendor/modules.txt b/vendor/modules.txt index 64826e09d..571ca44f0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -234,12 +234,14 @@ gopkg.in/yaml.v3 ## explicit; go 1.12 sigs.k8s.io/yaml sigs.k8s.io/yaml/goyaml.v2 -# tags.cncf.io/container-device-interface v1.1.0 +# tags.cncf.io/container-device-interface v1.1.0 => ../container-device-interface ## explicit; go 1.21 tags.cncf.io/container-device-interface/internal/validation tags.cncf.io/container-device-interface/internal/validation/k8s tags.cncf.io/container-device-interface/pkg/cdi +tags.cncf.io/container-device-interface/pkg/cdi-producer tags.cncf.io/container-device-interface/pkg/parser # tags.cncf.io/container-device-interface/specs-go v1.1.0 ## explicit; go 1.19 tags.cncf.io/container-device-interface/specs-go +# tags.cncf.io/container-device-interface => ../container-device-interface diff --git a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/spec_linux.go b/vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/renamein_linux.go similarity index 98% rename from vendor/tags.cncf.io/container-device-interface/pkg/cdi/spec_linux.go rename to vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/renamein_linux.go index 88fd9bbf5..275a2f53a 100644 --- a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/spec_linux.go +++ b/vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/renamein_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package cdi +package producer import ( "fmt" diff --git a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/spec_other.go b/vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/renamein_other.go similarity index 98% rename from vendor/tags.cncf.io/container-device-interface/pkg/cdi/spec_other.go rename to vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/renamein_other.go index f102c46bd..5f60e62b8 100644 --- a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/spec_other.go +++ b/vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/renamein_other.go @@ -16,7 +16,7 @@ limitations under the License. */ -package cdi +package producer import ( "os" diff --git a/vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/save.go b/vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/save.go new file mode 100644 index 000000000..44adbb31f --- /dev/null +++ b/vendor/tags.cncf.io/container-device-interface/pkg/cdi-producer/save.go @@ -0,0 +1,249 @@ +/* + 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 validator interface { + Validate(*cdi.Spec) error +} + +type options struct { + format string + overwrite bool + permissions os.FileMode + validator validator +} + +// An Option is used to supply additional configuration to the functions of the +// producer API. +type Option func(*options) + +// Save a CDI specification to the requested path. +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. +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) + } + + n, err := w.Write(data) + if err != nil { + return 0, err + } + return int64(n), nil +} + +// EnsureExtension adds an extension defined by the specified options 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.format + } +} + +func populateOptions(opts ...Option) *options { + o := &options{ + format: "yaml", + overwrite: false, + permissions: 0644, + } + for _, opt := range opts { + opt(o) + } + return o +} + +// 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.format = format + } +} + +// 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 + } +} + +// 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 + } +} + +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.format + } +} + +// 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.Lstat(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 +} + +func (o *options) marshal(v any) ([]byte, error) { + switch o.format { + case "yaml": + data, err := orderedyaml.Marshal(v) + if err != nil { + return nil, err + } + data = append([]byte("---\n"), data...) + return data, err + case "json": + return json.Marshal(v) + default: + return nil, fmt.Errorf("invalid output format: %v", o.format) + } +} + +// 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/vendor/tags.cncf.io/container-device-interface/pkg/cdi/cache.go b/vendor/tags.cncf.io/container-device-interface/pkg/cdi/cache.go index ba817a55b..d438823f3 100644 --- a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/cache.go +++ b/vendor/tags.cncf.io/container-device-interface/pkg/cdi/cache.go @@ -29,6 +29,7 @@ import ( "github.com/fsnotify/fsnotify" oci "github.com/opencontainers/runtime-spec/specs-go" + producer "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/vendor/tags.cncf.io/container-device-interface/pkg/cdi/container-edits.go b/vendor/tags.cncf.io/container-device-interface/pkg/cdi/container-edits.go index f498049c6..387219f53 100644 --- a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/container-edits.go +++ b/vendor/tags.cncf.io/container-device-interface/pkg/cdi/container-edits.go @@ -42,6 +42,9 @@ const ( PoststartHook = "poststart" // PoststopHook is the name of the OCI "poststop" hook. PoststopHook = "poststop" + + // NoPermissions requests empty cgroup permissions for a device. + NoPermissions = "none" ) var ( @@ -106,8 +109,11 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { if dev.Type == "b" || dev.Type == "c" { access := d.Permissions - if access == "" { + switch access { + case "": access = "rwm" + case NoPermissions: + access = "" } specgen.AddLinuxResourcesDevice(true, dev.Type, &dev.Major, &dev.Minor, access) } @@ -123,8 +129,15 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { if len(e.Mounts) > 0 { for _, m := range e.Mounts { + mnt := &Mount{m} + specgen.RemoveMount(m.ContainerPath) - specgen.AddMount((&Mount{m}).toOCI()) + + if !specHasUserNamespace(spec) { + specgen.AddMount(mnt.toOCI()) + } else { + specgen.AddMount(mnt.toOCI(withIDMapForBindMount())) + } } sortMounts(&specgen) } @@ -354,12 +367,14 @@ func (d *DeviceNode) Validate() error { if _, ok := validTypes[d.Type]; !ok { return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type) } - for _, bit := range d.Permissions { - if bit != 'r' && bit != 'w' && bit != 'm' { - return fmt.Errorf("device %q: invalid permissions %q", - d.Path, d.Permissions) - } + switch { + case d.Permissions == "": + case d.Permissions == NoPermissions: + case strings.Trim(d.Permissions, "rwm") != "": + return fmt.Errorf("device %q: invalid permissions %q", + d.Path, d.Permissions) } + return nil } @@ -465,3 +480,16 @@ func (m orderedMounts) Swap(i, j int) { func (m orderedMounts) parts(i int) int { return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator)) } + +// specHasUserNamespace returns true if the OCI Spec has a Linux UserNamespace. +func specHasUserNamespace(spec *oci.Spec) bool { + if spec == nil || spec.Linux == nil { + return false + } + for _, ns := range spec.Linux.Namespaces { + if ns.Type == oci.UserNamespace { + return true + } + } + return false +} diff --git a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/oci.go b/vendor/tags.cncf.io/container-device-interface/pkg/cdi/oci.go index f37499fcf..68ffafe9e 100644 --- a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/oci.go +++ b/vendor/tags.cncf.io/container-device-interface/pkg/cdi/oci.go @@ -30,14 +30,46 @@ func (h *Hook) toOCI() spec.Hook { } } +// Additional OCI mount option to apply to injected mounts. +type ociMountOption func(*spec.Mount) + +// withIDMapForBindMount adds any necessary ID mapping options for a bind mount. +func withIDMapForBindMount() ociMountOption { + return func(m *spec.Mount) { + option := "" + if m.Type == "bind" { + option = "idmap" + } + + for _, o := range m.Options { + switch o { + case "idmap", "ridmap": + return + case "bind": + option = "idmap" + case "rbind": + option = "ridmap" + } + } + + if option != "" { + m.Options = append(m.Options, option) + } + } +} + // toOCI returns the opencontainers runtime Spec Mount for this Mount. -func (m *Mount) toOCI() spec.Mount { - return spec.Mount{ +func (m *Mount) toOCI(options ...ociMountOption) spec.Mount { + om := spec.Mount{ Source: m.HostPath, Destination: m.ContainerPath, Options: m.Options, Type: m.Type, } + for _, o := range options { + o(&om) + } + return om } // toOCI returns the opencontainers runtime Spec LinuxDevice for this DeviceNode. diff --git a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/spec.go b/vendor/tags.cncf.io/container-device-interface/pkg/cdi/spec.go index fdaa26849..e51829254 100644 --- a/vendor/tags.cncf.io/container-device-interface/pkg/cdi/spec.go +++ b/vendor/tags.cncf.io/container-device-interface/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" + producer "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 { @@ -123,52 +113,14 @@ 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 ( - data []byte - dir string - tmp *os.File - err error + 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.WithValidator(getSpecValidator()), ) - - err = validateSpec(s.Spec) - if err != nil { - 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 } // GetVendor returns the vendor of this Spec. @@ -267,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() @@ -346,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 +} From dd67e3a6cb466c749a67ae1b32071a6ae4db6662 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 18 Feb 2026 22:42:28 +0100 Subject: [PATCH 2/2] refactor: Consolidate handling of output to stdout Signed-off-by: Evan Lezar --- cmd/nvidia-ctk/cdi/generate/generate.go | 9 --------- cmd/nvidia-ctk/cdi/transform/root/root.go | 8 -------- pkg/nvcdi/spec/spec.go | 13 +++++++++---- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/cmd/nvidia-ctk/cdi/generate/generate.go b/cmd/nvidia-ctk/cdi/generate/generate.go index 2fff6eac5..9cced63c4 100644 --- a/cmd/nvidia-ctk/cdi/generate/generate.go +++ b/cmd/nvidia-ctk/cdi/generate/generate.go @@ -20,7 +20,6 @@ import ( "context" "errors" "fmt" - "os" "path/filepath" "slices" "strings" @@ -352,14 +351,6 @@ type generatedSpecs struct { func (g *generatedSpecs) Save(filename string) error { filename = g.updateFilename(filename) - if filename == "" { - _, err := g.WriteTo(os.Stdout) - if err != nil { - return fmt.Errorf("failed to write CDI spec to STDOUT: %v", err) - } - return nil - } - return g.Interface.Save(filename) } diff --git a/cmd/nvidia-ctk/cdi/transform/root/root.go b/cmd/nvidia-ctk/cdi/transform/root/root.go index 21f8daf67..838e0bf35 100644 --- a/cmd/nvidia-ctk/cdi/transform/root/root.go +++ b/cmd/nvidia-ctk/cdi/transform/root/root.go @@ -159,13 +159,5 @@ func (o transformOptions) getContents() ([]byte, error) { // Save saves the CDI specification to the output file func (o transformOptions) Save(s spec.Interface) error { - if o.output == "" { - _, err := s.WriteTo(os.Stdout) - if err != nil { - return fmt.Errorf("failed to write CDI spec to STDOUT: %v", err) - } - return nil - } - return s.Save(o.output) } diff --git a/pkg/nvcdi/spec/spec.go b/pkg/nvcdi/spec/spec.go index b55cc6724..bb5ca74d4 100644 --- a/pkg/nvcdi/spec/spec.go +++ b/pkg/nvcdi/spec/spec.go @@ -86,11 +86,16 @@ func (t *transfromAsValidator) Validate(s *specs.Spec) error { // Save writes the spec to the specified path and overwrites the file if it exists. func (s *spec) Save(path string) error { - pathWithExtension := s.ensureExtension(path) + if pathWithExtension := s.ensureExtension(path); pathWithExtension != "" { + return producer.Save(s.Raw(), pathWithExtension, + s.producerOptions()..., + ) + } + if _, err := s.WriteTo(os.Stdout); err != nil { + return fmt.Errorf("failed to write CDI spec to STDOUT: %w", err) + } - return producer.Save(s.Raw(), pathWithExtension, - s.producerOptions()..., - ) + return nil } // WriteTo writes the spec to the specified writer.