diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index a47b109af9..a05d1c7bc3 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -111,6 +111,9 @@ func GetDriver(name, home string, options []string) (Driver, error) { if initFunc, exists := drivers[name]; exists { return initFunc(filepath.Join(home, name), options) } + if pluginDriver, err := lookupPlugin(name, home, options); err == nil { + return pluginDriver, nil + } logrus.Errorf("Failed to GetDriver graph %s %s", name, home) return nil, ErrNotSupported } diff --git a/daemon/graphdriver/plugin.go b/daemon/graphdriver/plugin.go new file mode 100644 index 0000000000..e816cd0d84 --- /dev/null +++ b/daemon/graphdriver/plugin.go @@ -0,0 +1,33 @@ +// +build experimental +// +build daemon + +package graphdriver + +import ( + "fmt" + "io" + + "github.com/docker/docker/pkg/plugins" +) + +type pluginClient interface { + // Call calls the specified method with the specified arguments for the plugin. + Call(string, interface{}, interface{}) error + // Stream calls the specified method with the specified arguments for the plugin and returns the response IO stream + Stream(string, interface{}) (io.ReadCloser, error) + // SendFile calls the specified method, and passes through the IO stream + SendFile(string, io.Reader, interface{}) error +} + +func lookupPlugin(name, home string, opts []string) (Driver, error) { + pl, err := plugins.Get(name, "GraphDriver") + if err != nil { + return nil, fmt.Errorf("Error looking up graphdriver plugin %s: %v", name, err) + } + return newPluginDriver(name, home, opts, pl.Client) +} + +func newPluginDriver(name, home string, opts []string, c pluginClient) (Driver, error) { + proxy := &graphDriverProxy{name, c} + return proxy, proxy.Init(home, opts) +} diff --git a/daemon/graphdriver/plugin_unsupported.go b/daemon/graphdriver/plugin_unsupported.go new file mode 100644 index 0000000000..daa7a170e4 --- /dev/null +++ b/daemon/graphdriver/plugin_unsupported.go @@ -0,0 +1,7 @@ +// +build !experimental + +package graphdriver + +func lookupPlugin(name, home string, opts []string) (Driver, error) { + return nil, ErrNotSupported +} diff --git a/daemon/graphdriver/proxy.go b/daemon/graphdriver/proxy.go new file mode 100644 index 0000000000..ff19dcdfa8 --- /dev/null +++ b/daemon/graphdriver/proxy.go @@ -0,0 +1,210 @@ +// +build experimental +// +build daemon + +package graphdriver + +import ( + "errors" + "fmt" + + "github.com/docker/docker/pkg/archive" +) + +type graphDriverProxy struct { + name string + client pluginClient +} + +type graphDriverRequest struct { + ID string `json:",omitempty"` + Parent string `json:",omitempty"` + MountLabel string `json:",omitempty"` +} + +type graphDriverResponse struct { + Err string `json:",omitempty"` + Dir string `json:",omitempty"` + Exists bool `json:",omitempty"` + Status [][2]string `json:",omitempty"` + Changes []archive.Change `json:",omitempty"` + Size int64 `json:",omitempty"` + Metadata map[string]string `json:",omitempty"` +} + +type graphDriverInitRequest struct { + Home string + Opts []string +} + +func (d *graphDriverProxy) Init(home string, opts []string) error { + args := &graphDriverInitRequest{ + Home: home, + Opts: opts, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Init", args, &ret); err != nil { + return err + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) String() string { + return d.name +} + +func (d *graphDriverProxy) Create(id, parent string) error { + args := &graphDriverRequest{ + ID: id, + Parent: parent, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Create", args, &ret); err != nil { + return err + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) Remove(id string) error { + args := &graphDriverRequest{ID: id} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Remove", args, &ret); err != nil { + return err + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) Get(id, mountLabel string) (string, error) { + args := &graphDriverRequest{ + ID: id, + MountLabel: mountLabel, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Get", args, &ret); err != nil { + return "", err + } + var err error + if ret.Err != "" { + err = errors.New(ret.Err) + } + return ret.Dir, err +} + +func (d *graphDriverProxy) Put(id string) error { + args := &graphDriverRequest{ID: id} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Put", args, &ret); err != nil { + return err + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) Exists(id string) bool { + args := &graphDriverRequest{ID: id} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Exists", args, &ret); err != nil { + return false + } + return ret.Exists +} + +func (d *graphDriverProxy) Status() [][2]string { + args := &graphDriverRequest{} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Status", args, &ret); err != nil { + return nil + } + return ret.Status +} + +func (d *graphDriverProxy) GetMetadata(id string) (map[string]string, error) { + args := &graphDriverRequest{ + ID: id, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.GetMetadata", args, &ret); err != nil { + return nil, err + } + if ret.Err != "" { + return nil, errors.New(ret.Err) + } + return ret.Metadata, nil +} + +func (d *graphDriverProxy) Cleanup() error { + args := &graphDriverRequest{} + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Cleanup", args, &ret); err != nil { + return nil + } + if ret.Err != "" { + return errors.New(ret.Err) + } + return nil +} + +func (d *graphDriverProxy) Diff(id, parent string) (archive.Archive, error) { + args := &graphDriverRequest{ + ID: id, + Parent: parent, + } + body, err := d.client.Stream("GraphDriver.Diff", args) + if err != nil { + body.Close() + return nil, err + } + return archive.Archive(body), nil +} + +func (d *graphDriverProxy) Changes(id, parent string) ([]archive.Change, error) { + args := &graphDriverRequest{ + ID: id, + Parent: parent, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.Changes", args, &ret); err != nil { + return nil, err + } + if ret.Err != "" { + return nil, errors.New(ret.Err) + } + + return ret.Changes, nil +} + +func (d *graphDriverProxy) ApplyDiff(id, parent string, diff archive.Reader) (int64, error) { + var ret graphDriverResponse + if err := d.client.SendFile(fmt.Sprintf("GraphDriver.ApplyDiff?id=%s&parent=%s", id, parent), diff, &ret); err != nil { + return -1, err + } + if ret.Err != "" { + return -1, errors.New(ret.Err) + } + return ret.Size, nil +} + +func (d *graphDriverProxy) DiffSize(id, parent string) (int64, error) { + args := &graphDriverRequest{ + ID: id, + Parent: parent, + } + var ret graphDriverResponse + if err := d.client.Call("GraphDriver.DiffSize", args, &ret); err != nil { + return -1, err + } + if ret.Err != "" { + return -1, errors.New(ret.Err) + } + return ret.Size, nil +} diff --git a/experimental/plugins_graphdriver.md b/experimental/plugins_graphdriver.md new file mode 100644 index 0000000000..700fdde13b --- /dev/null +++ b/experimental/plugins_graphdriver.md @@ -0,0 +1,321 @@ +# Experimental: Docker graph driver plugins + +Docker graph driver plugins enable admins to use an external/out-of-process +graph driver for use with Docker engine. This is an alternative to using the +built-in storage drivers, such as aufs/overlay/devicemapper/btrfs. + +A graph driver plugin is used for image and container fs storage, as such +the plugin must be started and available for connections prior to Docker Engine +being started. + +# Write a graph driver plugin + +See the [plugin documentation](/docs/extend/plugins.md) for detailed information +on the underlying plugin protocol. + + +## Graph Driver plugin protocol + +If a plugin registers itself as a `GraphDriver` when activated, then it is +expected to provide the rootfs for containers as well as image layer storage. + +### /GraphDriver.Init + +**Request**: +``` +{ + "Home": "/graph/home/path", + "Opts": [] +} +``` + +Initialize the graph driver plugin with a home directory and array of options. +Plugins are not required to accept these options as the Docker Engine does not +require that the plugin use this path or options, they are only being passed +through from the user. + +**Response**: +``` +{ + "Err": null +} +``` + +Respond with a string error if an error occurred. + + +### /GraphDriver.Create + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142" +} +``` + +Create a new, empty, filesystem layer with the specified `ID` and `Parent`. +`Parent` may be an empty string, which would indicate that there is no parent +layer. + +**Response**: +``` +{ + "Err: null +} +``` + +Respond with a string error if an error occurred. + + +### /GraphDriver.Remove + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" +} +``` + +Remove the filesystem layer with this given `ID`. + +**Response**: +``` +{ + "Err: null +} +``` + +Respond with a string error if an error occurred. + +### /GraphDriver.Get + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" + "MountLabel": "" +} +``` + +Get the mountpoint for the layered filesystem referred to by the given `ID`. + +**Response**: +``` +{ + "Dir": "/var/mygraph/46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Err": "" +} +``` + +Respond with the absolute path to the mounted layered filesystem. +Respond with a string error if an error occurred. + +### /GraphDriver.Put + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" +} +``` + +Release the system resources for the specified `ID`, such as unmounting the +filesystem layer. + +**Response**: +``` +{ + "Err: null +} +``` + +Respond with a string error if an error occurred. + +### /GraphDriver.Exists + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" +} +``` + +Determine if a filesystem layer with the specified `ID` exists. + +**Response**: +``` +{ + "Exists": true +} +``` + +Respond with a boolean for whether or not the filesystem layer with the specified +`ID` exists. + +### /GraphDriver.Status + +**Request**: +``` +{} +``` + +Get low-level diagnostic information about the graph driver. + +**Response**: +``` +{ + "Status": [[]] +} +``` + +Respond with a 2-D array with key/value pairs for the underlying status +information. + + +### /GraphDriver.GetMetadata + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187" +} +``` + +Get low-level diagnostic information about the layered filesystem with the +with the specified `ID` + +**Response**: +``` +{ + "Metadata": {}, + "Err": null +} +``` + +Respond with a set of key/value pairs containing the low-level diagnostic +information about the layered filesystem. +Respond with a string error if an error occurred. + +### /GraphDriver.Cleanup + +**Request**: +``` +{} +``` + +Perform neccessary tasks to release resources help by the plugin, for example +unmounting all the layered file systems. + +**Response**: +``` +{ + "Err: null +} +``` + +Respond with a string error if an error occurred. + + +### /GraphDriver.Diff + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142" +} +``` + +Get an archive of the changes between the filesystem layers specified by the `ID` +and `Parent`. `Parent` may be an empty string, in which case there is no parent. + +**Response**: +``` +{{ TAR STREAM }} +``` + +### /GraphDriver.Changes + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142" +} +``` + +Get a list of changes between the filesystem layers specified by the `ID` and +`Parent`. `Parent` may be an empty string, in which case there is no parent. + +**Response**: +``` +{ + "Changes": [{}], + "Err": null +} +``` + +Responds with a list of changes. The structure of a change is: +``` + "Path": "/some/path", + "Kind": 0, +``` + +Where teh `Path` is the filesystem path within the layered filesystem that is +changed and `Kind` is an integer specifying the type of change that occurred: + +- 0 - Modified +- 1 - Added +- 2 - Deleted + +Respond with a string error if an error occurred. + +### /GraphDriver.ApplyDiff + +**Request**: +``` +{{ TAR STREAM }} +``` + +Extract the changeset from the given diff into the layer with the specified `ID` +and `Parent` + +**Query Parameters**: + +- id (required)- the `ID` of the new filesystem layer to extract the diff to +- parent (required)- the `Parent` of the given `ID` + +**Response**: +``` +{ + "Size": 512366, + "Err": null +} +``` + +Respond with the size of the new layer in bytes. +Respond with a string error if an error occurred. + +### /GraphDriver.DiffSize + +**Request**: +``` +{ + "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187", + "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142" +} +``` + +Calculate the changes between the specified `ID` + +**Response**: +``` +{ + "Size": 512366, + "Err": null +} +``` + +Respond with the size changes between the specified `ID` and `Parent` +Respond with a string error if an error occurred. diff --git a/integration-cli/check_test.go b/integration-cli/check_test.go index 438ca2a8ab..9abe6dff46 100644 --- a/integration-cli/check_test.go +++ b/integration-cli/check_test.go @@ -1,12 +1,22 @@ package main import ( + "fmt" "testing" + "github.com/docker/docker/pkg/reexec" "github.com/go-check/check" ) func Test(t *testing.T) { + reexec.Init() // This is required for external graphdriver tests + + if !isLocalDaemon { + fmt.Println("INFO: Testing against a remote daemon") + } else { + fmt.Println("INFO: Testing against a local daemon") + } + check.TestingT(t) } diff --git a/integration-cli/docker_cli_external_graphdriver_unix_test.go b/integration-cli/docker_cli_external_graphdriver_unix_test.go new file mode 100644 index 0000000000..842dd21781 --- /dev/null +++ b/integration-cli/docker_cli_external_graphdriver_unix_test.go @@ -0,0 +1,348 @@ +// +build experimental +// +build !windows + +package main + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/daemon/graphdriver/vfs" + "github.com/docker/docker/pkg/archive" + "github.com/go-check/check" +) + +func init() { + check.Suite(&DockerExternalGraphdriverSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerExternalGraphdriverSuite struct { + server *httptest.Server + ds *DockerSuite + d *Daemon + ec *graphEventsCounter +} + +type graphEventsCounter struct { + activations int + creations int + removals int + gets int + puts int + stats int + cleanups int + exists int + init int + metadata int + diff int + applydiff int + changes int + diffsize int +} + +func (s *DockerExternalGraphdriverSuite) SetUpTest(c *check.C) { + s.d = NewDaemon(c) + s.ec = &graphEventsCounter{} +} + +func (s *DockerExternalGraphdriverSuite) TearDownTest(c *check.C) { + s.d.Stop() + s.ds.TearDownTest(c) +} + +func (s *DockerExternalGraphdriverSuite) SetUpSuite(c *check.C) { + mux := http.NewServeMux() + s.server = httptest.NewServer(mux) + + type graphDriverRequest struct { + ID string `json:",omitempty"` + Parent string `json:",omitempty"` + MountLabel string `json:",omitempty"` + } + + type graphDriverResponse struct { + Err error `json:",omitempty"` + Dir string `json:",omitempty"` + Exists bool `json:",omitempty"` + Status [][2]string `json:",omitempty"` + Metadata map[string]string `json:",omitempty"` + Changes []archive.Change `json:",omitempty"` + Size int64 `json:",omitempty"` + } + + respond := func(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") + switch t := data.(type) { + case error: + fmt.Fprintln(w, fmt.Sprintf(`{"Err": %s}`, t.Error())) + case string: + fmt.Fprintln(w, t) + default: + json.NewEncoder(w).Encode(&data) + } + } + + base, err := ioutil.TempDir("", "external-graph-test") + c.Assert(err, check.IsNil) + vfsProto, err := vfs.Init(base, []string{}) + if err != nil { + c.Fatalf("error initializing graph driver: %v", err) + } + driver := graphdriver.NaiveDiffDriver(vfsProto) + + mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + s.ec.activations++ + respond(w, `{"Implements": ["GraphDriver"]}`) + }) + + mux.HandleFunc("/GraphDriver.Init", func(w http.ResponseWriter, r *http.Request) { + s.ec.init++ + respond(w, "{}") + }) + + mux.HandleFunc("/GraphDriver.Create", func(w http.ResponseWriter, r *http.Request) { + s.ec.creations++ + + var req graphDriverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 500) + return + } + if err := driver.Create(req.ID, req.Parent); err != nil { + respond(w, err) + return + } + respond(w, "{}") + }) + + mux.HandleFunc("/GraphDriver.Remove", func(w http.ResponseWriter, r *http.Request) { + s.ec.removals++ + + var req graphDriverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 500) + return + } + + if err := driver.Remove(req.ID); err != nil { + respond(w, err) + return + } + respond(w, "{}") + }) + + mux.HandleFunc("/GraphDriver.Get", func(w http.ResponseWriter, r *http.Request) { + s.ec.gets++ + + var req graphDriverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 500) + } + + dir, err := driver.Get(req.ID, req.MountLabel) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Dir: dir}) + }) + + mux.HandleFunc("/GraphDriver.Put", func(w http.ResponseWriter, r *http.Request) { + s.ec.puts++ + + var req graphDriverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 500) + return + } + + if err := driver.Put(req.ID); err != nil { + respond(w, err) + return + } + respond(w, "{}") + }) + + mux.HandleFunc("/GraphDriver.Exists", func(w http.ResponseWriter, r *http.Request) { + s.ec.exists++ + + var req graphDriverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 500) + return + } + respond(w, &graphDriverResponse{Exists: driver.Exists(req.ID)}) + }) + + mux.HandleFunc("/GraphDriver.Status", func(w http.ResponseWriter, r *http.Request) { + s.ec.stats++ + respond(w, `{"Status":{}}`) + }) + + mux.HandleFunc("/GraphDriver.Cleanup", func(w http.ResponseWriter, r *http.Request) { + s.ec.cleanups++ + err := driver.Cleanup() + if err != nil { + respond(w, err) + return + } + respond(w, `{}`) + }) + + mux.HandleFunc("/GraphDriver.GetMetadata", func(w http.ResponseWriter, r *http.Request) { + s.ec.metadata++ + + var req graphDriverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 500) + return + } + + data, err := driver.GetMetadata(req.ID) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Metadata: data}) + }) + + mux.HandleFunc("/GraphDriver.Diff", func(w http.ResponseWriter, r *http.Request) { + s.ec.diff++ + + var req graphDriverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 500) + return + } + + diff, err := driver.Diff(req.ID, req.Parent) + if err != nil { + respond(w, err) + return + } + io.Copy(w, diff) + }) + + mux.HandleFunc("/GraphDriver.Changes", func(w http.ResponseWriter, r *http.Request) { + s.ec.changes++ + var req graphDriverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 500) + return + } + + changes, err := driver.Changes(req.ID, req.Parent) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Changes: changes}) + }) + + mux.HandleFunc("/GraphDriver.ApplyDiff", func(w http.ResponseWriter, r *http.Request) { + s.ec.applydiff++ + id := r.URL.Query().Get("id") + parent := r.URL.Query().Get("parent") + + size, err := driver.ApplyDiff(id, parent, r.Body) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Size: size}) + }) + + mux.HandleFunc("/GraphDriver.DiffSize", func(w http.ResponseWriter, r *http.Request) { + s.ec.diffsize++ + + var req graphDriverRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 500) + return + } + + size, err := driver.DiffSize(req.ID, req.Parent) + if err != nil { + respond(w, err) + return + } + respond(w, &graphDriverResponse{Size: size}) + }) + + if err := os.MkdirAll("/etc/docker/plugins", 0755); err != nil { + c.Fatal(err) + } + + if err := ioutil.WriteFile("/etc/docker/plugins/test-external-graph-driver.spec", []byte(s.server.URL), 0644); err != nil { + c.Fatal(err) + } +} + +func (s *DockerExternalGraphdriverSuite) TearDownSuite(c *check.C) { + s.server.Close() + + if err := os.RemoveAll("/etc/docker/plugins"); err != nil { + c.Fatal(err) + } +} + +func (s *DockerExternalGraphdriverSuite) TestExternalGraphDriver(c *check.C) { + c.Assert(s.d.StartWithBusybox("-s", "test-external-graph-driver"), check.IsNil) + + out, err := s.d.Cmd("run", "-d", "--name=graphtest", "busybox", "sh", "-c", "echo hello > /hello") + c.Assert(err, check.IsNil, check.Commentf(out)) + + err = s.d.Restart("-s", "test-external-graph-driver") + + out, err = s.d.Cmd("inspect", "--format='{{.GraphDriver.Name}}'", "graphtest") + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(strings.TrimSpace(out), check.Equals, "test-external-graph-driver") + + out, err = s.d.Cmd("diff", "graphtest") + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(strings.Contains(out, "A /hello"), check.Equals, true) + + out, err = s.d.Cmd("rm", "-f", "graphtest") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("info") + c.Assert(err, check.IsNil, check.Commentf(out)) + + err = s.d.Stop() + c.Assert(err, check.IsNil) + + c.Assert(s.ec.activations, check.Equals, 2) + c.Assert(s.ec.init, check.Equals, 2) + c.Assert(s.ec.creations >= 1, check.Equals, true) + c.Assert(s.ec.removals >= 1, check.Equals, true) + c.Assert(s.ec.gets >= 1, check.Equals, true) + c.Assert(s.ec.puts >= 1, check.Equals, true) + c.Assert(s.ec.stats, check.Equals, 1) + c.Assert(s.ec.cleanups, check.Equals, 2) + c.Assert(s.ec.exists >= 1, check.Equals, true) + c.Assert(s.ec.applydiff >= 1, check.Equals, true) + c.Assert(s.ec.changes, check.Equals, 1) + c.Assert(s.ec.diffsize, check.Equals, 0) + c.Assert(s.ec.diff, check.Equals, 0) + c.Assert(s.ec.metadata, check.Equals, 1) +} + +func (s *DockerExternalGraphdriverSuite) TestExternalGraphDriverPull(c *check.C) { + testRequires(c, Network) + c.Assert(s.d.Start(), check.IsNil) + + out, err := s.d.Cmd("pull", "busybox:latest") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("run", "-d", "busybox", "top") + c.Assert(err, check.IsNil, check.Commentf(out)) +} diff --git a/integration-cli/docker_test_vars.go b/integration-cli/docker_test_vars.go index a7f2e3dc08..5e2355cd54 100644 --- a/integration-cli/docker_test_vars.go +++ b/integration-cli/docker_test_vars.go @@ -68,11 +68,8 @@ func init() { // Similarly, it will be perfectly valid to also run CLI tests from // a Linux CLI (built with the daemon tag) against a Windows daemon. if len(os.Getenv("DOCKER_REMOTE_DAEMON")) > 0 { - fmt.Println("INFO: Testing against a remote daemon") isLocalDaemon = false } else { - fmt.Println("INFO: Testing against a local daemon") isLocalDaemon = true } - } diff --git a/pkg/plugins/client.go b/pkg/plugins/client.go index 973858c903..b1234d6fd1 100644 --- a/pkg/plugins/client.go +++ b/pkg/plugins/client.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "strings" @@ -52,19 +53,41 @@ type Client struct { // Call calls the specified method with the specified arguments for the plugin. // It will retry for 30 seconds if a failure occurs when calling. func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) error { - return c.callWithRetry(serviceMethod, args, ret, true) -} - -func (c *Client) callWithRetry(serviceMethod string, args interface{}, ret interface{}, retry bool) error { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(args); err != nil { return err } - - req, err := http.NewRequest("POST", "/"+serviceMethod, &buf) + body, err := c.callWithRetry(serviceMethod, &buf, true) if err != nil { return err } + defer body.Close() + return json.NewDecoder(body).Decode(&ret) +} + +// Stream calls the specified method with the specified arguments for the plugin and returns the response body +func (c *Client) Stream(serviceMethod string, args interface{}) (io.ReadCloser, error) { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(args); err != nil { + return nil, err + } + return c.callWithRetry(serviceMethod, &buf, true) +} + +// SendFile calls the specified method, and passes through the IO stream +func (c *Client) SendFile(serviceMethod string, data io.Reader, ret interface{}) error { + body, err := c.callWithRetry(serviceMethod, data, true) + if err != nil { + return err + } + return json.NewDecoder(body).Decode(&ret) +} + +func (c *Client) callWithRetry(serviceMethod string, data io.Reader, retry bool) (io.ReadCloser, error) { + req, err := http.NewRequest("POST", "/"+serviceMethod, data) + if err != nil { + return nil, err + } req.Header.Add("Accept", versionMimetype) req.URL.Scheme = "http" req.URL.Host = c.addr @@ -76,12 +99,12 @@ func (c *Client) callWithRetry(serviceMethod string, args interface{}, ret inter resp, err := c.http.Do(req) if err != nil { if !retry { - return err + return nil, err } timeOff := backoff(retries) if abort(start, timeOff) { - return err + return nil, err } retries++ logrus.Warnf("Unable to connect to plugin: %s, retrying in %v", c.addr, timeOff) @@ -89,16 +112,14 @@ func (c *Client) callWithRetry(serviceMethod string, args interface{}, ret inter continue } - defer resp.Body.Close() if resp.StatusCode != http.StatusOK { remoteErr, err := ioutil.ReadAll(resp.Body) if err != nil { - return &remoteError{err.Error(), serviceMethod} + return nil, &remoteError{err.Error(), serviceMethod} } - return &remoteError{string(remoteErr), serviceMethod} + return nil, &remoteError{string(remoteErr), serviceMethod} } - - return json.NewDecoder(resp.Body).Decode(&ret) + return resp.Body, nil } } diff --git a/pkg/plugins/client_test.go b/pkg/plugins/client_test.go index 6a2c96f713..1c70bc7c02 100644 --- a/pkg/plugins/client_test.go +++ b/pkg/plugins/client_test.go @@ -30,7 +30,7 @@ func teardownRemotePluginServer() { func TestFailedConnection(t *testing.T) { c, _ := NewClient("tcp://127.0.0.1:1", tlsconfig.Options{InsecureSkipVerify: true}) - err := c.callWithRetry("Service.Method", nil, nil, false) + _, err := c.callWithRetry("Service.Method", nil, false) if err == nil { t.Fatal("Unexpected successful connection") }