Skip to content

Commit 99eb0c7

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 e6a5c26 commit 99eb0c7

File tree

18 files changed

+609
-175
lines changed

18 files changed

+609
-175
lines changed

api/producer/api.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
const (
22+
// DefaultSpecFormat defines the default encoding used to write CDI specs.
23+
DefaultSpecFormat = SpecFormatYAML
24+
25+
// SpecFormatJSON defines a CDI spec formatted as JSON.
26+
SpecFormatJSON = SpecFormat(".json")
27+
// SpecFormatYAML defines a CDI spec formatted as YAML.
28+
SpecFormatYAML = SpecFormat(".yaml")
29+
)
30+
31+
// A SpecValidator is used to validate a CDI spec.
32+
type SpecValidator interface {
33+
Validate(*cdi.Spec) error
34+
}

api/producer/go.mod

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module tags.cncf.io/container-device-interface/api/producer
2+
3+
go 1.20
4+
5+
require (
6+
github.com/stretchr/testify v1.7.0
7+
golang.org/x/sys v0.1.0
8+
sigs.k8s.io/yaml v1.3.0
9+
tags.cncf.io/container-device-interface/api/validator v0.0.0
10+
tags.cncf.io/container-device-interface/specs-go v0.8.0
11+
)
12+
13+
require (
14+
github.com/davecgh/go-spew v1.1.1 // indirect
15+
github.com/pmezard/go-difflib v1.0.0 // indirect
16+
golang.org/x/mod v0.19.0 // indirect
17+
gopkg.in/yaml.v2 v2.4.0 // indirect
18+
gopkg.in/yaml.v3 v3.0.1 // indirect
19+
)
20+
21+
replace (
22+
tags.cncf.io/container-device-interface/api/validator => ../validator
23+
tags.cncf.io/container-device-interface/specs-go => ../../specs-go
24+
)

api/producer/go.sum

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
6+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
8+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
9+
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
10+
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
11+
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
12+
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
14+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
15+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
16+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
17+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
18+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
19+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
20+
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
21+
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

