Skip to content

Commit 0fe26ed

Browse files
committed
dockerfile: device support with frontend attribute
Signed-off-by: CrazyMax <[email protected]>
1 parent 319bf56 commit 0fe26ed

File tree

5 files changed

+134
-0
lines changed

5 files changed

+134
-0
lines changed

frontend/dockerfile/dockerfile2llb/convert.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
724724
extraHosts: opt.ExtraHosts,
725725
shmSize: opt.ShmSize,
726726
ulimit: opt.Ulimits,
727+
devices: opt.Devices,
727728
cgroupParent: opt.CgroupParent,
728729
llbCaps: opt.LLBCaps,
729730
sourceMap: opt.SourceMap,
@@ -859,6 +860,7 @@ type dispatchOpt struct {
859860
extraHosts []llb.HostIP
860861
shmSize int64
861862
ulimit []*pb.Ulimit
863+
devices []*pb.CDIDevice
862864
cgroupParent string
863865
llbCaps *apicaps.CapSet
864866
sourceMap *llb.SourceMap
@@ -1303,6 +1305,13 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
13031305
}
13041306
}
13051307

1308+
// TODO: use entitlements and put this on labs
1309+
if dopt.llbCaps != nil && dopt.llbCaps.Supports(pb.CapExecMetaCDI) == nil {
1310+
for _, u := range dopt.devices {
1311+
opt = append(opt, llb.AddCDIDevice(u.Name))
1312+
}
1313+
}
1314+
13061315
shlex := *dopt.shlex
13071316
shlex.RawQuotes = true
13081317
shlex.SkipUnsetEnv = true
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package dockerfile
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/containerd/continuity/fs/fstest"
9+
"github.com/moby/buildkit/client"
10+
"github.com/moby/buildkit/frontend/dockerui"
11+
"github.com/moby/buildkit/util/testutil/integration"
12+
"github.com/stretchr/testify/require"
13+
"github.com/tonistiigi/fsutil"
14+
)
15+
16+
var deviceTests = integration.TestFuncs(
17+
testDeviceEnv,
18+
)
19+
20+
func testDeviceEnv(t *testing.T, sb integration.Sandbox) {
21+
if sb.Rootless() {
22+
t.SkipNow()
23+
}
24+
25+
integration.SkipOnPlatform(t, "windows")
26+
f := getFrontend(t, sb)
27+
28+
require.NoError(t, os.WriteFile(filepath.Join(sb.CDISpecDir(), "vendor1-device.yaml"), []byte(`
29+
cdiVersion: "0.3.0"
30+
kind: "vendor1.com/device"
31+
devices:
32+
- name: foo
33+
containerEdits:
34+
env:
35+
- FOO=injected
36+
`), 0600))
37+
38+
dockerfile := []byte(`
39+
FROM busybox AS base
40+
RUN env|sort | tee foo.env
41+
FROM scratch
42+
COPY --from=base /foo.env /
43+
`)
44+
45+
dir := integration.Tmpdir(
46+
t,
47+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
48+
)
49+
50+
c, err := client.New(sb.Context(), sb.Address())
51+
require.NoError(t, err)
52+
defer c.Close()
53+
54+
destDir := t.TempDir()
55+
56+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
57+
FrontendAttrs: map[string]string{
58+
"device": "vendor1.com/device=foo",
59+
},
60+
LocalMounts: map[string]fsutil.FS{
61+
dockerui.DefaultLocalNameDockerfile: dir,
62+
dockerui.DefaultLocalNameContext: dir,
63+
},
64+
Exports: []client.ExportEntry{
65+
{
66+
Type: client.ExporterLocal,
67+
OutputDir: destDir,
68+
},
69+
},
70+
}, nil)
71+
require.NoError(t, err)
72+
73+
dt, err := os.ReadFile(filepath.Join(destDir, "foo.env"))
74+
require.NoError(t, err)
75+
require.Contains(t, string(dt), `FOO=injected`)
76+
}

