Skip to content

Commit 68087b7

Browse files
committed
fixup! Add basic cdi-producer package
1 parent 356bd47 commit 68087b7

2 files changed

Lines changed: 217 additions & 14 deletions

File tree

pkg/cdi-producer/save.go

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ package producer
1818

1919
import (
2020
"encoding/json"
21+
"errors"
2122
"fmt"
2223
"io"
2324
"os"
2425
"path/filepath"
26+
"strings"
2527

2628
orderedyaml "gopkg.in/yaml.v3"
2729

@@ -45,19 +47,25 @@ type Option func(*options)
4547

4648
// Save a CDI specification to the requested path.
4749
func Save(raw *cdi.Spec, path string, opts ...Option) error {
48-
o := populateOptions(opts...)
49-
50+
if strings.TrimSpace(path) == "" {
51+
return fmt.Errorf("a path is required")
52+
}
5053
dir, filename, err := splitPath(path)
5154
if err != nil {
5255
return err
5356
}
57+
if filename == "" {
58+
return fmt.Errorf("unexpected empty filename")
59+
}
5460
if dir == "" {
5561
return fmt.Errorf("unexpected empty directory name")
5662
}
5763
if err := os.MkdirAll(dir, 0o755); err != nil {
5864
return fmt.Errorf("failed to create Spec dir: %w", err)
5965
}
6066

67+
o := populateOptions(opts...)
68+
6169
tmpFile, err := os.CreateTemp(dir, "spec.*.tmp")
6270
if err != nil {
6371
return fmt.Errorf("failed to create Spec file: %w", err)
@@ -66,23 +74,20 @@ func Save(raw *cdi.Spec, path string, opts ...Option) error {
6674
_ = os.Remove(tmpFile.Name())
6775
}()
6876

69-
err = func() error {
70-
defer func() {
71-
_ = tmpFile.Close()
72-
}()
73-
format := o.formatFromFilename(filename)
74-
return WriteSpec(raw, tmpFile, append(opts, WithOutputFormat(format))...)
75-
}()
76-
if err != nil {
77-
return fmt.Errorf("failed to write Spec file: %w", err)
78-
}
79-
8077
if o.permissions != 0 {
8178
err = tmpFile.Chmod(o.permissions)
8279
if err != nil {
8380
return fmt.Errorf("failed to set file permissions: %w", err)
8481
}
8582
}
83+
84+
format := o.formatFromFilename(filename)
85+
err = WriteSpec(raw, tmpFile, append(opts, WithOutputFormat(format))...)
86+
_ = tmpFile.Close()
87+
if err != nil {
88+
return fmt.Errorf("failed to write Spec file: %w", err)
89+
}
90+
8691
return renameIn(dir, filepath.Base(tmpFile.Name()), filename, o.overwrite)
8792
}
8893

@@ -178,7 +183,11 @@ func (o *options) formatFromFilename(path string) string {
178183
// If the directory is unspecified or '.' the current working directory is
179184
// returned instead.
180185
func splitPath(path string) (string, string, error) {
181-
dir, filename := filepath.Split(filepath.Clean(path))
186+
path = filepath.Clean(path)
187+
if err := assertNotDirectory(path); err != nil {
188+
return "", "", err
189+
}
190+
dir, filename := filepath.Split(path)
182191
if dir != "." {
183192
return dir, filename, nil
184193
}
@@ -190,6 +199,20 @@ func splitPath(path string) (string, string, error) {
190199
return cwd, filename, nil
191200
}
192201

202+
func assertNotDirectory(path string) error {
203+
info, err := os.Lstat(path)
204+
if errors.Is(err, os.ErrNotExist) {
205+
return nil
206+
}
207+
if err != nil {
208+
return err
209+
}
210+
if info.IsDir() {
211+
return fmt.Errorf("specified path is a directory")
212+
}
213+
return nil
214+
}
215+
193216
func (o *options) marshal(v any) ([]byte, error) {
194217
switch o.format {
195218
case "yaml":

pkg/cdi-producer/save_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
Copyright © 2026 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+
"os"
22+
"path/filepath"
23+
"testing"
24+
25+
"github.com/stretchr/testify/require"
26+
cdi "tags.cncf.io/container-device-interface/specs-go"
27+
)
28+
29+
func TestSave(t *testing.T) {
30+
testCases := []struct {
31+
description string
32+
spec *cdi.Spec
33+
filename string
34+
options []Option
35+
expectedError string
36+
assert func(*testing.T, string)
37+
}{
38+
{
39+
description: "empty filename returns directory error",
40+
filename: "",
41+
expectedError: "specified path is a directory",
42+
},
43+
{
44+
description: "spec is written with default permissions",
45+
spec: &cdi.Spec{
46+
Version: "v1.1.0",
47+
},
48+
filename: "test.yaml",
49+
assert: func(t *testing.T, fullpath string) {
50+
require.FileExists(t, fullpath)
51+
info, err := os.Stat(fullpath)
52+
require.NoError(t, err)
53+
require.EqualValues(t, os.FileMode(0644), info.Mode().Perm())
54+
contents, err := os.ReadFile(fullpath)
55+
require.NoError(t, err)
56+
expectedContents := `---
57+
cdiVersion: v1.1.0
58+
kind: ""
59+
devices: []
60+
`
61+
require.EqualValues(t, expectedContents, string(contents))
62+
},
63+
},
64+
{
65+
description: "spec is written as json with default permissions",
66+
spec: &cdi.Spec{
67+
Version: "v1.1.0",
68+
},
69+
filename: "test.json",
70+
assert: func(t *testing.T, fullpath string) {
71+
require.FileExists(t, fullpath)
72+
info, err := os.Stat(fullpath)
73+
require.NoError(t, err)
74+
require.EqualValues(t, os.FileMode(0644), info.Mode().Perm())
75+
contents, err := os.ReadFile(fullpath)
76+
require.NoError(t, err)
77+
expectedContents := `{"cdiVersion":"v1.1.0","kind":"","devices":null,"containerEdits":{}}`
78+
require.EqualValues(t, expectedContents, string(contents))
79+
},
80+
},
81+
{
82+
description: "spec is written with format specified as json",
83+
spec: &cdi.Spec{
84+
Version: "v1.1.0",
85+
},
86+
filename: "test",
87+
options: []Option{WithOutputFormat("json")},
88+
assert: func(t *testing.T, fullpath string) {
89+
require.FileExists(t, fullpath)
90+
info, err := os.Stat(fullpath)
91+
require.NoError(t, err)
92+
require.EqualValues(t, os.FileMode(0644), info.Mode().Perm())
93+
contents, err := os.ReadFile(fullpath)
94+
require.NoError(t, err)
95+
expectedContents := `{"cdiVersion":"v1.1.0","kind":"","devices":null,"containerEdits":{}}`
96+
require.EqualValues(t, expectedContents, string(contents))
97+
},
98+
},
99+
{
100+
description: "spec is written with format specified as yaml",
101+
spec: &cdi.Spec{
102+
Version: "v1.1.0",
103+
},
104+
filename: "test",
105+
options: []Option{WithOutputFormat("yaml")},
106+
assert: func(t *testing.T, fullpath string) {
107+
require.FileExists(t, fullpath)
108+
info, err := os.Stat(fullpath)
109+
require.NoError(t, err)
110+
require.EqualValues(t, os.FileMode(0644), info.Mode().Perm())
111+
contents, err := os.ReadFile(fullpath)
112+
require.NoError(t, err)
113+
expectedContents := `---
114+
cdiVersion: v1.1.0
115+
kind: ""
116+
devices: []
117+
`
118+
require.EqualValues(t, expectedContents, string(contents))
119+
},
120+
},
121+
{
122+
description: "spec is written with specified permissions",
123+
spec: &cdi.Spec{
124+
Version: "v1.1.0",
125+
},
126+
filename: "test.yaml",
127+
options: []Option{
128+
WithPermissions(os.FileMode(0666)),
129+
},
130+
assert: func(t *testing.T, fullpath string) {
131+
require.FileExists(t, fullpath)
132+
info, err := os.Stat(fullpath)
133+
require.NoError(t, err)
134+
require.EqualValues(t, os.FileMode(0666), info.Mode().Perm())
135+
contents, err := os.ReadFile(fullpath)
136+
require.NoError(t, err)
137+
expectedContents := `---
138+
cdiVersion: v1.1.0
139+
kind: ""
140+
devices: []
141+
`
142+
require.EqualValues(t, expectedContents, string(contents))
143+
},
144+
},
145+
{
146+
description: "validator is called before save",
147+
spec: &cdi.Spec{
148+
Version: "v1.1.0",
149+
},
150+
filename: "test.yaml",
151+
options: []Option{
152+
WithValidator(validatorFunction(func(s *cdi.Spec) error { return fmt.Errorf("invalid spec") })),
153+
WithPermissions(os.FileMode(0666)),
154+
},
155+
expectedError: "failed to write Spec file: spec validation failed: invalid spec",
156+
assert: func(t *testing.T, fullpath string) {
157+
require.NoFileExists(t, fullpath)
158+
},
159+
},
160+
}
161+
162+
for _, tc := range testCases {
163+
t.Run(tc.description, func(t *testing.T) {
164+
dir := t.TempDir()
165+
166+
fullpath := filepath.Join(dir, tc.filename)
167+
err := Save(tc.spec, fullpath, tc.options...)
168+
169+
if tc.expectedError == "" {
170+
require.NoError(t, err)
171+
} else {
172+
require.EqualError(t, err, tc.expectedError)
173+
}
174+
if tc.assert != nil {
175+
tc.assert(t, fullpath)
176+
}
177+
})
178+
}
179+
180+
}

0 commit comments

Comments
 (0)