Skip to content

Commit 3c072dc

Browse files
committed
cdi: support custom and wildcard class for injection
Signed-off-by: CrazyMax <[email protected]>
1 parent 88509a9 commit 3c072dc

File tree

5 files changed

+320
-64
lines changed

5 files changed

+320
-64
lines changed

client/client_test.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ func testIntegration(t *testing.T, funcs ...func(t *testing.T, sb integration.Sa
277277

278278
integration.Run(t, integration.TestFuncs(
279279
testCDI,
280+
testCDIFirst,
281+
testCDIWildcard,
282+
testCDIClass,
280283
), mirrors)
281284
}
282285

@@ -11054,3 +11057,193 @@ devices:
1105411057
require.NoError(t, err)
1105511058
require.Contains(t, strings.TrimSpace(string(dt2)), `BAR=injected`)
1105611059
}
11060+
11061+
func testCDIFirst(t *testing.T, sb integration.Sandbox) {
11062+
if sb.Rootless() {
11063+
t.SkipNow()
11064+
}
11065+
11066+
integration.SkipOnPlatform(t, "windows")
11067+
c, err := New(sb.Context(), sb.Address())
11068+
require.NoError(t, err)
11069+
defer c.Close()
11070+
11071+
require.NoError(t, os.WriteFile(filepath.Join(sb.CDISpecDir(), "vendor1-device.yaml"), []byte(`
11072+
cdiVersion: "0.3.0"
11073+
kind: "vendor1.com/device"
11074+
devices:
11075+
- name: foo
11076+
containerEdits:
11077+
env:
11078+
- FOO=injected
11079+
- name: bar
11080+
containerEdits:
11081+
env:
11082+
- BAR=injected
11083+
- name: baz
11084+
containerEdits:
11085+
env:
11086+
- BAZ=injected
11087+
- name: qux
11088+
containerEdits:
11089+
env:
11090+
- QUX=injected
11091+
`), 0600))
11092+
11093+
busybox := llb.Image("busybox:latest")
11094+
st := llb.Scratch()
11095+
11096+
run := func(cmd string, ro ...llb.RunOption) {
11097+
st = busybox.Run(append(ro, llb.Shlex(cmd), llb.Dir("/wd"))...).AddMount("/wd", st)
11098+
}
11099+
11100+
run(`sh -c 'env|sort | tee first.env'`, llb.AddCDIDevice(llb.CDIDeviceName("vendor1.com/device")))
11101+
11102+
def, err := st.Marshal(sb.Context())
11103+
require.NoError(t, err)
11104+
11105+
destDir := t.TempDir()
11106+
11107+
_, err = c.Solve(sb.Context(), def, SolveOpt{
11108+
Exports: []ExportEntry{
11109+
{
11110+
Type: ExporterLocal,
11111+
OutputDir: destDir,
11112+
},
11113+
},
11114+
}, nil)
11115+
require.NoError(t, err)
11116+
11117+
dt, err := os.ReadFile(filepath.Join(destDir, "first.env"))
11118+
require.NoError(t, err)
11119+
require.Contains(t, strings.TrimSpace(string(dt)), `BAR=injected`)
11120+
require.NotContains(t, strings.TrimSpace(string(dt)), `FOO=injected`)
11121+
require.NotContains(t, strings.TrimSpace(string(dt)), `BAZ=injected`)
11122+
require.NotContains(t, strings.TrimSpace(string(dt)), `QUX=injected`)
11123+
}
11124+
11125+
func testCDIWildcard(t *testing.T, sb integration.Sandbox) {
11126+
if sb.Rootless() {
11127+
t.SkipNow()
11128+
}
11129+
11130+
integration.SkipOnPlatform(t, "windows")
11131+
c, err := New(sb.Context(), sb.Address())
11132+
require.NoError(t, err)
11133+
defer c.Close()
11134+
11135+
require.NoError(t, os.WriteFile(filepath.Join(sb.CDISpecDir(), "vendor1-device.yaml"), []byte(`
11136+
cdiVersion: "0.3.0"
11137+
kind: "vendor1.com/device"
11138+
devices:
11139+
- name: foo
11140+
containerEdits:
11141+
env:
11142+
- FOO=injected
11143+
- name: bar
11144+
containerEdits:
11145+
env:
11146+
- BAR=injected
11147+
`), 0600))
11148+
11149+
busybox := llb.Image("busybox:latest")
11150+
st := llb.Scratch()
11151+
11152+
run := func(cmd string, ro ...llb.RunOption) {
11153+
st = busybox.Run(append(ro, llb.Shlex(cmd), llb.Dir("/wd"))...).AddMount("/wd", st)
11154+
}
11155+
11156+
run(`sh -c 'env|sort | tee all.env'`, llb.AddCDIDevice(llb.CDIDeviceName("vendor1.com/device=*")))
11157+
11158+
def, err := st.Marshal(sb.Context())
11159+
require.NoError(t, err)
11160+
11161+
destDir := t.TempDir()
11162+
11163+
_, err = c.Solve(sb.Context(), def, SolveOpt{
11164+
Exports: []ExportEntry{
11165+
{
11166+
Type: ExporterLocal,
11167+
OutputDir: destDir,
11168+
},
11169+
},
11170+
}, nil)
11171+
require.NoError(t, err)
11172+
11173+
dt, err := os.ReadFile(filepath.Join(destDir, "all.env"))
11174+
require.NoError(t, err)
11175+
require.Contains(t, strings.TrimSpace(string(dt)), `FOO=injected`)
11176+
require.Contains(t, strings.TrimSpace(string(dt)), `BAR=injected`)
11177+
}
11178+
11179+
func testCDIClass(t *testing.T, sb integration.Sandbox) {
11180+
if sb.Rootless() {
11181+
t.SkipNow()
11182+
}
11183+
11184+
integration.SkipOnPlatform(t, "windows")
11185+
c, err := New(sb.Context(), sb.Address())
11186+
require.NoError(t, err)
11187+
defer c.Close()
11188+
11189+
require.NoError(t, os.WriteFile(filepath.Join(sb.CDISpecDir(), "vendor1-device.yaml"), []byte(`
11190+
cdiVersion: "0.6.0"
11191+
kind: "vendor1.com/device"
11192+
annotations:
11193+
foo.bar.baz: FOO
11194+
devices:
11195+
- name: foo
11196+
annotations:
11197+
org.mobyproject.buildkit.device.class: class1
11198+
containerEdits:
11199+
env:
11200+
- FOO=injected
11201+
- name: bar
11202+
annotations:
11203+
org.mobyproject.buildkit.device.class: class1
11204+
containerEdits:
11205+
env:
11206+
- BAR=injected
11207+
- name: baz
11208+
annotations:
11209+
org.mobyproject.buildkit.device.class: class2
11210+
containerEdits:
11211+
env:
11212+
- BAZ=injected
11213+
- name: qux
11214+
containerEdits:
11215+
env:
11216+
- QUX=injected
11217+
`), 0600))
11218+
11219+
busybox := llb.Image("busybox:latest")
11220+
st := llb.Scratch()
11221+
11222+
run := func(cmd string, ro ...llb.RunOption) {
11223+
st = busybox.Run(append(ro, llb.Shlex(cmd), llb.Dir("/wd"))...).AddMount("/wd", st)
11224+
}
11225+
11226+
run(`sh -c 'env|sort | tee class.env'`, llb.AddCDIDevice(llb.CDIDeviceName("vendor1.com/device=class1")))
11227+
11228+
def, err := st.Marshal(sb.Context())
11229+
require.NoError(t, err)
11230+
11231+
destDir := t.TempDir()
11232+
11233+
_, err = c.Solve(sb.Context(), def, SolveOpt{
11234+
Exports: []ExportEntry{
11235+
{
11236+
Type: ExporterLocal,
11237+
OutputDir: destDir,
11238+
},
11239+
},
11240+
}, nil)
11241+
require.NoError(t, err)
11242+
11243+
dt, err := os.ReadFile(filepath.Join(destDir, "class.env"))
11244+
require.NoError(t, err)
11245+
require.Contains(t, strings.TrimSpace(string(dt)), `FOO=injected`)
11246+
require.Contains(t, strings.TrimSpace(string(dt)), `BAR=injected`)
11247+
require.NotContains(t, strings.TrimSpace(string(dt)), `BAZ=injected`)
11248+
require.NotContains(t, strings.TrimSpace(string(dt)), `QUX=injected`)
11249+
}

