Skip to content

Commit b67d046

Browse files
committed
Add producer API to write specs
Signed-off-by: Evan Lezar <[email protected]>
1 parent 91c77d0 commit b67d046

File tree

7 files changed

+250
-42
lines changed

7 files changed

+250
-42
lines changed

pkg/cdi/producer/api.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
type specFormat string
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+
)

pkg/cdi/producer/options.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 "fmt"
20+
21+
// An Option defines a functional option for constructing a producer.
22+
type Option func(*Producer) error
23+
24+
// WithSpecFormat sets the output format of a CDI specification.
25+
func WithSpecFormat(format specFormat) Option {
26+
return func(p *Producer) error {
27+
switch format {
28+
case SpecFormatJSON, SpecFormatYAML:
29+
p.format = format
30+
default:
31+
return fmt.Errorf("invalid CDI spec format %v", format)
32+
}
33+
return nil
34+
}
35+
}
36+
37+
// WithOverwrite specifies whether a producer should overwrite a CDI spec when
38+
// saving to file.
39+
func WithOverwrite(overwrite bool) Option {
40+
return func(p *Producer) error {
41+
p.failIfExists = !overwrite
42+
return nil
43+
}
44+
}

pkg/cdi/producer/producer.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
"path/filepath"
21+
22+
cdi "tags.cncf.io/container-device-interface/specs-go"
23+
)
24+
25+
// A Producer defines a structure for outputting CDI specifications.
26+
type Producer struct {
27+
format specFormat
28+
failIfExists bool
29+
}
30+
31+
// New creates a new producer with the supplied options.
32+
func New(opts ...Option) (*Producer, error) {
33+
p := &Producer{
34+
format: DefaultSpecFormat,
35+
}
36+
for _, opt := range opts {
37+
err := opt(p)
38+
if err != nil {
39+
return nil, err
40+
}
41+
}
42+
return p, nil
43+
}
44+
45+
// SaveSpec writes the specified CDI spec to a file with the specified name.
46+
// If the filename ends in a supported extension, the format implied by the
47+
// extension takes precedence over the format with which the Producer was
48+
// configured.
49+
func (p *Producer) SaveSpec(s *cdi.Spec, filename string) (string, error) {
50+
filename = p.normalizeFilename(filename)
51+
52+
sp := spec{
53+
Spec: s,
54+
format: p.specFormatFromFilename(filename),
55+
}
56+
57+
if err := sp.save(filename, !p.failIfExists); err != nil {
58+
return "", err
59+
}
60+
61+
return filename, nil
62+
}
63+
64+
// specFormatFromFilename determines the CDI spec format for the given filename.
65+
func (p *Producer) specFormatFromFilename(filename string) specFormat {
66+
switch filepath.Ext(filename) {
67+
case ".json":
68+
return SpecFormatJSON
69+
case ".yaml", ".yml":
70+
return SpecFormatYAML
71+
default:
72+
return p.format
73+
}
74+
}
75+
76+
// normalizeFilename ensures that the specified filename ends in a supported extension.
77+
func (p *Producer) normalizeFilename(filename string) string {
78+
switch filepath.Ext(filename) {
79+
case ".json":
80+
fallthrough
81+
case ".yaml", ".yml":
82+
return filename
83+
default:
84+
return filename + string(p.format)
85+
}
86+
}

