1package container // import "github.com/docker/docker/integration/container"
2
3import (
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
27func 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
88func 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
215func 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
271func 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
324func 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}
394