moby/integration-cli/docker_cli_external_volume_driver_unix_test.go
Aaron Lehmann 501083e82a Fix volume Create to check against canonical driver name
Previously, it was comparing against the driver name passed in by the
caller. This could lead to subtle issues when using plugins, like
"plugin" vs. "plugin:latest".

Also, remove "conflict:" prefix to improve the error message.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
(cherry picked from commit a854cf262336e5625ec06e8e12e8ebc1500ce656)
2016-12-16 10:44:26 -08:00

627 lines
19 KiB
Go

// +build !windows
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/integration/checker"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/volume"
"github.com/go-check/check"
)
const volumePluginName = "test-external-volume-driver"
func init() {
check.Suite(&DockerExternalVolumeSuite{
ds: &DockerSuite{},
})
}
type eventCounter struct {
activations int
creations int
removals int
mounts int
unmounts int
paths int
lists int
gets int
caps int
}
type DockerExternalVolumeSuite struct {
ds *DockerSuite
d *Daemon
*volumePlugin
}
func (s *DockerExternalVolumeSuite) SetUpTest(c *check.C) {
s.d = NewDaemon(c)
s.ec = &eventCounter{}
}
func (s *DockerExternalVolumeSuite) TearDownTest(c *check.C) {
s.d.Stop()
s.ds.TearDownTest(c)
}
func (s *DockerExternalVolumeSuite) SetUpSuite(c *check.C) {
s.volumePlugin = newVolumePlugin(c, volumePluginName)
}
type volumePlugin struct {
ec *eventCounter
*httptest.Server
vols map[string]vol
}
type vol struct {
Name string
Mountpoint string
Ninja bool // hack used to trigger a null volume return on `Get`
Status map[string]interface{}
Options map[string]string
}
func (p *volumePlugin) Close() {
p.Server.Close()
}
func newVolumePlugin(c *check.C, name string) *volumePlugin {
mux := http.NewServeMux()
s := &volumePlugin{Server: httptest.NewServer(mux), ec: &eventCounter{}, vols: make(map[string]vol)}
type pluginRequest struct {
Name string
Opts map[string]string
ID string
}
type pluginResp struct {
Mountpoint string `json:",omitempty"`
Err string `json:",omitempty"`
}
read := func(b io.ReadCloser) (pluginRequest, error) {
defer b.Close()
var pr pluginRequest
if err := json.NewDecoder(b).Decode(&pr); err != nil {
return pr, err
}
return pr, nil
}
send := func(w http.ResponseWriter, data interface{}) {
switch t := data.(type) {
case error:
http.Error(w, t.Error(), 500)
case string:
w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
fmt.Fprintln(w, t)
default:
w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
json.NewEncoder(w).Encode(&data)
}
}
mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
s.ec.activations++
send(w, `{"Implements": ["VolumeDriver"]}`)
})
mux.HandleFunc("/VolumeDriver.Create", func(w http.ResponseWriter, r *http.Request) {
s.ec.creations++
pr, err := read(r.Body)
if err != nil {
send(w, err)
return
}
_, isNinja := pr.Opts["ninja"]
status := map[string]interface{}{"Hello": "world"}
s.vols[pr.Name] = vol{Name: pr.Name, Ninja: isNinja, Status: status, Options: pr.Opts}
send(w, nil)
})
mux.HandleFunc("/VolumeDriver.List", func(w http.ResponseWriter, r *http.Request) {
s.ec.lists++
vols := make([]vol, 0, len(s.vols))
for _, v := range s.vols {
if v.Ninja {
continue
}
vols = append(vols, v)
}
send(w, map[string][]vol{"Volumes": vols})
})
mux.HandleFunc("/VolumeDriver.Get", func(w http.ResponseWriter, r *http.Request) {
s.ec.gets++
pr, err := read(r.Body)
if err != nil {
send(w, err)
return
}
v, exists := s.vols[pr.Name]
if !exists {
send(w, `{"Err": "no such volume"}`)
}
if v.Ninja {
send(w, map[string]vol{})
return
}
v.Mountpoint = hostVolumePath(pr.Name)
send(w, map[string]vol{"Volume": v})
return
})
mux.HandleFunc("/VolumeDriver.Remove", func(w http.ResponseWriter, r *http.Request) {
s.ec.removals++
pr, err := read(r.Body)
if err != nil {
send(w, err)
return
}
v, ok := s.vols[pr.Name]
if !ok {
send(w, nil)
return
}
if err := os.RemoveAll(hostVolumePath(v.Name)); err != nil {
send(w, &pluginResp{Err: err.Error()})
return
}
delete(s.vols, v.Name)
send(w, nil)
})
mux.HandleFunc("/VolumeDriver.Path", func(w http.ResponseWriter, r *http.Request) {
s.ec.paths++
pr, err := read(r.Body)
if err != nil {
send(w, err)
return
}
p := hostVolumePath(pr.Name)
send(w, &pluginResp{Mountpoint: p})
})
mux.HandleFunc("/VolumeDriver.Mount", func(w http.ResponseWriter, r *http.Request) {
s.ec.mounts++
pr, err := read(r.Body)
if err != nil {
send(w, err)
return
}
if v, exists := s.vols[pr.Name]; exists {
// Use this to simulate a mount failure
if _, exists := v.Options["invalidOption"]; exists {
send(w, fmt.Errorf("invalid argument"))
return
}
}
p := hostVolumePath(pr.Name)
if err := os.MkdirAll(p, 0755); err != nil {
send(w, &pluginResp{Err: err.Error()})
return
}
if err := ioutil.WriteFile(filepath.Join(p, "test"), []byte(s.Server.URL), 0644); err != nil {
send(w, err)
return
}
if err := ioutil.WriteFile(filepath.Join(p, "mountID"), []byte(pr.ID), 0644); err != nil {
send(w, err)
return
}
send(w, &pluginResp{Mountpoint: p})
})
mux.HandleFunc("/VolumeDriver.Unmount", func(w http.ResponseWriter, r *http.Request) {
s.ec.unmounts++
_, err := read(r.Body)
if err != nil {
send(w, err)
return
}
send(w, nil)
})
mux.HandleFunc("/VolumeDriver.Capabilities", func(w http.ResponseWriter, r *http.Request) {
s.ec.caps++
_, err := read(r.Body)
if err != nil {
send(w, err)
return
}
send(w, `{"Capabilities": { "Scope": "global" }}`)
})
err := os.MkdirAll("/etc/docker/plugins", 0755)
c.Assert(err, checker.IsNil)
err = ioutil.WriteFile("/etc/docker/plugins/"+name+".spec", []byte(s.Server.URL), 0644)
c.Assert(err, checker.IsNil)
return s
}
func (s *DockerExternalVolumeSuite) TearDownSuite(c *check.C) {
s.volumePlugin.Close()
err := os.RemoveAll("/etc/docker/plugins")
c.Assert(err, checker.IsNil)
}
func (s *DockerExternalVolumeSuite) TestVolumeCLICreateOptionConflict(c *check.C) {
dockerCmd(c, "volume", "create", "test")
out, _, err := dockerCmdWithError("volume", "create", "test", "--driver", volumePluginName)
c.Assert(err, check.NotNil, check.Commentf("volume create exception name already in use with another driver"))
c.Assert(out, checker.Contains, "A volume named test already exists")
out, _ = dockerCmd(c, "volume", "inspect", "--format={{ .Driver }}", "test")
_, _, err = dockerCmdWithError("volume", "create", "test", "--driver", strings.TrimSpace(out))
c.Assert(err, check.IsNil)
// make sure hidden --name option conflicts with positional arg name
out, _, err = dockerCmdWithError("volume", "create", "--name", "test2", "test2")
c.Assert(err, check.NotNil, check.Commentf("Conflicting options: either specify --name or provide positional arg, not both"))
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverNamed(c *check.C) {
err := s.d.StartWithBusybox()
c.Assert(err, checker.IsNil)
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")
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(out, checker.Contains, s.Server.URL)
_, err = s.d.Cmd("volume", "rm", "external-volume-test")
c.Assert(err, checker.IsNil)
p := hostVolumePath("external-volume-test")
_, err = os.Lstat(p)
c.Assert(err, checker.NotNil)
c.Assert(os.IsNotExist(err), checker.True, check.Commentf("Expected volume path in host to not exist: %s, %v\n", p, err))
c.Assert(s.ec.activations, checker.Equals, 1)
c.Assert(s.ec.creations, checker.Equals, 1)
c.Assert(s.ec.removals, checker.Equals, 1)
c.Assert(s.ec.mounts, checker.Equals, 1)
c.Assert(s.ec.unmounts, checker.Equals, 1)
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverUnnamed(c *check.C) {
err := s.d.StartWithBusybox()
c.Assert(err, checker.IsNil)
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")
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(out, checker.Contains, s.Server.URL)
c.Assert(s.ec.activations, checker.Equals, 1)
c.Assert(s.ec.creations, checker.Equals, 1)
c.Assert(s.ec.removals, checker.Equals, 1)
c.Assert(s.ec.mounts, checker.Equals, 1)
c.Assert(s.ec.unmounts, checker.Equals, 1)
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverVolumesFrom(c *check.C) {
err := s.d.StartWithBusybox()
c.Assert(err, checker.IsNil)
out, err := s.d.Cmd("run", "--name", "vol-test1", "-v", "/foo", "--volume-driver", volumePluginName, "busybox:latest")
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = s.d.Cmd("run", "--rm", "--volumes-from", "vol-test1", "--name", "vol-test2", "busybox", "ls", "/tmp")
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = s.d.Cmd("rm", "-fv", "vol-test1")
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(s.ec.activations, checker.Equals, 1)
c.Assert(s.ec.creations, checker.Equals, 1)
c.Assert(s.ec.removals, checker.Equals, 1)
c.Assert(s.ec.mounts, checker.Equals, 2)
c.Assert(s.ec.unmounts, checker.Equals, 2)
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverDeleteContainer(c *check.C) {
err := s.d.StartWithBusybox()
c.Assert(err, checker.IsNil)
out, err := s.d.Cmd("run", "--name", "vol-test1", "-v", "/foo", "--volume-driver", volumePluginName, "busybox:latest")
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = s.d.Cmd("rm", "-fv", "vol-test1")
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(s.ec.activations, checker.Equals, 1)
c.Assert(s.ec.creations, checker.Equals, 1)
c.Assert(s.ec.removals, checker.Equals, 1)
c.Assert(s.ec.mounts, checker.Equals, 1)
c.Assert(s.ec.unmounts, checker.Equals, 1)
}
func hostVolumePath(name string) string {
return fmt.Sprintf("/var/lib/docker/volumes/%s", name)
}
// Make sure a request to use a down driver doesn't block other requests
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverLookupNotBlocked(c *check.C) {
specPath := "/etc/docker/plugins/down-driver.spec"
err := ioutil.WriteFile(specPath, []byte("tcp://127.0.0.7:9999"), 0644)
c.Assert(err, check.IsNil)
defer os.RemoveAll(specPath)
chCmd1 := make(chan struct{})
chCmd2 := make(chan error)
cmd1 := exec.Command(dockerBinary, "volume", "create", "-d", "down-driver")
cmd2 := exec.Command(dockerBinary, "volume", "create")
c.Assert(cmd1.Start(), checker.IsNil)
defer cmd1.Process.Kill()
time.Sleep(100 * time.Millisecond) // ensure API has been called
c.Assert(cmd2.Start(), checker.IsNil)
go func() {
cmd1.Wait()
close(chCmd1)
}()
go func() {
chCmd2 <- cmd2.Wait()
}()
select {
case <-chCmd1:
cmd2.Process.Kill()
c.Fatalf("volume create with down driver finished unexpectedly")
case err := <-chCmd2:
c.Assert(err, checker.IsNil)
case <-time.After(5 * time.Second):
cmd2.Process.Kill()
c.Fatal("volume creates are blocked by previous create requests when previous driver is down")
}
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverRetryNotImmediatelyExists(c *check.C) {
err := s.d.StartWithBusybox()
c.Assert(err, checker.IsNil)
specPath := "/etc/docker/plugins/test-external-volume-driver-retry.spec"
os.RemoveAll(specPath)
defer os.RemoveAll(specPath)
errchan := make(chan error)
go func() {
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 {
errchan <- fmt.Errorf("%v:\n%s", err, out)
}
close(errchan)
}()
go func() {
// wait for a retry to occur, then create spec to allow plugin to register
time.Sleep(2000 * time.Millisecond)
// no need to check for an error here since it will get picked up by the timeout later
ioutil.WriteFile(specPath, []byte(s.Server.URL), 0644)
}()
select {
case err := <-errchan:
c.Assert(err, checker.IsNil)
case <-time.After(8 * time.Second):
c.Fatal("volume creates fail when plugin not immediately available")
}
_, err = s.d.Cmd("volume", "rm", "external-volume-test")
c.Assert(err, checker.IsNil)
c.Assert(s.ec.activations, checker.Equals, 1)
c.Assert(s.ec.creations, checker.Equals, 1)
c.Assert(s.ec.removals, checker.Equals, 1)
c.Assert(s.ec.mounts, checker.Equals, 1)
c.Assert(s.ec.unmounts, checker.Equals, 1)
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverBindExternalVolume(c *check.C) {
dockerCmd(c, "volume", "create", "-d", volumePluginName, "foo")
dockerCmd(c, "run", "-d", "--name", "testing", "-v", "foo:/bar", "busybox", "top")
var mounts []struct {
Name string
Driver string
}
out := inspectFieldJSON(c, "testing", "Mounts")
c.Assert(json.NewDecoder(strings.NewReader(out)).Decode(&mounts), checker.IsNil)
c.Assert(len(mounts), checker.Equals, 1, check.Commentf(out))
c.Assert(mounts[0].Name, checker.Equals, "foo")
c.Assert(mounts[0].Driver, checker.Equals, volumePluginName)
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverList(c *check.C) {
dockerCmd(c, "volume", "create", "-d", volumePluginName, "abc3")
out, _ := dockerCmd(c, "volume", "ls")
ls := strings.Split(strings.TrimSpace(out), "\n")
c.Assert(len(ls), check.Equals, 2, check.Commentf("\n%s", out))
vol := strings.Fields(ls[len(ls)-1])
c.Assert(len(vol), check.Equals, 2, check.Commentf("%v", vol))
c.Assert(vol[0], check.Equals, volumePluginName)
c.Assert(vol[1], check.Equals, "abc3")
c.Assert(s.ec.lists, check.Equals, 1)
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverGet(c *check.C) {
out, _, err := dockerCmdWithError("volume", "inspect", "dummy")
c.Assert(err, check.NotNil, check.Commentf(out))
c.Assert(out, checker.Contains, "No such volume")
c.Assert(s.ec.gets, check.Equals, 1)
dockerCmd(c, "volume", "create", "test", "-d", volumePluginName)
out, _ = dockerCmd(c, "volume", "inspect", "test")
type vol struct {
Status map[string]string
}
var st []vol
c.Assert(json.Unmarshal([]byte(out), &st), checker.IsNil)
c.Assert(st, checker.HasLen, 1)
c.Assert(st[0].Status, checker.HasLen, 1, check.Commentf("%v", st[0]))
c.Assert(st[0].Status["Hello"], checker.Equals, "world", check.Commentf("%v", st[0].Status))
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverWithDaemonRestart(c *check.C) {
dockerCmd(c, "volume", "create", "-d", volumePluginName, "abc1")
err := s.d.Restart()
c.Assert(err, checker.IsNil)
dockerCmd(c, "run", "--name=test", "-v", "abc1:/foo", "busybox", "true")
var mounts []types.MountPoint
inspectFieldAndMarshall(c, "test", "Mounts", &mounts)
c.Assert(mounts, checker.HasLen, 1)
c.Assert(mounts[0].Driver, checker.Equals, volumePluginName)
}
// Ensures that the daemon handles when the plugin responds to a `Get` request with a null volume and a null error.
// Prior the daemon would panic in this scenario.
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverGetEmptyResponse(c *check.C) {
c.Assert(s.d.Start(), checker.IsNil)
out, err := s.d.Cmd("volume", "create", "-d", volumePluginName, "abc2", "--opt", "ninja=1")
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = s.d.Cmd("volume", "inspect", "abc2")
c.Assert(err, checker.NotNil, check.Commentf(out))
c.Assert(out, checker.Contains, "No such volume")
}
// Ensure only cached paths are used in volume list to prevent N+1 calls to `VolumeDriver.Path`
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverPathCalls(c *check.C) {
c.Assert(s.d.Start(), checker.IsNil)
c.Assert(s.ec.paths, checker.Equals, 0)
out, err := s.d.Cmd("volume", "create", "test", "--driver=test-external-volume-driver")
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(s.ec.paths, checker.Equals, 1)
out, err = s.d.Cmd("volume", "ls")
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(s.ec.paths, checker.Equals, 1)
out, err = s.d.Cmd("volume", "inspect", "--format='{{.Mountpoint}}'", "test")
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
c.Assert(s.ec.paths, checker.Equals, 1)
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverMountID(c *check.C) {
err := s.d.StartWithBusybox()
c.Assert(err, checker.IsNil)
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")
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
}
// Check that VolumeDriver.Capabilities gets called, and only called once
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverCapabilities(c *check.C) {
c.Assert(s.d.Start(), checker.IsNil)
c.Assert(s.ec.caps, checker.Equals, 0)
for i := 0; i < 3; i++ {
out, err := s.d.Cmd("volume", "create", "-d", volumePluginName, fmt.Sprintf("test%d", i))
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(s.ec.caps, checker.Equals, 1)
out, err = s.d.Cmd("volume", "inspect", "--format={{.Scope}}", fmt.Sprintf("test%d", i))
c.Assert(err, checker.IsNil)
c.Assert(strings.TrimSpace(out), checker.Equals, volume.GlobalScope)
}
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverOutOfBandDelete(c *check.C) {
driverName := stringid.GenerateNonCryptoID()
p := newVolumePlugin(c, driverName)
defer p.Close()
c.Assert(s.d.StartWithBusybox(), checker.IsNil)
out, err := s.d.Cmd("volume", "create", "-d", driverName, "--name", "test")
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = s.d.Cmd("volume", "create", "-d", "local", "--name", "test")
c.Assert(err, checker.NotNil, check.Commentf(out))
c.Assert(out, checker.Contains, "volume named test already exists")
// simulate out of band volume deletion on plugin level
delete(p.vols, "test")
// test re-create with same driver
out, err = s.d.Cmd("volume", "create", "-d", driverName, "--opt", "foo=bar", "--name", "test")
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = s.d.Cmd("volume", "inspect", "test")
c.Assert(err, checker.IsNil, check.Commentf(out))
var vs []types.Volume
err = json.Unmarshal([]byte(out), &vs)
c.Assert(err, checker.IsNil)
c.Assert(vs, checker.HasLen, 1)
c.Assert(vs[0].Driver, checker.Equals, driverName)
c.Assert(vs[0].Options, checker.NotNil)
c.Assert(vs[0].Options["foo"], checker.Equals, "bar")
c.Assert(vs[0].Driver, checker.Equals, driverName)
// simulate out of band volume deletion on plugin level
delete(p.vols, "test")
// test create with different driver
out, err = s.d.Cmd("volume", "create", "-d", "local", "--name", "test")
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = s.d.Cmd("volume", "inspect", "test")
c.Assert(err, checker.IsNil, check.Commentf(out))
vs = nil
err = json.Unmarshal([]byte(out), &vs)
c.Assert(err, checker.IsNil)
c.Assert(vs, checker.HasLen, 1)
c.Assert(vs[0].Options, checker.HasLen, 0)
c.Assert(vs[0].Driver, checker.Equals, "local")
}
func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverUnmountOnMountFail(c *check.C) {
c.Assert(s.d.StartWithBusybox(), checker.IsNil)
s.d.Cmd("volume", "create", "-d", "test-external-volume-driver", "--opt=invalidOption=1", "--name=testumount")
out, _ := s.d.Cmd("run", "-v", "testumount:/foo", "busybox", "true")
c.Assert(s.ec.unmounts, checker.Equals, 0, check.Commentf(out))
out, _ = s.d.Cmd("run", "-w", "/foo", "-v", "testumount:/foo", "busybox", "true")
c.Assert(s.ec.unmounts, checker.Equals, 0, check.Commentf(out))
}