Browse code

Do not remove containers from memory on error

Before this, if `forceRemove` is set the container data will be removed
no matter what, including if there are issues with removing container
on-disk state (rw layer, container root).

In practice this causes a lot of issues with leaked data sitting on
disk that users are not able to clean up themselves.
This is particularly a problem while the `EBUSY` errors on remove are so
prevalent. So for now let's not keep this behavior.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2017/02/15 03:35:20
Showing 12 changed files
... ...
@@ -12,6 +12,7 @@ import (
12 12
 	"github.com/docker/docker/api/types"
13 13
 	"github.com/docker/docker/container"
14 14
 	"github.com/docker/docker/layer"
15
+	"github.com/docker/docker/pkg/system"
15 16
 	volumestore "github.com/docker/docker/volume/store"
16 17
 	"github.com/pkg/errors"
17 18
 )
... ...
@@ -111,37 +112,30 @@ func (daemon *Daemon) cleanupContainer(container *container.Container, forceRemo
111 111
 		logrus.Errorf("Error saving dying container to disk: %v", err)
112 112
 	}
113 113
 
114
-	// If force removal is required, delete container from various
115
-	// indexes even if removal failed.
116
-	defer func() {
117
-		if err == nil || forceRemove {
118
-			daemon.nameIndex.Delete(container.ID)
119
-			daemon.linkIndex.delete(container)
120
-			selinuxFreeLxcContexts(container.ProcessLabel)
121
-			daemon.idIndex.Delete(container.ID)
122
-			daemon.containers.Delete(container.ID)
123
-			if e := daemon.removeMountPoints(container, removeVolume); e != nil {
124
-				logrus.Error(e)
125
-			}
126
-			daemon.LogContainerEvent(container, "destroy")
127
-			stateCtr.del(container.ID)
128
-		}
129
-	}()
130
-
131
-	if err = os.RemoveAll(container.Root); err != nil {
132
-		return fmt.Errorf("Unable to remove filesystem for %v: %v", container.ID, err)
133
-	}
134
-
135 114
 	// When container creation fails and `RWLayer` has not been created yet, we
136 115
 	// do not call `ReleaseRWLayer`
137 116
 	if container.RWLayer != nil {
138 117
 		metadata, err := daemon.layerStore.ReleaseRWLayer(container.RWLayer)
139 118
 		layer.LogReleaseMetadata(metadata)
140 119
 		if err != nil && err != layer.ErrMountDoesNotExist {
141
-			return fmt.Errorf("Driver %s failed to remove root filesystem %s: %s", daemon.GraphDriverName(), container.ID, err)
120
+			return errors.Wrapf(err, "driver %q failed to remove root filesystem for %s", daemon.GraphDriverName(), container.ID)
142 121
 		}
143 122
 	}
144 123
 
124
+	if err := system.EnsureRemoveAll(container.Root); err != nil {
125
+		return errors.Wrapf(err, "unable to remove filesystem for %s", container.ID)
126
+	}
127
+
128
+	daemon.nameIndex.Delete(container.ID)
129
+	daemon.linkIndex.delete(container)
130
+	selinuxFreeLxcContexts(container.ProcessLabel)
131
+	daemon.idIndex.Delete(container.ID)
132
+	daemon.containers.Delete(container.ID)
133
+	if e := daemon.removeMountPoints(container, removeVolume); e != nil {
134
+		logrus.Error(e)
135
+	}
136
+	stateCtr.del(container.ID)
137
+	daemon.LogContainerEvent(container, "destroy")
145 138
 	return nil
146 139
 }
147 140
 
... ...
@@ -46,6 +46,7 @@ import (
46 46
 	"github.com/docker/docker/pkg/idtools"
47 47
 	"github.com/docker/docker/pkg/locker"
48 48
 	mountpk "github.com/docker/docker/pkg/mount"
49
+	"github.com/docker/docker/pkg/system"
49 50
 
50 51
 	rsystem "github.com/opencontainers/runc/libcontainer/system"
51 52
 	"github.com/opencontainers/selinux/go-selinux/label"
... ...
@@ -319,13 +320,13 @@ func (a *Driver) Remove(id string) error {
319 319
 		}
320 320
 		return err
321 321
 	}
