Files
moby/integration/container/cdi_test.go
Paweł Gronowski 9095698a5c daemon: Discover devices and include in system info
Add ability for the device driver to implement a device discovery
mechanism and expose discovered devices in the `docker info` output.

Currently it's only implemented for CDI devices.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-05-16 17:03:29 +02:00

204 lines
6.3 KiB
Go

package container // import "github.com/docker/docker/integration/container"
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/testutil"
"github.com/docker/docker/testutil/daemon"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/poll"
"gotest.tools/v3/skip"
)
func TestCreateWithCDIDevices(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType != "linux", "CDI devices are only supported on Linux")
skip.If(t, testEnv.IsRemoteDaemon, "cannot run cdi tests with a remote daemon")
ctx := testutil.StartSpan(baseContext, t)
cwd, err := os.Getwd()
assert.NilError(t, err)
d := daemon.New(t)
d.StartWithBusybox(ctx, t, "--cdi-spec-dir="+filepath.Join(cwd, "testdata", "cdi"))
defer d.Stop(t)
apiClient := d.NewClientT(t)
id := container.Run(ctx, t, apiClient,
container.WithCmd("/bin/sh", "-c", "env"),
container.WithCDIDevices("vendor1.com/device=foo"),
)
defer apiClient.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
inspect, err := apiClient.ContainerInspect(ctx, id)
assert.NilError(t, err)
expectedRequests := []containertypes.DeviceRequest{
{
Driver: "cdi",
DeviceIDs: []string{"vendor1.com/device=foo"},
},
}
assert.Check(t, is.DeepEqual(inspect.HostConfig.DeviceRequests, expectedRequests))
poll.WaitOn(t, container.IsStopped(ctx, apiClient, id))
reader, err := apiClient.ContainerLogs(ctx, id, containertypes.LogsOptions{
ShowStdout: true,
})
assert.NilError(t, err)
actualStdout := new(bytes.Buffer)
actualStderr := io.Discard
_, err = stdcopy.StdCopy(actualStdout, actualStderr, reader)
assert.NilError(t, err)
outlines := strings.Split(actualStdout.String(), "\n")
assert.Assert(t, is.Contains(outlines, "FOO=injected"))
}
func TestCDISpecDirsAreInSystemInfo(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType == "windows") // d.Start fails on Windows with `protocol not available`
// TODO: This restriction can be relaxed with https://github.com/moby/moby/pull/46158
skip.If(t, testEnv.IsRootless, "the t.TempDir test creates a folder with incorrect permissions for rootless")
testCases := []struct {
description string
config string
specDirs []string
expectedInfoCDISpecDirs []string
}{
{
description: "No config returns default CDI spec dirs",
config: `{}`,
specDirs: nil,
expectedInfoCDISpecDirs: []string{"/etc/cdi", "/var/run/cdi"},
},
{
description: "CDI explicitly enabled with no spec dirs specified returns default",
config: `{"features": {"cdi": true}}`,
specDirs: nil,
expectedInfoCDISpecDirs: []string{"/etc/cdi", "/var/run/cdi"},
},
{
description: "CDI enabled with specified spec dirs are returned",
config: `{"features": {"cdi": true}}`,
specDirs: []string{"/foo/bar", "/baz/qux"},
expectedInfoCDISpecDirs: []string{"/foo/bar", "/baz/qux"},
},
{
description: "CDI enabled with empty string as spec dir returns empty slice",
config: `{"features": {"cdi": true}}`,
specDirs: []string{""},
expectedInfoCDISpecDirs: []string{},
},
{
description: "CDI enabled with empty config option returns empty slice",
config: `{"features": {"cdi": true}, "cdi-spec-dirs": []}`,
expectedInfoCDISpecDirs: []string{},
},
{
description: "CDI explicitly disabled with no spec dirs specified returns empty slice",
config: `{"features": {"cdi": false}}`,
specDirs: nil,
expectedInfoCDISpecDirs: []string{},
},
{
description: "CDI explicitly disabled with specified spec dirs returns empty slice",
config: `{"features": {"cdi": false}}`,
specDirs: []string{"/foo/bar", "/baz/qux"},
expectedInfoCDISpecDirs: []string{},
},
{
description: "CDI explicitly disabled with empty string as spec dir returns empty slice",
config: `{"features": {"cdi": false}}`,
specDirs: []string{""},
expectedInfoCDISpecDirs: []string{},
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
var opts []daemon.Option
d := daemon.New(t, opts...)
var args []string
for _, specDir := range tc.specDirs {
args = append(args, "--cdi-spec-dir="+specDir)
}
if tc.config != "" {
configPath := filepath.Join(t.TempDir(), "daemon.json")
err := os.WriteFile(configPath, []byte(tc.config), 0o644)
assert.NilError(t, err)
args = append(args, "--config-file="+configPath)
}
d.Start(t, args...)
defer d.Stop(t)
info := d.Info(t)
assert.Check(t, is.DeepEqual(tc.expectedInfoCDISpecDirs, info.CDISpecDirs))
})
}
}
func TestCDIInfoDiscoveredDevices(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "CDI not supported on Windows")
ctx := testutil.StartSpan(baseContext, t)
// Create a sample CDI spec file
specContent := `{
"cdiVersion": "0.5.0",
"kind": "test.com/device",
"devices": [
{
"name": "mygpu0",
"containerEdits": {
"deviceNodes": [
{"path": "/dev/null"}
]
}
}
]
}`
cdiDir := testutil.TempDir(t)
specFilePath := filepath.Join(cdiDir, "test-device.json")
err := os.WriteFile(specFilePath, []byte(specContent), 0644)
assert.NilError(t, err, "Failed to write sample CDI spec file")
d := daemon.New(t)
d.Start(t, "--feature", "cdi", "--cdi-spec-dir="+cdiDir)
defer d.Stop(t)
c := d.NewClientT(t)
info, err := c.Info(ctx)
assert.NilError(t, err)
assert.Check(t, is.Len(info.CDISpecDirs, 1))
assert.Check(t, is.Equal(info.CDISpecDirs[0], cdiDir))
expectedDevice := system.DeviceInfo{
Source: "cdi",
ID: "test.com/device=mygpu0",
}
assert.Check(t, is.Equal(len(info.DiscoveredDevices), 1), "Expected one discovered device")
assert.Check(t, is.DeepEqual(info.DiscoveredDevices, []system.DeviceInfo{expectedDevice}))
}