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