322
-	defer os.RemoveAll(tmpMntPath)
322
+	defer system.EnsureRemoveAll(tmpMntPath)
323 323
 
324 324
 	tmpDiffpath := path.Join(a.diffPath(), fmt.Sprintf("%s-removing", id))
325 325
 	if err := os.Rename(a.getDiffPath(id), tmpDiffpath); err != nil && !os.IsNotExist(err) {
326 326
 		return err
327 327
 	}
328
-	defer os.RemoveAll(tmpDiffpath)
328
+	defer system.EnsureRemoveAll(tmpDiffpath)
329 329
 
330 330
 	// Remove the layers file for the id
331 331
 	if err := os.Remove(path.Join(a.rootPath(), "layers", id)); err != nil && !os.IsNotExist(err) {
... ...
@@ -27,6 +27,7 @@ import (
27 27
 	"github.com/docker/docker/pkg/idtools"
28 28
 	"github.com/docker/docker/pkg/mount"
29 29
 	"github.com/docker/docker/pkg/parsers"
30
+	"github.com/docker/docker/pkg/system"
30 31
 	"github.com/docker/go-units"
31 32
 	"github.com/opencontainers/selinux/go-selinux/label"
32 33
 )
... ...
@@ -535,7 +536,7 @@ func (d *Driver) Remove(id string) error {
535 535
 	if err := subvolDelete(d.subvolumesDir(), id); err != nil {
536 536
 		return err
537 537
 	}
538
-	if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) {
538
+	if err := system.EnsureRemoveAll(dir); err != nil {
539 539
 		return err
540 540
 	}
541 541
 	if err := d.subvolRescanQuota(); err != nil {
... ...
@@ -16,6 +16,7 @@ import (
16 16
 	"github.com/docker/docker/pkg/idtools"
17 17
 	"github.com/docker/docker/pkg/locker"
18 18
 	"github.com/docker/docker/pkg/mount"
19
+	"github.com/docker/docker/pkg/system"
19 20
 	units "github.com/docker/go-units"
20 21
 )
21 22
 
... ...
@@ -160,7 +161,7 @@ func (d *Driver) Remove(id string) error {
160 160
 	}
161 161
 
162 162
 	mp := path.Join(d.home, "mnt", id)
163
-	if err := os.RemoveAll(mp); err != nil && !os.IsNotExist(err) {
163
+	if err := system.EnsureRemoveAll(mp); err != nil {
164 164
 		return err
165 165
 	}
166 166
 
... ...
@@ -21,6 +21,7 @@ import (
21 21
 	"github.com/docker/docker/pkg/idtools"
22 22
 	"github.com/docker/docker/pkg/locker"
23 23
 	"github.com/docker/docker/pkg/mount"
24
+	"github.com/docker/docker/pkg/system"
24 25
 	"github.com/opencontainers/selinux/go-selinux/label"
25 26
 )
26 27
 
... ...
@@ -339,10 +340,7 @@ func (d *Driver) dir(id string) string {
339 339
 func (d *Driver) Remove(id string) error {
340 340
 	d.locker.Lock(id)
341 341
 	defer d.locker.Unlock(id)
342
-	if err := os.RemoveAll(d.dir(id)); err != nil && !os.IsNotExist(err) {
343
-		return err
344
-	}
345
-	return nil
342
+	return system.EnsureRemoveAll(d.dir(id))
346 343
 }
347 344
 
348 345
 // Get creates and mounts the required file system for the given id and returns the mount path.
... ...
@@ -31,6 +31,7 @@ import (
31 31
 	"github.com/docker/docker/pkg/mount"
32 32
 	"github.com/docker/docker/pkg/parsers"
33 33
 	"github.com/docker/docker/pkg/parsers/kernel"
34
+	"github.com/docker/docker/pkg/system"
34 35
 	units "github.com/docker/go-units"
35 36
 
36 37
 	"github.com/opencontainers/selinux/go-selinux/label"
... ...
@@ -464,7 +465,7 @@ func (d *Driver) Remove(id string) error {
464 464
 		}
465 465
 	}
466 466
 
467
-	if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) {
467
+	if err := system.EnsureRemoveAll(dir); err != nil && !os.IsNotExist(err) {
468 468
 		return err
469 469
 	}
470 470
 	return nil
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	"github.com/docker/docker/daemon/graphdriver"
9 9
 	"github.com/docker/docker/pkg/chrootarchive"
10 10
 	"github.com/docker/docker/pkg/idtools"
11
+	"github.com/docker/docker/pkg/system"
11 12
 
12 13
 	"github.com/opencontainers/selinux/go-selinux/label"
13 14
 )
... ...
@@ -114,7 +115,7 @@ func (d *Driver) dir(id string) string {
114 114
 
115 115
 // Remove deletes the content from the directory for a given id.
116 116
 func (d *Driver) Remove(id string) error {
117
-	if err := os.RemoveAll(d.dir(id)); err != nil && !os.IsNotExist(err) {
117
+	if err := system.EnsureRemoveAll(d.dir(id)); err != nil {
118 118
 		return err
119 119
 	}
120 120
 	return nil
... ...
@@ -41,17 +41,6 @@ func (daemon *Daemon) StateChanged(id string, e libcontainerd.StateInfo) error {
41 41
 		daemon.updateHealthMonitor(c)
42 42
 		daemon.LogContainerEvent(c, "oom")
43 43
 	case libcontainerd.StateExit:
44
-		// if container's AutoRemove flag is set, remove it after clean up
45
-		autoRemove := func() {
46
-			c.Lock()
47
-			ar := c.HostConfig.AutoRemove
48
-			c.Unlock()
49
-			if ar {
50
-				if err := daemon.ContainerRm(c.ID, &types.ContainerRmConfig{ForceRemove: true, RemoveVolume: true}); err != nil {
51
-					logrus.Errorf("can't remove container %s: %v", c.ID, err)
52
-				}
53
-			}
54
-		}
55 44
 
56 45
 		c.Lock()
57 46
 		c.StreamConfig.Wait()
... ...
@@ -63,7 +52,7 @@ func (daemon *Daemon) StateChanged(id string, e libcontainerd.StateInfo) error {
63 63
 			c.SetRestarting(platformConstructExitStatus(e))
64 64
 		} else {
65 65
 			c.SetStopped(platformConstructExitStatus(e))
66
-			defer autoRemove()
66
+			defer daemon.autoRemove(c)
67 67
 		}
68 68
 
69 69
 		// cancel healthcheck here, they will be automatically
... ...
@@ -85,7 +74,7 @@ func (daemon *Daemon) StateChanged(id string, e libcontainerd.StateInfo) error {
85 85
 				}
86 86
 				if err != nil {
87 87
 					c.SetStopped(platformConstructExitStatus(e))
88
-					defer autoRemove()
88
+					defer daemon.autoRemove(c)
89 89
 					if err != restartmanager.ErrRestartCanceled {
90 90
 						logrus.Errorf("restartmanger wait error: %+v", err)
91 91
 					}
... ...
@@ -153,3 +142,24 @@ func (daemon *Daemon) StateChanged(id string, e libcontainerd.StateInfo) error {
153 153
 	}
154 154
 	return nil
155 155
 }
156
+
157
+func (daemon *Daemon) autoRemove(c *container.Container) {
158
+	c.Lock()
159
+	ar := c.HostConfig.AutoRemove
160
+	c.Unlock()
161
+	if !ar {
162
+		return
163
+	}
164
+
165
+	var err error
166
+	if err = daemon.ContainerRm(c.ID, &types.ContainerRmConfig{ForceRemove: true, RemoveVolume: true}); err == nil {
167
+		return
168
+	}
169
+	if c := daemon.containers.Get(c.ID); c == nil {
170
+		return
171
+	}
172
+
173
+	if err != nil {
174
+		logrus.WithError(err).WithField("container", c.ID).Error("error removing container")
175
+	}
176
+}
... ...
@@ -1,5 +1,10 @@
1 1
 package mount
2 2
 
3
+import (
4
+	"sort"
5
+	"strings"
6
+)
7
+
3 8
 // GetMounts retrieves a list of mounts for the current running process.
4 9
 func GetMounts() ([]*Info, error) {
5 10
 	return parseMountTable()
... ...
@@ -53,3 +58,29 @@ func Unmount(target string) error {
53 53
 	}
54 54
 	return unmount(target, mntDetach)
55 55
 }
56
+
57
+// RecursiveUnmount unmounts the target and all mounts underneath, starting with
58
+// the deepsest mount first.
59
+func RecursiveUnmount(target string) error {
60
+	mounts, err := GetMounts()
61
+	if err != nil {
62
+		return err
63
+	}
64
+
65
+	// Make the deepest mount be first
66
+	sort.Sort(sort.Reverse(byMountpoint(mounts)))
67
+
68
+	for i, m := range mounts {
69
+		if !strings.HasPrefix(m.Mountpoint, target) {
70
+			continue
71
+		}
72
+		if err := Unmount(m.Mountpoint); err != nil && i == len(mounts)-1 {
73
+			if mounted, err := Mounted(m.Mountpoint); err != nil || mounted {
74
+				return err
75
+			}
76
+			// Ignore errors for submounts and continue trying to unmount others
77
+			// The final unmount should fail if there ane any submounts remaining
78
+		}
79
+	}
80
+	return nil
81
+}
... ...
@@ -38,3 +38,17 @@ type Info struct {
38 38
 	// VfsOpts represents per super block options.
39 39
 	VfsOpts string
40 40
 }
41
+
42
+type byMountpoint []*Info
43
+
44
+func (by byMountpoint) Len() int {
45
+	return len(by)
46
+}
47
+
48
+func (by byMountpoint) Less(i, j int) bool {
49
+	return by[i].Mountpoint < by[j].Mountpoint
50
+}
51
+
52
+func (by byMountpoint) Swap(i, j int) {
53
+	by[i], by[j] = by[j], by[i]
54
+}
41 55
new file mode 100644
... ...
@@ -0,0 +1,80 @@
0
+package system
1
+
2
+import (
3
+	"os"
4
+	"syscall"
5
+	"time"
6
+
7
+	"github.com/docker/docker/pkg/mount"
8
+	"github.com/pkg/errors"
9
+)
10
+
11
+// EnsureRemoveAll wraps `os.RemoveAll` to check for specific errors that can
12
+// often be remedied.
13
+// Only use `EnsureRemoveAll` if you really want to make every effort to remove
14
+// a directory.
15
+//
16
+// Because of the way `os.Remove` (and by extension `os.RemoveAll`) works, there
17
+// can be a race between reading directory entries and then actually attempting
18
+// to remove everything in the directory.
19
+// These types of errors do not need to be returned since it's ok for the dir to
20
+// be gone we can just retry the remove operation.
21
+//
22
+// This should not return a `os.ErrNotExist` kind of error under any cirucmstances
23
+func EnsureRemoveAll(dir string) error {
24
+	notExistErr := make(map[string]bool)
25
+
26
+	// track retries
27
+	exitOnErr := make(map[string]int)
28
+	maxRetry := 5
29
+
30
+	// Attempt to unmount anything beneath this dir first
31
+	mount.RecursiveUnmount(dir)
32
+
33
+	for {
34
+		err := os.RemoveAll(dir)
35
+		if err == nil {
36
+			return err
37
+		}
38
+
39
+		pe, ok := err.(*os.PathError)
40
+		if !ok {
41
+			return err
42
+		}
43
+
44
+		if os.IsNotExist(err) {
45
+			if notExistErr[pe.Path] {
46
+				return err
47
+			}
48
+			notExistErr[pe.Path] = true
49
+
50
+			// There is a race where some subdir can be removed but after the parent
51
+			//   dir entries have been read.
52
+			// So the path could be from `os.Remove(subdir)`
53
+			// If the reported non-existent path is not the passed in `dir` we
54
+			// should just retry, but otherwise return with no error.
55
+			if pe.Path == dir {
56
+				return nil
57
+			}
58
+			continue
59
+		}
60
+
61
+		if pe.Err != syscall.EBUSY {
62
+			return err
63
+		}
64
+
65
+		if mounted, _ := mount.Mounted(pe.Path); mounted {
66
+			if e := mount.Unmount(pe.Path); e != nil {
67
+				if mounted, _ := mount.Mounted(pe.Path); mounted {
68
+					return errors.Wrapf(e, "error while removing %s", dir)
69
+				}
70
+			}
71
+		}
72
+
73
+		if exitOnErr[pe.Path] == maxRetry {
74
+			return err
75
+		}
76
+		exitOnErr[pe.Path]++
77
+		time.Sleep(100 * time.Millisecond)
78
+	}
79
+}
0 80
new file mode 100644
... ...
@@ -0,0 +1,84 @@
0
+package system
1
+
2
+import (
3
+	"io/ioutil"
4
+	"os"
5
+	"path/filepath"
6
+	"runtime"
7
+	"testing"
8
+	"time"
9
+
10
+	"github.com/docker/docker/pkg/mount"
11
+)
12
+
13
+func TestEnsureRemoveAllNotExist(t *testing.T) {
14
+	// should never return an error for a non-existent path
15
+	if err := EnsureRemoveAll("/non/existent/path"); err != nil {
16
+		t.Fatal(err)
17
+	}
18
+}
19
+
20
+func TestEnsureRemoveAllWithDir(t *testing.T) {
21
+	dir, err := ioutil.TempDir("", "test-ensure-removeall-with-dir")
22
+	if err != nil {
23
+		t.Fatal(err)
24
+	}
25
+	if err := EnsureRemoveAll(dir); err != nil {
26
+		t.Fatal(err)
27
+	}
28
+}
29
+
30
+func TestEnsureRemoveAllWithFile(t *testing.T) {
31
+	tmp, err := ioutil.TempFile("", "test-ensure-removeall-with-dir")
32
+	if err != nil {
33
+		t.Fatal(err)
34
+	}
35
+	tmp.Close()
36
+	if err := EnsureRemoveAll(tmp.Name()); err != nil {
37
+		t.Fatal(err)
38
+	}
39
+}
40
+
41
+func TestEnsureRemoveAllWithMount(t *testing.T) {
42
+	if runtime.GOOS == "windows" {
43
+		t.Skip("mount not supported on Windows")
44
+	}
45
+
46
+	dir1, err := ioutil.TempDir("", "test-ensure-removeall-with-dir1")
47
+	if err != nil {
48
+		t.Fatal(err)
49
+	}
50
+	dir2, err := ioutil.TempDir("", "test-ensure-removeall-with-dir2")
51
+	if err != nil {
52
+		t.Fatal(err)
53
+	}
54
+	defer os.RemoveAll(dir2)
55
+
56
+	bindDir := filepath.Join(dir1, "bind")
57
+	if err := os.MkdirAll(bindDir, 0755); err != nil {
58
+		t.Fatal(err)
59
+	}
60
+
61
+	if err := mount.Mount(dir2, bindDir, "none", "bind"); err != nil {
62
+		t.Fatal(err)
63
+	}
64
+
65
+	done := make(chan struct{})
66
+	go func() {
67
+		err = EnsureRemoveAll(dir1)
68
+		close(done)
69
+	}()
70
+
71
+	select {
72
+	case <-done:
73
+		if err != nil {
74
+			t.Fatal(err)
75
+		}
76
+	case <-time.After(5 * time.Second):
77
+		t.Fatal("timeout waiting for EnsureRemoveAll to finish")
78
+	}
79
+
80
+	if _, err := os.Stat(dir1); !os.IsNotExist(err) {
81
+		t.Fatalf("expected %q to not exist", dir1)
82
+	}
83
+}