mirror of
https://github.com/moby/moby.git
synced 2026-01-11 10:41:43 +00:00
644 lines
21 KiB
Go
644 lines
21 KiB
Go
package container
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
containertypes "github.com/moby/moby/api/types/container"
|
|
mounttypes "github.com/moby/moby/api/types/mount"
|
|
"github.com/moby/moby/api/types/network"
|
|
"github.com/moby/moby/api/types/versions"
|
|
"github.com/moby/moby/client"
|
|
"github.com/moby/moby/v2/daemon/config"
|
|
"github.com/moby/moby/v2/daemon/volume"
|
|
"github.com/moby/moby/v2/integration/internal/container"
|
|
"github.com/moby/moby/v2/internal/testutil"
|
|
"github.com/moby/moby/v2/pkg/parsers/kernel"
|
|
"github.com/moby/sys/mount"
|
|
"github.com/moby/sys/mountinfo"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
"gotest.tools/v3/fs"
|
|
"gotest.tools/v3/poll"
|
|
"gotest.tools/v3/skip"
|
|
)
|
|
|
|
// testNonExistingPlugin is a special plugin-name, which overrides defaultTimeOut in tests.
|
|
// this is a copy of https://github.com/moby/moby/blob/9e00a63d65434cdedc444e79a2b33a7c202b10d8/pkg/plugins/client.go#L253-L254
|
|
const testNonExistingPlugin = "this-plugin-does-not-exist"
|
|
|
|
func TestContainerNetworkMountsNoChown(t *testing.T) {
|
|
// chown only applies to Linux bind mounted volumes; must be same host to verify
|
|
skip.If(t, testEnv.IsRemoteDaemon)
|
|
|
|
ctx := setupTest(t)
|
|
|
|
tmpDir := fs.NewDir(t, "network-file-mounts", fs.WithMode(0o755), fs.WithFile("nwfile", "network file bind mount", fs.WithMode(0o644)))
|
|
tmpNWFileMount := tmpDir.Join("nwfile")
|
|
|
|
config := containertypes.Config{
|
|
Image: "busybox",
|
|
}
|
|
hostConfig := containertypes.HostConfig{
|
|
Mounts: []mounttypes.Mount{
|
|
{
|
|
Type: "bind",
|
|
Source: tmpNWFileMount,
|
|
Target: "/etc/resolv.conf",
|
|
},
|
|
{
|
|
Type: "bind",
|
|
Source: tmpNWFileMount,
|
|
Target: "/etc/hostname",
|
|
},
|
|
{
|
|
Type: "bind",
|
|
Source: tmpNWFileMount,
|
|
Target: "/etc/hosts",
|
|
},
|
|
},
|
|
}
|
|
|
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
|
assert.NilError(t, err)
|
|
defer cli.Close()
|
|
|
|
ctrCreate, err := cli.ContainerCreate(ctx, client.ContainerCreateOptions{
|
|
Config: &config,
|
|
HostConfig: &hostConfig,
|
|
NetworkingConfig: &network.NetworkingConfig{},
|
|
})
|
|
assert.NilError(t, err)
|
|
// container will exit immediately because of no tty, but we only need the start sequence to test the condition
|
|
err = cli.ContainerStart(ctx, ctrCreate.ID, client.ContainerStartOptions{})
|
|
assert.NilError(t, err)
|
|
|
|
// Check that host-located bind mount network file did not change ownership when the container was started
|
|
// Note: If the user specifies a mountpath from the host, we should not be
|
|
// attempting to chown files outside the daemon's metadata directory
|
|
// (represented by `daemon.repository` at init time).
|
|
// This forces users who want to use user namespaces to handle the
|
|
// ownership needs of any external files mounted as network files
|
|
// (/etc/resolv.conf, /etc/hosts, /etc/hostname) separately from the
|
|
// daemon. In all other volume/bind mount situations we have taken this
|
|
// same line--we don't chown host file content.
|
|
// See GitHub PR 34224 for details.
|
|
info, err := os.Stat(tmpNWFileMount)
|
|
assert.NilError(t, err)
|
|
fi := info.Sys().(*syscall.Stat_t)
|
|
assert.Check(t, is.Equal(fi.Uid, uint32(0)), "bind mounted network file should not change ownership from root")
|
|
}
|
|
|
|
func TestMountDaemonRoot(t *testing.T) {
|
|
skip.If(t, testEnv.IsRemoteDaemon)
|
|
|
|
ctx := setupTest(t)
|
|
apiClient := testEnv.APIClient()
|
|
info, err := apiClient.Info(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
desc string
|
|
propagation mounttypes.Propagation
|
|
expected mounttypes.Propagation
|
|
}{
|
|
{
|
|
desc: "default",
|
|
propagation: "",
|
|
expected: mounttypes.PropagationRSlave,
|
|
},
|
|
{
|
|
desc: "private",
|
|
propagation: mounttypes.PropagationPrivate,
|
|
},
|
|
{
|
|
desc: "rprivate",
|
|
propagation: mounttypes.PropagationRPrivate,
|
|
},
|
|
{
|
|
desc: "slave",
|
|
propagation: mounttypes.PropagationSlave,
|
|
},
|
|
{
|
|
desc: "rslave",
|
|
propagation: mounttypes.PropagationRSlave,
|
|
expected: mounttypes.PropagationRSlave,
|
|
},
|
|
{
|
|
desc: "shared",
|
|
propagation: mounttypes.PropagationShared,
|
|
},
|
|
{
|
|
desc: "rshared",
|
|
propagation: mounttypes.PropagationRShared,
|
|
expected: mounttypes.PropagationRShared,
|
|
},
|
|
} {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
test := test
|
|
t.Parallel()
|
|
|
|
ctx := testutil.StartSpan(ctx, t)
|
|
|
|
propagationSpec := fmt.Sprintf(":%s", test.propagation)
|
|
if test.propagation == "" {
|
|
propagationSpec = ""
|
|
}
|
|
bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec
|
|
bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec
|
|
|
|
for name, hc := range map[string]*containertypes.HostConfig{
|
|
"bind root": {Binds: []string{bindSpecRoot}},
|
|
"bind subpath": {Binds: []string{bindSpecSub}},
|
|
"mount root": {
|
|
Mounts: []mounttypes.Mount{
|
|
{
|
|
Type: mounttypes.TypeBind,
|
|
Source: info.DockerRootDir,
|
|
Target: "/foo",
|
|
BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
|
|
},
|
|
},
|
|
},
|
|
"mount subpath": {
|
|
Mounts: []mounttypes.Mount{
|
|
{
|
|
Type: mounttypes.TypeBind,
|
|
Source: filepath.Join(info.DockerRootDir, "containers"),
|
|
Target: "/foo",
|
|
BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
hc := hc
|
|
t.Parallel()
|
|
|
|
ctx := testutil.StartSpan(ctx, t)
|
|
|
|
c, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
|
|
Config: &containertypes.Config{
|
|
Image: "busybox",
|
|
Cmd: []string{"true"},
|
|
},
|
|
HostConfig: hc,
|
|
})
|
|
if err != nil {
|
|
if test.expected != "" {
|
|
t.Fatal(err)
|
|
}
|
|
// expected an error, so this is ok and should not continue
|
|
return
|
|
}
|
|
if test.expected == "" {
|
|
t.Fatal("expected create to fail")
|
|
}
|
|
|
|
defer func() {
|
|
if err := apiClient.ContainerRemove(ctx, c.ID, client.ContainerRemoveOptions{Force: true}); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
inspect, err := apiClient.ContainerInspect(ctx, c.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(inspect.Mounts) != 1 {
|
|
t.Fatalf("unexpected number of mounts: %+v", inspect.Mounts)
|
|
}
|
|
|
|
m := inspect.Mounts[0]
|
|
if m.Propagation != test.expected {
|
|
t.Fatalf("got unexpected propagation mode, expected %q, got: %v", test.expected, m.Propagation)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContainerBindMountNonRecursive(t *testing.T) {
|
|
skip.If(t, testEnv.IsRemoteDaemon)
|
|
skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)")
|
|
|
|
ctx := setupTest(t)
|
|
|
|
tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0o755), fs.WithDir("mnt", fs.WithMode(0o755)))
|
|
tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt")
|
|
tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0o755), fs.WithFile("file", "should not be visible when NonRecursive", fs.WithMode(0o644)))
|
|
|
|
err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind,ro")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := mount.Unmount(tmpDir1Mnt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
// implicit is recursive (NonRecursive: false)
|
|
implicit := mounttypes.Mount{
|
|
Type: "bind",
|
|
Source: tmpDir1.Path(),
|
|
Target: "/foo",
|
|
ReadOnly: true,
|
|
}
|
|
recursive := implicit
|
|
recursive.BindOptions = &mounttypes.BindOptions{
|
|
NonRecursive: false,
|
|
}
|
|
recursiveVerifier := []string{"test", "-f", "/foo/mnt/file"}
|
|
nonRecursive := implicit
|
|
nonRecursive.BindOptions = &mounttypes.BindOptions{
|
|
NonRecursive: true,
|
|
}
|
|
nonRecursiveVerifier := []string{"test", "!", "-f", "/foo/mnt/file"}
|
|
|
|
apiClient := testEnv.APIClient()
|
|
containers := []string{
|
|
container.Run(ctx, t, apiClient, container.WithMount(implicit), container.WithCmd(recursiveVerifier...)),
|
|
container.Run(ctx, t, apiClient, container.WithMount(recursive), container.WithCmd(recursiveVerifier...)),
|
|
container.Run(ctx, t, apiClient, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)),
|
|
}
|
|
|
|
for _, c := range containers {
|
|
poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, c))
|
|
}
|
|
}
|
|
|
|
func TestContainerVolumesMountedAsShared(t *testing.T) {
|
|
// Volume propagation is linux only. Also it creates directories for
|
|
// bind mounting, so needs to be same host.
|
|
skip.If(t, testEnv.IsRemoteDaemon)
|
|
skip.If(t, testEnv.IsUserNamespace)
|
|
skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)")
|
|
|
|
ctx := setupTest(t)
|
|
|
|
// Prepare a source directory to bind mount
|
|
tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0o755), fs.WithDir("mnt1", fs.WithMode(0o755)))
|
|
tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1")
|
|
|
|
// Convert this directory into a shared mount point so that we do
|
|
// not rely on propagation properties of parent mount.
|
|
if err := mount.MakePrivate(tmpDir1.Path()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := mount.Unmount(tmpDir1.Path()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
if err := mount.MakeShared(tmpDir1.Path()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sharedMount := mounttypes.Mount{
|
|
Type: mounttypes.TypeBind,
|
|
Source: tmpDir1.Path(),
|
|
Target: "/volume-dest",
|
|
BindOptions: &mounttypes.BindOptions{
|
|
Propagation: mounttypes.PropagationShared,
|
|
},
|
|
}
|
|
|
|
bindMountCmd := []string{"mount", "--bind", "/volume-dest/mnt1", "/volume-dest/mnt1"}
|
|
|
|
apiClient := testEnv.APIClient()
|
|
containerID := container.Run(ctx, t, apiClient, container.WithPrivileged(true), container.WithMount(sharedMount), container.WithCmd(bindMountCmd...))
|
|
poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, containerID))
|
|
|
|
// Make sure a bind mount under a shared volume propagated to host.
|
|
if mounted, _ := mountinfo.Mounted(tmpDir1Mnt); !mounted {
|
|
t.Fatalf("Bind mount under shared volume did not propagate to host")
|
|
}
|
|
|
|
mount.Unmount(tmpDir1Mnt)
|
|
}
|
|
|
|
func TestContainerVolumesMountedAsSlave(t *testing.T) {
|
|
// Volume propagation is linux only. Also it creates directories for
|
|
// bind mounting, so needs to be same host.
|
|
skip.If(t, testEnv.IsRemoteDaemon)
|
|
skip.If(t, testEnv.IsUserNamespace)
|
|
skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)")
|
|
|
|
ctx := testutil.StartSpan(baseContext, t)
|
|
|
|
// Prepare a source directory to bind mount
|
|
tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0o755), fs.WithDir("mnt1", fs.WithMode(0o755)))
|
|
tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1")
|
|
|
|
// Prepare a source directory with file in it. We will bind mount this
|
|
// directory and see if file shows up.
|
|
tmpDir2 := fs.NewDir(t, "volume-source2", fs.WithMode(0o755), fs.WithFile("slave-testfile", "Test", fs.WithMode(0o644)))
|
|
|
|
// Convert this directory into a shared mount point so that we do
|
|
// not rely on propagation properties of parent mount.
|
|
if err := mount.MakePrivate(tmpDir1.Path()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := mount.Unmount(tmpDir1.Path()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
if err := mount.MakeShared(tmpDir1.Path()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
slaveMount := mounttypes.Mount{
|
|
Type: mounttypes.TypeBind,
|
|
Source: tmpDir1.Path(),
|
|
Target: "/volume-dest",
|
|
BindOptions: &mounttypes.BindOptions{
|
|
Propagation: mounttypes.PropagationSlave,
|
|
},
|
|
}
|
|
|
|
topCmd := []string{"top"}
|
|
|
|
apiClient := testEnv.APIClient()
|
|
containerID := container.Run(ctx, t, apiClient, container.WithTty(true), container.WithMount(slaveMount), container.WithCmd(topCmd...))
|
|
|
|
// Bind mount tmpDir2/ onto tmpDir1/mnt1. If mount propagates inside
|
|
// container then contents of tmpDir2/slave-testfile should become
|
|
// visible at "/volume-dest/mnt1/slave-testfile"
|
|
if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := mount.Unmount(tmpDir1Mnt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
mountCmd := []string{"cat", "/volume-dest/mnt1/slave-testfile"}
|
|
|
|
if result, err := container.Exec(ctx, apiClient, containerID, mountCmd); err == nil {
|
|
if result.Stdout() != "Test" {
|
|
t.Fatalf("Bind mount under slave volume did not propagate to container")
|
|
}
|
|
} else {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// TestContainerVolumeAnonymous verifies that anonymous volumes created through
|
|
// the Mounts API get a random name generated, and have the "AnonymousLabel"
|
|
// (com.docker.volume.anonymous) label set.
|
|
//
|
|
// regression test for https://github.com/moby/moby/issues/48748
|
|
func TestContainerVolumeAnonymous(t *testing.T) {
|
|
skip.If(t, testEnv.IsRemoteDaemon)
|
|
|
|
ctx := setupTest(t)
|
|
apiClient := testEnv.APIClient()
|
|
|
|
t.Run("no driver specified", func(t *testing.T) {
|
|
mntOpts := mounttypes.Mount{Type: mounttypes.TypeVolume, Target: "/foo"}
|
|
cID := container.Create(ctx, t, apiClient, container.WithMount(mntOpts))
|
|
|
|
inspect := container.Inspect(ctx, t, apiClient, cID)
|
|
assert.Assert(t, is.Len(inspect.HostConfig.Mounts, 1))
|
|
assert.Check(t, is.Equal(inspect.HostConfig.Mounts[0], mntOpts))
|
|
|
|
assert.Assert(t, is.Len(inspect.Mounts, 1))
|
|
vol := inspect.Mounts[0]
|
|
assert.Check(t, is.Len(vol.Name, 64), "volume name should be 64 bytes (from stringid.GenerateRandomID())")
|
|
assert.Check(t, is.Equal(vol.Driver, volume.DefaultDriverName))
|
|
|
|
res, err := apiClient.VolumeInspect(ctx, vol.Name, client.VolumeInspectOptions{})
|
|
assert.NilError(t, err)
|
|
|
|
// see [daemon.AnonymousLabel]; we don't want to import the daemon package here.
|
|
const expectedAnonymousLabel = "com.docker.volume.anonymous"
|
|
assert.Check(t, is.Contains(res.Volume.Labels, expectedAnonymousLabel))
|
|
assert.Check(t, is.Equal(res.Volume.Driver, volume.DefaultDriverName))
|
|
})
|
|
|
|
// Verify that specifying a custom driver is still taken into account.
|
|
t.Run("custom driver", func(t *testing.T) {
|
|
config := container.NewTestConfig(container.WithMount(mounttypes.Mount{
|
|
Type: mounttypes.TypeVolume,
|
|
Target: "/foo",
|
|
VolumeOptions: &mounttypes.VolumeOptions{
|
|
DriverConfig: &mounttypes.Driver{
|
|
Name: testNonExistingPlugin,
|
|
},
|
|
},
|
|
}))
|
|
_, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
|
|
Config: config.Config,
|
|
HostConfig: config.HostConfig,
|
|
NetworkingConfig: config.NetworkingConfig,
|
|
Platform: config.Platform,
|
|
ContainerName: config.Name,
|
|
})
|
|
// We use [testNonExistingPlugin] for this, which produces an error
|
|
// when used, which we use as indicator that the driver was passed
|
|
// through. We should have a cleaner way for this, but that would
|
|
// require a custom volume plugin to be installed.
|
|
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
|
|
assert.Check(t, is.ErrorContains(err, fmt.Sprintf(`plugin %q not found`, testNonExistingPlugin)))
|
|
})
|
|
}
|
|
|
|
// Regression test for #38995 and #43390.
|
|
func TestContainerCopyLeaksMounts(t *testing.T) {
|
|
ctx := setupTest(t)
|
|
|
|
bindMount := mounttypes.Mount{
|
|
Type: mounttypes.TypeBind,
|
|
Source: "/var",
|
|
Target: "/hostvar",
|
|
BindOptions: &mounttypes.BindOptions{
|
|
Propagation: mounttypes.PropagationRSlave,
|
|
},
|
|
}
|
|
|
|
apiClient := testEnv.APIClient()
|
|
cid := container.Run(ctx, t, apiClient, container.WithMount(bindMount), container.WithCmd("sleep", "120s"))
|
|
|
|
getMounts := func() string {
|
|
t.Helper()
|
|
res, err := container.Exec(ctx, apiClient, cid, []string{"cat", "/proc/self/mountinfo"})
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, res.ExitCode, 0)
|
|
return res.Stdout()
|
|
}
|
|
|
|
mountsBefore := getMounts()
|
|
|
|
_, _, err := apiClient.CopyFromContainer(ctx, cid, "/etc/passwd")
|
|
assert.NilError(t, err)
|
|
|
|
mountsAfter := getMounts()
|
|
|
|
assert.Equal(t, mountsBefore, mountsAfter)
|
|
}
|
|
|
|
func TestContainerBindMountReadOnlyDefault(t *testing.T) {
|
|
skip.If(t, testEnv.IsRemoteDaemon)
|
|
skip.If(t, !isRROSupported(), "requires recursive read-only mounts")
|
|
|
|
ctx := setupTest(t)
|
|
|
|
// The test will run a container with a simple readonly /dev bind mount (-v /dev:/dev:ro)
|
|
// It will then check /proc/self/mountinfo for the mount type of /dev/shm (submount of /dev)
|
|
// If /dev/shm is rw, that will mean that the read-only mounts are NOT recursive by default.
|
|
const nonRecursive = " /dev/shm rw,"
|
|
// If /dev/shm is ro, that will mean that the read-only mounts ARE recursive by default.
|
|
const recursive = " /dev/shm ro,"
|
|
|
|
for _, tc := range []struct {
|
|
clientVersion string
|
|
expectedOut string
|
|
name string
|
|
}{
|
|
{clientVersion: "", expectedOut: recursive, name: "latest should be the same as 1.44"},
|
|
{clientVersion: "1.44", expectedOut: recursive, name: "submount should be recursive by default on 1.44"},
|
|
|
|
{clientVersion: "1.43", expectedOut: nonRecursive, name: "older than 1.44 should be non-recursive by default"},
|
|
|
|
// TODO: Remove when DefaultMinAPIVersion >= 1.44
|
|
{clientVersion: config.MinAPIVersion, expectedOut: nonRecursive, name: "minimum API should be non-recursive by default"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
apiClient := testEnv.APIClient()
|
|
|
|
minDaemonVersion := tc.clientVersion
|
|
if minDaemonVersion == "" {
|
|
minDaemonVersion = "1.44"
|
|
}
|
|
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), minDaemonVersion), "requires API v"+minDaemonVersion)
|
|
|
|
if tc.clientVersion != "" {
|
|
c, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(tc.clientVersion))
|
|
assert.NilError(t, err, "failed to create client with version v%s", tc.clientVersion)
|
|
apiClient = c
|
|
}
|
|
|
|
for _, tc2 := range []struct {
|
|
subname string
|
|
mountOpt func(*container.TestContainerConfig)
|
|
}{
|
|
{"mount", container.WithMount(mounttypes.Mount{
|
|
Type: mounttypes.TypeBind,
|
|
Source: "/dev",
|
|
Target: "/dev",
|
|
ReadOnly: true,
|
|
})},
|
|
{"bind mount", container.WithBindRaw("/dev:/dev:ro")},
|
|
} {
|
|
t.Run(tc2.subname, func(t *testing.T) {
|
|
cid := container.Run(ctx, t, apiClient, tc2.mountOpt,
|
|
container.WithCmd("sh", "-c", "grep /dev/shm /proc/self/mountinfo"),
|
|
)
|
|
out, err := container.Output(ctx, apiClient, cid)
|
|
assert.NilError(t, err)
|
|
|
|
assert.Check(t, is.Equal(out.Stderr, ""))
|
|
// Output should be either:
|
|
// 545 526 0:160 / /dev/shm ro,nosuid,nodev,noexec,relatime shared:90 - tmpfs shm rw,size=65536k
|
|
// or
|
|
// 545 526 0:160 / /dev/shm rw,nosuid,nodev,noexec,relatime shared:90 - tmpfs shm rw,size=65536k
|
|
assert.Check(t, is.Contains(out.Stdout, tc.expectedOut))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContainerBindMountRecursivelyReadOnly(t *testing.T) {
|
|
skip.If(t, testEnv.IsRemoteDaemon)
|
|
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "requires API v1.44")
|
|
|
|
ctx := setupTest(t)
|
|
|
|
// 0o777 for allowing rootless containers to write to this directory
|
|
tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0o777),
|
|
fs.WithDir("mnt", fs.WithMode(0o777)))
|
|
defer tmpDir1.Remove()
|
|
tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt")
|
|
tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0o777),
|
|
fs.WithFile("file", "should not be writable when recursively read only", fs.WithMode(0o666)))
|
|
defer tmpDir2.Remove()
|
|
|
|
if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := mount.Unmount(tmpDir1Mnt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
rroSupported := isRROSupported()
|
|
|
|
nonRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? = 0 ]`}
|
|
forceRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? != 0 ]`}
|
|
|
|
// ro (recursive if kernel >= 5.12)
|
|
ro := mounttypes.Mount{
|
|
Type: mounttypes.TypeBind,
|
|
Source: tmpDir1.Path(),
|
|
Target: "/foo",
|
|
ReadOnly: true,
|
|
BindOptions: &mounttypes.BindOptions{
|
|
Propagation: mounttypes.PropagationRPrivate,
|
|
},
|
|
}
|
|
roAsStr := ro.Source + ":" + ro.Target + ":ro,rprivate"
|
|
roVerifier := nonRecursiveVerifier
|
|
if rroSupported {
|
|
roVerifier = forceRecursiveVerifier
|
|
}
|
|
|
|
// Non-recursive
|
|
nonRecursive := ro
|
|
nonRecursive.BindOptions = &mounttypes.BindOptions{
|
|
ReadOnlyNonRecursive: true,
|
|
Propagation: mounttypes.PropagationRPrivate,
|
|
}
|
|
|
|
// Force recursive
|
|
forceRecursive := ro
|
|
forceRecursive.BindOptions = &mounttypes.BindOptions{
|
|
ReadOnlyForceRecursive: true,
|
|
Propagation: mounttypes.PropagationRPrivate,
|
|
}
|
|
|
|
apiClient := testEnv.APIClient()
|
|
|
|
containers := []string{
|
|
container.Run(ctx, t, apiClient, container.WithMount(ro), container.WithCmd(roVerifier...)),
|
|
container.Run(ctx, t, apiClient, container.WithBindRaw(roAsStr), container.WithCmd(roVerifier...)),
|
|
|
|
container.Run(ctx, t, apiClient, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)),
|
|
}
|
|
|
|
if rroSupported {
|
|
containers = append(containers,
|
|
container.Run(ctx, t, apiClient, container.WithMount(forceRecursive), container.WithCmd(forceRecursiveVerifier...)),
|
|
)
|
|
}
|
|
|
|
for _, c := range containers {
|
|
poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, c))
|
|
}
|
|
}
|
|
|
|
func isRROSupported() bool {
|
|
return kernel.CheckKernelVersion(5, 12, 0)
|
|
}
|