github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/integration/container/mounts_linux_test.go (about)

     1  package container // import "github.com/docker/docker/integration/container"
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"syscall"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/docker/docker/api/types"
    13  	containertypes "github.com/docker/docker/api/types/container"
    14  	mounttypes "github.com/docker/docker/api/types/mount"
    15  	"github.com/docker/docker/api/types/network"
    16  	"github.com/docker/docker/api/types/versions"
    17  	"github.com/docker/docker/client"
    18  	"github.com/docker/docker/integration/internal/container"
    19  	"github.com/moby/sys/mount"
    20  	"github.com/moby/sys/mountinfo"
    21  	"gotest.tools/v3/assert"
    22  	is "gotest.tools/v3/assert/cmp"
    23  	"gotest.tools/v3/fs"
    24  	"gotest.tools/v3/poll"
    25  	"gotest.tools/v3/skip"
    26  )
    27  
    28  func TestContainerNetworkMountsNoChown(t *testing.T) {
    29  	// chown only applies to Linux bind mounted volumes; must be same host to verify
    30  	skip.If(t, testEnv.IsRemoteDaemon)
    31  
    32  	defer setupTest(t)()
    33  
    34  	ctx := context.Background()
    35  
    36  	tmpDir := fs.NewDir(t, "network-file-mounts", fs.WithMode(0755), fs.WithFile("nwfile", "network file bind mount", fs.WithMode(0644)))
    37  	defer tmpDir.Remove()
    38  
    39  	tmpNWFileMount := tmpDir.Join("nwfile")
    40  
    41  	config := containertypes.Config{
    42  		Image: "busybox",
    43  	}
    44  	hostConfig := containertypes.HostConfig{
    45  		Mounts: []mounttypes.Mount{
    46  			{
    47  				Type:   "bind",
    48  				Source: tmpNWFileMount,
    49  				Target: "/etc/resolv.conf",
    50  			},
    51  			{
    52  				Type:   "bind",
    53  				Source: tmpNWFileMount,
    54  				Target: "/etc/hostname",
    55  			},
    56  			{
    57  				Type:   "bind",
    58  				Source: tmpNWFileMount,
    59  				Target: "/etc/hosts",
    60  			},
    61  		},
    62  	}
    63  
    64  	cli, err := client.NewClientWithOpts(client.FromEnv)
    65  	assert.NilError(t, err)
    66  	defer cli.Close()
    67  
    68  	ctrCreate, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{}, nil, "")
    69  	assert.NilError(t, err)
    70  	// container will exit immediately because of no tty, but we only need the start sequence to test the condition
    71  	err = cli.ContainerStart(ctx, ctrCreate.ID, types.ContainerStartOptions{})
    72  	assert.NilError(t, err)
    73  
    74  	// Check that host-located bind mount network file did not change ownership when the container was started
    75  	// Note: If the user specifies a mountpath from the host, we should not be
    76  	// attempting to chown files outside the daemon's metadata directory
    77  	// (represented by `daemon.repository` at init time).
    78  	// This forces users who want to use user namespaces to handle the
    79  	// ownership needs of any external files mounted as network files
    80  	// (/etc/resolv.conf, /etc/hosts, /etc/hostname) separately from the
    81  	// daemon. In all other volume/bind mount situations we have taken this
    82  	// same line--we don't chown host file content.
    83  	// See GitHub PR 34224 for details.
    84  	info, err := os.Stat(tmpNWFileMount)
    85  	assert.NilError(t, err)
    86  	fi := info.Sys().(*syscall.Stat_t)
    87  	assert.Check(t, is.Equal(fi.Uid, uint32(0)), "bind mounted network file should not change ownership from root")
    88  }
    89  
    90  func TestMountDaemonRoot(t *testing.T) {
    91  	skip.If(t, testEnv.IsRemoteDaemon)
    92  
    93  	defer setupTest(t)()
    94  	client := testEnv.APIClient()
    95  	ctx := context.Background()
    96  	info, err := client.Info(ctx)
    97  	if err != nil {
    98  		t.Fatal(err)
    99  	}
   100  
   101  	for _, test := range []struct {
   102  		desc        string
   103  		propagation mounttypes.Propagation
   104  		expected    mounttypes.Propagation
   105  	}{
   106  		{
   107  			desc:        "default",
   108  			propagation: "",
   109  			expected:    mounttypes.PropagationRSlave,
   110  		},
   111  		{
   112  			desc:        "private",
   113  			propagation: mounttypes.PropagationPrivate,
   114  		},
   115  		{
   116  			desc:        "rprivate",
   117  			propagation: mounttypes.PropagationRPrivate,
   118  		},
   119  		{
   120  			desc:        "slave",
   121  			propagation: mounttypes.PropagationSlave,
   122  		},
   123  		{
   124  			desc:        "rslave",
   125  			propagation: mounttypes.PropagationRSlave,
   126  			expected:    mounttypes.PropagationRSlave,
   127  		},
   128  		{
   129  			desc:        "shared",
   130  			propagation: mounttypes.PropagationShared,
   131  		},
   132  		{
   133  			desc:        "rshared",
   134  			propagation: mounttypes.PropagationRShared,
   135  			expected:    mounttypes.PropagationRShared,
   136  		},
   137  	} {
   138  		t.Run(test.desc, func(t *testing.T) {
   139  			test := test
   140  			t.Parallel()
   141  
   142  			propagationSpec := fmt.Sprintf(":%s", test.propagation)
   143  			if test.propagation == "" {
   144  				propagationSpec = ""
   145  			}
   146  			bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec
   147  			bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec
   148  
   149  			for name, hc := range map[string]*containertypes.HostConfig{
   150  				"bind root":    {Binds: []string{bindSpecRoot}},
   151  				"bind subpath": {Binds: []string{bindSpecSub}},
   152  				"mount root": {
   153  					Mounts: []mounttypes.Mount{
   154  						{
   155  							Type:        mounttypes.TypeBind,
   156  							Source:      info.DockerRootDir,
   157  							Target:      "/foo",
   158  							BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
   159  						},
   160  					},
   161  				},
   162  				"mount subpath": {
   163  					Mounts: []mounttypes.Mount{
   164  						{
   165  							Type:        mounttypes.TypeBind,
   166  							Source:      filepath.Join(info.DockerRootDir, "containers"),
   167  							Target:      "/foo",
   168  							BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
   169  						},
   170  					},
   171  				},
   172  			} {
   173  				t.Run(name, func(t *testing.T) {
   174  					hc := hc
   175  					t.Parallel()
   176  
   177  					c, err := client.ContainerCreate(ctx, &containertypes.Config{
   178  						Image: "busybox",
   179  						Cmd:   []string{"true"},
   180  					}, hc, nil, nil, "")
   181  
   182  					if err != nil {
   183  						if test.expected != "" {
   184  							t.Fatal(err)
   185  						}
   186  						// expected an error, so this is ok and should not continue
   187  						return
   188  					}
   189  					if test.expected == "" {
   190  						t.Fatal("expected create to fail")
   191  					}
   192  
   193  					defer func() {
   194  						if err := client.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{Force: true}); err != nil {
   195  							panic(err)
   196  						}
   197  					}()
   198  
   199  					inspect, err := client.ContainerInspect(ctx, c.ID)
   200  					if err != nil {
   201  						t.Fatal(err)
   202  					}
   203  					if len(inspect.Mounts) != 1 {
   204  						t.Fatalf("unexpected number of mounts: %+v", inspect.Mounts)
   205  					}
   206  
   207  					m := inspect.Mounts[0]
   208  					if m.Propagation != test.expected {
   209  						t.Fatalf("got unexpected propagation mode, expected %q, got: %v", test.expected, m.Propagation)
   210  					}
   211  				})
   212  			}
   213  		})
   214  	}
   215  }
   216  
   217  func TestContainerBindMountNonRecursive(t *testing.T) {
   218  	skip.If(t, testEnv.IsRemoteDaemon)
   219  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "BindOptions.NonRecursive requires API v1.40")
   220  	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)")
   221  
   222  	defer setupTest(t)()
   223  
   224  	tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0755),
   225  		fs.WithDir("mnt", fs.WithMode(0755)))
   226  	defer tmpDir1.Remove()
   227  	tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt")
   228  	tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0755),
   229  		fs.WithFile("file", "should not be visible when NonRecursive", fs.WithMode(0644)))
   230  	defer tmpDir2.Remove()
   231  
   232  	err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind,ro")
   233  	if err != nil {
   234  		t.Fatal(err)
   235  	}
   236  	defer func() {
   237  		if err := mount.Unmount(tmpDir1Mnt); err != nil {
   238  			t.Fatal(err)
   239  		}
   240  	}()
   241  
   242  	// implicit is recursive (NonRecursive: false)
   243  	implicit := mounttypes.Mount{
   244  		Type:     "bind",
   245  		Source:   tmpDir1.Path(),
   246  		Target:   "/foo",
   247  		ReadOnly: true,
   248  	}
   249  	recursive := implicit
   250  	recursive.BindOptions = &mounttypes.BindOptions{
   251  		NonRecursive: false,
   252  	}
   253  	recursiveVerifier := []string{"test", "-f", "/foo/mnt/file"}
   254  	nonRecursive := implicit
   255  	nonRecursive.BindOptions = &mounttypes.BindOptions{
   256  		NonRecursive: true,
   257  	}
   258  	nonRecursiveVerifier := []string{"test", "!", "-f", "/foo/mnt/file"}
   259  
   260  	ctx := context.Background()
   261  	client := testEnv.APIClient()
   262  	containers := []string{
   263  		container.Run(ctx, t, client, container.WithMount(implicit), container.WithCmd(recursiveVerifier...)),
   264  		container.Run(ctx, t, client, container.WithMount(recursive), container.WithCmd(recursiveVerifier...)),
   265  		container.Run(ctx, t, client, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)),
   266  	}
   267  
   268  	for _, c := range containers {
   269  		poll.WaitOn(t, container.IsSuccessful(ctx, client, c), poll.WithDelay(100*time.Millisecond))
   270  	}
   271  }
   272  
   273  func TestContainerVolumesMountedAsShared(t *testing.T) {
   274  	// Volume propagation is linux only. Also it creates directories for
   275  	// bind mounting, so needs to be same host.
   276  	skip.If(t, testEnv.IsRemoteDaemon)
   277  	skip.If(t, testEnv.IsUserNamespace)
   278  	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)")
   279  
   280  	defer setupTest(t)()
   281  
   282  	// Prepare a source directory to bind mount
   283  	tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0755),
   284  		fs.WithDir("mnt1", fs.WithMode(0755)))
   285  	defer tmpDir1.Remove()
   286  	tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1")
   287  
   288  	// Convert this directory into a shared mount point so that we do
   289  	// not rely on propagation properties of parent mount.
   290  	if err := mount.MakePrivate(tmpDir1.Path()); err != nil {
   291  		t.Fatal(err)
   292  	}
   293  	defer func() {
   294  		if err := mount.Unmount(tmpDir1.Path()); err != nil {
   295  			t.Fatal(err)
   296  		}
   297  	}()
   298  	if err := mount.MakeShared(tmpDir1.Path()); err != nil {
   299  		t.Fatal(err)
   300  	}
   301  
   302  	sharedMount := mounttypes.Mount{
   303  		Type:   mounttypes.TypeBind,
   304  		Source: tmpDir1.Path(),
   305  		Target: "/volume-dest",
   306  		BindOptions: &mounttypes.BindOptions{
   307  			Propagation: mounttypes.PropagationShared,
   308  		},
   309  	}
   310  
   311  	bindMountCmd := []string{"mount", "--bind", "/volume-dest/mnt1", "/volume-dest/mnt1"}
   312  
   313  	ctx := context.Background()
   314  	client := testEnv.APIClient()
   315  	containerID := container.Run(ctx, t, client, container.WithPrivileged(true), container.WithMount(sharedMount), container.WithCmd(bindMountCmd...))
   316  	poll.WaitOn(t, container.IsSuccessful(ctx, client, containerID), poll.WithDelay(100*time.Millisecond))
   317  
   318  	// Make sure a bind mount under a shared volume propagated to host.
   319  	if mounted, _ := mountinfo.Mounted(tmpDir1Mnt); !mounted {
   320  		t.Fatalf("Bind mount under shared volume did not propagate to host")
   321  	}
   322  
   323  	mount.Unmount(tmpDir1Mnt)
   324  }
   325  
   326  func TestContainerVolumesMountedAsSlave(t *testing.T) {
   327  	// Volume propagation is linux only. Also it creates directories for
   328  	// bind mounting, so needs to be same host.
   329  	skip.If(t, testEnv.IsRemoteDaemon)
   330  	skip.If(t, testEnv.IsUserNamespace)
   331  	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)")
   332  
   333  	// Prepare a source directory to bind mount
   334  	tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0755),
   335  		fs.WithDir("mnt1", fs.WithMode(0755)))
   336  	defer tmpDir1.Remove()
   337  	tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1")
   338  
   339  	// Prepare a source directory with file in it. We will bind mount this
   340  	// directory and see if file shows up.
   341  	tmpDir2 := fs.NewDir(t, "volume-source2", fs.WithMode(0755),
   342  		fs.WithFile("slave-testfile", "Test", fs.WithMode(0644)))
   343  	defer tmpDir2.Remove()
   344  
   345  	// Convert this directory into a shared mount point so that we do
   346  	// not rely on propagation properties of parent mount.
   347  	if err := mount.MakePrivate(tmpDir1.Path()); err != nil {
   348  		t.Fatal(err)
   349  	}
   350  	defer func() {
   351  		if err := mount.Unmount(tmpDir1.Path()); err != nil {
   352  			t.Fatal(err)
   353  		}
   354  	}()
   355  	if err := mount.MakeShared(tmpDir1.Path()); err != nil {
   356  		t.Fatal(err)
   357  	}
   358  
   359  	slaveMount := mounttypes.Mount{
   360  		Type:   mounttypes.TypeBind,
   361  		Source: tmpDir1.Path(),
   362  		Target: "/volume-dest",
   363  		BindOptions: &mounttypes.BindOptions{
   364  			Propagation: mounttypes.PropagationSlave,
   365  		},
   366  	}
   367  
   368  	topCmd := []string{"top"}
   369  
   370  	ctx := context.Background()
   371  	client := testEnv.APIClient()
   372  	containerID := container.Run(ctx, t, client, container.WithTty(true), container.WithMount(slaveMount), container.WithCmd(topCmd...))
   373  
   374  	// Bind mount tmpDir2/ onto tmpDir1/mnt1. If mount propagates inside
   375  	// container then contents of tmpDir2/slave-testfile should become
   376  	// visible at "/volume-dest/mnt1/slave-testfile"
   377  	if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil {
   378  		t.Fatal(err)
   379  	}
   380  	defer func() {
   381  		if err := mount.Unmount(tmpDir1Mnt); err != nil {
   382  			t.Fatal(err)
   383  		}
   384  	}()
   385  
   386  	mountCmd := []string{"cat", "/volume-dest/mnt1/slave-testfile"}
   387  
   388  	if result, err := container.Exec(ctx, client, containerID, mountCmd); err == nil {
   389  		if result.Stdout() != "Test" {
   390  			t.Fatalf("Bind mount under slave volume did not propagate to container")
   391  		}
   392  	} else {
   393  		t.Fatal(err)
   394  	}
   395  }
   396  
   397  // Regression test for #38995 and #43390.
   398  func TestContainerCopyLeaksMounts(t *testing.T) {
   399  	defer setupTest(t)()
   400  
   401  	bindMount := mounttypes.Mount{
   402  		Type:   mounttypes.TypeBind,
   403  		Source: "/var",
   404  		Target: "/hostvar",
   405  		BindOptions: &mounttypes.BindOptions{
   406  			Propagation: mounttypes.PropagationRSlave,
   407  		},
   408  	}
   409  
   410  	ctx := context.Background()
   411  	client := testEnv.APIClient()
   412  	cid := container.Run(ctx, t, client, container.WithMount(bindMount), container.WithCmd("sleep", "120s"))
   413  
   414  	getMounts := func() string {
   415  		t.Helper()
   416  		res, err := container.Exec(ctx, client, cid, []string{"cat", "/proc/self/mountinfo"})
   417  		assert.NilError(t, err)
   418  		assert.Equal(t, res.ExitCode, 0)
   419  		return res.Stdout()
   420  	}
   421  
   422  	mountsBefore := getMounts()
   423  
   424  	_, _, err := client.CopyFromContainer(ctx, cid, "/etc/passwd")
   425  	assert.NilError(t, err)
   426  
   427  	mountsAfter := getMounts()
   428  
   429  	assert.Equal(t, mountsBefore, mountsAfter)
   430  }