Browse code

Fix duplicate mount point for `--volumes-from` in `docker run`

This fix tries to fix the issue raised in 21845. The issue with 21845
is that if multiple `--volumes-from` with the same destination has been
specified, then one volume will be overridden by the other. This will mess
up with volumes reference and prevent the overridden volume from
being removed at the end.

Issue 21845 was observed with `docker-compose` though it is possible to
emulate the same behavior with `docker` alone:
```
$ cat Dockerfile
FROM busybox
VOLUME ["/tmp/data"]
$ docker build -t vimage .
$ docker run --name=data1 vimage true
$ docker run --name=data2 vimage true
$ docker run --name=app --volumes-from=data1 --volumes-from=data2 -d busybox top
$ docker rm -f -v $(docker ps -aq)
$ docker volume ls
$ docker volume rm ...
```
NOTE: Second case:
```
$ cat Dockerfile
FROM busybox
VOLUME ["/tmp/data"]
$ docker build -t vimage .
$ docker run --name=data1 vimage true
$ docker run --name=data2 vimage true
$ docker run --name=app --volumes-from=data1 --volumes-from=data2 -v /tmp/data:/tmp/data -d busybox top
$ docker rm -f -v $(docker ps -aq)
$ docker volume ls
$ docker volume rm ...
```
NOTE: Third case: Combination of --volumes-from and `HostConfig.Mounts` (API only)

This fix tries to address the issue by return an error if duplicate
mount points was used with `--volumes-from`.

An integration test has been added.

This fix fixes 21845.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2017/01/28 07:12:45
Showing 2 changed files
... ...
@@ -85,6 +85,15 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
85 85
 		}
86 86
 	}()
87 87
 
88
+	dereferenceIfExists := func(destination string) {
89
+		if v, ok := mountPoints[destination]; ok {
90
+			logrus.Debugf("Duplicate mount point '%s'", destination)
91
+			if v.Volume != nil {
92
+				daemon.volumes.Dereference(v.Volume, container.ID)
93
+			}
94
+		}
95
+	}
96
+
88 97
 	// 1. Read already configured mount points.
89 98
 	for destination, point := range container.MountPoints {
90 99
 		mountPoints[destination] = point
... ...
@@ -121,7 +130,7 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
121 121
 				}
122 122
 				cp.Volume = v
123 123
 			}
124
-
124
+			dereferenceIfExists(cp.Destination)
125 125
 			mountPoints[cp.Destination] = cp
126 126
 		}
127 127
 	}
... ...
@@ -155,6 +164,7 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
155 155
 		}
156 156
 
157 157
 		binds[bind.Destination] = true
158
+		dereferenceIfExists(bind.Destination)
158 159
 		mountPoints[bind.Destination] = bind
159 160
 	}
160 161
 
... ...
@@ -199,6 +209,7 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
199 199
 		}
200 200
 
201 201
 		binds[mp.Destination] = true
202
+		dereferenceIfExists(mp.Destination)
202 203
 		mountPoints[mp.Destination] = mp
203 204
 	}
204 205
 
... ...
@@ -3,12 +3,14 @@ package main
3 3
 import (
4 4
 	"fmt"
5 5
 	"io/ioutil"
6
+	"net/http"
6 7
 	"os"
7 8
 	"os/exec"
8 9
 	"path/filepath"
9 10
 	"strings"
10 11
 
11 12
 	"github.com/docker/docker/integration-cli/checker"
13
+	"github.com/docker/docker/integration-cli/request"
12 14
 	icmd "github.com/docker/docker/pkg/testutil/cmd"
13 15
 	"github.com/go-check/check"
14 16
 )
... ...
@@ -449,3 +451,149 @@ func (s *DockerSuite) TestVolumeCliInspectWithVolumeOpts(c *check.C) {
449 449
 	c.Assert(strings.TrimSpace(out), checker.Contains, fmt.Sprintf("%s:%s", k2, v2))
450 450
 	c.Assert(strings.TrimSpace(out), checker.Contains, fmt.Sprintf("%s:%s", k3, v3))
451 451
 }
