Skip to content

Commit b730453

Browse files
committed
pkg/cdi: implement cache of specs and devices.
Implement discovery and caching of spec files and devices, refreshing the cache. Signed-off-by: Krisztian Litkey <[email protected]>
1 parent 43fc5a0 commit b730453

File tree

4 files changed

+712
-0
lines changed

4 files changed

+712
-0
lines changed

pkg/cdi/cache.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
Copyright © 2021 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 cdi
18+
19+
import (
20+
"path/filepath"
21+
"sync"
22+
23+
"github.com/hashicorp/go-multierror"
24+
"github.com/pkg/errors"
25+
)
26+
27+
// Option is an option to change some aspect of default CDI behavior.
28+
type Option func(*Cache) error
29+
30+
// Cache stores CDI Specs loaded from Spec directories.
31+
type Cache struct {
32+
sync.Mutex
33+
specDirs []string
34+
specs map[string][]*Spec
35+
devices map[string]*Device
36+
errors map[string][]error
37+
}
38+
39+
// NewCache creates a new CDI Cache. The cache is populated from a set
40+
// of CDI Spec directories. These can be specified using a WithSpecDirs
41+
// option. The default set of directories is exposed in DefaultSpecDirs.
42+
func NewCache(options ...Option) (*Cache, error) {
43+
c := &Cache{}
44+
45+
if err := c.Configure(options...); err != nil {
46+
return nil, err
47+
}
48+
if len(c.specDirs) == 0 {
49+
c.Configure(WithSpecDirs(DefaultSpecDirs...))
50+
}
51+
52+
return c, c.Refresh()
53+
}
54+
55+
// Configure applies options to the cache. Updates the cache if options have
56+
// changed.
57+
func (c *Cache) Configure(options ...Option) error {
58+
if len(options) == 0 {
59+
return nil
60+
}
61+
62+
c.Lock()
63+
defer c.Unlock()
64+
65+
for _, o := range options {
66+
if err := o(c); err != nil {
67+
return errors.Wrapf(err, "failed to apply cache options")
68+
}
69+
}
70+
71+
return nil
72+
}
73+
74+
// Refresh rescans the CDI Spec directories and refreshes the Cache.
75+
func (c *Cache) Refresh() error {
76+
var (
77+
specs = map[string][]*Spec{}
78+
devices = map[string]*Device{}
79+
conflicts = map[string]struct{}{}
80+
specErrors = map[string][]error{}
81+
result []error
82+
)
83+
84+
// collect errors per spec file path and once globally
85+
collectError := func(err error, paths ...string) {
86+
result = append(result, err)
87+
for _, path := range paths {
88+
specErrors[path] = append(specErrors[path], err)
89+
}
90+
}
91+
// resolve conflicts based on device Spec priority (order of precedence)
92+
resolveConflict := func(name string, dev *Device, old *Device) bool {
93+
devSpec, oldSpec := dev.GetSpec(), old.GetSpec()
94+
devPrio, oldPrio := devSpec.GetPriority(), oldSpec.GetPriority()
95+
switch {
96+
case devPrio > oldPrio:
97+
return false
98+
case devPrio == oldPrio:
99+
devPath, oldPath := devSpec.GetPath(), oldSpec.GetPath()
100+
collectError(errors.Errorf("conflicting device %q (specs %q, %q)",
101+
name, devPath, oldPath), devPath, oldPath)
102+
conflicts[name] = struct{}{}
103+
}
104+
return true
105+
}
106+
107+
_ = scanSpecDirs(c.specDirs, func(path string, priority int, spec *Spec, err error) error {
108+
path = filepath.Clean(path)
109+
if err != nil {
110+
collectError(errors.Wrapf(err, "failed to load CDI Spec"), path)
111+
return nil
112+
}
113+
114+
vendor := spec.GetVendor()
115+
specs[vendor] = append(specs[vendor], spec)
116+
117+
for _, dev := range spec.devices {
118+
qualified := dev.GetQualifiedName()
119+
other, ok := devices[qualified]
120+
if ok {
121+
if resolveConflict(qualified, dev, other) {
122+
continue
123+
}
124+
}
125+
devices[qualified] = dev
126+
}
127+
128+
return nil
129+
})
130+
131+
for conflict := range conflicts {
132+
delete(devices, conflict)
133+
}
134+
135+
c.Lock()
136+
defer c.Unlock()
137+
138+
c.specs = specs
139+
c.devices = devices
140+
c.errors = specErrors
141+
142+
if len(result) > 0 {
143+
return multierror.Append(nil, result...)
144+
}
145+
146+
return nil
147+
}

