Browse code

Add `--force` in `docker volume rm` to fix out-of-band volume driver deletion

This fix tries to address the issue in raised #23367 where an out-of-band
volume driver deletion leaves some data in docker. This prevent the
reuse of deleted volume names (by out-of-band volume driver like flocker).

This fix adds a `--force` field in `docker volume rm` to forcefully purge
the data of the volume that has already been deleted.

Related documentations have been updated.

This fix is tested manually with flocker, as is specified in #23367.
An integration test has also been added for the scenario described.

This fix fixes #23367.

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

Yong Tang authored on 2016/06/10 23:40:09
Showing 9 changed files
... ...
@@ -10,27 +10,41 @@ import (
10 10
 	"github.com/spf13/cobra"
11 11
 )
12 12
 
13
+type removeOptions struct {
14
+	force bool
15
+
16
+	volumes []string
17
+}
18
+
13 19
 func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
14
-	return &cobra.Command{
15
-		Use:     "rm VOLUME [VOLUME...]",
20
+	var opts removeOptions
21
+
22
+	cmd := &cobra.Command{
23
+		Use:     "rm [OPTIONS] VOLUME [VOLUME]...",
16 24
 		Aliases: []string{"remove"},
17 25
 		Short:   "Remove one or more volumes",
18 26
 		Long:    removeDescription,
19 27
 		Example: removeExample,
20 28
 		Args:    cli.RequiresMinArgs(1),
21 29
 		RunE: func(cmd *cobra.Command, args []string) error {
22
-			return runRemove(dockerCli, args)
30
+			opts.volumes = args
31
+			return runRemove(dockerCli, &opts)
23 32
 		},
24 33
 	}
34
+
35
+	flags := cmd.Flags()
36
+	flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of one or more volumes")
37
+
38
+	return cmd
25 39
 }
26 40
 
