diff --git a/api/producer/api.go b/api/producer/api.go new file mode 100644 index 00000000..7845b2e9 --- /dev/null +++ b/api/producer/api.go @@ -0,0 +1,34 @@ +/* + Copyright © 2024 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 cdi "tags.cncf.io/container-device-interface/specs-go" + +const ( + // DefaultSpecFormat defines the default encoding used to write CDI specs. + DefaultSpecFormat = SpecFormatYAML + + // SpecFormatJSON defines a CDI spec formatted as JSON. + SpecFormatJSON = SpecFormat(".json") + // SpecFormatYAML defines a CDI spec formatted as YAML. + SpecFormatYAML = SpecFormat(".yaml") +) + +// A SpecValidator is used to validate a CDI spec. +type SpecValidator interface { + Validate(*cdi.Spec) error +} diff --git a/api/producer/go.mod b/api/producer/go.mod new file mode 100644 index 00000000..03e48e1b --- /dev/null +++ b/api/producer/go.mod @@ -0,0 +1,24 @@ +module tags.cncf.io/container-device-interface/api/producer + +go 1.20 + +require ( + github.com/stretchr/testify v1.7.0 + golang.org/x/sys v0.1.0 + sigs.k8s.io/yaml v1.3.0 + tags.cncf.io/container-device-interface/api/validator v0.0.0 + tags.cncf.io/container-device-interface/specs-go v0.8.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.19.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace ( + tags.cncf.io/container-device-interface/api/validator => ../validator + tags.cncf.io/container-device-interface/specs-go => ../../specs-go +) diff --git a/api/producer/go.sum b/api/producer/go.sum new file mode 100644 index 00000000..9df47ee0 --- /dev/null +++ b/api/producer/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/api/producer/options.go b/api/producer/options.go new file mode 100644 index 00000000..46b8ce67 --- /dev/null +++ b/api/producer/options.go @@ -0,0 +1,70 @@ +/* + Copyright © 2024 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 ( + "fmt" + "io/fs" +) + +// An Option defines a functional option for constructing a producer. +type Option func(*options) error + +type options struct { + specFormat SpecFormat + specValidator SpecValidator + overwrite bool + permissions fs.FileMode +} + +// WithSpecFormat sets the output format of a CDI specification. +func WithSpecFormat(format SpecFormat) Option { + return func(o *options) error { + switch format { + case SpecFormatJSON, SpecFormatYAML: + o.specFormat = format + default: + return fmt.Errorf("invalid CDI spec format %v", format) + } + return nil + } +} + +// WithSpecValidator sets a validator to be used when writing an output spec. +func WithSpecValidator(specValidator SpecValidator) Option { + return func(o *options) error { + o.specValidator = specValidator + return nil + } +} + +// WithOverwrite specifies whether a producer should overwrite a CDI spec when +// saving to file. +func WithOverwrite(overwrite bool) Option { + return func(o *options) error { + o.overwrite = overwrite + return nil + } +} + +// WithPermissions sets the file mode to be used for a saved CDI spec. +func WithPermissions(permissions fs.FileMode) Option { + return func(o *options) error { + o.permissions = permissions + return nil + } +} diff --git a/pkg/cdi/spec_linux.go b/api/producer/renamein_linux.go similarity index 98% rename from pkg/cdi/spec_linux.go rename to api/producer/renamein_linux.go index 9ad27392..7d17b2f3 100644 --- a/pkg/cdi/spec_linux.go +++ b/api/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/api/producer/renamein_other.go similarity index 98% rename from pkg/cdi/spec_other.go rename to api/producer/renamein_other.go index 285e04e2..96ba268a 100644 --- a/pkg/cdi/spec_other.go +++ b/api/producer/renamein_other.go @@ -17,7 +17,7 @@ limitations under the License. */ -package cdi +package producer import ( "os" diff --git a/api/producer/spec-format.go b/api/producer/spec-format.go new file mode 100644 index 00000000..a8f6ed6b --- /dev/null +++ b/api/producer/spec-format.go @@ -0,0 +1,114 @@ +/* + Copyright © 2024 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" + "fmt" + "io" + "path/filepath" + + "sigs.k8s.io/yaml" + + "tags.cncf.io/container-device-interface/api/validator" + cdi "tags.cncf.io/container-device-interface/specs-go" +) + +// A SpecFormat defines the encoding to use when reading or writing a CDI specification. +type SpecFormat string + +type specFormatter struct { + *cdi.Spec + options +} + +// NewSpecFormatter creates a spec formatter with the specified options. +func NewSpecFormatter(spec *cdi.Spec, opts ...Option) (*specFormatter, error) { + sf := &specFormatter{ + Spec: spec, + options: options{ + specFormat: DefaultSpecFormat, + specValidator: validator.Default, + }, + } + for _, opt := range opts { + err := opt(&sf.options) + if err != nil { + return nil, err + } + } + return sf, nil +} + +// WriteTo writes the spec to the specified writer. +func (p *specFormatter) WriteTo(w io.Writer) (int64, error) { + data, err := p.contents() + if err != nil { + return 0, fmt.Errorf("failed to marshal Spec file: %w", err) + } + + n, err := w.Write(data) + return int64(n), err +} + +// marshal returns the raw contents of a CDI specification. +// No validation is performed. +func (p SpecFormat) marshal(spec *cdi.Spec) ([]byte, error) { + switch p { + case SpecFormatYAML: + data, err := yaml.Marshal(spec) + if err != nil { + return nil, err + } + data = append([]byte("---\n"), data...) + return data, nil + case SpecFormatJSON: + return json.Marshal(spec) + default: + return nil, fmt.Errorf("undefined CDI spec format %v", p) + } +} + +// normalizeFilename ensures that the specified filename ends in a supported extension. +func (p SpecFormat) normalizeFilename(filename string) (string, SpecFormat) { + switch filepath.Ext(filename) { + case ".json": + return filename, SpecFormatJSON + case ".yaml": + return filename, SpecFormatYAML + default: + return filename + string(p), p + } +} + +// validate performs an explicit validation of the spec. +// If no validator is configured, the spec is considered unconditionally valid. +func (p *specFormatter) validate() error { + if p == nil || p.specValidator == nil { + return nil + } + return p.specValidator.Validate(p.Spec) +} + +// contents returns the raw contents of a CDI specification. +// Validation is performed before marshalling the contentent based on the spec format. +func (p *specFormatter) contents() ([]byte, error) { + if err := p.validate(); err != nil { + return nil, fmt.Errorf("spec validation failed: %w", err) + } + return p.specFormat.marshal(p.Spec) +} diff --git a/api/producer/writer.go b/api/producer/writer.go new file mode 100644 index 00000000..a3b05743 --- /dev/null +++ b/api/producer/writer.go @@ -0,0 +1,110 @@ +/* + Copyright © 2024 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 ( + "fmt" + "io" + "os" + "path/filepath" + + "tags.cncf.io/container-device-interface/api/validator" + cdi "tags.cncf.io/container-device-interface/specs-go" +) + +// A SpecWriter defines a structure for outputting CDI specifications. +type SpecWriter struct { + options +} + +// NewSpecWriter creates a spec writer with the supplied options. +func NewSpecWriter(opts ...Option) (*SpecWriter, error) { + sw := &SpecWriter{ + options: options{ + overwrite: true, + // TODO: This could be updated to 0644 to be world-readable. + permissions: 0600, + specFormat: DefaultSpecFormat, + specValidator: validator.Default, + }, + } + for _, opt := range opts { + err := opt(&sw.options) + if err != nil { + return nil, err + } + } + return sw, nil +} + +// Save writes a CDI spec to a file with the specified name. +// If the filename ends in a supported extension, the format implied by the +// extension takes precedence over the format with which the SpecWriter was +// configured. +func (p *SpecWriter) Save(spec *cdi.Spec, filename string) (string, error) { + filename, outputFormat := p.specFormat.normalizeFilename(filename) + + specFormatter := specFormatter{ + Spec: spec, + options: options{ + specFormat: outputFormat, + specValidator: p.specValidator, + }, + } + + dir := filepath.Dir(filename) + if dir != "" { + if err := os.MkdirAll(dir, 0o755); 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 = specFormatter.WriteTo(tmp) + tmp.Close() + if err != nil { + return "", fmt.Errorf("failed to write Spec file: %w", err) + } + + if err := os.Chmod(tmp.Name(), p.permissions); err != nil { + return "", fmt.Errorf("failed to set permissions on spec file: %w", err) + } + + err = renameIn(dir, filepath.Base(tmp.Name()), filepath.Base(filename), p.overwrite) + if err != nil { + _ = os.Remove(tmp.Name()) + return "", fmt.Errorf("failed to write Spec file: %w", err) + } + return filename, nil +} + +// WriteSpecTo writes the specified spec to the specified writer. +func (p *SpecWriter) WriteSpecTo(spec *cdi.Spec, w io.Writer) (int64, error) { + specFormatter := specFormatter{ + Spec: spec, + options: options{ + specFormat: p.specFormat, + specValidator: p.specValidator, + }, + } + + return specFormatter.WriteTo(w) +} diff --git a/api/producer/writer_test.go b/api/producer/writer_test.go new file mode 100644 index 00000000..0f60f001 --- /dev/null +++ b/api/producer/writer_test.go @@ -0,0 +1,165 @@ +/* + Copyright © 2024 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" + "tags.cncf.io/container-device-interface/api/validator" + cdi "tags.cncf.io/container-device-interface/specs-go" +) + +func TestSave(t *testing.T) { + testCases := []struct { + description string + spec cdi.Spec + options []Option + filename string + expectedError error + expectedFilename string + expectedPermissions os.FileMode + expectedOutput string + }{ + { + description: "output as json", + spec: cdi.Spec{ + Version: "v0.3.0", + Kind: "example.com/class", + ContainerEdits: cdi.ContainerEdits{ + DeviceNodes: []*cdi.DeviceNode{ + { + Path: "/dev/foo", + }, + }, + }, + }, + options: []Option{}, + filename: "foo.json", + expectedFilename: "foo.json", + expectedPermissions: 0600, + expectedOutput: `{"cdiVersion":"v0.3.0","kind":"example.com/class","devices":null,"containerEdits":{"deviceNodes":[{"path":"/dev/foo"}]}}`, + }, + { + description: "output with permissions", + spec: cdi.Spec{ + Version: "v0.3.0", + Kind: "example.com/class", + ContainerEdits: cdi.ContainerEdits{ + DeviceNodes: []*cdi.DeviceNode{ + { + Path: "/dev/foo", + }, + }, + }, + }, + options: []Option{WithPermissions(0644)}, + filename: "foo.json", + expectedFilename: "foo.json", + expectedPermissions: 0644, + expectedOutput: `{"cdiVersion":"v0.3.0","kind":"example.com/class","devices":null,"containerEdits":{"deviceNodes":[{"path":"/dev/foo"}]}}`, + }, + { + description: "spec is validated on save", + spec: cdi.Spec{ + Version: "v99.3.0", + }, + options: []Option{}, + filename: "foo.json", + expectedError: validator.ErrInvalid, + }, + { + description: "filename overwrites format", + spec: cdi.Spec{ + Version: "v0.3.0", + Kind: "example.com/class", + ContainerEdits: cdi.ContainerEdits{ + DeviceNodes: []*cdi.DeviceNode{ + { + Path: "/dev/foo", + }, + }, + }, + }, + options: []Option{WithSpecFormat(SpecFormatJSON)}, + filename: "foo.yaml", + expectedFilename: "foo.yaml", + expectedPermissions: 0600, + expectedOutput: `--- +cdiVersion: v0.3.0 +containerEdits: + deviceNodes: + - path: /dev/foo +devices: null +kind: example.com/class +`, + }, + { + description: "filename is inferred from format", + spec: cdi.Spec{ + Version: "v0.3.0", + Kind: "example.com/class", + ContainerEdits: cdi.ContainerEdits{ + DeviceNodes: []*cdi.DeviceNode{ + { + Path: "/dev/foo", + }, + }, + }, + }, + options: []Option{WithSpecFormat(SpecFormatYAML)}, + filename: "foo", + expectedFilename: "foo.yaml", + expectedPermissions: 0600, + expectedOutput: `--- +cdiVersion: v0.3.0 +containerEdits: + deviceNodes: + - path: /dev/foo +devices: null +kind: example.com/class +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + + outputDir := t.TempDir() + + p, err := New(tc.options...) + require.NoError(t, err) + + f, err := p.Save(&tc.spec, filepath.Join(outputDir, tc.filename)) + require.ErrorIs(t, err, tc.expectedError) + if tc.expectedError != nil { + return + } + + require.Equal(t, filepath.Join(outputDir, tc.expectedFilename), f) + info, err := os.Stat(f) + require.NoError(t, err) + + require.Equal(t, tc.expectedPermissions, info.Mode()) + + contents, _ := os.ReadFile(f) + require.Equal(t, tc.expectedOutput, string(contents)) + }) + } +} diff --git a/cmd/cdi/go.mod b/cmd/cdi/go.mod index 6370a806..7d238828 100644 --- a/cmd/cdi/go.mod +++ b/cmd/cdi/go.mod @@ -7,7 +7,7 @@ require ( github.com/opencontainers/runtime-spec v1.1.0 github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 github.com/spf13/cobra v1.6.0 - sigs.k8s.io/yaml v1.3.0 + sigs.k8s.io/yaml v1.4.0 tags.cncf.io/container-device-interface v0.0.0 ) @@ -21,13 +21,14 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/sys v0.19.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + tags.cncf.io/container-device-interface/api/producer v0.0.0 // indirect tags.cncf.io/container-device-interface/api/validator v0.0.0 // indirect tags.cncf.io/container-device-interface/specs-go v0.8.0 // indirect ) replace ( tags.cncf.io/container-device-interface => ../.. + tags.cncf.io/container-device-interface/api/producer => ../../api/producer tags.cncf.io/container-device-interface/api/validator => ../../api/validator tags.cncf.io/container-device-interface/specs-go => ../../specs-go ) diff --git a/cmd/cdi/go.sum b/cmd/cdi/go.sum index 2f7d2c1c..8a912c8d 100644 --- a/cmd/cdi/go.sum +++ b/cmd/cdi/go.sum @@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -54,9 +56,8 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/cmd/validate/go.mod b/cmd/validate/go.mod index 5713a517..d5a8b1df 100644 --- a/cmd/validate/go.mod +++ b/cmd/validate/go.mod @@ -11,12 +11,13 @@ require ( golang.org/x/mod v0.19.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect sigs.k8s.io/yaml v1.3.0 // indirect - tags.cncf.io/container-device-interface/api/validator v0.0.0-00010101000000-000000000000 // indirect + tags.cncf.io/container-device-interface/api/validator v0.0.0 // indirect tags.cncf.io/container-device-interface/specs-go v0.8.0 // indirect ) replace ( tags.cncf.io/container-device-interface => ../.. + tags.cncf.io/container-device-interface/api/producer => ../../api/producer tags.cncf.io/container-device-interface/api/validator => ../../api/validator tags.cncf.io/container-device-interface/specs-go => ../../specs-go ) diff --git a/go.mod b/go.mod index 9a7ce77d..d187a410 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,8 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/sys v0.19.0 sigs.k8s.io/yaml v1.3.0 - tags.cncf.io/container-device-interface/api/validator v0.0.0-00010101000000-000000000000 + tags.cncf.io/container-device-interface/api/producer v0.0.0 + tags.cncf.io/container-device-interface/api/validator v0.0.0 tags.cncf.io/container-device-interface/specs-go v0.8.0 ) @@ -26,6 +27,7 @@ require ( ) replace ( + tags.cncf.io/container-device-interface/api/producer => ./api/producer tags.cncf.io/container-device-interface/api/validator => ./api/validator tags.cncf.io/container-device-interface/specs-go => ./specs-go ) diff --git a/pkg/cdi/cache.go b/pkg/cdi/cache.go index 04f15e02..b18836a0 100644 --- a/pkg/cdi/cache.go +++ b/pkg/cdi/cache.go @@ -28,6 +28,7 @@ import ( "github.com/fsnotify/fsnotify" oci "github.com/opencontainers/runtime-spec/specs-go" + "tags.cncf.io/container-device-interface/api/producer" cdi "tags.cncf.io/container-device-interface/specs-go" ) @@ -281,30 +282,31 @@ 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, _ := 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 + // Ideally we would like to pass the configured spec validator to the + // producer, but we would need to handle the synchronisation. + // Instead we call `validateSpec` here which is a no-op if no validator is + // configured. + if err := validateSpec(raw); err != nil { + return err } - spec, err = newSpec(raw, path, prio) + path := filepath.Join(specDir, name) + + p, err := producer.NewSpecWriter( + producer.WithOverwrite(true), + ) if err != nil { return err } - - return spec.write(true) + if _, err := p.Save(raw, path); err != nil { + return err + } + return nil } // RemoveSpec removes a Spec with the given name from the highest diff --git a/pkg/cdi/container-edits.go b/pkg/cdi/container-edits.go index 8f40a49f..5edf342b 100644 --- a/pkg/cdi/container-edits.go +++ b/pkg/cdi/container-edits.go @@ -186,16 +186,6 @@ func (e *ContainerEdits) Append(o *ContainerEdits) *ContainerEdits { return e } -// ValidateEnv validates the given environment variables. -func ValidateEnv(env []string) error { - for _, v := range env { - if strings.IndexByte(v, byte('=')) <= 0 { - return fmt.Errorf("invalid environment variable %q", v) - } - } - return nil -} - // DeviceNode is a CDI Spec DeviceNode wrapper, used for validating DeviceNodes. type DeviceNode struct { *cdi.DeviceNode @@ -203,27 +193,7 @@ type DeviceNode struct { // Validate a CDI Spec DeviceNode. func (d *DeviceNode) Validate() error { - validTypes := map[string]struct{}{ - "": {}, - "b": {}, - "c": {}, - "u": {}, - "p": {}, - } - - if d.Path == "" { - return errors.New("invalid (empty) device path") - } - 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) - } - } - return nil + return validator.Default.ValidateAny(d.DeviceNode) } // Hook is a CDI Spec Hook wrapper, used for validating hooks. diff --git a/pkg/cdi/spec.go b/pkg/cdi/spec.go index fa018333..292bddde 100644 --- a/pkg/cdi/spec.go +++ b/pkg/cdi/spec.go @@ -17,7 +17,6 @@ package cdi import ( - "encoding/json" "fmt" "os" "path/filepath" @@ -27,6 +26,7 @@ import ( oci "github.com/opencontainers/runtime-spec/specs-go" "sigs.k8s.io/yaml" + "tags.cncf.io/container-device-interface/api/producer" "tags.cncf.io/container-device-interface/api/validator" "tags.cncf.io/container-device-interface/pkg/parser" cdi "tags.cncf.io/container-device-interface/specs-go" @@ -131,53 +131,22 @@ 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(). +// +// Deprecated: Use producer.SpecWriter instead. func (s *Spec) write(overwrite bool) error { - var ( - data []byte - dir string - tmp *os.File - err error + p, err := producer.NewSpecWriter( + producer.WithOverwrite(overwrite), ) - - err = validateSpec(s.Spec) if err != nil { return err } - if filepath.Ext(s.path) == ".yaml" { - data, err = yaml.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") + savedPath, err := p.Save(s.Spec, s.path) 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 err + s.path = savedPath + return nil } // GetVendor returns the vendor of this Spec. @@ -240,7 +209,7 @@ func SetSpecValidator(fn func(*cdi.Spec) error) { specValidator = fn } -// validateSpec validates the Spec using the extneral validator. +// validateSpec validates the Spec using the extneral validation. func validateSpec(raw *cdi.Spec) error { validatorLock.RLock() defer validatorLock.RUnlock()