Skip to content

Commit f9885af

Browse files
committed
chore(integration): add basic integration test for Nvidia GPUs
1 parent 02ae192 commit f9885af

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

integration/gpu_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package integration
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
"slices"
7+
"strings"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/coder/envbox/integration/integrationtest"
14+
)
15+
16+
func TestDocker_Nvidia(t *testing.T) {
17+
// Only run this test if the nvidia container runtime is detected.
18+
// Check if the nvidia runtime is available using `docker info`.
19+
// The docker client doesn't expose this information so we need to fetch it ourselves.
20+
if !slices.Contains(dockerRuntimes(t), "nvidia") {
21+
t.Skip("this test requires nvidia runtime to be available")
22+
}
23+
24+
t.Run("Ubuntu", func(t *testing.T) {
25+
ctx, cancel := context.WithCancel(context.Background())
26+
t.Cleanup(cancel)
27+
28+
// Start the envbox container.
29+
ctID := startEnvboxCmd(ctx, t, integrationtest.UbuntuImage, "root",
30+
"--env", "CODER_ADD_GPU=true",
31+
"--env", "CODER_USR_LIB_DIR=/usr/lib/x86_64-linux-gnu",
32+
"--runtime=nvidia",
33+
"--gpus=all",
34+
)
35+
36+
// Assert that we can run nvidia-smi in the inner container.
37+
_, err := execContainerCmd(ctx, t, ctID, "docker", "exec", "workspace_cvm", "nvidia-smi")
38+
require.NoError(t, err, "failed to run nvidia-smi in the inner container")
39+
})
40+
41+
t.Run("Redhat", func(t *testing.T) {
42+
ctx, cancel := context.WithCancel(context.Background())
43+
t.Cleanup(cancel)
44+
45+
// Start the envbox container.
46+
ctID := startEnvboxCmd(ctx, t, integrationtest.UbuntuImage, "root",
47+
"--env", "CODER_ADD_GPU=true",
48+
"--env", "CODER_USR_LIB_DIR=/usr/lib/x86_64-linux-gnu",
49+
"--runtime=nvidia",
50+
"--gpus=all",
51+
)
52+
53+
// Assert that we can run nvidia-smi in the inner container.
54+
_, err := execContainerCmd(ctx, t, ctID, "docker", "exec", "workspace_cvm", "nvidia-smi")
55+
require.NoError(t, err, "failed to run nvidia-smi in the inner container")
56+
})
57+
}
58+
59+
// dockerRuntimes returns the list of container runtimes available on the host.
60+
// It does this by running `docker info` and parsing the output.
61+
func dockerRuntimes(t *testing.T) []string {
62+
t.Helper()
63+
64+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
65+
defer cancel()
66+
67+
cmd := exec.CommandContext(ctx, "docker", "info", "--format", "{{ range $k, $v := .Runtimes}}{{ println $k }}{{ end }}")
68+
out, err := cmd.CombinedOutput()
69+
require.NoError(t, err, "failed to get docker runtimes: %s", out)
70+
raw := strings.TrimSpace(string(out))
71+
return strings.Split(raw, "\n")
72+
}
73+
74+
func startEnvboxCmd(ctx context.Context, t *testing.T, innerImage, innerUser string, addlArgs ...string) (containerID string) {
75+
t.Helper()
76+
77+
var (
78+
tmpDir = integrationtest.TmpDir(t)
79+
binds = integrationtest.DefaultBinds(t, tmpDir)
80+
cancelCtx, cancel = context.WithCancel(ctx)
81+
)
82+
t.Cleanup(cancel)
83+
84+
// Unfortunately ory/dockertest does not allow us to specify runtime.
85+
// We're instead going to just run the container directly via the docker cli.
86+
startEnvboxArgs := []string{
87+
"run",
88+
"--detach",
89+
"--rm",
90+
"--privileged",
91+
"--env", "CODER_INNER_IMAGE=" + innerImage,
92+
"--env", "CODER_INNER_USERNAME=" + innerUser,
93+
}
94+
for _, bind := range binds {
95+
bindParts := []string{bind.Source, bind.Target}
96+
if bind.ReadOnly {
97+
bindParts = append(bindParts, "ro")
98+
}
99+
startEnvboxArgs = append(startEnvboxArgs, []string{"-v", strings.Join(bindParts, ":")}...)
100+
}
101+
startEnvboxArgs = append(startEnvboxArgs, addlArgs...)
102+
startEnvboxArgs = append(startEnvboxArgs, "envbox:latest", "/envbox", "docker")
103+
t.Logf("envbox docker cmd: docker %s", strings.Join(startEnvboxArgs, " "))
104+
105+
// Start the envbox container without attaching.
106+
startEnvboxCmd := exec.CommandContext(cancelCtx, "docker", startEnvboxArgs...)
107+
out, err := startEnvboxCmd.CombinedOutput()
108+
require.NoError(t, err, "failed to start envbox container")
109+
containerID = strings.TrimSpace(string(out))
110+
t.Logf("envbox container ID: %s", containerID)
111+
t.Cleanup(func() {
112+
if t.Failed() {
113+
// Dump the logs if the test failed.
114+
logsCmd := exec.Command("docker", "logs", containerID)
115+
out, err := logsCmd.CombinedOutput()
116+
if err != nil {
117+
t.Logf("failed to read logs: %s", err)
118+
}
119+
t.Logf("envbox logs:\n%s", string(out))
120+
}
121+
// Stop the envbox container.
122+
stopEnvboxCmd := exec.Command("docker", "rm", "-f", containerID)
123+
out, err := stopEnvboxCmd.CombinedOutput()
124+
if err != nil {
125+
t.Errorf("failed to stop envbox container: %s", out)
126+
}
127+
})
128+
129+
// Wait for the Docker CVM to come up.
130+
waitCtx, waitCancel := context.WithTimeout(cancelCtx, 5*time.Minute)
131+
defer waitCancel()
132+
WAITLOOP:
133+
for {
134+
select {
135+
case <-waitCtx.Done():
136+
t.Fatal("timed out waiting for inner container to come up")
137+
default:
138+
execCmd := exec.CommandContext(cancelCtx, "docker", "exec", containerID, "docker", "inspect", "workspace_cvm")
139+
out, err := execCmd.CombinedOutput()
140+
if err != nil {
141+
t.Logf("waiting for inner container to come up:\n%s", string(out))
142+
<-time.After(time.Second)
143+
continue WAITLOOP
144+
}
145+
t.Logf("inner container is up")
146+
break WAITLOOP
147+
}
148+
}
149+
150+
return containerID
151+
}
152+
153+
func execContainerCmd(ctx context.Context, t *testing.T, containerID string, cmdArgs ...string) (string, error) {
154+
t.Helper()
155+
156+
execArgs := []string{"exec", containerID}
157+
execArgs = append(execArgs, cmdArgs...)
158+
t.Logf("exec cmd: docker %s", strings.Join(execArgs, " "))
159+
execCmd := exec.CommandContext(ctx, "docker", execArgs...)
160+
out, err := execCmd.CombinedOutput()
161+
if err != nil {
162+
t.Logf("exec cmd failed: %s\n%s", err.Error(), string(out))
163+
} else {
164+
t.Logf("exec cmd success: %s", out)
165+
}
166+
return strings.TrimSpace(string(out)), err
167+
}

integration/integrationtest/docker.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const (
4040
// UbuntuImage is just vanilla ubuntu (80MB) but the user is set to a non-root
4141
// user .
4242
UbuntuImage = "gcr.io/coder-dev-1/sreya/ubuntu-coder"
43+
// Redhat UBI9 image as of 2025-03-05
44+
RedhatImage = "registry.access.redhat.com/ubi9/ubi:9.5"
4345

4446
// RegistryImage is used to assert that we add certs
4547
// correctly to the docker daemon when pulling an image

0 commit comments

Comments
 (0)