27
-func runRemove(dockerCli *client.DockerCli, volumes []string) error {
41
+func runRemove(dockerCli *client.DockerCli, opts *removeOptions) error {
28 42
 	client := dockerCli.Client()
29 43
 	ctx := context.Background()
30 44
 	status := 0
31 45
 
32
-	for _, name := range volumes {
33
-		if err := client.VolumeRemove(ctx, name, false); err != nil {
46
+	for _, name := range opts.volumes {
47
+		if err := client.VolumeRemove(ctx, name, opts.force); err != nil {
34 48
 			fmt.Fprintf(dockerCli.Err(), "%s\n", err)
35 49
 			status = 1
36 50
 			continue
... ...
@@ -11,5 +11,5 @@ type Backend interface {
11 11
 	Volumes(filter string) ([]*types.Volume, []string, error)
12 12
 	VolumeInspect(name string) (*types.Volume, error)
13 13
 	VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
14
-	VolumeRm(name string) error
14
+	VolumeRm(name string, force bool) error
15 15
 }
... ...
@@ -58,7 +58,8 @@ func (v *volumeRouter) deleteVolumes(ctx context.Context, w http.ResponseWriter,
58 58
 	if err := httputils.ParseForm(r); err != nil {
59 59
 		return err
60 60
 	}
61
-	if err := v.backend.VolumeRm(vars["name"]); err != nil {
61
+	force := httputils.BoolValue(r, "force")
62
+	if err := v.backend.VolumeRm(vars["name"], force); err != nil {
62 63
 		return err
63 64
 	}
64 65
 	w.WriteHeader(http.StatusNoContent)
... ...
@@ -135,7 +135,16 @@ func (daemon *Daemon) cleanupContainer(container *container.Container, forceRemo
135 135
 // VolumeRm removes the volume with the given name.
136 136
 // If the volume is referenced by a container it is not removed
137 137
 // This is called directly from the remote API
138
-func (daemon *Daemon) VolumeRm(name string) error {
138
+func (daemon *Daemon) VolumeRm(name string, force bool) error {
139
+	err := daemon.volumeRm(name)
140
+	if err == nil || force {
141
+		daemon.volumes.Purge(name)
142
+		return nil
143
+	}
144
+	return err
145
+}
146
+
147
+func (daemon *Daemon) volumeRm(name string) error {
139 148
 	v, err := daemon.volumes.Get(name)
140 149
 	if err != nil {
141 150
 		return err
... ...
@@ -119,6 +119,7 @@ This section lists each version from latest to oldest.  Each listing includes a
119 119
 * `POST /containers/create` now takes `AutoRemove` in HostConfig, to enable auto-removal of the container on daemon side when the container's process exits.
120 120
 * `GET /containers/json` and `GET /containers/(id or name)/json` now return `"removing"` as a value for the `State.Status` field if the container is being removed. Previously, "exited" was returned as status.
121 121
 * `GET /containers/json` now accepts `removing` as a valid value for the `status` filter.
122
+* `DELETE /volumes/(name)` now accepts a `force` query parameter to force removal of volumes that were already removed out of band by the volume driver plugin.
122 123
 
123 124
 ### v1.24 API changes
124 125
 
... ...
@@ -3062,6 +3062,11 @@ Instruct the driver to remove the volume (`name`).
3062 3062
 
3063 3063
     HTTP/1.1 204 No Content
3064 3064
 
3065
+**Query Parameters**:
3066
+
3067
+-   **force** - 1/True/true or 0/False/false, Force the removal of the volume.
3068
+        Default `false`.
3069
+
3065 3070
 **Status codes**:
3066 3071
 
3067 3072
 -   **204** - no error
... ...
@@ -11,7 +11,7 @@ parent = "smn_cli"
11 11
 # volume rm
12 12
 
13 13
 ```markdown
14
-Usage:  docker volume rm VOLUME [VOLUME...]
14
+Usage:  docker volume rm [OPTIONS] VOLUME [VOLUME...]
15 15
 
16 16
 Remove one or more volumes
17 17
 
... ...
@@ -19,6 +19,7 @@ Aliases:
19 19
   rm, remove
20 20
 
21 21
 Options:
22
+  -f, --force  Force the removal of one or more volumes
22 23
       --help   Print usage
23 24
 ```
24 25
 
... ...
@@ -374,3 +374,38 @@ func (s *DockerSuite) TestVolumeCliLsFilterLabels(c *check.C) {
374 374
 	outArr = strings.Split(strings.TrimSpace(out), "\n")
375 375
 	c.Assert(len(outArr), check.Equals, 1, check.Commentf("\n%s", out))
376 376
 }
377
+
378
+func (s *DockerSuite) TestVolumeCliRmForceUsage(c *check.C) {
379
+	out, _ := dockerCmd(c, "volume", "create")
380
+	id := strings.TrimSpace(out)
381
+
382
+	dockerCmd(c, "volume", "rm", "-f", id)
383
+	dockerCmd(c, "volume", "rm", "--force", "nonexist")
384
+
385
+	out, _ = dockerCmd(c, "volume", "ls")
386
+	outArr := strings.Split(strings.TrimSpace(out), "\n")
387
+	c.Assert(len(outArr), check.Equals, 1, check.Commentf("%s\n", out))
388
+}
389
+
390
+func (s *DockerSuite) TestVolumeCliRmForce(c *check.C) {
391
+	testRequires(c, SameHostDaemon, DaemonIsLinux)
392
+
393
+	name := "test"
394
+	out, _ := dockerCmd(c, "volume", "create", "--name", name)
395
+	id := strings.TrimSpace(out)
396
+	c.Assert(id, checker.Equals, name)
397
+
398
+	out, _ = dockerCmd(c, "volume", "inspect", "--format", "{{.Mountpoint}}", name)
399
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
400
+	// Mountpoint is in the form of "/var/lib/docker/volumes/.../_data", removing `/_data`
401
+	path := strings.TrimSuffix(strings.TrimSpace(out), "/_data")
402
+	out, _, err := runCommandWithOutput(exec.Command("rm", "-rf", path))
403
+	c.Assert(err, check.IsNil)
404
+
405
+	dockerCmd(c, "volume", "rm", "-f", "test")
406
+	out, _ = dockerCmd(c, "volume", "ls")
407
+	c.Assert(out, checker.Not(checker.Contains), name)
408
+	dockerCmd(c, "volume", "create", "--name", "test")
409
+	out, _ = dockerCmd(c, "volume", "ls")
410
+	c.Assert(out, checker.Contains, name)
411
+}
... ...
@@ -104,7 +104,9 @@ func (s *VolumeStore) setNamed(v volume.Volume, ref string) {
104 104
 	s.globalLock.Unlock()
105 105
 }
106 106
 
107
-func (s *VolumeStore) purge(name string) {
107
+// Purge allows the cleanup of internal data on docker in case
108
+// the internal data is out of sync with volumes driver plugins.
109
+func (s *VolumeStore) Purge(name string) {
108 110
 	s.globalLock.Lock()
109 111
 	delete(s.names, name)
110 112
 	delete(s.refs, name)
... ...
@@ -425,7 +427,7 @@ func (s *VolumeStore) Remove(v volume.Volume) error {
425 425
 		return &OpErr{Err: err, Name: name, Op: "remove"}
426 426
 	}
427 427
 
428
-	s.purge(name)
428
+	s.Purge(name)
429 429
 	return nil
430 430
 }
431 431