Skip to content

Commit 7776e5e

Browse files
committed
Support adding devices by dir
This enables cases where devices exist in a subdirectory of /dev, particularly where those device names are not portable across machines, which makes it problematic to specify from a runtime such as cri. Added this to `ctr` as well so I could test that the code at least works. Signed-off-by: Brian Goff <[email protected]>
1 parent a113818 commit 7776e5e

8 files changed

Lines changed: 217 additions & 45 deletions

File tree

cmd/ctr/commands/commands.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ var (
153153
},
154154
cli.StringSliceFlag{
155155
Name: "device",
156-
Usage: "add a device to a container",
156+
Usage: "file path to a device to add to the container; or a path to a directory tree of devices to add to the container",
157157
},
158158
cli.BoolFlag{
159159
Name: "seccomp",

cmd/ctr/commands/run/run_unix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli
264264
opts = append(opts, oci.WithMemoryLimit(limit))
265265
}
266266
for _, dev := range context.StringSlice("device") {
267-
opts = append(opts, oci.WithLinuxDevice(dev, "rwm"))
267+
opts = append(opts, oci.WithDevices(dev, "", "rwm"))
268268
}
269269
}
270270

oci/spec_opts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1178,7 +1178,7 @@ func WithLinuxDevice(path, permissions string) SpecOpts {
11781178
setLinux(s)
11791179
setResources(s)
11801180

1181-
dev, err := deviceFromPath(path, permissions)
1181+
dev, err := deviceFromPath(path)
11821182
if err != nil {
11831183
return err
11841184
}

oci/spec_opts_linux.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import (
3535
func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
3636
setLinux(s)
3737

38-
devs, err := getDevices("/dev")
38+
devs, err := getDevices("/dev", "")
3939
if err != nil {
4040
return err
4141
}
@@ -45,7 +45,47 @@ func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Sp
4545

4646
var errNotADevice = errors.New("not a device node")
4747

48-
func getDevices(path string) ([]specs.LinuxDevice, error) {
48+
// WithDevices recursively adds devices from the passed in path and associated cgroup rules for that device.
49+
// If devicePath is a dir it traverses the dir to add all devices in that dir.
50+
// If devicePath is not a dir, it attempts to add the single device.
51+
// If containerPath is not set then the device path is used for the container path.
52+
func WithDevices(devicePath, containerPath, permissions string) SpecOpts {
53+
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
54+
devs, err := getDevices(devicePath, containerPath)
55+
if err != nil {
56+
return err
57+
}
58+
for _, dev := range devs {
59+
s.Linux.Devices = append(s.Linux.Devices, dev)
60+
s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, specs.LinuxDeviceCgroup{
61+
Allow: true,
62+
Type: dev.Type,
63+
Major: &dev.Major,
64+
Minor: &dev.Minor,
65+
Access: permissions,
66+
})
67+
}
68+
return nil
69+
}
70+
}
71+
72+
func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
73+
stat, err := os.Stat(path)
74+
if err != nil {
75+
return nil, errors.Wrap(err, "error stating device path")
76+
}
77+
78+
if !stat.IsDir() {
79+
dev, err := deviceFromPath(path)
80+
if err != nil {
81+
return nil, err
82+
}
83+
if containerPath != "" {
84+
dev.Path = containerPath
85+
}
86+
return []specs.LinuxDevice{*dev}, nil
87+
}
88+
4989
files, err := ioutil.ReadDir(path)
5090
if err != nil {
5191
return nil, err
@@ -60,7 +100,11 @@ func getDevices(path string) ([]specs.LinuxDevice, error) {
60100
case "pts", "shm", "fd", "mqueue", ".lxc", ".lxd-mounts", ".udev":
61101
continue
62102
default:
63-
sub, err := getDevices(filepath.Join(path, f.Name()))
103+
var cp string
104+
if containerPath != "" {
105+
cp = filepath.Join(containerPath, filepath.Base(f.Name()))
106+
}
107+
sub, err := getDevices(filepath.Join(path, f.Name()), cp)
64108
if err != nil {
65109
return nil, err
66110
}
@@ -71,7 +115,7 @@ func getDevices(path string) ([]specs.LinuxDevice, error) {
71115
case f.Name() == "console":
72116
continue
73117
}
74-
device, err := deviceFromPath(filepath.Join(path, f.Name()), "rwm")
118+
device, err := deviceFromPath(filepath.Join(path, f.Name()))
75119
if err != nil {
76120
if err == errNotADevice {
77121
continue
@@ -81,12 +125,15 @@ func getDevices(path string) ([]specs.LinuxDevice, error) {
81125
}
82126
return nil, err
83127
}
128+
if containerPath != "" {
129+
device.Path = filepath.Join(containerPath, filepath.Base(f.Name()))
130+
}
84131
out = append(out, *device)
85132
}
86133
return out, nil
87134
}
88135

89-
func deviceFromPath(path, permissions string) (*specs.LinuxDevice, error) {
136+
func deviceFromPath(path string) (*specs.LinuxDevice, error) {
90137
var stat unix.Stat_t
91138
if err := unix.Lstat(path, &stat); err != nil {
92139
return nil, err

oci/spec_opts_linux_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ package oci
1818

1919
import (
2020
"context"
21+
"io/ioutil"
22+
"os"
23+
"path/filepath"
2124
"testing"
2225

26+
"github.com/containerd/containerd/pkg/testutil"
2327
specs "github.com/opencontainers/runtime-spec/specs-go"
28+
"golang.org/x/sys/unix"
2429
)
2530

2631
func TestAddCaps(t *testing.T) {
@@ -106,3 +111,113 @@ func TestDropCaps(t *testing.T) {
106111
}
107112
}
108113
}
114+
115+
func TestGetDevices(t *testing.T) {
116+
testutil.RequiresRoot(t)
117+
118+
dir, err := ioutil.TempDir("/dev", t.Name())
119+
if err != nil {
120+
t.Fatal(err)
121+
}
122+
defer os.RemoveAll(dir)
123+
124+
zero := filepath.Join(dir, "zero")
125+
if err := ioutil.WriteFile(zero, nil, 0600); err != nil {
126+
t.Fatal(err)
127+
}
128+
129+
if err := unix.Mount("/dev/zero", zero, "", unix.MS_BIND, ""); err != nil {
130+
t.Fatal(err)
131+
}
132+
defer unix.Unmount(filepath.Join(dir, "zero"), unix.MNT_DETACH)
133+
134+
t.Run("single device", func(t *testing.T) {
135+
t.Run("no container path", func(t *testing.T) {
136+
devices, err := getDevices(dir, "")
137+
if err != nil {
138+
t.Fatal(err)
139+
}
140+
141+
if len(devices) != 1 {
142+
t.Fatalf("expected one device %v", devices)
143+
}
144+
if devices[0].Path != zero {
145+
t.Fatalf("got unexpected device path %s", devices[0].Path)
146+
}
147+
})
148+
t.Run("with container path", func(t *testing.T) {
149+
newPath := "/dev/testNew"
150+
devices, err := getDevices(dir, newPath)
151+
if err != nil {
152+
t.Fatal(err)
153+
}
154+
155+
if len(devices) != 1 {
156+
t.Fatalf("expected one device %v", devices)
157+
}
158+
if devices[0].Path != filepath.Join(newPath, "zero") {
159+
t.Fatalf("got unexpected device path %s", devices[0].Path)
160+
}
161+
})
162+
})
163+
t.Run("With symlink in dir", func(t *testing.T) {
164+
if err := os.Symlink("/dev/zero", filepath.Join(dir, "zerosym")); err != nil {
165+
t.Fatal(err)
166+
}
167+
devices, err := getDevices(dir, "")
168+
if err != nil {
169+
t.Fatal(err)
170+
}
171+
if len(devices) != 1 {
172+
t.Fatalf("expected one device %v", devices)
173+
}
174+
if devices[0].Path != filepath.Join(dir, "zero") {
175+
t.Fatalf("got unexpected device path, expected %q, got %q", filepath.Join(dir, "zero"), devices[0].Path)
176+
}
177+
})
178+
t.Run("No devices", func(t *testing.T) {
179+
dir := dir + "2"
180+
if err := os.MkdirAll(dir, 0755); err != nil {
181+
t.Fatal(err)
182+
}
183+
defer os.RemoveAll(dir)
184+
185+
t.Run("empty dir", func(T *testing.T) {
186+
devices, err := getDevices(dir, "")
187+
if err != nil {
188+
t.Fatal(err)
189+
}
190+
if len(devices) != 0 {
191+
t.Fatalf("expected no devices, got %+v", devices)
192+
}
193+
})
194+
t.Run("symlink to device in dir", func(t *testing.T) {
195+
if err := os.Symlink("/dev/zero", filepath.Join(dir, "zerosym")); err != nil {
196+
t.Fatal(err)
197+
}
198+
defer os.Remove(filepath.Join(dir, "zerosym"))
199+
200+
devices, err := getDevices(dir, "")
201+
if err != nil {
202+
t.Fatal(err)
203+
}
204+
if len(devices) != 0 {
205+
t.Fatalf("expected no devices, got %+v", devices)
206+
}
207+
})
208+
t.Run("regular file in dir", func(t *testing.T) {
209+
if err := ioutil.WriteFile(filepath.Join(dir, "somefile"), []byte("hello"), 0600); err != nil {
210+
t.Fatal(err)
211+
}
212+
defer os.Remove(filepath.Join(dir, "somefile"))
213+
214+
devices, err := getDevices(dir, "")
215+
if err != nil {
216+
t.Fatal(err)
217+
}
218+
if len(devices) != 0 {
219+
t.Fatalf("expected no devices, got %+v", devices)
220+
}
221+
})
222+
})
223+
}

oci/spec_opts_unix.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import (
3434
func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
3535
setLinux(s)
3636

37-
devs, err := getDevices("/dev")
37+
devs, err := getDevices("/dev", "")
3838
if err != nil {
3939
return err
4040
}
@@ -44,7 +44,37 @@ func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Sp
4444

4545
var errNotADevice = errors.New("not a device node")
4646

47-
func getDevices(path string) ([]specs.LinuxDevice, error) {
47+
// WithDevices recursively adds devices from the passed in path and associated cgroup rules for that device.
48+
// If devicePath is a dir it traverses the dir to add all devices in that dir.
49+
// If devicePath is not a dir, it attempts to add the signle device.
50+
func WithDevices(devicePath, containerPath, permissions string) SpecOpts {
51+
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
52+
devs, err := getDevices(devicePath, containerPath)
53+
if err != nil {
54+
return err
55+
}
56+
s.Linux.Devices = append(s.Linux.Devices, devs...)
57+
return nil
58+
}
59+
}
60+
61+
func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
62+
stat, err := os.Stat(path)
63+
if err != nil {
64+
return nil, errors.Wrap(err, "error stating device path")
65+
}
66+
67+
if !stat.IsDir() {
68+
dev, err := deviceFromPath(path)
69+
if err != nil {
70+
return nil, err
71+
}
72+
if containerPath != "" {
73+
dev.Path = containerPath
74+
}
75+
return []specs.LinuxDevice{*dev}, nil
76+
}
77+
4878
files, err := ioutil.ReadDir(path)
4979
if err != nil {
5080
return nil, err
@@ -59,7 +89,11 @@ func getDevices(path string) ([]specs.LinuxDevice, error) {
5989
case "pts", "shm", "fd", "mqueue", ".lxc", ".lxd-mounts", ".udev":
6090
continue
6191
default:
62-
sub, err := getDevices(filepath.Join(path, f.Name()))
92+
var cp string
93+
if containerPath != "" {
94+
cp = filepath.Join(containerPath, filepath.Base(f.Name()))
95+
}
96+
sub, err := getDevices(filepath.Join(path, f.Name()), cp)
6397
if err != nil {
6498
return nil, err
6599
}
@@ -70,7 +104,7 @@ func getDevices(path string) ([]specs.LinuxDevice, error) {
70104
case f.Name() == "console":
71105
continue
72106
}
73-
device, err := deviceFromPath(filepath.Join(path, f.Name()), "rwm")
107+
device, err := deviceFromPath(filepath.Join(path, f.Name()))
74108
if err != nil {
75109
if err == errNotADevice {
76110
continue
@@ -80,12 +114,15 @@ func getDevices(path string) ([]specs.LinuxDevice, error) {
80114
}
81115
return nil, err
82116
}
117+
if containerPath != "" {
118+
device.Path = filepath.Join(containerPath, filepath.Base(f.Name()))
119+
}
83120
out = append(out, *device)
84121
}
85122
return out, nil
86123
}
87124

88-
func deviceFromPath(path, permissions string) (*specs.LinuxDevice, error) {
125+
func deviceFromPath(path string) (*specs.LinuxDevice, error) {
89126
var stat unix.Stat_t
90127
if err := unix.Lstat(path, &stat); err != nil {
91128
return nil, err

oci/spec_opts_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,6 @@ func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Sp
7474
return nil
7575
}
7676

77-
func deviceFromPath(path, permissions string) (*specs.LinuxDevice, error) {
77+
func deviceFromPath(path string) (*specs.LinuxDevice, error) {
7878
return nil, errors.New("device from path not supported on Windows")
7979
}

0 commit comments

Comments
 (0)