Skip to content

Commit 3cb3f68

Browse files
committed
dockerfile: support optional cdi devices
Signed-off-by: CrazyMax <[email protected]>
1 parent 6667434 commit 3cb3f68

File tree

6 files changed

+185
-29
lines changed

6 files changed

+185
-29
lines changed

frontend/dockerfile/dockerfile2llb/convert_rundevice.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ import (
99

1010
func dispatchRunDevices(c *instructions.RunCommand) ([]llb.RunOption, error) {
1111
var out []llb.RunOption
12-
devices := instructions.GetDevices(c)
13-
for _, device := range devices {
14-
out = append(out, llb.AddCDIDevice(llb.CDIDeviceName(device), llb.CDIDeviceOptional))
12+
for _, device := range instructions.GetDevices(c) {
13+
deviceOpts := []llb.CDIDeviceOption{
14+
llb.CDIDeviceName(device.Name),
15+
}
16+
if !device.Required {
17+
deviceOpts = append(deviceOpts, llb.CDIDeviceOptional)
18+
}
19+
out = append(out, llb.AddCDIDevice(deviceOpts...))
1520
}
1621
return out, nil
1722
}

frontend/dockerfile/dockerfile_rundevice_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ COPY --from=base /foo.env /
6060

6161
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
6262
FrontendAttrs: map[string]string{
63-
"device": "vendor1.com/device=foo",
63+
"device:0": "vendor1.com/device=foo,required",
64+
"device:1": "vendor2.com/device=bar",
6465
},
6566
LocalMounts: map[string]fsutil.FS{
6667
dockerui.DefaultLocalNameDockerfile: dir,
@@ -100,7 +101,9 @@ devices:
100101

101102
dockerfile := []byte(`
102103
FROM busybox AS base
103-
RUN --device=vendor1.com/device=foo env|sort | tee foo.env
104+
RUN --device=vendor1.com/device=foo,required \
105+
--device=vendor2.com/device=bar \
106+
env|sort | tee foo.env
104107
FROM scratch
105108
COPY --from=base /foo.env /
106109
`)
Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package instructions
22

33
import (
4+
"strconv"
5+
"strings"
6+
7+
"github.com/moby/buildkit/util/suggest"
48
"github.com/pkg/errors"
9+
"github.com/tonistiigi/go-csvvalue"
10+
"tags.cncf.io/container-device-interface/pkg/parser"
511
)
612

7-
var deviceKey = "dockerfile/run/device"
13+
var devicesKey = "dockerfile/run/devices"
814

915
func init() {
1016
parseRunPreHooks = append(parseRunPreHooks, runDevicePreHook)
@@ -14,7 +20,7 @@ func init() {
1420
func runDevicePreHook(cmd *RunCommand, req parseRequest) error {
1521
st := &deviceState{}
1622
st.flag = req.flags.AddStrings("device")
17-
cmd.setExternalValue(deviceKey, st)
23+
cmd.setExternalValue(devicesKey, st)
1824
return nil
1925
}
2026

@@ -27,23 +33,95 @@ func setDeviceState(cmd *RunCommand) error {
2733
if st == nil {
2834
return errors.Errorf("no device state")
2935
}
30-
st.names = st.flag.StringValues
36+
devices := make([]*Device, len(st.flag.StringValues))
37+
for i, str := range st.flag.StringValues {
38+
d, err := ParseDevice(str)
39+
if err != nil {
40+
return err
41+
}
42+
devices[i] = d
43+
}
44+
st.devices = devices
3145
return nil
3246
}
3347

3448
func getDeviceState(cmd *RunCommand) *deviceState {
35-
v := cmd.getExternalValue(deviceKey)
49+
v := cmd.getExternalValue(devicesKey)
3650
if v == nil {
3751
return nil
3852
}
3953
return v.(*deviceState)
4054
}
4155

42-
func GetDevices(cmd *RunCommand) []string {
43-
return getDeviceState(cmd).names
56+
func GetDevices(cmd *RunCommand) []*Device {
57+
return getDeviceState(cmd).devices
4458
}
4559

4660
type deviceState struct {
47-
flag *Flag
48-
names []string
61+
flag *Flag
62+
devices []*Device
63+
}
64+
65+
type Device struct {
66+
Name string
67+
Required bool
68+
}
69+
70+
func ParseDevice(val string) (*Device, error) {
71+
fields, err := csvvalue.Fields(val, nil)
72+
if err != nil {
73+
return nil, errors.Wrap(err, "failed to parse csv devices")
74+
}
75+
76+
d := &Device{}
77+
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+
88+
key, value, ok := strings.Cut(field, "=")
89+
key = strings.ToLower(key)
90+
91+
if !ok {
92+
if len(fields) == 1 && firstFieldErr != nil {
93+
return nil, errors.Wrapf(firstFieldErr, "invalid device name %s", field)
94+
}
95+
switch key {
96+
case "required":
97+
d.Required = true
98+
continue
99+
default:
100+
// any other option requires a value.
101+
return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field)
102+
}
103+
}
104+
105+
switch key {
106+
case "name":
107+
if d.Name != "" {
108+
return nil, errors.Errorf("device name already set to %s", d.Name)
109+
}
110+
d.Name = value
111+
case "required":
112+
d.Required, err = strconv.ParseBool(value)
113+
if err != nil {
114+
return nil, errors.Errorf("invalid value for %s: %s", key, value)
115+
}
116+
default:
117+
allKeys := []string{"name", "required"}
118+
return nil, suggest.WrapError(errors.Errorf("unexpected key '%s' in '%s'", key, field), key, allKeys, true)
119+
}
120+
}
121+
122+
if _, _, _, err := parser.ParseQualifiedName(d.Name); err != nil {
123+
return nil, errors.Wrapf(err, "invalid device name %s", d.Name)
124+
}
125+
126+
return d, nil
49127
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package instructions
2+
3+
import (
4+
"testing"
5+
6+
"github.com/pkg/errors"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestParseDevice(t *testing.T) {
11+
cases := []struct {
12+
input string
13+
expected *Device
14+
expectedErr error
15+
}{
16+
{
17+
input: "vendor1.com/device=foo",
18+
expected: &Device{Name: "vendor1.com/device=foo", Required: false},
19+
expectedErr: nil,
20+
},
21+
{
22+
input: "vendor1.com/device=foo,required",
23+
expected: &Device{Name: "vendor1.com/device=foo", Required: true},
24+
expectedErr: nil,
25+
},
26+
{
27+
input: "vendor1.com/device=foo,required=true",
28+
expected: &Device{Name: "vendor1.com/device=foo", Required: true},
29+
expectedErr: nil,
30+
},
31+
{
32+
input: "vendor1.com/device=foo,required=false",
33+
expected: &Device{Name: "vendor1.com/device=foo", Required: false},
34+
expectedErr: nil,
35+
},
36+
{
37+
input: "name=vendor1.com/device=foo",
38+
expected: &Device{Name: "vendor1.com/device=foo", Required: false},
39+
expectedErr: nil,
40+
},
41+
{
42+
input: "name=vendor1.com/device=foo,required",
43+
expected: &Device{Name: "vendor1.com/device=foo", Required: true},
44+
expectedErr: nil,
45+
},
46+
{
47+
input: "vendor1.com/device=foo,name=vendor2.com/device=bar",
48+
expected: nil,
49+
expectedErr: errors.New("device name already set to vendor1.com/device=foo"),
50+
},
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+
},
61+
}
62+
for _, tt := range cases {
63+
t.Run(tt.input, func(t *testing.T) {
64+
result, err := ParseDevice(tt.input)
65+
if tt.expectedErr != nil {
66+
require.Error(t, err)
67+
require.EqualError(t, err, tt.expectedErr.Error())
68+
} else {
69+
require.NoError(t, err)
70+
require.Equal(t, tt.expected, result)
71+
}
72+
})
73+
}
74+
}

frontend/dockerui/attr.go

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

33
import (
4-
"encoding/csv"
54
"net"
65
"strconv"
76
"strings"
@@ -10,11 +9,11 @@ import (
109
"github.com/containerd/platforms"
1110
"github.com/docker/go-units"
1211
"github.com/moby/buildkit/client/llb"
12+
"github.com/moby/buildkit/frontend/dockerfile/instructions"
1313
"github.com/moby/buildkit/solver/pb"
1414
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
1515
"github.com/pkg/errors"
1616
"github.com/tonistiigi/go-csvvalue"
17-
"tags.cncf.io/container-device-interface/pkg/parser"
1817
)
1918

2019
func parsePlatforms(v string) ([]ocispecs.Platform, error) {
@@ -99,22 +98,19 @@ func parseUlimits(v string) ([]*pb.Ulimit, error) {
9998
return out, nil
10099
}
101100

102-
func parseDevices(v string) ([]*pb.CDIDevice, error) {
103-
if v == "" {
101+
func parseDevices(v map[string]string) ([]*pb.CDIDevice, error) {
102+
if v == nil {
104103
return nil, nil
105104
}
106105
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)
106+
for _, attrs := range v {
107+
device, err := instructions.ParseDevice(attrs)
108+
if err != nil {
109+
return nil, err
115110
}
116111
out = append(out, &pb.CDIDevice{
117-
Name: name,
112+
Name: device.Name,
113+
Optional: !device.Required,
118114
})
119115
}
120116
return out, nil

frontend/dockerui/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const (
2828
buildArgPrefix = "build-arg:"
2929
labelPrefix = "label:"
3030
localSessionIDPrefix = "local-sessionid:"
31+
devicePrefix = "device:"
3132

3233
keyTarget = "target"
3334
keyCgroupParent = "cgroup-parent"
@@ -40,7 +41,6 @@ const (
4041
keyShmSize = "shm-size"
4142
keyTargetPlatform = "platform"
4243
keyUlimit = "ulimit"
43-
keyDevice = "device"
4444
keyCacheFrom = "cache-from" // for registry only. deprecated in favor of keyCacheImports
4545
keyCacheImports = "cache-imports" // JSON representation of []CacheOptionsEntry
4646

@@ -189,9 +189,9 @@ func (bc *Client) init() error {
189189
}
190190
bc.Ulimits = ulimits
191191

192-
devices, err := parseDevices(opts[keyDevice])
192+
devices, err := parseDevices(filter(opts, devicePrefix))
193193
if err != nil {
194-
return errors.Wrap(err, "failed to parse device")
194+
return errors.Wrap(err, "failed to parse devices")
195195
}
196196
bc.Devices = devices
197197

0 commit comments

Comments
 (0)