Skip to content

Commit 319bf56

Browse files
committed
exec: cdi device support
Signed-off-by: CrazyMax <[email protected]>
1 parent ca49048 commit 319bf56

File tree

18 files changed

+859
-264
lines changed

18 files changed

+859
-264
lines changed

client/client_test.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,15 @@ func testIntegration(t *testing.T, funcs ...func(t *testing.T, sb integration.Sa
274274
"dns": bridgeDNSNetwork,
275275
}),
276276
)
277+
278+
integration.Run(t, integration.TestFuncs(
279+
testCDI,
280+
),
281+
mirrors,
282+
integration.WithMatrix("cdi", map[string]interface{}{
283+
"enabled": enableCDI,
284+
}),
285+
)
277286
}
278287

279288
func newContainerd(cdAddress string) (*ctd.Client, error) {
@@ -7436,8 +7445,8 @@ func testMergeOp(t *testing.T, sb integration.Sandbox) {
74367445
File(llb.Mkfile("bar/D", 0644, []byte("D"))).
74377446
File(llb.Mkfile("bar/E", 0755, nil)).
74387447
File(llb.Mkfile("qaz", 0644, nil)),
7439-
// /foo from stateE is not here because it is deleted in stateB, which is part of a submerge of mergeD
74407448
)
7449+
// /foo from stateE is not here because it is deleted in stateB, which is part of a submerge of mergeD
74417450
}
74427451

74437452
func testMergeOpCacheInline(t *testing.T, sb integration.Sandbox) {
@@ -10986,3 +10995,79 @@ func (w warningsListOutput) String() string {
1098610995
}
1098710996
return b.String()
1098810997
}
10998+
10999+
type cdiEnabled struct{}
11000+
11001+
func (*cdiEnabled) UpdateConfigFile(in string) string {
11002+
return in + `
11003+
[cdi]
11004+
enabled = true
11005+
`
11006+
}
11007+
11008+
var (
11009+
enableCDI integration.ConfigUpdater = &cdiEnabled{}
11010+
)
11011+
11012+
func testCDI(t *testing.T, sb integration.Sandbox) {
11013+
if sb.Rootless() {
11014+
t.SkipNow()
11015+
}
11016+
11017+
integration.SkipOnPlatform(t, "windows")
11018+
c, err := New(sb.Context(), sb.Address())
11019+
require.NoError(t, err)
11020+
defer c.Close()
11021+
11022+
require.NoError(t, os.WriteFile(filepath.Join(sb.CDISpecDir(), "vendor1-device.yaml"), []byte(`
11023+
cdiVersion: "0.3.0"
11024+
kind: "vendor1.com/device"
11025+
devices:
11026+
- name: foo
11027+
containerEdits:
11028+
env:
11029+
- FOO=injected
11030+
`), 0600))
11031+
require.NoError(t, os.WriteFile(filepath.Join(sb.CDISpecDir(), "vendor2-device.yaml"), []byte(`
11032+
cdiVersion: "0.3.0"
11033+
kind: "vendor2.com/device"
11034+
devices:
11035+
- name: bar
11036+
containerEdits:
11037+
env:
11038+
- BAR=injected
11039+
`), 0600))
11040+
11041+
busybox := llb.Image("busybox:latest")
11042+
st := llb.Scratch()
11043+
11044+
run := func(cmd string, ro ...llb.RunOption) {
11045+
st = busybox.Run(append(ro, llb.Shlex(cmd), llb.Dir("/wd"))...).AddMount("/wd", st)
11046+
}
11047+
11048+
run(`sh -c 'env|sort | tee foo.env'`, llb.AddCDIDevice("vendor1.com/device=foo"))
11049+
run(`sh -c 'env|sort | tee bar.env'`, llb.AddCDIDevice("vendor2.com/device=bar"))
11050+
11051+
def, err := st.Marshal(sb.Context())
11052+
require.NoError(t, err)
11053+
11054+
destDir := t.TempDir()
11055+
11056+
_, err = c.Solve(sb.Context(), def, SolveOpt{
11057+
Exports: []ExportEntry{
11058+
{
11059+
Type: ExporterLocal,
11060+
OutputDir: destDir,
11061+
},
11062+
},
11063+
}, nil)
11064+
require.NoError(t, err)
11065+
11066+
dt, err := os.ReadFile(filepath.Join(destDir, "foo.env"))
11067+
require.NoError(t, err)
11068+
require.Contains(t, strings.TrimSpace(string(dt)), `FOO=injected`)
11069+
11070+
dt2, err := os.ReadFile(filepath.Join(destDir, "bar.env"))
11071+
require.NoError(t, err)
11072+
require.Contains(t, strings.TrimSpace(string(dt2)), `BAR=injected`)
11073+
}

