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