api/producer/options.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
24+
// An Option defines a functional option for constructing a producer.
25+
type Option func(*options) error
26+
27+
type options struct {
28+
specFormat SpecFormat
29+
specValidator SpecValidator
30+
overwrite bool
31+
permissions fs.FileMode
32+
}
33+
34+
// WithSpecFormat sets the output format of a CDI specification.
35+
func WithSpecFormat(format SpecFormat) Option {
36+
return func(o *options) error {
37+
switch format {
38+
case SpecFormatJSON, SpecFormatYAML:
39+
o.specFormat = format
40+
default:
41+
return fmt.Errorf("invalid CDI spec format %v", format)
42+
}
43+
return nil
44+
}
45+
}
46+
47+
// WithSpecValidator sets a validator to be used when writing an output spec.
48+
func WithSpecValidator(specValidator SpecValidator) Option {
49+
return func(o *options) error {
50+
o.specValidator = specValidator
51+
return nil
52+
}
53+
}
54+
55+
// WithOverwrite specifies whether a producer should overwrite a CDI spec when
56+
// saving to file.
57+
func WithOverwrite(overwrite bool) Option {
58+
return func(o *options) error {
59+
o.overwrite = overwrite
60+
return nil
61+
}
62+
}
63+
64+
// WithPermissions sets the file mode to be used for a saved CDI spec.
65+
func WithPermissions(permissions fs.FileMode) Option {
66+
return func(o *options) error {
67+
o.permissions = permissions
68+
return nil
69+
}
70+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
limitations under the License.
1515
*/
1616

17-
package cdi
17+
package producer
1818

1919
import (
2020
"fmt"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
limitations under the License.
1818
*/
1919

20-
package cdi
20+
package producer
2121

2222
import (
2323
"os"

api/producer/spec-format.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
"encoding/json"
21+
"fmt"
22+
"io"
23+
"path/filepath"
24+
25+
"sigs.k8s.io/yaml"
26+
27+
"tags.cncf.io/container-device-interface/api/validator"
28+
cdi "tags.cncf.io/container-device-interface/specs-go"
29+
)
30+
31+
// A SpecFormat defines the encoding to use when reading or writing a CDI specification.
32+
type SpecFormat string
33+
34+
type specFormatter struct {
35+
*cdi.Spec
36+
options
37+
}
38+
39+
// NewSpecFormatter creates a spec formatter with the specified options.
40+
func NewSpecFormatter(spec *cdi.Spec, opts ...Option) (*specFormatter, error) {
41+
sf := &specFormatter{
42+
Spec: spec,
43+
options: options{
44+
specFormat: DefaultSpecFormat,
45+
specValidator: validator.Default,
46+
},
47+
}
48+
for _, opt := range opts {
49+
err := opt(&sf.options)
50+
if err != nil {
51+
return nil, err
52+
}
53+
}
54+
return sf, nil
55+
}
56+
57+
// WriteTo writes the spec to the specified writer.
58+
func (p *specFormatter) WriteTo(w io.Writer) (int64, error) {
59+
data, err := p.contents()
60+
if err != nil {
61+
return 0, fmt.Errorf("failed to marshal Spec file: %w", err)
62+
}
63+
64+
n, err := w.Write(data)
65+
return int64(n), err
66+
}
67+
68+
// marshal returns the raw contents of a CDI specification.
69+
// No validation is performed.
70+
func (p SpecFormat) marshal(spec *cdi.Spec) ([]byte, error) {
71+
switch p {
72+
case SpecFormatYAML:
73+
data, err := yaml.Marshal(spec)
74+
if err != nil {
75+
return nil, err
76+
}
77+
data = append([]byte("---\n"), data...)
78+
return data, nil
79+
case SpecFormatJSON:
80+
return json.Marshal(spec)
81+
default:
82+
return nil, fmt.Errorf("undefined CDI spec format %v", p)
83+
}
84+
}
85+
86+
// normalizeFilename ensures that the specified filename ends in a supported extension.
87+
func (p SpecFormat) normalizeFilename(filename string) (string, SpecFormat) {
88+
switch filepath.Ext(filename) {
89+
case ".json":
90+
return filename, SpecFormatJSON
91+
case ".yaml":
92+
return filename, SpecFormatYAML
93+
default:
94+
return filename + string(p), p
95+
}
96+
}
97+
98+
// validate performs an explicit validation of the spec.
99+
// If no validator is configured, the spec is considered unconditionally valid.
100+
func (p *specFormatter) validate() error {
101+
if p == nil || p.specValidator == nil {
102+
return nil
103+
}
104+
return p.specValidator.Validate(p.Spec)
105+
}
106+
107+
// contents returns the raw contents of a CDI specification.
108+
// Validation is performed before marshalling the contentent based on the spec format.
109+
func (p *specFormatter) contents() ([]byte, error) {
110+
if err := p.validate(); err != nil {
111+
return nil, fmt.Errorf("spec validation failed: %w", err)
112+
}
113+
return p.specFormat.marshal(p.Spec)
114+
}

api/producer/writer.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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"
22+
"os"
23+
"path/filepath"
24+
25+
"tags.cncf.io/container-device-interface/api/validator"
26+
cdi "tags.cncf.io/container-device-interface/specs-go"
27+
)
28+
29+
// A SpecWriter defines a structure for outputting CDI specifications.
30+
type SpecWriter struct {
31+
options
32+
}
33+
34+
// NewSpecWriter creates a spec writer with the supplied options.
35+
func NewSpecWriter(opts ...Option) (*SpecWriter, error) {
36+
sw := &SpecWriter{
37+
options: options{
38+
overwrite: true,
39+
// TODO: This could be updated to 0644 to be world-readable.
40+
permissions: 0600,
41+
specFormat: DefaultSpecFormat,
42+
specValidator: validator.Default,
43+
},
44+
}
45+
for _, opt := range opts {
46+
err := opt(&sw.options)
47+
if err != nil {
48+
return nil, err
49+
}
50+
}
51+
return sw, nil
52+
}
53+
54+
// Save writes a CDI spec to a file with the specified name.
55+
// If the filename ends in a supported extension, the format implied by the
56+
// extension takes precedence over the format with which the SpecWriter was
57+
// configured.
58+
func (p *SpecWriter) Save(spec *cdi.Spec, filename string) (string, error) {
59+
filename, outputFormat := p.specFormat.normalizeFilename(filename)
60+
61+
specFormatter := specFormatter{
62+
Spec: spec,
63+
options: options{
64+
specFormat: outputFormat,
65+
specValidator: p.specValidator,
66+
},
67+
}
68+
69+
dir := filepath.Dir(filename)
70+
if dir != "" {
71+
if err := os.MkdirAll(dir, 0o755); err != nil {
72+
return "", fmt.Errorf("failed to create Spec dir: %w", err)
73+
}
74+
}
75+
76+
tmp, err := os.CreateTemp(dir, "spec.*.tmp")
77+
if err != nil {
78+
return "", fmt.Errorf("failed to create Spec file: %w", err)
79+
}
80+
81+
_, err = specFormatter.WriteTo(tmp)
82+
tmp.Close()
83+
if err != nil {
84+
return "", fmt.Errorf("failed to write Spec file: %w", err)
85+
}
86+
87+
if err := os.Chmod(tmp.Name(), p.permissions); err != nil {
88+
return "", fmt.Errorf("failed to set permissions on spec file: %w", err)
89+
}
90+
91+
err = renameIn(dir, filepath.Base(tmp.Name()), filepath.Base(filename), p.overwrite)
92+
if err != nil {
93+
_ = os.Remove(tmp.Name())
94+
return "", fmt.Errorf("failed to write Spec file: %w", err)
95+
}
96+
return filename, nil
97+
}
98+
99+
// WriteSpecTo writes the specified spec to the specified writer.
100+
func (p *SpecWriter) WriteSpecTo(spec *cdi.Spec, w io.Writer) (int64, error) {
101+
specFormatter := specFormatter{
102+
Spec: spec,
103+
options: options{
104+
specFormat: p.specFormat,
105+
specValidator: p.specValidator,
106+
},
107+
}
108+
109+
return specFormatter.WriteTo(w)
110+
}

0 commit comments

Comments
 (0)