client/llb/exec.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,22 @@ func (e *ExecOp) Marshal(ctx context.Context, c *Constraints) (digest.Digest, []
266266
Network: network,
267267
Security: security,
268268
}
269+
270+
cdiDevices, err := getCDIDevice(e.base)(ctx, c)
271+
if err != nil {
272+
return "", nil, nil, nil, err
273+
}
274+
if len(cdiDevices) > 0 {
275+
addCap(&e.constraints, pb.CapExecMetaCDI)
276+
cd := make([]*pb.CDIDevice, len(cdiDevices))
277+
for i, d := range cdiDevices {
278+
cd[i] = &pb.CDIDevice{
279+
Name: d.Name,
280+
}
281+
}
282+
peo.CdiDevices = cd
283+
}
284+
269285
if network != NetModeSandbox {
270286
addCap(&e.constraints, pb.CapExecMetaNetwork)
271287
}
@@ -624,6 +640,12 @@ func AddUlimit(name UlimitName, soft int64, hard int64) RunOption {
624640
})
625641
}
626642

643+
func AddCDIDevice(name string) RunOption {
644+
return runOptionFunc(func(ei *ExecInfo) {
645+
ei.State = ei.State.AddCDIDevice(name)
646+
})
647+
}
648+
627649
func ValidExitCodes(codes ...int) RunOption {
628650
return runOptionFunc(func(ei *ExecInfo) {
629651
ei.State = validExitCodes(codes...)(ei.State)

client/llb/meta.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var (
2424
keyExtraHost = contextKeyT("llb.exec.extrahost")
2525
keyHostname = contextKeyT("llb.exec.hostname")
2626
keyUlimit = contextKeyT("llb.exec.ulimit")
27+
keyDevice = contextKeyT("llb.exec.device")
2728
keyCgroupParent = contextKeyT("llb.exec.cgroup.parent")
2829
keyUser = contextKeyT("llb.exec.user")
2930
keyValidExitCodes = contextKeyT("llb.exec.validexitcodes")
@@ -305,6 +306,33 @@ func getUlimit(s State) func(context.Context, *Constraints) ([]*pb.Ulimit, error
305306
}
306307
}
307308

309+
func cdiDevice(name string) StateOption {
310+
return func(s State) State {
311+
return s.withValue(keyDevice, func(ctx context.Context, c *Constraints) (interface{}, error) {
312+
v, err := getCDIDevice(s)(ctx, c)
313+
if err != nil {
314+
return nil, err
315+
}
316+
return append(v, &pb.CDIDevice{
317+
Name: name,
318+
}), nil
319+
})
320+
}
321+
}
322+
323+
func getCDIDevice(s State) func(context.Context, *Constraints) ([]*pb.CDIDevice, error) {
324+
return func(ctx context.Context, c *Constraints) ([]*pb.CDIDevice, error) {
325+
v, err := s.getValue(keyDevice)(ctx, c)
326+
if err != nil {
327+
return nil, err
328+
}
329+
if v != nil {
330+
return v.([]*pb.CDIDevice), nil
331+
}
332+
return nil, nil
333+
}
334+
}
335+
308336
func cgroupParent(cp string) StateOption {
309337
return func(s State) State {
310338
return s.WithValue(keyCgroupParent, cp)

client/llb/state.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,12 @@ func (s State) AddUlimit(name UlimitName, soft int64, hard int64) State {
476476
return ulimit(name, soft, hard)(s)
477477
}
478478

479+
// AddCDIDevice sets the fully qualified CDI device name.
480+
// https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md
481+
func (s State) AddCDIDevice(name string) State {
482+
return cdiDevice(name)(s)
483+
}
484+
479485
// WithCgroupParent sets the parent cgroup for any containers created from this state.
480486
// This is useful when you want to apply resource constraints to a group of containers.
481487
// Cgroups are Linux specific and only applies to containers created from this state such as via `[State.Run]`

executor/executor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Meta struct {
2222
ReadonlyRootFS bool
2323
ExtraHosts []HostIP
2424
Ulimit []*pb.Ulimit
25+
CDIDevices []*pb.CDIDevice
2526
CgroupParent string
2627
NetMode pb.NetMode
2728
SecurityMode pb.SecurityMode

executor/oci/spec.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ func GenerateSpec(ctx context.Context, meta executor.Meta, mounts []executor.Mou
110110
return nil, nil, err
111111
}
112112

113+
if cdiOpts, err := generateCDIOpts(ctx, meta.CDIDevices); err == nil {
114+
opts = append(opts, cdiOpts...)
115+
} else {
116+
return nil, nil, err
117+
}
118+
113119
hostname := defaultHostname
114120
if meta.Hostname != "" {
115121
hostname = meta.Hostname

executor/oci/spec_darwin.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package oci
22

33
import (
4+
"context"
5+
46
"github.com/containerd/containerd/v2/core/mount"
57
"github.com/containerd/containerd/v2/pkg/oci"
68
"github.com/containerd/continuity/fs"
@@ -62,3 +64,10 @@ func sub(m mount.Mount, subPath string) (mount.Mount, func() error, error) {
6264
m.Source = src
6365
return m, func() error { return nil }, nil
6466
}
67+
68+
func generateCDIOpts(ctx context.Context, devices []*pb.CDIDevice) ([]oci.SpecOpts, error) {
69+
if len(devices) == 0 {
70+
return nil, nil
71+
}
72+
return nil, errors.New("no support for CDI on Darwin")
73+
}

executor/oci/spec_freebsd.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package oci
22

33
import (
4+
"context"
5+
46
"github.com/containerd/containerd/v2/core/mount"
57
"github.com/containerd/containerd/v2/pkg/oci"
68
"github.com/containerd/continuity/fs"
@@ -70,3 +72,10 @@ func sub(m mount.Mount, subPath string) (mount.Mount, func() error, error) {
7072
m.Source = src
7173
return m, func() error { return nil }, nil
7274
}
75+
76+
func generateCDIOpts(ctx context.Context, devices []*pb.CDIDevice) ([]oci.SpecOpts, error) {
77+
if len(devices) == 0 {
78+
return nil, nil
79+
}
80+
return nil, errors.New("no support for CDI on FreeBSD")
81+
}

executor/oci/spec_linux.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@ import (
1414
"github.com/containerd/containerd/v2/pkg/oci"
1515
cdseccomp "github.com/containerd/containerd/v2/pkg/seccomp"
1616
"github.com/containerd/continuity/fs"
17+
"github.com/containerd/log"
1718
"github.com/docker/docker/pkg/idtools"
1819
"github.com/docker/docker/profiles/seccomp"
1920
"github.com/moby/buildkit/snapshot"
2021
"github.com/moby/buildkit/solver/pb"
22+
"github.com/moby/buildkit/util/bklog"
2123
"github.com/moby/buildkit/util/entitlements/security"
2224
specs "github.com/opencontainers/runtime-spec/specs-go"
2325
selinux "github.com/opencontainers/selinux/go-selinux"
2426
"github.com/opencontainers/selinux/go-selinux/label"
2527
"github.com/pkg/errors"
2628
"golang.org/x/sys/unix"
29+
"tags.cncf.io/container-device-interface/pkg/cdi"
30+
"tags.cncf.io/container-device-interface/pkg/parser"
2731
)
2832

2933
var (
@@ -148,6 +152,72 @@ func generateRlimitOpts(ulimits []*pb.Ulimit) ([]oci.SpecOpts, error) {
148152
}, nil
149153
}
150154

155+
// genereateCDIOptions creates the OCI runtime spec options for injecting CDI
156+
// devices. Two options are returned: The first ensures that the CDI registry
157+
// is initialized with refresh disabled, and the second injects the devices
158+
// into the container.
159+
func generateCDIOpts(ctx context.Context, devices []*pb.CDIDevice) ([]oci.SpecOpts, error) {
160+
if len(devices) == 0 {
161+
return nil, nil
162+
}
163+
var dd []string
164+
for _, d := range devices {
165+
if d == nil {
166+
continue
167+
}
168+
if _, _, _, err := parser.ParseQualifiedName(d.Name); err != nil {
169+
return nil, errors.Wrapf(err, "invalid CDI device name %s", d.Name)
170+
}
171+
dd = append(dd, d.Name)
172+
}
173+
174+
// withStaticCDIRegistry inits the CDI registry and disables auto-refresh.
175+
// This is used from the `run` command to avoid creating a registry with
176+
// auto-refresh enabled. It also provides a way to override the CDI spec
177+
// file paths if required.
178+
withStaticCDIRegistry := func() oci.SpecOpts {
179+
return func(ctx context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
180+
_ = cdi.Configure(cdi.WithAutoRefresh(false))
181+
if err := cdi.Refresh(); err != nil {
182+
// We don't consider registry refresh failure a fatal error.
183+
// For instance, a dynamically generated invalid CDI Spec file
184+
// for any particular vendor shouldn't prevent injection of
185+
// devices of different vendors. CDI itself knows better, and
186+
// it will fail the injection if necessary.
187+
bklog.G(ctx).Warnf("CDI registry refresh failed: %v", err)
188+
}
189+
return nil
190+
}
191+
}
192+
193+
// withCDIDevices injects the requested CDI devices into the OCI specification.
194+
// FIXME: Use oci.WithCDIDevices once we switch to containerd 2.0.
195+
withCDIDevices := func(devices ...string) oci.SpecOpts {
196+
return func(ctx context.Context, _ oci.Client, c *containers.Container, s *specs.Spec) error {
197+
if len(devices) == 0 {
198+
return nil
199+
}
200+
if err := cdi.Refresh(); err != nil {
201+
log.G(ctx).Warnf("CDI registry refresh failed: %v", err)
202+
}
203+
bklog.G(ctx).Debugf("Injecting CDI devices %v", devices)
204+
if _, err := cdi.InjectDevices(s, devices...); err != nil {
205+
return errors.Wrapf(err, "CDI device injection failed")
206+
}
207+
// One crucial thing to keep in mind is that CDI device injection
208+
// might add OCI Spec environment variables, hooks, and mounts as
209+
// well. Therefore, it is important that none of the corresponding
210+
// OCI Spec fields are reset up in the call stack once we return.
211+
return nil
212+
}
213+
}
214+
215+
return []oci.SpecOpts{
216+
withStaticCDIRegistry(),
217+
withCDIDevices(dd...),
218+
}, nil
219+
}
220+
151221
// withDefaultProfile sets the default seccomp profile to the spec.
152222
// Note: must follow the setting of process capabilities
153223
func withDefaultProfile() oci.SpecOpts {

executor/oci/spec_windows.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,11 @@ func sub(m mount.Mount, subPath string) (mount.Mount, func() error, error) {
110110
m.Source = src
111111
return m, func() error { return nil }, nil
112112
}
113+
114+
func generateCDIOpts(ctx context.Context, devices []*pb.CDIDevice) ([]oci.SpecOpts, error) {
115+
if len(devices) == 0 {
116+
return nil, nil
117+
}
118+
// https://github.com/cncf-tags/container-device-interface/issues/28
119+
return nil, errors.New("no support for CDI on Windows")
120+
}

0 commit comments

Comments
 (0)