frontend/dockerfile/dockerfile_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,11 @@ func TestIntegration(t *testing.T) {
301301
"granted": networkHostGranted,
302302
"denied": networkHostDenied,
303303
}))...)
304+
305+
integration.Run(t, deviceTests, append(opts,
306+
integration.WithMatrix("cdi", map[string]interface{}{
307+
"enabled": enableCDI,
308+
}))...)
304309
}
305310

306311
func testEmptyStringArgInEnv(t *testing.T, sb integration.Sandbox) {
@@ -9897,6 +9902,19 @@ var (
98979902
networkHostDenied integration.ConfigUpdater = &networkModeSandbox{}
98989903
)
98999904

9905+
type cdiEnabled struct{}
9906+
9907+
func (*cdiEnabled) UpdateConfigFile(in string) string {
9908+
return in + `
9909+
[cdi]
9910+
enabled = true
9911+
`
9912+
}
9913+
9914+
var (
9915+
enableCDI integration.ConfigUpdater = &cdiEnabled{}
9916+
)
9917+
99009918
func fixedWriteCloser(wc io.WriteCloser) filesync.FileOutputFunc {
99019919
return func(map[string]string) (io.WriteCloser, error) {
99029920
return wc, nil

frontend/dockerui/attr.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dockerui
22

33
import (
4+
"encoding/csv"
45
"net"
56
"strconv"
67
"strings"
@@ -13,6 +14,7 @@ import (
1314
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
1415
"github.com/pkg/errors"
1516
"github.com/tonistiigi/go-csvvalue"
17+
"tags.cncf.io/container-device-interface/pkg/parser"
1618
)
1719

1820
func parsePlatforms(v string) ([]ocispecs.Platform, error) {
@@ -97,6 +99,27 @@ func parseUlimits(v string) ([]*pb.Ulimit, error) {
9799
return out, nil
98100
}
99101

102+
func parseDevices(v string) ([]*pb.CDIDevice, error) {
103+
if v == "" {
104+
return nil, nil
105+
}
106+
out := make([]*pb.CDIDevice, 0)
107+
csvReader := csv.NewReader(strings.NewReader(v))
108+
names, err := csvReader.Read()
109+
if err != nil {
110+
return nil, err
111+
}
112+
for _, name := range names {
113+
if _, _, _, err := parser.ParseQualifiedName(name); err != nil {
114+
return nil, errors.Wrapf(err, "invalid CDI device name %q", name)
115+
}
116+
out = append(out, &pb.CDIDevice{
117+
Name: name,
118+
})
119+
}
120+
return out, nil
121+
}
122+
100123
func parseNetMode(v string) (pb.NetMode, error) {
101124
if v == "" {
102125
return llb.NetModeSandbox, nil

frontend/dockerui/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const (
4040
keyShmSize = "shm-size"
4141
keyTargetPlatform = "platform"
4242
keyUlimit = "ulimit"
43+
keyDevice = "device"
4344
keyCacheFrom = "cache-from" // for registry only. deprecated in favor of keyCacheImports
4445
keyCacheImports = "cache-imports" // JSON representation of []CacheOptionsEntry
4546

@@ -66,6 +67,7 @@ type Config struct {
6667
ShmSize int64
6768
Target string
6869
Ulimits []*pb.Ulimit
70+
Devices []*pb.CDIDevice
6971
LinterConfig *linter.Config
7072

7173
CacheImports []client.CacheOptionsEntry
@@ -187,6 +189,12 @@ func (bc *Client) init() error {
187189
}
188190
bc.Ulimits = ulimits
189191

192+
devices, err := parseDevices(opts[keyDevice])
193+
if err != nil {
194+
return errors.Wrap(err, "failed to parse device")
195+
}
196+
bc.Devices = devices
197+
190198
defaultNetMode, err := parseNetMode(opts[keyForceNetwork])
191199
if err != nil {
192200
return err

0 commit comments

Comments
 (0)