executor/oci/spec_linux.go

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"github.com/opencontainers/selinux/go-selinux/label"
2727
"github.com/pkg/errors"
2828
"golang.org/x/sys/unix"
29-
"tags.cncf.io/container-device-interface/pkg/parser"
3029
)
3130

3231
var (
@@ -153,47 +152,19 @@ func generateRlimitOpts(ulimits []*pb.Ulimit) ([]oci.SpecOpts, error) {
153152

154153
// genereateCDIOptions creates the OCI runtime spec options for injecting CDI
155154
// devices.
156-
func generateCDIOpts(manager *cdidevices.Manager, devices []*pb.CDIDevice) ([]oci.SpecOpts, error) {
157-
if len(devices) == 0 {
155+
func generateCDIOpts(manager *cdidevices.Manager, devs []*pb.CDIDevice) ([]oci.SpecOpts, error) {
156+
if len(devs) == 0 {
158157
return nil, nil
159158
}
160159

161-
withCDIDevices := func(devices []*pb.CDIDevice) oci.SpecOpts {
160+
withCDIDevices := func(devs []*pb.CDIDevice) oci.SpecOpts {
162161
return func(ctx context.Context, _ oci.Client, c *containers.Container, s *specs.Spec) error {
163162
if err := manager.Refresh(); err != nil {
164163
bklog.G(ctx).Warnf("CDI registry refresh failed: %v", err)
165164
}
166-
167-
registeredDevices := manager.ListDevices()
168-
isDeviceRegistered := func(device *pb.CDIDevice) bool {
169-
for _, d := range registeredDevices {
170-
if device.Name == d.Name {
171-
return true
172-
}
173-
}
174-
return false
175-
}
176-
177-
var dd []string
178-
for _, d := range devices {
179-
if d == nil {
180-
continue
181-
}
182-
if _, _, _, err := parser.ParseQualifiedName(d.Name); err != nil {
183-
return errors.Wrapf(err, "invalid CDI device name %s", d.Name)
184-
}
185-
if !isDeviceRegistered(d) && d.Optional {
186-
bklog.G(ctx).Warnf("Optional CDI device %q is not registered", d.Name)
187-
continue
188-
}
189-
dd = append(dd, d.Name)
190-
}
191-
192-
bklog.G(ctx).Debugf("Injecting CDI devices %v", dd)
193-
if err := manager.InjectDevices(s, dd...); err != nil {
165+
if err := manager.InjectDevices(s, devs...); err != nil {
194166
return errors.Wrapf(err, "CDI device injection failed")
195167
}
196-
197168
// One crucial thing to keep in mind is that CDI device injection
198169
// might add OCI Spec environment variables, hooks, and mounts as
199170
// well. Therefore, it is important that none of the corresponding
@@ -203,7 +174,7 @@ func generateCDIOpts(manager *cdidevices.Manager, devices []*pb.CDIDevice) ([]oc
203174
}
204175

205176
return []oci.SpecOpts{
206-
withCDIDevices(devices),
177+
withCDIDevices(devs),
207178
}, nil
208179
}
209180

frontend/dockerfile/instructions/commands_rundevice.go

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"github.com/moby/buildkit/util/suggest"
88
"github.com/pkg/errors"
99
"github.com/tonistiigi/go-csvvalue"
10-
"tags.cncf.io/container-device-interface/pkg/parser"
1110
)
1211

1312
var devicesKey = "dockerfile/run/devices"
@@ -75,28 +74,20 @@ func ParseDevice(val string) (*Device, error) {
7574

7675
d := &Device{}
7776

78-
for i, field := range fields {
79-
// check if the first field is a valid device name
80-
var firstFieldErr error
81-
if i == 0 {
82-
if _, _, _, firstFieldErr = parser.ParseQualifiedName(field); firstFieldErr == nil {
83-
d.Name = field
84-
continue
85-
}
86-
}
87-
77+
for _, field := range fields {
8878
key, value, ok := strings.Cut(field, "=")
8979
key = strings.ToLower(key)
9080

9181
if !ok {
92-
if len(fields) == 1 && firstFieldErr != nil {
93-
return nil, errors.Wrapf(firstFieldErr, "invalid device name %s", field)
94-
}
9582
switch key {
9683
case "required":
9784
d.Required = true
9885
continue
9986
default:
87+
if d.Name == "" {
88+
d.Name = field
89+
continue
90+
}
10091
// any other option requires a value.
10192
return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field)
10293
}
@@ -114,14 +105,14 @@ func ParseDevice(val string) (*Device, error) {
114105
return nil, errors.Errorf("invalid value for %s: %s", key, value)
115106
}
116107
default:
108+
if d.Name == "" {
109+
d.Name = field
110+
continue
111+
}
117112
allKeys := []string{"name", "required"}
118113
return nil, suggest.WrapError(errors.Errorf("unexpected key '%s' in '%s'", key, field), key, allKeys, true)
119114
}
120115
}
121116

122-
if _, _, _, err := parser.ParseQualifiedName(d.Name); err != nil {
123-
return nil, errors.Wrapf(err, "invalid device name %s", d.Name)
124-
}
125-
126117
return d, nil
127118
}

frontend/dockerfile/instructions/commands_rundevice_test.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ func TestParseDevice(t *testing.T) {
1818
expected: &Device{Name: "vendor1.com/device=foo", Required: false},
1919
expectedErr: nil,
2020
},
21+
{
22+
input: "vendor1.com/device",
23+
expected: &Device{Name: "vendor1.com/device", Required: false},
24+
expectedErr: nil,
25+
},
2126
{
2227
input: "vendor1.com/device=foo,required",
2328
expected: &Device{Name: "vendor1.com/device=foo", Required: true},
@@ -48,16 +53,6 @@ func TestParseDevice(t *testing.T) {
4853
expected: nil,
4954
expectedErr: errors.New("device name already set to vendor1.com/device=foo"),
5055
},
51-
{
52-
input: "invalid-device-name",
53-
expected: nil,
54-
expectedErr: errors.New(`invalid device name invalid-device-name: unqualified device "invalid-device-name", missing vendor`),
55-
},
56-
{
57-
input: "name=invalid-device-name",
58-
expected: nil,
59-
expectedErr: errors.New(`invalid device name invalid-device-name: unqualified device "invalid-device-name", missing vendor`),
60-
},
6156
}
6257
for _, tt := range cases {
6358
t.Run(tt.input, func(t *testing.T) {

0 commit comments

Comments
 (0)