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>
| ... | ... |
@@ -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 |
|