pkg/cdi/producer/spec.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
"os"
23+
"path/filepath"
24+
25+
"sigs.k8s.io/yaml"
26+
27+
cdi "tags.cncf.io/container-device-interface/specs-go"
28+
)
29+
30+
type spec struct {
31+
*cdi.Spec
32+
format specFormat
33+
}
34+
35+
// save saves a CDI spec to the specified filename.
36+
func (s *spec) save(filename string, overwrite bool) error {
37+
data, err := s.contents()
38+
if err != nil {
39+
return fmt.Errorf("failed to marshal Spec file: %w", err)
40+
}
41+
42+
dir := filepath.Dir(filename)
43+
if dir != "" {
44+
if err := os.MkdirAll(dir, 0o755); err != nil {
45+
return fmt.Errorf("failed to create Spec dir: %w", err)
46+
}
47+
}
48+
49+
tmp, err := os.CreateTemp(dir, "spec.*.tmp")
50+
if err != nil {
51+
return fmt.Errorf("failed to create Spec file: %w", err)
52+
}
53+
_, err = tmp.Write(data)
54+
tmp.Close()
55+
if err != nil {
56+
return fmt.Errorf("failed to write Spec file: %w", err)
57+
}
58+
59+
err = renameIn(dir, filepath.Base(tmp.Name()), filepath.Base(filename), overwrite)
60+
if err != nil {
61+
_ = os.Remove(tmp.Name())
62+
return fmt.Errorf("failed to write Spec file: %w", err)
63+
}
64+
return nil
65+
}
66+
67+
// contents returns the raw contents of a CDI specification.
68+
func (s *spec) contents() ([]byte, error) {
69+
switch s.format {
70+
case SpecFormatYAML:
71+
data, err := yaml.Marshal(s.Spec)
72+
if err != nil {
73+
return nil, err
74+
}
75+
data = append([]byte("---\n"), data...)
76+
return data, nil
77+
case SpecFormatJSON:
78+
return json.Marshal(s.Spec)
79+
default:
80+
return nil, fmt.Errorf("undefined CDI spec format %v", s.format)
81+
}
82+
}

pkg/cdi/spec_linux.go renamed to pkg/cdi/producer/spec_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 pkg/cdi/producer/spec_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"

pkg/cdi/spec.go

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package cdi
1818

1919
import (
20-
"encoding/json"
2120
"fmt"
2221
"os"
2322
"path/filepath"
@@ -28,6 +27,7 @@ import (
2827
"sigs.k8s.io/yaml"
2928

3029
"tags.cncf.io/container-device-interface/internal/validation"
30+
"tags.cncf.io/container-device-interface/pkg/cdi/producer"
3131
"tags.cncf.io/container-device-interface/pkg/parser"
3232
cdi "tags.cncf.io/container-device-interface/specs-go"
3333
)
@@ -118,52 +118,19 @@ func newSpec(raw *cdi.Spec, path string, priority int) (*Spec, error) {
118118
// Write the CDI Spec to the file associated with it during instantiation
119119
// by newSpec() or ReadSpec().
120120
func (s *Spec) write(overwrite bool) error {
121-
var (
122-
data []byte
123-
dir string
124-
tmp *os.File
125-
err error
121+
p, err := producer.New(
122+
producer.WithOverwrite(overwrite),
126123
)
127-
128-
err = validateSpec(s.Spec)
129124
if err != nil {
130125
return err
131126
}
132127

133-
if filepath.Ext(s.path) == ".yaml" {
134-
data, err = yaml.Marshal(s.Spec)
135-
data = append([]byte("---\n"), data...)
136-
} else {
137-
data, err = json.Marshal(s.Spec)
138-
}
139-
if err != nil {
140-
return fmt.Errorf("failed to marshal Spec file: %w", err)
141-
}
142-
143-
dir = filepath.Dir(s.path)
144-
err = os.MkdirAll(dir, 0o755)
128+
savedPath, err := p.SaveSpec(s.Spec, s.path)
145129
if err != nil {
146-
return fmt.Errorf("failed to create Spec dir: %w", err)
147-
}
148-
149-
tmp, err = os.CreateTemp(dir, "spec.*.tmp")
150-
if err != nil {
151-
return fmt.Errorf("failed to create Spec file: %w", err)
152-
}
153-
_, err = tmp.Write(data)
154-
tmp.Close()
155-
if err != nil {
156-
return fmt.Errorf("failed to write Spec file: %w", err)
157-
}
158-
159-
err = renameIn(dir, filepath.Base(tmp.Name()), filepath.Base(s.path), overwrite)
160-
161-
if err != nil {
162-
os.Remove(tmp.Name())
163-
err = fmt.Errorf("failed to write Spec file: %w", err)
130+
return err
164131
}
165-
166-
return err
132+
s.path = savedPath
133+
return nil
167134
}
168135

169136
// GetVendor returns the vendor of this Spec.

0 commit comments

Comments
 (0)