pkg/cdi/cache_test.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
Copyright © 2021 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 cdi
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
func TestNewCache(t *testing.T) {
28+
type testCase struct {
29+
name string
30+
etc map[string]string
31+
run map[string]string
32+
sources map[string]string
33+
errors map[string]struct{}
34+
}
35+
for _, tc := range []*testCase{
36+
{
37+
name: "no spec dirs",
38+
},
39+
{
40+
name: "no spec files",
41+
etc: map[string]string{},
42+
run: map[string]string{},
43+
},
44+
{
45+
name: "one spec file",
46+
etc: map[string]string{
47+
"vendor1.yaml": `
48+
cdiVersion: "0.2.0"
49+
kind: "vendor1.com/device"
50+
devices:
51+
- name: "dev1"
52+
containerEdits:
53+
deviceNodes:
54+
- path: "/dev/vendor1-dev1"
55+
`,
56+
},
57+
sources: map[string]string{
58+
"vendor1.com/device=dev1": "etc/vendor1.yaml",
59+
},
60+
},
61+
{
62+
name: "multiple spec files with override",
63+
etc: map[string]string{
64+
"vendor1.yaml": `
65+
cdiVersion: "0.2.0"
66+
kind: "vendor1.com/device"
67+
devices:
68+
- name: "dev1"
69+
containerEdits:
70+
deviceNodes:
71+
- path: "/dev/vendor1-dev1"
72+
- name: "dev2"
73+
containerEdits:
74+
deviceNodes:
75+
- path: "/dev/vendor1-dev2"
76+
`,
77+
},
78+
run: map[string]string{
79+
"vendor1.yaml": `
80+
cdiVersion: "0.2.0"
81+
kind: "vendor1.com/device"
82+
devices:
83+
- name: "dev1"
84+
containerEdits:
85+
deviceNodes:
86+
- path: "/dev/vendor1-dev1"
87+
`,
88+
},
89+
sources: map[string]string{
90+
"vendor1.com/device=dev1": "run/vendor1.yaml",
91+
"vendor1.com/device=dev2": "etc/vendor1.yaml",
92+
},
93+
},
94+
{
95+
name: "multiple spec files, with conflicts",
96+
run: map[string]string{
97+
"vendor1.yaml": `
98+
cdiVersion: "0.2.0"
99+
kind: "vendor1.com/device"
100+
devices:
101+
- name: "dev1"
102+
containerEdits:
103+
deviceNodes:
104+
- path: "/dev/vendor1-dev1"
105+
- name: "dev2"
106+
containerEdits:
107+
deviceNodes:
108+
- path: "/dev/vendor1-dev2"
109+
`,
110+
"vendor1-other.yaml": `
111+
cdiVersion: "0.2.0"
112+
kind: "vendor1.com/device"
113+
devices:
114+
- name: "dev1"
115+
containerEdits:
116+
deviceNodes:
117+
- path: "/dev/vendor1-dev1"
118+
`,
119+
},
120+
sources: map[string]string{
121+
"vendor1.com/device=dev2": "run/vendor1.yaml",
122+
},
123+
errors: map[string]struct{}{
124+
"run/vendor1.yaml": {},
125+
"run/vendor1-other.yaml": {},
126+
},
127+
},
128+
} {
129+
t.Run(tc.name, func(t *testing.T) {
130+
var (
131+
dir string
132+
err error
133+
cache *Cache
134+
)
135+
if tc.etc != nil || tc.run != nil {
136+
dir, err = createSpecDirs(t, tc.etc, tc.run)
137+
if err != nil {
138+
t.Errorf("failed to create test directory: %v", err)
139+
return
140+
}
141+
}
142+
cache, err = NewCache(WithSpecDirs(
143+
filepath.Join(dir, "etc"),
144+
filepath.Join(dir, "run")),
145+
)
146+
147+
if len(tc.errors) == 0 {
148+
require.Nil(t, err)
149+
}
150+
require.NotNil(t, cache)
151+
152+
for name, dev := range cache.devices {
153+
require.Equal(t, filepath.Join(dir, tc.sources[name]),
154+
dev.GetSpec().GetPath())
155+
}
156+
for name, path := range tc.sources {
157+
dev := cache.devices[name]
158+
require.NotNil(t, dev)
159+
require.Equal(t, filepath.Join(dir, path),
160+
dev.GetSpec().GetPath())
161+
}
162+
163+
for path := range tc.errors {
164+
fullPath := filepath.Join(dir, path)
165+
_, ok := cache.errors[fullPath]
166+
require.True(t, ok)
167+
}
168+
for fullPath := range cache.errors {
169+
path, err := filepath.Rel(dir, fullPath)
170+
require.Nil(t, err)
171+
_, ok := tc.errors[path]
172+
require.True(t, ok)
173+
}
174+
})
175+
}
176+
}
177+
178+
// Create and populate automatically cleaned up spec directories.
179+
func createSpecDirs(t *testing.T, etc, run map[string]string) (string, error) {
180+
return mkTestDir(t, map[string]map[string]string{
181+
"etc": etc,
182+
"run": run,
183+
})
184+
}
185+
186+
// Update spec directories with new data.
187+
func updateSpecDirs(t *testing.T, dir string, etc, run map[string]string) error {
188+
updates := map[string]map[string]string{
189+
"etc": {},
190+
"run": {},
191+
}
192+
for sub, entries := range map[string]map[string]string{
193+
"etc": etc,
194+
"run": run,
195+
} {
196+
path := filepath.Join(dir, sub)
197+
for name, data := range entries {
198+
if data == "remove" {
199+
os.Remove(filepath.Join(path, name))
200+
} else {
201+
updates[sub][name] = data
202+
}
203+
}
204+
}
205+
return updateTestDir(t, dir, updates)
206+
}

0 commit comments

Comments
 (0)