Skip to content

Commit 243b74f

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, and file permissions to be specified. Signed-off-by: Evan Lezar <[email protected]>
1 parent 4149a8a commit 243b74f

File tree

17 files changed

+554
-77
lines changed

17 files changed

+554
-77
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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/specs-go v0.8.0
10+
)
11+
12+
require (
13+
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/pmezard/go-difflib v1.0.0 // indirect
15+
golang.org/x/mod v0.19.0 // indirect
16+
gopkg.in/yaml.v2 v2.4.0 // indirect
17+
gopkg.in/yaml.v3 v3.0.1 // indirect
18+
)
19+
20+
replace tags.cncf.io/container-device-interface/specs-go => ../../specs-go

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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
overwrite bool
30+
permissions fs.FileMode
31+
}
32+
33+
// WithSpecFormat sets the output format of a CDI specification.
34+
func WithSpecFormat(format SpecFormat) Option {
35+
return func(o *options) error {
36+
switch format {
37+
case SpecFormatJSON, SpecFormatYAML:
38+
o.specFormat = format
39+
default:
40+
return fmt.Errorf("invalid CDI spec format %v", format)
41+
}
42+
return nil
43+
}
44+
}
45+
46+
// WithOverwrite specifies whether a producer should overwrite a CDI spec when
47+
// saving to file.
48+
func WithOverwrite(overwrite bool) Option {
49+
return func(o *options) error {
50+
o.overwrite = overwrite
51+
return nil
52+
}
53+
}
54+
55+
// WithPermissions sets the file mode to be used for a saved CDI spec.
56+
func WithPermissions(permissions fs.FileMode) Option {
57+
return func(o *options) error {
58+
o.permissions = permissions
59+
return nil
60+
}
61+
}

pkg/cdi/spec_linux.go renamed to api/producer/renamein_linux.go

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"

pkg/cdi/spec_other.go renamed to api/producer/renamein_other.go

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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
cdi "tags.cncf.io/container-device-interface/specs-go"
28+
)
29+
30+
// A SpecFormat defines the encoding to use when reading or writing a CDI specification.
31+
type SpecFormat string
32+
33+
type specFormatter struct {
34+
*cdi.Spec
35+
options
36+
}
37+
38+
// WriteTo writes the spec to the specified writer.
39+
func (p *specFormatter) WriteTo(w io.Writer) (int64, error) {
40+
data, err := p.contents()
41+
if err != nil {
42+
return 0, fmt.Errorf("failed to marshal Spec file: %w", err)
43+
}
44+
45+
n, err := w.Write(data)
46+
return int64(n), err
47+
}
48+
49+
// marshal returns the raw contents of a CDI specification.
50+
// No validation is performed.
51+
func (p SpecFormat) marshal(spec *cdi.Spec) ([]byte, error) {
52+
switch p {
53+
case SpecFormatYAML:
54+
data, err := yaml.Marshal(spec)
55+
if err != nil {
56+
return nil, err
57+
}
58+
data = append([]byte("---\n"), data...)
59+
return data, nil
60+
case SpecFormatJSON:
61+
return json.Marshal(spec)
62+
default:
63+
return nil, fmt.Errorf("undefined CDI spec format %v", p)
64+
}
65+
}
66+
67+
// normalizeFilename ensures that the specified filename ends in a supported extension.
68+
func (p SpecFormat) normalizeFilename(filename string) (string, SpecFormat) {
69+
switch filepath.Ext(filename) {
70+
case ".json":
71+
return filename, SpecFormatJSON
72+
case ".yaml":
73+
return filename, SpecFormatYAML
74+
default:
75+
return filename + string(p), p
76+
}
77+
}
78+
79+
// validate performs an explicit validation of the spec.
80+
// This is currently a placeholder for validation that should be performed when
81+
// saving a spec.
82+
func (p *specFormatter) validate() error {
83+
return nil
84+
}
85+
86+
// contents returns the raw contents of a CDI specification.
87+
// Validation is performed before marshalling the contentent based on the spec format.
88+
func (p *specFormatter) contents() ([]byte, error) {
89+
if err := p.validate(); err != nil {
90+
return nil, fmt.Errorf("spec validation failed: %w", err)
91+
}
92+
return p.specFormat.marshal(p.Spec)
93+
}

api/producer/writer.go

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

0 commit comments

Comments
 (0)