Skip to content

Commit 361631d

Browse files
fix(xunix): also mount shared symlinked shared object files (#123)
* fix(xunix): also mount shared object files with .so.N * chore(Makefile): add test and test-integration targets * chore(README.md): add hacking and troubleshooting sections * chore(integration): fix tests under cgroupv2 Signed-off-by: Cian Johnston <[email protected]> Co-authored-by: Dean Sheather <[email protected]>
1 parent 2b091cf commit 361631d

File tree

5 files changed

+92
-24
lines changed

5 files changed

+92
-24
lines changed

Makefile

+8
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,11 @@ fmt/go:
3232
.PHONY: fmt/md
3333
fmt/md:
3434
go run github.com/Kunde21/markdownfmt/v3/cmd/[email protected] -w ./README.md
35+
36+
.PHONY: test
37+
test:
38+
go test -v -count=1 ./...
39+
40+
.PHONY: test-integration
41+
test-integration:
42+
go test -v -count=1 -tags=integration ./integration/

README.md

+34
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,37 @@ env {
8686
> }
8787
> }
8888
> ```
89+
90+
## GPUs
91+
92+
When passing through GPUs to the inner container, you may end up using associated tooling such as the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html) or the [NVIDIA GPU Operator](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/index.html). These will inject required utilities and libraries inside the inner container. You can verify this by directly running (without Envbox) a barebones image like `debian:bookworm` and running `mount` or `nvidia-smi` inside the container.
93+
94+
Envbox will detect these mounts and pass them inside the inner container it creates, so that GPU-aware tools run inside the inner container can still utilize these libraries.
95+
96+
## Hacking
97+
98+
Here's a simple one-liner to run the `codercom/enterprise-minimal:ubuntu` image in Envbox using Docker:
99+
100+
```
101+
docker run -it --rm \
102+
-v /tmp/envbox/docker:/var/lib/coder/docker \
103+
-v /tmp/envbox/containers:/var/lib/coder/containers \
104+
-v /tmp/envbox/sysbox:/var/lib/sysbox \
105+
-v /tmp/envbox/docker:/var/lib/docker \
106+
-v /usr/src:/usr/src:ro \
107+
-v /lib/modules:/lib/modules:ro \
108+
--privileged \
109+
-e CODER_INNER_IMAGE=codercom/enterprise-minimal:ubuntu \
110+
-e CODER_INNER_USERNAME=coder \
111+
envbox:latest /envbox docker
112+
```
113+
114+
This will store persistent data under `/tmp/envbox`.
115+
116+
## Troubleshooting
117+
118+
### `failed to write <number> to cgroup.procs: write /sys/fs/cgroup/docker/<id>/init.scope/cgroup.procs: operation not supported: unknown`
119+
120+
This issue occurs in Docker if you have `cgroupns-mode` set to `private`. To validate, add `--cgroupns=host` to your `docker run` invocation and re-run.
121+
122+
To permanently set this as the default in your Docker daemon, add `"default-cgroupns-mode": "host"` to your `/etc/docker/daemon.json` and restart Docker.

integration/docker_test.go

+43-18
Original file line numberDiff line numberDiff line change
@@ -240,28 +240,53 @@ func TestDocker(t *testing.T) {
240240
require.Equal(t, "1000", strings.TrimSpace(string(out)))
241241

242242
// Validate that memory limit is being applied to the inner container.
243-
out, err = integrationtest.ExecInnerContainer(t, pool, integrationtest.ExecConfig{
243+
// First check under cgroupv2 path.
244+
if out, err = integrationtest.ExecInnerContainer(t, pool, integrationtest.ExecConfig{
244245
ContainerID: resource.Container.ID,
245-
Cmd: []string{"cat", "/sys/fs/cgroup/memory/memory.limit_in_bytes"},
246-
})
247-
require.NoError(t, err)
248-
require.Equal(t, expectedMemoryLimit, strings.TrimSpace(string(out)))
246+
Cmd: []string{"cat", "/sys/fs/cgroup/memory.max"},
247+
}); err == nil {
248+
require.Equal(t, expectedMemoryLimit, strings.TrimSpace(string(out)))
249+
} else { // fall back to cgroupv1 path.
250+
out, err = integrationtest.ExecInnerContainer(t, pool, integrationtest.ExecConfig{
251+
ContainerID: resource.Container.ID,
252+
Cmd: []string{"cat", "/sys/fs/cgroup/memory/memory.limit_in_bytes"},
253+
})
254+
require.NoError(t, err)
255+
require.Equal(t, expectedMemoryLimit, strings.TrimSpace(string(out)))
256+
}
249257

250-
periodStr, err := integrationtest.ExecInnerContainer(t, pool, integrationtest.ExecConfig{
258+
// Validate the cpu limits are being applied to the inner container.
259+
// First check under cgroupv2 path.
260+
var quota, period int64
261+
if out, err = integrationtest.ExecInnerContainer(t, pool, integrationtest.ExecConfig{
251262
ContainerID: resource.Container.ID,
252-
Cmd: []string{"cat", "/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us"},
253-
})
254-
require.NoError(t, err)
255-
period, err := strconv.ParseInt(strings.TrimSpace(string(periodStr)), 10, 64)
256-
require.NoError(t, err)
263+
Cmd: []string{"cat", "/sys/fs/cgroup/cpu.max"},
264+
}); err == nil {
265+
// out is in the format "period quota"
266+
// e.g. "100000 100000"
267+
fields := strings.Fields(string(out))
268+
require.Len(t, fields, 2)
269+
period, err = strconv.ParseInt(fields[0], 10, 64)
270+
require.NoError(t, err)
271+
quota, err = strconv.ParseInt(fields[1], 10, 64)
272+
require.NoError(t, err)
273+
} else { // fall back to cgroupv1 path.
274+
periodStr, err := integrationtest.ExecInnerContainer(t, pool, integrationtest.ExecConfig{
275+
ContainerID: resource.Container.ID,
276+
Cmd: []string{"cat", "/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us"},
277+
})
278+
require.NoError(t, err)
279+
period, err = strconv.ParseInt(strings.TrimSpace(string(periodStr)), 10, 64)
280+
require.NoError(t, err)
257281

258-
quotaStr, err := integrationtest.ExecInnerContainer(t, pool, integrationtest.ExecConfig{
259-
ContainerID: resource.Container.ID,
260-
Cmd: []string{"cat", "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"},
261-
})
262-
require.NoError(t, err)
263-
quota, err := strconv.ParseInt(strings.TrimSpace(string(quotaStr)), 10, 64)
264-
require.NoError(t, err)
282+
quotaStr, err := integrationtest.ExecInnerContainer(t, pool, integrationtest.ExecConfig{
283+
ContainerID: resource.Container.ID,
284+
Cmd: []string{"cat", "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"},
285+
})
286+
require.NoError(t, err)
287+
quota, err = strconv.ParseInt(strings.TrimSpace(string(quotaStr)), 10, 64)
288+
require.NoError(t, err)
289+
}
265290

266291
// Validate that the CPU limit is being applied to the inner container.
267292
actualLimit := float64(quota) / float64(period)

xunix/gpu.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import (
1717
)
1818

1919
var (
20-
gpuMountRegex = regexp.MustCompile("(?i)(nvidia|vulkan|cuda)")
21-
gpuExtraRegex = regexp.MustCompile("(?i)(libgl|nvidia|vulkan|cuda)")
22-
gpuEnvRegex = regexp.MustCompile("(?i)nvidia")
20+
gpuMountRegex = regexp.MustCompile("(?i)(nvidia|vulkan|cuda)")
21+
gpuExtraRegex = regexp.MustCompile("(?i)(libgl|nvidia|vulkan|cuda)")
22+
gpuEnvRegex = regexp.MustCompile("(?i)nvidia")
23+
sharedObjectRegex = regexp.MustCompile(`\.so(\.[0-9\.]+)?$`)
2324
)
2425

2526
func GPUEnvs(ctx context.Context) []string {
@@ -103,7 +104,7 @@ func usrLibGPUs(ctx context.Context, log slog.Logger, usrLibDir string) ([]mount
103104
return nil
104105
}
105106

106-
if filepath.Ext(path) != ".so" || !gpuExtraRegex.MatchString(path) {
107+
if !sharedObjectRegex.MatchString(path) || !gpuExtraRegex.MatchString(path) {
107108
return nil
108109
}
109110

xunix/gpu_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ func TestGPUs(t *testing.T) {
5656
expectedUsrLibFiles = []string{
5757
filepath.Join(usrLibMountpoint, "nvidia", "libglxserver_nvidia.so"),
5858
filepath.Join(usrLibMountpoint, "libnvidia-ml.so"),
59+
filepath.Join(usrLibMountpoint, "nvidia", "libglxserver_nvidia.so.1"),
5960
}
6061

6162
// fakeUsrLibFiles are files that should be written to the "mounted"
6263
// /usr/lib directory. It includes files that shouldn't be returned.
6364
fakeUsrLibFiles = append([]string{
6465
filepath.Join(usrLibMountpoint, "libcurl-gnutls.so"),
65-
filepath.Join(usrLibMountpoint, "nvidia", "libglxserver_nvidia.so.1"),
6666
}, expectedUsrLibFiles...)
6767
)
6868

@@ -98,7 +98,7 @@ func TestGPUs(t *testing.T) {
9898
devices, binds, err := xunix.GPUs(ctx, log, usrLibMountpoint)
9999
require.NoError(t, err)
100100
require.Len(t, devices, 2, "unexpected 2 nvidia devices")
101-
require.Len(t, binds, 3, "expected 4 nvidia binds")
101+
require.Len(t, binds, 4, "expected 4 nvidia binds")
102102
require.Contains(t, binds, mount.MountPoint{
103103
Device: "/dev/sda1",
104104
Path: "/usr/local/nvidia",

0 commit comments

Comments
 (0)