github.com/docker/engine@v22.0.0-20211208180946-d456264580cf+incompatible/integration-cli/docker_cli_external_volume_driver_test.go (about) 1 package main 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/docker/docker/api/types" 17 "github.com/docker/docker/integration-cli/daemon" 18 "github.com/docker/docker/pkg/stringid" 19 testdaemon "github.com/docker/docker/testutil/daemon" 20 "github.com/docker/docker/volume" 21 "gotest.tools/v3/assert" 22 ) 23 24 const volumePluginName = "test-external-volume-driver" 25 26 type eventCounter struct { 27 activations int 28 creations int 29 removals int 30 mounts int 31 unmounts int 32 paths int 33 lists int 34 gets int 35 caps int 36 } 37 38 type DockerExternalVolumeSuite struct { 39 ds *DockerSuite 40 d *daemon.Daemon 41 *volumePlugin 42 } 43 44 func (s *DockerExternalVolumeSuite) SetUpTest(c *testing.T) { 45 testRequires(c, testEnv.IsLocalDaemon) 46 s.d = daemon.New(c, dockerBinary, dockerdBinary, testdaemon.WithEnvironment(testEnv.Execution)) 47 s.ec = &eventCounter{} 48 } 49 50 func (s *DockerExternalVolumeSuite) TearDownTest(c *testing.T) { 51 if s.d != nil { 52 s.d.Stop(c) 53 s.ds.TearDownTest(c) 54 } 55 } 56 57 func (s *DockerExternalVolumeSuite) SetUpSuite(c *testing.T) { 58 s.volumePlugin = newVolumePlugin(c, volumePluginName) 59 } 60 61 type volumePlugin struct { 62 ec *eventCounter 63 *httptest.Server 64 vols map[string]vol 65 } 66 67 type vol struct { 68 Name string 69 Mountpoint string 70 Ninja bool // hack used to trigger a null volume return on `Get` 71 Status map[string]interface{} 72 Options map[string]string 73 } 74 75 func (p *volumePlugin) Close() { 76 p.Server.Close() 77 } 78 79 func newVolumePlugin(c *testing.T, name string) *volumePlugin { 80 mux := http.NewServeMux() 81 s := &volumePlugin{Server: httptest.NewServer(mux), ec: &eventCounter{}, vols: make(map[string]vol)} 82 83 type pluginRequest struct { 84 Name string 85 Opts map[string]string 86 ID string 87 } 88 89 type pluginResp struct { 90 Mountpoint string `json:",omitempty"` 91 Err string `json:",omitempty"` 92 } 93 94 read := func(b io.ReadCloser) (pluginRequest, error) { 95 defer b.Close() 96 var pr pluginRequest 97 err := json.NewDecoder(b).Decode(&pr) 98 return pr, err 99 } 100 101 send := func(w http.ResponseWriter, data interface{}) { 102 switch t := data.(type) { 103 case error: 104 http.Error(w, t.Error(), 500) 105 case string: 106 w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") 107 fmt.Fprintln(w, t) 108 default: 109 w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") 110 json.NewEncoder(w).Encode(&data) 111 } 112 } 113 114 mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { 115 s.ec.activations++ 116 send(w, `{"Implements": ["VolumeDriver"]}`) 117 }) 118 119 mux.HandleFunc("/VolumeDriver.Create", func(w http.ResponseWriter, r *http.Request) { 120 s.ec.creations++ 121 pr, err := read(r.Body) 122 if err != nil { 123 send(w, err) 124 return 125 } 126 _, isNinja := pr.Opts["ninja"] 127 status := map[string]interface{}{"Hello": "world"} 128 s.vols[pr.Name] = vol{Name: pr.Name, Ninja: isNinja, Status: status, Options: pr.Opts} 129 send(w, nil) 130 }) 131 132 mux.HandleFunc("/VolumeDriver.List", func(w http.ResponseWriter, r *http.Request) { 133 s.ec.lists++ 134 vols := make([]vol, 0, len(s.vols)) 135 for _, v := range s.vols { 136 if v.Ninja { 137 continue 138 } 139 vols = append(vols, v) 140 } 141 send(w, map[string][]vol{"Volumes": vols}) 142 }) 143 144 mux.HandleFunc("/VolumeDriver.Get", func(w http.ResponseWriter, r *http.Request) { 145 s.ec.gets++ 146 pr, err := read(r.Body) 147 if err != nil { 148 send(w, err) 149 return 150 } 151 152 v, exists := s.vols[pr.Name] 153 if !exists { 154 send(w, `{"Err": "no such volume"}`) 155 } 156 157 if v.Ninja { 158 send(w, map[string]vol{}) 159 return 160 } 161 162 v.Mountpoint = hostVolumePath(pr.Name) 163 send(w, map[string]vol{"Volume": v}) 164 }) 165 166 mux.HandleFunc("/VolumeDriver.Remove", func(w http.ResponseWriter, r *http.Request) { 167 s.ec.removals++ 168 pr, err := read(r.Body) 169 if err != nil { 170 send(w, err) 171 return 172 } 173 174 v, ok := s.vols[pr.Name] 175 if !ok { 176 send(w, nil) 177 return 178 } 179 180 if err := os.RemoveAll(hostVolumePath(v.Name)); err != nil { 181 send(w, &pluginResp{Err: err.Error()}) 182 return 183 } 184 delete(s.vols, v.Name) 185 send(w, nil) 186 }) 187 188 mux.HandleFunc("/VolumeDriver.Path", func(w http.ResponseWriter, r *http.Request) { 189 s.ec.paths++ 190 191 pr, err := read(r.Body) 192 if err != nil { 193 send(w, err) 194 return 195 } 196 p := hostVolumePath(pr.Name) 197 send(w, &pluginResp{Mountpoint: p}) 198 }) 199 200 mux.HandleFunc("/VolumeDriver.Mount", func(w http.ResponseWriter, r *http.Request) { 201 s.ec.mounts++ 202 203 pr, err := read(r.Body) 204 if err != nil { 205 send(w, err) 206 return 207 } 208 209 if v, exists := s.vols[pr.Name]; exists { 210 // Use this to simulate a mount failure 211 if _, exists := v.Options["invalidOption"]; exists { 212 send(w, fmt.Errorf("invalid argument")) 213 return 214 } 215 } 216 217 p := hostVolumePath(pr.Name) 218 if err := os.MkdirAll(p, 0755); err != nil { 219 send(w, &pluginResp{Err: err.Error()}) 220 return 221 } 222 223 if err := os.WriteFile(filepath.Join(p, "test"), []byte(s.Server.URL), 0644); err != nil { 224 send(w, err) 225 return 226 } 227 228 if err := os.WriteFile(filepath.Join(p, "mountID"), []byte(pr.ID), 0644); err != nil { 229 send(w, err) 230 return 231 } 232 233 send(w, &pluginResp{Mountpoint: p}) 234 }) 235 236 mux.HandleFunc("/VolumeDriver.Unmount", func(w http.ResponseWriter, r *http.Request) { 237 s.ec.unmounts++ 238 239 _, err := read(r.Body) 240 if err != nil { 241 send(w, err) 242 return 243 } 244 245 send(w, nil) 246 }) 247 248 mux.HandleFunc("/VolumeDriver.Capabilities", func(w http.ResponseWriter, r *http.Request) { 249 s.ec.caps++ 250 251 _, err := read(r.Body) 252 if err != nil { 253 send(w, err) 254 return 255 } 256 257 send(w, `{"Capabilities": { "Scope": "global" }}`) 258 }) 259 260 err := os.MkdirAll("/etc/docker/plugins", 0755) 261 assert.NilError(c, err) 262 263 err = os.WriteFile("/etc/docker/plugins/"+name+".spec", []byte(s.Server.URL), 0644) 264 assert.NilError(c, err) 265 return s 266 } 267 268 func (s *DockerExternalVolumeSuite) TearDownSuite(c *testing.T) { 269 s.volumePlugin.Close() 270 271 err := os.RemoveAll("/etc/docker/plugins") 272 assert.NilError(c, err) 273 } 274 275 func (s *DockerExternalVolumeSuite) TestVolumeCLICreateOptionConflict(c *testing.T) { 276 dockerCmd(c, "volume", "create", "test") 277 278 out, _, err := dockerCmdWithError("volume", "create", "test", "--driver", volumePluginName) 279 assert.Assert(c, err != nil, "volume create exception name already in use with another driver") 280 assert.Assert(c, strings.Contains(out, "must be unique")) 281 out, _ = dockerCmd(c, "volume", "inspect", "--format={{ .Driver }}", "test") 282 _, _, err = dockerCmdWithError("volume", "create", "test", "--driver", strings.TrimSpace(out)) 283 assert.NilError(c, err) 284 } 285 286 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverNamed(c *testing.T) { 287 s.d.StartWithBusybox(c) 288 289 out, err := s.d.Cmd("run", "--rm", "--name", "test-data", "-v", "external-volume-test:/tmp/external-volume-test", "--volume-driver", volumePluginName, "busybox:latest", "cat", "/tmp/external-volume-test/test") 290 assert.NilError(c, err, out) 291 assert.Assert(c, strings.Contains(out, s.Server.URL)) 292 _, err = s.d.Cmd("volume", "rm", "external-volume-test") 293 assert.NilError(c, err) 294 295 p := hostVolumePath("external-volume-test") 296 _, err = os.Lstat(p) 297 assert.ErrorContains(c, err, "") 298 assert.Assert(c, os.IsNotExist(err), "Expected volume path in host to not exist: %s, %v\n", p, err) 299 300 assert.Equal(c, s.ec.activations, 1) 301 assert.Equal(c, s.ec.creations, 1) 302 assert.Equal(c, s.ec.removals, 1) 303 assert.Equal(c, s.ec.mounts, 1) 304 assert.Equal(c, s.ec.unmounts, 1) 305 } 306 307 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverUnnamed(c *testing.T) { 308 s.d.StartWithBusybox(c) 309 310 out, err := s.d.Cmd("run", "--rm", "--name", "test-data", "-v", "/tmp/external-volume-test", "--volume-driver", volumePluginName, "busybox:latest", "cat", "/tmp/external-volume-test/test") 311 assert.NilError(c, err, out) 312 assert.Assert(c, strings.Contains(out, s.Server.URL)) 313 assert.Equal(c, s.ec.activations, 1) 314 assert.Equal(c, s.ec.creations, 1) 315 assert.Equal(c, s.ec.removals, 1) 316 assert.Equal(c, s.ec.mounts, 1) 317 assert.Equal(c, s.ec.unmounts, 1) 318 } 319 320 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverVolumesFrom(c *testing.T) { 321 s.d.StartWithBusybox(c) 322 323 out, err := s.d.Cmd("run", "--name", "vol-test1", "-v", "/foo", "--volume-driver", volumePluginName, "busybox:latest") 324 assert.NilError(c, err, out) 325 326 out, err = s.d.Cmd("run", "--rm", "--volumes-from", "vol-test1", "--name", "vol-test2", "busybox", "ls", "/tmp") 327 assert.NilError(c, err, out) 328 329 out, err = s.d.Cmd("rm", "-fv", "vol-test1") 330 assert.NilError(c, err, out) 331 332 assert.Equal(c, s.ec.activations, 1) 333 assert.Equal(c, s.ec.creations, 1) 334 assert.Equal(c, s.ec.removals, 1) 335 assert.Equal(c, s.ec.mounts, 2) 336 assert.Equal(c, s.ec.unmounts, 2) 337 } 338 339 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverDeleteContainer(c *testing.T) { 340 s.d.StartWithBusybox(c) 341 342 out, err := s.d.Cmd("run", "--name", "vol-test1", "-v", "/foo", "--volume-driver", volumePluginName, "busybox:latest") 343 assert.NilError(c, err, out) 344 345 out, err = s.d.Cmd("rm", "-fv", "vol-test1") 346 assert.NilError(c, err, out) 347 348 assert.Equal(c, s.ec.activations, 1) 349 assert.Equal(c, s.ec.creations, 1) 350 assert.Equal(c, s.ec.removals, 1) 351 assert.Equal(c, s.ec.mounts, 1) 352 assert.Equal(c, s.ec.unmounts, 1) 353 } 354 355 func hostVolumePath(name string) string { 356 return fmt.Sprintf("/var/lib/docker/volumes/%s", name) 357 } 358 359 // Make sure a request to use a down driver doesn't block other requests 360 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverLookupNotBlocked(c *testing.T) { 361 specPath := "/etc/docker/plugins/down-driver.spec" 362 err := os.WriteFile(specPath, []byte("tcp://127.0.0.7:9999"), 0644) 363 assert.NilError(c, err) 364 defer os.RemoveAll(specPath) 365 366 chCmd1 := make(chan struct{}) 367 chCmd2 := make(chan error, 1) 368 cmd1 := exec.Command(dockerBinary, "volume", "create", "-d", "down-driver") 369 cmd2 := exec.Command(dockerBinary, "volume", "create") 370 371 assert.Assert(c, cmd1.Start() == nil) 372 defer cmd1.Process.Kill() 373 time.Sleep(100 * time.Millisecond) // ensure API has been called 374 assert.Assert(c, cmd2.Start() == nil) 375 376 go func() { 377 cmd1.Wait() 378 close(chCmd1) 379 }() 380 go func() { 381 chCmd2 <- cmd2.Wait() 382 }() 383 384 select { 385 case <-chCmd1: 386 cmd2.Process.Kill() 387 c.Fatalf("volume create with down driver finished unexpectedly") 388 case err := <-chCmd2: 389 assert.NilError(c, err) 390 case <-time.After(5 * time.Second): 391 cmd2.Process.Kill() 392 c.Fatal("volume creates are blocked by previous create requests when previous driver is down") 393 } 394 } 395 396 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverRetryNotImmediatelyExists(c *testing.T) { 397 s.d.StartWithBusybox(c) 398 driverName := "test-external-volume-driver-retry" 399 400 errchan := make(chan error, 1) 401 started := make(chan struct{}) 402 go func() { 403 close(started) 404 if out, err := s.d.Cmd("run", "--rm", "--name", "test-data-retry", "-v", "external-volume-test:/tmp/external-volume-test", "--volume-driver", driverName, "busybox:latest"); err != nil { 405 errchan <- fmt.Errorf("%v:\n%s", err, out) 406 } 407 close(errchan) 408 }() 409 410 <-started 411 // wait for a retry to occur, then create spec to allow plugin to register 412 time.Sleep(2 * time.Second) 413 p := newVolumePlugin(c, driverName) 414 defer p.Close() 415 416 select { 417 case err := <-errchan: 418 assert.NilError(c, err) 419 case <-time.After(8 * time.Second): 420 c.Fatal("volume creates fail when plugin not immediately available") 421 } 422 423 _, err := s.d.Cmd("volume", "rm", "external-volume-test") 424 assert.NilError(c, err) 425 426 assert.Equal(c, p.ec.activations, 1) 427 assert.Equal(c, p.ec.creations, 1) 428 assert.Equal(c, p.ec.removals, 1) 429 assert.Equal(c, p.ec.mounts, 1) 430 assert.Equal(c, p.ec.unmounts, 1) 431 } 432 433 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverBindExternalVolume(c *testing.T) { 434 dockerCmd(c, "volume", "create", "-d", volumePluginName, "foo") 435 dockerCmd(c, "run", "-d", "--name", "testing", "-v", "foo:/bar", "busybox", "top") 436 437 var mounts []struct { 438 Name string 439 Driver string 440 } 441 out := inspectFieldJSON(c, "testing", "Mounts") 442 assert.Assert(c, json.NewDecoder(strings.NewReader(out)).Decode(&mounts) == nil) 443 assert.Equal(c, len(mounts), 1, out) 444 assert.Equal(c, mounts[0].Name, "foo") 445 assert.Equal(c, mounts[0].Driver, volumePluginName) 446 } 447 448 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverList(c *testing.T) { 449 dockerCmd(c, "volume", "create", "-d", volumePluginName, "abc3") 450 out, _ := dockerCmd(c, "volume", "ls") 451 ls := strings.Split(strings.TrimSpace(out), "\n") 452 assert.Equal(c, len(ls), 2, fmt.Sprintf("\n%s", out)) 453 454 vol := strings.Fields(ls[len(ls)-1]) 455 assert.Equal(c, len(vol), 2, fmt.Sprintf("%v", vol)) 456 assert.Equal(c, vol[0], volumePluginName) 457 assert.Equal(c, vol[1], "abc3") 458 459 assert.Equal(c, s.ec.lists, 1) 460 } 461 462 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverGet(c *testing.T) { 463 out, _, err := dockerCmdWithError("volume", "inspect", "dummy") 464 assert.ErrorContains(c, err, "", out) 465 assert.Assert(c, strings.Contains(out, "No such volume")) 466 assert.Equal(c, s.ec.gets, 1) 467 468 dockerCmd(c, "volume", "create", "test", "-d", volumePluginName) 469 out, _ = dockerCmd(c, "volume", "inspect", "test") 470 471 type vol struct { 472 Status map[string]string 473 } 474 var st []vol 475 476 assert.Assert(c, json.Unmarshal([]byte(out), &st) == nil) 477 assert.Equal(c, len(st), 1) 478 assert.Equal(c, len(st[0].Status), 1, fmt.Sprintf("%v", st[0])) 479 assert.Equal(c, st[0].Status["Hello"], "world", fmt.Sprintf("%v", st[0].Status)) 480 } 481 482 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverWithDaemonRestart(c *testing.T) { 483 dockerCmd(c, "volume", "create", "-d", volumePluginName, "abc1") 484 s.d.Restart(c) 485 486 dockerCmd(c, "run", "--name=test", "-v", "abc1:/foo", "busybox", "true") 487 var mounts []types.MountPoint 488 inspectFieldAndUnmarshall(c, "test", "Mounts", &mounts) 489 assert.Equal(c, len(mounts), 1) 490 assert.Equal(c, mounts[0].Driver, volumePluginName) 491 } 492 493 // Ensures that the daemon handles when the plugin responds to a `Get` request with a null volume and a null error. 494 // Prior the daemon would panic in this scenario. 495 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverGetEmptyResponse(c *testing.T) { 496 s.d.Start(c) 497 498 out, err := s.d.Cmd("volume", "create", "-d", volumePluginName, "abc2", "--opt", "ninja=1") 499 assert.NilError(c, err, out) 500 501 out, err = s.d.Cmd("volume", "inspect", "abc2") 502 assert.ErrorContains(c, err, "", out) 503 assert.Assert(c, strings.Contains(out, "No such volume")) 504 } 505 506 // Ensure only cached paths are used in volume list to prevent N+1 calls to `VolumeDriver.Path` 507 // 508 // TODO(@cpuguy83): This test is testing internal implementation. In all the cases here, there may not even be a path 509 // available because the volume is not even mounted. Consider removing this test. 510 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverPathCalls(c *testing.T) { 511 s.d.Start(c) 512 assert.Equal(c, s.ec.paths, 0) 513 514 out, err := s.d.Cmd("volume", "create", "test", "--driver=test-external-volume-driver") 515 assert.NilError(c, err, out) 516 assert.Equal(c, s.ec.paths, 0) 517 518 out, err = s.d.Cmd("volume", "ls") 519 assert.NilError(c, err, out) 520 assert.Equal(c, s.ec.paths, 0) 521 } 522 523 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverMountID(c *testing.T) { 524 s.d.StartWithBusybox(c) 525 526 out, err := s.d.Cmd("run", "--rm", "-v", "external-volume-test:/tmp/external-volume-test", "--volume-driver", volumePluginName, "busybox:latest", "cat", "/tmp/external-volume-test/test") 527 assert.NilError(c, err, out) 528 assert.Assert(c, strings.TrimSpace(out) != "") 529 } 530 531 // Check that VolumeDriver.Capabilities gets called, and only called once 532 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverCapabilities(c *testing.T) { 533 s.d.Start(c) 534 assert.Equal(c, s.ec.caps, 0) 535 536 for i := 0; i < 3; i++ { 537 out, err := s.d.Cmd("volume", "create", "-d", volumePluginName, fmt.Sprintf("test%d", i)) 538 assert.NilError(c, err, out) 539 assert.Equal(c, s.ec.caps, 1) 540 out, err = s.d.Cmd("volume", "inspect", "--format={{.Scope}}", fmt.Sprintf("test%d", i)) 541 assert.NilError(c, err) 542 assert.Equal(c, strings.TrimSpace(out), volume.GlobalScope) 543 } 544 } 545 546 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverOutOfBandDelete(c *testing.T) { 547 driverName := stringid.GenerateRandomID() 548 p := newVolumePlugin(c, driverName) 549 defer p.Close() 550 551 s.d.StartWithBusybox(c) 552 553 out, err := s.d.Cmd("volume", "create", "-d", driverName, "--name", "test") 554 assert.NilError(c, err, out) 555 556 out, err = s.d.Cmd("volume", "create", "-d", "local", "--name", "test") 557 assert.ErrorContains(c, err, "", out) 558 assert.Assert(c, strings.Contains(out, "must be unique")) 559 // simulate out of band volume deletion on plugin level 560 delete(p.vols, "test") 561 562 // test re-create with same driver 563 out, err = s.d.Cmd("volume", "create", "-d", driverName, "--opt", "foo=bar", "--name", "test") 564 assert.NilError(c, err, out) 565 out, err = s.d.Cmd("volume", "inspect", "test") 566 assert.NilError(c, err, out) 567 568 var vs []types.Volume 569 err = json.Unmarshal([]byte(out), &vs) 570 assert.NilError(c, err) 571 assert.Equal(c, len(vs), 1) 572 assert.Equal(c, vs[0].Driver, driverName) 573 assert.Assert(c, vs[0].Options != nil) 574 assert.Equal(c, vs[0].Options["foo"], "bar") 575 assert.Equal(c, vs[0].Driver, driverName) 576 577 // simulate out of band volume deletion on plugin level 578 delete(p.vols, "test") 579 580 // test create with different driver 581 out, err = s.d.Cmd("volume", "create", "-d", "local", "--name", "test") 582 assert.NilError(c, err, out) 583 584 out, err = s.d.Cmd("volume", "inspect", "test") 585 assert.NilError(c, err, out) 586 vs = nil 587 err = json.Unmarshal([]byte(out), &vs) 588 assert.NilError(c, err) 589 assert.Equal(c, len(vs), 1) 590 assert.Equal(c, len(vs[0].Options), 0) 591 assert.Equal(c, vs[0].Driver, "local") 592 } 593 594 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverUnmountOnMountFail(c *testing.T) { 595 s.d.StartWithBusybox(c) 596 s.d.Cmd("volume", "create", "-d", "test-external-volume-driver", "--opt=invalidOption=1", "--name=testumount") 597 598 out, _ := s.d.Cmd("run", "-v", "testumount:/foo", "busybox", "true") 599 assert.Equal(c, s.ec.unmounts, 0, out) 600 out, _ = s.d.Cmd("run", "-w", "/foo", "-v", "testumount:/foo", "busybox", "true") 601 assert.Equal(c, s.ec.unmounts, 0, out) 602 } 603 604 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverUnmountOnCp(c *testing.T) { 605 s.d.StartWithBusybox(c) 606 s.d.Cmd("volume", "create", "-d", "test-external-volume-driver", "--name=test") 607 608 out, _ := s.d.Cmd("run", "-d", "--name=test", "-v", "test:/foo", "busybox", "/bin/sh", "-c", "touch /test && top") 609 assert.Equal(c, s.ec.mounts, 1, out) 610 611 out, _ = s.d.Cmd("cp", "test:/test", "/tmp/test") 612 assert.Equal(c, s.ec.mounts, 2, out) 613 assert.Equal(c, s.ec.unmounts, 1, out) 614 615 out, _ = s.d.Cmd("kill", "test") 616 assert.Equal(c, s.ec.unmounts, 2, out) 617 }