Browse code

Move --rm to daemon side

`--rm` is a client side flag which caused lots of problems:
1. if client lost connection to daemon, including client crash or be
killed, there's no way to clean garbage container.
2. if docker stop a `--rm` container, this container won't be
autoremoved.
3. if docker daemon restart, container is also left over.
4. bug: `docker run --rm busybox fakecmd` will exit without cleanup.

In a word, client side `--rm` flag isn't sufficient for garbage
collection. Move the `--rm` flag to daemon will be more reasonable.

What this commit do is:
1. implement a `--rm` on daemon side, adding one flag `AutoRemove` into
HostConfig.
2. Allow `run --rm -d`, no conflicting `--rm` and `-d` any more,
auto-remove can work on detach mode.
3. `docker restart` a `--rm` container will succeed, the container won't
be autoremoved.

This commit will help a lot for daemon to do garbage collection for
temporary containers.

Signed-off-by: Zhang Wei <zhangwei555@huawei.com>

Zhang Wei authored on 2016/03/02 01:30:27
Showing 9 changed files
... ...
@@ -25,7 +25,6 @@ import (
25 25
 )
26 26
 
27 27
 type runOptions struct {
28
-	autoRemove bool
29 28
 	detach     bool
30 29
 	sigProxy   bool
31 30
 	name       string
... ...
@@ -55,7 +54,6 @@ func NewRunCommand(dockerCli *client.DockerCli) *cobra.Command {
55 55
 	flags.SetInterspersed(false)
56 56
 
57 57
 	// These are flags not stored in Config/HostConfig
58
-	flags.BoolVar(&opts.autoRemove, "rm", false, "Automatically remove the container when it exits")
59 58
 	flags.BoolVarP(&opts.detach, "detach", "d", false, "Run container in background and print container ID")
60 59
 	flags.BoolVar(&opts.sigProxy, "sig-proxy", true, "Proxy received signals to the process")
61 60
 	flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
... ...
@@ -87,7 +85,6 @@ func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions,
87 87
 		flAttach                              *opttypes.ListOpts
88 88
 		ErrConflictAttachDetach               = fmt.Errorf("Conflicting options: -a and -d")
89 89
 		ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm")
90
-		ErrConflictDetachAutoRemove           = fmt.Errorf("Conflicting options: --rm and -d")
91 90
 	)
92 91
 
93 92
 	config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts)
... ...
@@ -127,9 +124,6 @@ func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions,
127 127
 				return ErrConflictAttachDetach
128 128
 			}
129 129
 		}
130
-		if opts.autoRemove {
131
-			return ErrConflictDetachAutoRemove
132
-		}
133 130
 
134 131
 		config.AttachStdin = false
135 132
 		config.AttachStdout = false
... ...
@@ -172,7 +166,7 @@ func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions,
172 172
 			fmt.Fprintf(stdout, "%s\n", createResponse.ID)
173 173
 		}()
174 174
 	}
175
-	if opts.autoRemove && (hostConfig.RestartPolicy.IsAlways() || hostConfig.RestartPolicy.IsOnFailure()) {
175
+	if hostConfig.AutoRemove && !hostConfig.RestartPolicy.IsNone() {
176 176
 		return ErrConflictRestartPolicyAndAutoRemove
177 177
 	}
178 178
 	attach := config.AttachStdin || config.AttachStdout || config.AttachStderr
... ...
@@ -225,16 +219,6 @@ func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions,
225 225
 		})
226 226
 	}
227 227
 
228
-	if opts.autoRemove {
229
-		defer func() {
230
-			// Explicitly not sharing the context as it could be "Done" (by calling cancelFun)
231
-			// and thus the container would not be removed.
232
-			if err := removeContainer(dockerCli, context.Background(), createResponse.ID, true, false, true); err != nil {
233
-				fmt.Fprintf(stderr, "%v\n", err)
234
-			}
235
-		}()
236
-	}
237
-
238 228
 	//start the container