452
+
453
+// Test case (1) for 21845: duplicate targets for --volumes-from
454
+func (s *DockerSuite) TestDuplicateMountpointsForVolumesFrom(c *check.C) {
455
+	testRequires(c, DaemonIsLinux)
456
+
457
+	image := "vimage"
458
+	buildImageSuccessfully(c, image, withDockerfile(`
459
+		FROM busybox
460
+		VOLUME ["/tmp/data"]`))
461
+
462
+	dockerCmd(c, "run", "--name=data1", image, "true")
463
+	dockerCmd(c, "run", "--name=data2", image, "true")
464
+
465
+	out, _ := dockerCmd(c, "inspect", "--format", "{{(index .Mounts 0).Name}}", "data1")
466
+	data1 := strings.TrimSpace(out)
467
+	c.Assert(data1, checker.Not(checker.Equals), "")
468
+
469
+	out, _ = dockerCmd(c, "inspect", "--format", "{{(index .Mounts 0).Name}}", "data2")
470
+	data2 := strings.TrimSpace(out)
471
+	c.Assert(data2, checker.Not(checker.Equals), "")
472
+
473
+	// Both volume should exist
474
+	out, _ = dockerCmd(c, "volume", "ls", "-q")
475
+	c.Assert(strings.TrimSpace(out), checker.Contains, data1)
476
+	c.Assert(strings.TrimSpace(out), checker.Contains, data2)
477
+
478
+	out, _, err := dockerCmdWithError("run", "--name=app", "--volumes-from=data1", "--volumes-from=data2", "-d", "busybox", "top")
479
+	c.Assert(err, checker.IsNil, check.Commentf("Out: %s", out))
480
+
481
+	// Only the second volume will be referenced, this is backward compatible
482
+	out, _ = dockerCmd(c, "inspect", "--format", "{{(index .Mounts 0).Name}}", "app")
483
+	c.Assert(strings.TrimSpace(out), checker.Equals, data2)
484
+
485
+	dockerCmd(c, "rm", "-f", "-v", "app")
486
+	dockerCmd(c, "rm", "-f", "-v", "data1")
487
+	dockerCmd(c, "rm", "-f", "-v", "data2")
488
+
489
+	// Both volume should not exist
490
+	out, _ = dockerCmd(c, "volume", "ls", "-q")
491
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data1)
492
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data2)
493
+}
494
+
495
+// Test case (2) for 21845: duplicate targets for --volumes-from and -v (bind)
496
+func (s *DockerSuite) TestDuplicateMountpointsForVolumesFromAndBind(c *check.C) {
497
+	testRequires(c, DaemonIsLinux)
498
+
499
+	image := "vimage"
500
+	buildImageSuccessfully(c, image, withDockerfile(`
501
+                FROM busybox
502
+                VOLUME ["/tmp/data"]`))
503
+
504
+	dockerCmd(c, "run", "--name=data1", image, "true")
505
+	dockerCmd(c, "run", "--name=data2", image, "true")
506
+
507
+	out, _ := dockerCmd(c, "inspect", "--format", "{{(index .Mounts 0).Name}}", "data1")
508
+	data1 := strings.TrimSpace(out)
509
+	c.Assert(data1, checker.Not(checker.Equals), "")
510
+
511
+	out, _ = dockerCmd(c, "inspect", "--format", "{{(index .Mounts 0).Name}}", "data2")
512
+	data2 := strings.TrimSpace(out)
513
+	c.Assert(data2, checker.Not(checker.Equals), "")
514
+
515
+	// Both volume should exist
516
+	out, _ = dockerCmd(c, "volume", "ls", "-q")
517
+	c.Assert(strings.TrimSpace(out), checker.Contains, data1)
518
+	c.Assert(strings.TrimSpace(out), checker.Contains, data2)
519
+
520
+	out, _, err := dockerCmdWithError("run", "--name=app", "--volumes-from=data1", "--volumes-from=data2", "-v", "/tmp/data:/tmp/data", "-d", "busybox", "top")
521
+	c.Assert(err, checker.IsNil, check.Commentf("Out: %s", out))
522
+
523
+	// No volume will be referenced (mount is /tmp/data), this is backward compatible
524
+	out, _ = dockerCmd(c, "inspect", "--format", "{{(index .Mounts 0).Name}}", "app")
525
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data1)
526
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data2)
527
+
528
+	dockerCmd(c, "rm", "-f", "-v", "app")
529
+	dockerCmd(c, "rm", "-f", "-v", "data1")
530
+	dockerCmd(c, "rm", "-f", "-v", "data2")
531
+
532
+	// Both volume should not exist
533
+	out, _ = dockerCmd(c, "volume", "ls", "-q")
534
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data1)
535
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data2)
536
+}
537
+
538
+// Test case (3) for 21845: duplicate targets for --volumes-from and `Mounts` (API only)
539
+func (s *DockerSuite) TestDuplicateMountpointsForVolumesFromAndMounts(c *check.C) {
540
+	testRequires(c, DaemonIsLinux)
541
+
542
+	image := "vimage"
543
+	buildImageSuccessfully(c, image, withDockerfile(`
544
+                FROM busybox
545
+                VOLUME ["/tmp/data"]`))
546
+
547
+	dockerCmd(c, "run", "--name=data1", image, "true")
548
+	dockerCmd(c, "run", "--name=data2", image, "true")
549
+
550
+	out, _ := dockerCmd(c, "inspect", "--format", "{{(index .Mounts 0).Name}}", "data1")
551
+	data1 := strings.TrimSpace(out)
552
+	c.Assert(data1, checker.Not(checker.Equals), "")
553
+
554
+	out, _ = dockerCmd(c, "inspect", "--format", "{{(index .Mounts 0).Name}}", "data2")
555
+	data2 := strings.TrimSpace(out)
556
+	c.Assert(data2, checker.Not(checker.Equals), "")
557
+
558
+	// Both volume should exist
559
+	out, _ = dockerCmd(c, "volume", "ls", "-q")
560
+	c.Assert(strings.TrimSpace(out), checker.Contains, data1)
561
+	c.Assert(strings.TrimSpace(out), checker.Contains, data2)
562
+
563
+	// Mounts is available in API
564
+	status, body, err := request.SockRequest("POST", "/containers/create?name=app", map[string]interface{}{
565
+		"Image": "busybox",
566
+		"Cmd":   []string{"top"},
567
+		"HostConfig": map[string]interface{}{
568
+			"VolumesFrom": []string{
569
+				"data1",
570
+				"data2",
571
+			},
572
+			"Mounts": []map[string]interface{}{
573
+				{
574
+					"Type":   "bind",
575
+					"Source": "/tmp/data",
576
+					"Target": "/tmp/data",
577
+				},
578
+			}},
579
+	}, daemonHost())
580
+
581
+	c.Assert(err, checker.IsNil, check.Commentf(string(body)))
582
+	c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(body)))
583
+
584
+	// No volume will be referenced (mount is /tmp/data), this is backward compatible
585
+	out, _ = dockerCmd(c, "inspect", "--format", "{{(index .Mounts 0).Name}}", "app")
586
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data1)
587
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data2)
588
+
589
+	dockerCmd(c, "rm", "-f", "-v", "app")
590
+	dockerCmd(c, "rm", "-f", "-v", "data1")
591
+	dockerCmd(c, "rm", "-f", "-v", "data2")
592
+
593
+	// Both volume should not exist
594
+	out, _ = dockerCmd(c, "volume", "ls", "-q")
595
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data1)
596
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), data2)
597
+}