Browse code

Don't use AutoRemove on older daemons

Docker 1.13 moves the `--rm` flag to the daemon,
through an AutoRemove option in HostConfig.

When using API 1.24 and under, AutoRemove should not be
used, even if the daemon is version 1.13 or above and
"supports" this feature.

This patch fixes a situation where an 1.13 client,
talking to an 1.13 daemon, but using the 1.24 API
version, still set the AutoRemove property.

As a result, both the client _and_ the daemon
were attempting to remove the container, resulting
in an error:

ERRO[0000] error removing container: Error response from daemon:
removal of container ce0976ad22495c7cbe9487752ea32721a282164862db036b2f3377bd07461c3a
is already in progress

In addition, the validation of conflicting options
is moved from `docker run` to `opts.parse()`, so
that conflicting options are also detected when
running `docker create` and `docker start` separately.

To resolve the issue, the `AutoRemove` option is now
always set to `false` both by the client and the
daemon, if API version 1.24 or under is used.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2017/01/13 09:05:39
Showing 6 changed files
... ...
@@ -369,6 +369,11 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
369 369
 	version := httputils.VersionFromContext(ctx)
370 370
 	adjustCPUShares := versions.LessThan(version, "1.19")
371 371
 
372
+	// When using API 1.24 and under, the client is responsible for removing the container
373
+	if hostConfig != nil && versions.LessThan(version, "1.25") {
374
+		hostConfig.AutoRemove = false
375
+	}
376
+
372 377
 	ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
373 378
 		Name:             name,
374 379
 		Config:           config,
... ...
@@ -622,6 +622,10 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c
622 622
 		Runtime:        copts.runtime,
623 623
 	}
624 624
 
625
+	if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {
626
+		return nil, nil, nil, fmt.Errorf("Conflicting options: --restart and --rm")
627
+	}
628
+
625 629
 	// only set this value if the user provided the flag, else it should default to nil
626 630
 	if flags.Changed("init") {
627 631
 		hostConfig.Init = &copts.init
... ...
@@ -456,6 +456,14 @@ func TestParseRestartPolicy(t *testing.T) {
456 456
 	}
457 457
 }
458 458
 
459
+func TestParseRestartPolicyAutoRemove(t *testing.T) {
460
+	expected := "Conflicting options: --restart and --rm"
461
+	_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"})
462
+	if err == nil || err.Error() != expected {
463
+		t.Fatalf("Expected error %v, but got none", expected)
464
+	}
465
+}
466
+
459 467
 func TestParseHealth(t *testing.T) {
460 468
 	checkOk := func(args ...string) *container.HealthConfig {
461 469
 		config, _, _, err := parseRun(args)
... ...
@@ -73,9 +73,8 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions
73 73
 	cmdPath := "run"
74 74
 
75 75
 	var (
76
-		flAttach                              *opttypes.ListOpts
77
-		ErrConflictAttachDetach               = errors.New("Conflicting options: -a and -d")
78
-		ErrConflictRestartPolicyAndAutoRemove = errors.New("Conflicting options: --restart and --rm")
76
+		flAttach                *opttypes.ListOpts
77
+		ErrConflictAttachDetach = errors.New("Conflicting options: -a and -d")
79 78
 	)
80 79
 
81 80
 	config, hostConfig, networkingConfig, err := parse(flags, copts)
... ...
@@ -86,9 +85,6 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions
86 86
 		return cli.StatusError{StatusCode: 125}
87 87
 	}
88 88
 
89
-	if hostConfig.AutoRemove && !hostConfig.RestartPolicy.IsNone() {
90
-		return ErrConflictRestartPolicyAndAutoRemove
91
-	}
92 89
 	if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
93 90
 		fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
94 91
 	}
... ...
@@ -209,7 +205,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions
209 209
 		})
210 210
 	}
211 211
 
212
-	statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, hostConfig.AutoRemove)
212
+	statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, copts.autoRemove)
213 213
 
214 214
 	//start the container
215 215
 	if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil {
... ...
@@ -222,7 +218,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions
222 222
 		}
223 223
 
224 224
 		reportError(stderr, cmdPath, err.Error(), false)
225
-		if hostConfig.AutoRemove {
225
+		if copts.autoRemove {
226 226
 			// wait container to be removed
227 227
 			<-statusChan
228 228
 		}
... ...
@@ -7,6 +7,7 @@ import (
7 7
 
8 8
 	"github.com/docker/docker/api/types/container"
9 9
 	"github.com/docker/docker/api/types/network"
10
+	"github.com/docker/docker/api/types/versions"
10 11
 	"golang.org/x/net/context"
11 12
 )
12 13
 
... ...
@@ -25,6 +26,11 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config
25 25
 		return response, err
26 26
 	}
27 27
 
28
+	// When using API 1.24 and under, the client is responsible for removing the container
29
+	if hostConfig != nil && versions.LessThan(cli.ClientVersion(), "1.25") {
30
+		hostConfig.AutoRemove = false
31
+	}
32
+
28 33
 	query := url.Values{}
29 34
 	if containerName != "" {
30 35
 		query.Set("name", containerName)
... ...
@@ -74,3 +74,45 @@ func TestContainerCreateWithName(t *testing.T) {
74 74
 		t.Fatalf("expected `container_id`, got %s", r.ID)
75 75
 	}
76 76
 }
77
+
78
+// TestContainerCreateAutoRemove validates that a client using API 1.24 always disables AutoRemove. When using API 1.25
79
+// or up, AutoRemove should not be disabled.
80
+func TestContainerCreateAutoRemove(t *testing.T) {
81
+	autoRemoveValidator := func(expectedValue bool) func(req *http.Request) (*http.Response, error) {
82
+		return func(req *http.Request) (*http.Response, error) {
83
+			var config configWrapper
84
+
85
+			if err := json.NewDecoder(req.Body).Decode(&config); err != nil {
86
+				return nil, err
87
+			}
88
+			if config.HostConfig.AutoRemove != expectedValue {
89
+				return nil, fmt.Errorf("expected AutoRemove to be %v, got %v", expectedValue, config.HostConfig.AutoRemove)
90
+			}
91
+			b, err := json.Marshal(container.ContainerCreateCreatedBody{
92
+				ID: "container_id",
93
+			})
94
+			if err != nil {
95
+				return nil, err
96
+			}
97
+			return &http.Response{
98
+				StatusCode: http.StatusOK,
99
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
100
+			}, nil
101
+		}
102
+	}
103
+
104
+	client := &Client{
105
+		client:  newMockClient(autoRemoveValidator(false)),
106
+		version: "1.24",
107
+	}
108
+	if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, ""); err != nil {
109
+		t.Fatal(err)
110
+	}
111
+	client = &Client{
112
+		client:  newMockClient(autoRemoveValidator(true)),
113
+		version: "1.25",
114
+	}
115
+	if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, ""); err != nil {
116
+		t.Fatal(err)
117
+	}
118
+}