moby/integration/container/ipcmode_linux_test.go
Sebastiaan van Stijn 713c7d49a1
integration(-cli): remove skips for old daemon versions (<20.10)
This removes various skips that accounted for running the integration tests
against older versions of the daemon before 20.10 (API version v1.41). Those
versions are EOL, and we don't run tests against them.

This reverts most of e440831802, and similar
PRs.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-12-05 01:03:50 +01:00

323 lines
11 KiB
Go

package container // import "github.com/docker/docker/integration/container"
import (
"bufio"
"os"
"regexp"
"strings"
"testing"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/testutil"
"github.com/docker/docker/testutil/daemon"
"github.com/docker/docker/testutil/request"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
"gotest.tools/v3/skip"
)
// testIpcCheckDevExists checks whether a given mount (identified by its
// major:minor pair from /proc/self/mountinfo) exists on the host system.
//
// The format of /proc/self/mountinfo is like:
//
// 29 23 0:24 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw
// ^^^^\
// - this is the minor:major we look for
func testIpcCheckDevExists(mm string) (bool, error) {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return false, err
}
defer f.Close()
s := bufio.NewScanner(f)
for s.Scan() {
fields := strings.Fields(s.Text())
if len(fields) < 7 {
continue
}
if fields[2] == mm {
return true, nil
}
}
return false, s.Err()
}
// testIpcNonePrivateShareable is a helper function to test "none",
// "private" and "shareable" modes.
func testIpcNonePrivateShareable(t *testing.T, mode string, mustBeMounted bool, mustBeShared bool) {
ctx := setupTest(t)
cfg := containertypes.Config{
Image: "busybox",
Cmd: []string{"top"},
}
hostCfg := containertypes.HostConfig{
IpcMode: containertypes.IpcMode(mode),
}
apiClient := testEnv.APIClient()
resp, err := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))
err = apiClient.ContainerStart(ctx, resp.ID, containertypes.StartOptions{})
assert.NilError(t, err)
// get major:minor pair for /dev/shm from container's /proc/self/mountinfo
cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo"
result, err := container.Exec(ctx, apiClient, resp.ID, []string{"sh", "-c", cmd})
assert.NilError(t, err)
mm := result.Combined()
if !mustBeMounted {
assert.Check(t, is.Equal(mm, ""))
// no more checks to perform
return
}
assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm)))
shared, err := testIpcCheckDevExists(mm)
assert.NilError(t, err)
t.Logf("[testIpcPrivateShareable] ipcmode: %v, ipcdev: %v, shared: %v, mustBeShared: %v\n", mode, mm, shared, mustBeShared)
assert.Check(t, is.Equal(shared, mustBeShared))
}
// TestIpcModeNone checks the container "none" IPC mode
// (--ipc none) works as expected. It makes sure there is no
// /dev/shm mount inside the container.
func TestIpcModeNone(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
testIpcNonePrivateShareable(t, "none", false, false)
}
// TestAPIIpcModePrivate checks the container private IPC mode
// (--ipc private) works as expected. It gets the minor:major pair
// of /dev/shm mount from the container, and makes sure there is no
// such pair on the host.
func TestIpcModePrivate(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
testIpcNonePrivateShareable(t, "private", true, false)
}
// TestAPIIpcModeShareable checks the container shareable IPC mode
// (--ipc shareable) works as expected. It gets the minor:major pair
// of /dev/shm mount from the container, and makes sure such pair
// also exists on the host.
func TestIpcModeShareable(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
skip.If(t, testEnv.IsRootless, "no support for --ipc=shareable in rootless")
testIpcNonePrivateShareable(t, "shareable", true, true)
}
// testIpcContainer is a helper function to test --ipc container:NNN mode in various scenarios
func testIpcContainer(t *testing.T, donorMode string, mustWork bool) {
t.Helper()
ctx := setupTest(t)
cfg := containertypes.Config{
Image: "busybox",
Cmd: []string{"top"},
}
hostCfg := containertypes.HostConfig{
IpcMode: containertypes.IpcMode(donorMode),
}
apiClient := testEnv.APIClient()
// create and start the "donor" container
resp, err := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))
name1 := resp.ID
err = apiClient.ContainerStart(ctx, name1, containertypes.StartOptions{})
assert.NilError(t, err)
// create and start the second container
hostCfg.IpcMode = containertypes.IpcMode("container:" + name1)
resp, err = apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))
name2 := resp.ID
err = apiClient.ContainerStart(ctx, name2, containertypes.StartOptions{})
if !mustWork {
// start should fail with a specific error
assert.Check(t, is.ErrorContains(err, "non-shareable IPC"))
// no more checks to perform here
return
}
// start should succeed
assert.NilError(t, err)
// check that IPC is shared
// 1. create a file in the first container
_, err = container.Exec(ctx, apiClient, name1, []string{"sh", "-c", "printf covfefe > /dev/shm/bar"})
assert.NilError(t, err)
// 2. check it's the same file in the second one
result, err := container.Exec(ctx, apiClient, name2, []string{"cat", "/dev/shm/bar"})
assert.NilError(t, err)
out := result.Combined()
assert.Check(t, is.Equal(true, regexp.MustCompile("^covfefe$").MatchString(out)))
}
// TestAPIIpcModeShareableAndPrivate checks that
// 1) a container created with --ipc container:ID can use IPC of another shareable container.
// 2) a container created with --ipc container:ID can NOT use IPC of another private container.
func TestAPIIpcModeShareableAndContainer(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
testIpcContainer(t, "shareable", true)
testIpcContainer(t, "private", false)
}
/* TestAPIIpcModeHost checks that a container created with --ipc host
* can use IPC of the host system.
*/
func TestAPIIpcModeHost(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
skip.If(t, testEnv.IsUserNamespace)
cfg := containertypes.Config{
Image: "busybox",
Cmd: []string{"top"},
}
hostCfg := containertypes.HostConfig{
IpcMode: containertypes.IPCModeHost,
}
ctx := testutil.StartSpan(baseContext, t)
apiClient := testEnv.APIClient()
resp, err := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))
name := resp.ID
err = apiClient.ContainerStart(ctx, name, containertypes.StartOptions{})
assert.NilError(t, err)
// check that IPC is shared
// 1. create a file inside container
_, err = container.Exec(ctx, apiClient, name, []string{"sh", "-c", "printf covfefe > /dev/shm/." + name})
assert.NilError(t, err)
// 2. check it's the same on the host
bytes, err := os.ReadFile("/dev/shm/." + name)
assert.NilError(t, err)
assert.Check(t, is.Equal("covfefe", string(bytes)))
// 3. clean up
_, err = container.Exec(ctx, apiClient, name, []string{"rm", "-f", "/dev/shm/." + name})
assert.NilError(t, err)
}
// testDaemonIpcPrivateShareable is a helper function to test "private" and "shareable" daemon default ipc modes.
func testDaemonIpcPrivateShareable(t *testing.T, mustBeShared bool, arg ...string) {
ctx := setupTest(t)
d := daemon.New(t)
d.StartWithBusybox(ctx, t, arg...)
defer d.Stop(t)
c := d.NewClientT(t)
cfg := containertypes.Config{
Image: "busybox",
Cmd: []string{"top"},
}
resp, err := c.ContainerCreate(ctx, &cfg, &containertypes.HostConfig{}, nil, nil, "")
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))
err = c.ContainerStart(ctx, resp.ID, containertypes.StartOptions{})
assert.NilError(t, err)
// get major:minor pair for /dev/shm from container's /proc/self/mountinfo
cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo"
result, err := container.Exec(ctx, c, resp.ID, []string{"sh", "-c", cmd})
assert.NilError(t, err)
mm := result.Combined()
assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm)))
shared, err := testIpcCheckDevExists(mm)
assert.NilError(t, err)
t.Logf("[testDaemonIpcPrivateShareable] ipcdev: %v, shared: %v, mustBeShared: %v\n", mm, shared, mustBeShared)
assert.Check(t, is.Equal(shared, mustBeShared))
}
// TestDaemonIpcModeShareable checks that --default-ipc-mode shareable works as intended.
func TestDaemonIpcModeShareable(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
skip.If(t, testEnv.IsRootless, "no support for --ipc=shareable in rootless")
testDaemonIpcPrivateShareable(t, true, "--default-ipc-mode", "shareable")
}
// TestDaemonIpcModePrivate checks that --default-ipc-mode private works as intended.
func TestDaemonIpcModePrivate(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
testDaemonIpcPrivateShareable(t, false, "--default-ipc-mode", "private")
}
// used to check if an IpcMode given in config works as intended
func testDaemonIpcFromConfig(t *testing.T, mode string, mustExist bool) {
config := `{"default-ipc-mode": "` + mode + `"}`
// WithMode is needed for rootless
file := fs.NewFile(t, "test-daemon-ipc-config", fs.WithContent(config), fs.WithMode(0o644))
defer file.Remove()
testDaemonIpcPrivateShareable(t, mustExist, "--config-file", file.Path())
}
// TestDaemonIpcModePrivateFromConfig checks that "default-ipc-mode: private" config works as intended.
func TestDaemonIpcModePrivateFromConfig(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
testDaemonIpcFromConfig(t, "private", false)
}
// TestDaemonIpcModeShareableFromConfig checks that "default-ipc-mode: shareable" config works as intended.
func TestDaemonIpcModeShareableFromConfig(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
skip.If(t, testEnv.IsRootless, "no support for --ipc=shareable in rootless")
testDaemonIpcFromConfig(t, "shareable", true)
}
// TestIpcModeOlderClient checks that older client gets shareable IPC mode
// by default, even when the daemon default is private.
func TestIpcModeOlderClient(t *testing.T) {
apiClient := testEnv.APIClient()
skip.If(t, versions.LessThan(apiClient.ClientVersion(), "1.40"), "requires client API >= 1.40")
t.Parallel()
ctx := testutil.StartSpan(baseContext, t)
// pre-check: default ipc mode in daemon is private
cID := container.Create(ctx, t, apiClient, container.WithAutoRemove)
inspect, err := apiClient.ContainerInspect(ctx, cID)
assert.NilError(t, err)
assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "private"))
// main check: using older client creates "shareable" container
apiClient = request.NewAPIClient(t, client.WithVersion("1.39"))
cID = container.Create(ctx, t, apiClient, container.WithAutoRemove)
inspect, err = apiClient.ContainerInspect(ctx, cID)
assert.NilError(t, err)
assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "shareable"))
}