239 229
 	if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil {
240 230
 		// If we have holdHijackedConnection, we should notify
... ...
@@ -272,30 +256,17 @@ func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions,
272 272
 	var status int
273 273
 
274 274
 	// Attached mode
275
-	if opts.autoRemove {
276
-		// Autoremove: wait for the container to finish, retrieve
277
-		// the exit code and remove the container
278
-		if status, err = client.ContainerWait(ctx, createResponse.ID); err != nil {
279
-			return runStartContainerErr(err)
280
-		}
281
-		if _, status, err = getExitCode(dockerCli, ctx, createResponse.ID); err != nil {
282
-			return err
283
-		}
284
-	} else {
285
-		// No Autoremove: Simply retrieve the exit code
286
-		if !config.Tty && hostConfig.RestartPolicy.IsNone() {
287
-			// In non-TTY mode, we can't detach, so we must wait for container exit
288
-			if status, err = client.ContainerWait(ctx, createResponse.ID); err != nil {
289
-				return err
290
-			}
291
-		} else {
292
-			// In TTY mode, there is a race: if the process dies too slowly, the state could
293
-			// be updated after the getExitCode call and result in the wrong exit code being reported
294
-			if _, status, err = getExitCode(dockerCli, ctx, createResponse.ID); err != nil {
295
-				return err
296
-			}
297
-		}
275
+	if !config.Tty {
276
+		// In non-TTY mode, we can't detach, so we must wait for container exit
277
+		client.ContainerWait(context.Background(), createResponse.ID)
298 278
 	}
279
+
280
+	// In TTY mode, there is a race: if the process dies too slowly, the state could
281
+	// be updated after the getExitCode call and result in the wrong exit code being reported
282
+	if _, status, err = dockerCli.GetExitCode(ctx, createResponse.ID); err != nil {
283
+		return fmt.Errorf("tty: status: %d; error: %v;", status, err)
284
+	}
285
+
299 286
 	if status != 0 {
300 287
 		return cli.StatusError{StatusCode: status}
301 288
 	}
... ...
@@ -238,6 +238,10 @@ func (daemon *Daemon) verifyContainerSettings(hostConfig *containertypes.HostCon
238 238
 		return nil, nil
239 239
 	}
240 240
 
241
+	if hostConfig.AutoRemove && !hostConfig.RestartPolicy.IsNone() {
242
+		return nil, fmt.Errorf("Can't create 'AutoRemove' container with restart policy")
243
+	}
244
+
241 245
 	for port := range hostConfig.PortBindings {
242 246
 		_, portStr := nat.SplitProtoPort(string(port))
243 247
 		if _, err := nat.ParsePort(portStr); err != nil {
... ...
@@ -147,6 +147,7 @@ func (daemon *Daemon) restore() error {
147 147
 	}
148 148
 
149 149
 	var migrateLegacyLinks bool
150
+	removeContainers := make(map[string]*container.Container)
150 151
 	restartContainers := make(map[*container.Container]chan struct{})
151 152
 	activeSandboxes := make(map[string]interface{})
152 153
 	for _, c := range containers {
... ...
@@ -194,10 +195,14 @@ func (daemon *Daemon) restore() error {
194 194
 			}
195 195
 			// fixme: only if not running
196 196
 			// get list of containers we need to restart
197
-			if daemon.configStore.AutoRestart && !c.IsRunning() && !c.IsPaused() && c.ShouldRestart() {
198
-				mapLock.Lock()
199
-				restartContainers[c] = make(chan struct{})
200
-				mapLock.Unlock()
197
+			if !c.IsRunning() && !c.IsPaused() {
198
+				if daemon.configStore.AutoRestart && c.ShouldRestart() {
199
+					mapLock.Lock()
200
+					restartContainers[c] = make(chan struct{})
201
+					mapLock.Unlock()
202
+				} else if c.HostConfig != nil && c.HostConfig.AutoRemove {
203
+					removeContainers[c.ID] = c
204
+				}
201 205
 			}
202 206
 
203 207
 			if c.RemovalInProgress {
... ...
@@ -283,6 +288,18 @@ func (daemon *Daemon) restore() error {
283 283
 	}
284 284
 	group.Wait()
285 285
 
286
+	removeGroup := sync.WaitGroup{}
287
+	for id := range removeContainers {
288
+		removeGroup.Add(1)
289
+		go func(cid string) {
290
+			defer removeGroup.Done()
291
+			if err := daemon.ContainerRm(cid, &types.ContainerRmConfig{ForceRemove: true, RemoveVolume: true}); err != nil {
292
+				logrus.Errorf("Failed to remove container %s: %s", cid, err)
293
+			}
294
+		}(id)
295
+	}
296
+	removeGroup.Wait()
297
+
286 298
 	// any containers that were started above would already have had this done,
287 299
 	// however we need to now prepare the mountpoints for the rest of the containers as well.
288 300
 	// This shouldn't cause any issue running on the containers that already had this run.
... ...
@@ -295,7 +312,11 @@ func (daemon *Daemon) restore() error {
295 295
 		// has a volume and the volume dirver is not available.
296 296
 		if _, ok := restartContainers[c]; ok {
297 297
 			continue
298
+		} else if _, ok := removeContainers[c.ID]; ok {
299
+			// container is automatically removed, skip it.
300
+			continue
298 301
 		}
302
+
299 303
 		group.Add(1)
300 304
 		go func(c *container.Container) {
301 305
 			defer group.Done()
... ...
@@ -3,6 +3,7 @@ package daemon
3 3
 import (
4 4
 	"fmt"
5 5
 
6
+	"github.com/Sirupsen/logrus"
6 7
 	"github.com/docker/docker/container"
7 8
 )
8 9
 
... ...
@@ -35,11 +36,25 @@ func (daemon *Daemon) containerRestart(container *container.Container, seconds i
35 35
 		defer daemon.Unmount(container)
36 36
 	}
37 37
 
38
-	if err := daemon.containerStop(container, seconds); err != nil {
38
+	// set AutoRemove flag to false before stop so the container won't be
39
+	// removed during restart process
40
+	autoRemove := container.HostConfig.AutoRemove
41
+
42
+	container.HostConfig.AutoRemove = false
43
+	err := daemon.containerStop(container, seconds)
44
+	// restore AutoRemove irrespective of whether the stop worked or not
45
+	container.HostConfig.AutoRemove = autoRemove
46
+	// containerStop will write HostConfig to disk, we shall restore AutoRemove
47
+	// in disk too
48
+	if toDiskErr := container.ToDiskLocking(); toDiskErr != nil {
49
+		logrus.Errorf("Write container to disk error: %v", toDiskErr)
50
+	}
51
+
52
+	if err != nil {
39 53
 		return err
40 54
 	}
41 55
 
42
-	if err := daemon.containerStart(container); err != nil {
56
+	if err = daemon.containerStart(container); err != nil {
43 57
 		return err
44 58
 	}
45 59
 
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	"github.com/docker/docker/errors"
15 15
 	"github.com/docker/docker/libcontainerd"
16 16
 	"github.com/docker/docker/runconfig"
17
+	"github.com/docker/engine-api/types"
17 18
 	containertypes "github.com/docker/engine-api/types/container"
18 19
 )
19 20
 
... ...
@@ -197,4 +198,10 @@ func (daemon *Daemon) Cleanup(container *container.Container) {
197 197
 		}
198 198
 	}
199 199
 	container.CancelAttachContext()
200
+
201
+	// if containers AutoRemove flag is set, remove it after clean up
202
+	if container.HostConfig.AutoRemove {
203
+		// If containers lock is not released, goroutine will guarantee no block
204
+		go daemon.ContainerRm(container.ID, &types.ContainerRmConfig{ForceRemove: true, RemoveVolume: true})
205
+	}
200 206
 }
... ...
@@ -2742,3 +2742,28 @@ func (s *DockerDaemonSuite) TestRunWithRuntimeFromCommandLine(c *check.C) {
2742 2742
 	out, err = s.d.Cmd("run", "--rm", "--runtime=runc", "busybox", "ls")
2743 2743
 	c.Assert(err, check.IsNil, check.Commentf(out))
2744 2744
 }
2745
+
2746
+func (s *DockerDaemonSuite) TestDaemonRestartWithAutoRemoveContainer(c *check.C) {
2747
+	err := s.d.StartWithBusybox()
2748
+	c.Assert(err, checker.IsNil)
2749
+
2750
+	// top1 will exist after daemon restarts
2751
+	out, err := s.d.Cmd("run", "-d", "--name", "top1", "busybox:latest", "top")
2752
+	c.Assert(err, checker.IsNil, check.Commentf("run top1: %v", out))
2753
+	// top2 will be removed after daemon restarts
2754
+	out, err = s.d.Cmd("run", "-d", "--rm", "--name", "top2", "busybox:latest", "top")
2755
+	c.Assert(err, checker.IsNil, check.Commentf("run top2: %v", out))
2756
+
2757
+	out, err = s.d.Cmd("ps")
2758
+	c.Assert(out, checker.Contains, "top1", check.Commentf("top1 should be running"))
2759
+	c.Assert(out, checker.Contains, "top2", check.Commentf("top2 should be running"))
2760
+
2761
+	// now restart daemon gracefully
2762
+	err = s.d.Restart()
2763
+	c.Assert(err, checker.IsNil)
2764
+
2765
+	out, err = s.d.Cmd("ps", "-a")
2766
+	c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
2767
+	c.Assert(out, checker.Contains, "top1", check.Commentf("top1 should exist after daemon restarts"))
2768
+	c.Assert(out, checker.Not(checker.Contains), "top2", check.Commentf("top2 should be removed after daemon restarts"))
2769
+}
... ...
@@ -240,3 +240,15 @@ func (s *DockerSuite) TestRestartContainerwithRestartPolicy(c *check.C) {
240 240
 	dockerCmd(c, "start", id1)
241 241
 	dockerCmd(c, "start", id2)
242 242
 }
243
+
244
+func (s *DockerSuite) TestRestartAutoRmoveContainer(c *check.C) {
245
+	out, _ := runSleepingContainer(c, "--rm")
246
+
247
+	id := strings.TrimSpace(string(out))
248
+	dockerCmd(c, "restart", id)
249
+	err := waitInspect(id, "{{ .State.Restarting }} {{ .State.Running }}", "false true", 15*time.Second)
250
+	c.Assert(err, checker.IsNil)
251
+
252
+	out, _ = dockerCmd(c, "ps")
253
+	c.Assert(out, checker.Contains, id[:12], check.Commentf("container should be restarted instead of removed: %v", out))
254
+}
... ...
@@ -4498,6 +4498,15 @@ func (s *DockerSuite) TestRunAddHostInHostMode(c *check.C) {
4498 4498
 	c.Assert(out, checker.Contains, expectedOutput, check.Commentf("Expected '%s', but got %q", expectedOutput, out))
4499 4499
 }
4500 4500
 
4501
+func (s *DockerSuite) TestRunRmAndWait(c *check.C) {
4502
+	dockerCmd(c, "run", "--name=test", "--rm", "-d", "busybox", "sh", "-c", "sleep 3;exit 2")
4503
+
4504
+	out, code, err := dockerCmdWithError("wait", "test")
4505
+	c.Assert(err, checker.IsNil, check.Commentf("out: %s; exit code: %d", out, code))
4506
+	c.Assert(out, checker.Equals, "2\n", check.Commentf("exit code: %d", code))
4507
+	c.Assert(code, checker.Equals, 0)
4508
+}
4509
+
4501 4510
 // Test case for #23498
4502 4511
 func (s *DockerSuite) TestRunUnsetEntrypoint(c *check.C) {
4503 4512
 	testRequires(c, DaemonIsLinux)
... ...
@@ -103,6 +103,7 @@ type ContainerOptions struct {
103 103
 	flHealthTimeout     time.Duration
104 104
 	flHealthRetries     int
105 105
 	flRuntime           string
106
+	flAutoRemove        *bool
106 107
 
107 108
 	Image string
108 109
 	Args  []string
... ...
@@ -163,6 +164,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions {
163 163
 	flags.Var(copts.flUlimits, "ulimit", "Ulimit options")
164 164
 	flags.StringVarP(&copts.flUser, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
165 165
 	flags.StringVarP(&copts.flWorkingDir, "workdir", "w", "", "Working directory inside the container")
166
+	flags.BoolVarP(&copts.flAutoRemove, "rm", false, "Automatically remove the container when it exits")
166 167
 
167 168
 	// Security
168 169
 	flags.Var(&copts.flCapAdd, "cap-add", "Add Linux capabilities")
... ...
@@ -553,6 +555,7 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c
553 553
 		Binds:           binds,
554 554
 		ContainerIDFile: copts.flContainerIDFile,
555 555
 		OomScoreAdj:     copts.flOomScoreAdj,
556
+		AutoRemove:      copts.flAutoRemove,
556 557
 		Privileged:      copts.flPrivileged,
557 558
 		PortBindings:    portBindings,
558 559
 		Links:           copts.flLinks.GetAll(),