Browse code

Create extpoint for graphdrivers

Allows people to create out-of-process graphdrivers that can be used
with Docker.

Extensions must be started before Docker otherwise Docker will fail to
start.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2015/06/06 05:09:53
Showing 10 changed files
... ...
@@ -111,6 +111,9 @@ func GetDriver(name, home string, options []string) (Driver, error) {
111 111
 	if initFunc, exists := drivers[name]; exists {
112 112
 		return initFunc(filepath.Join(home, name), options)
113 113
 	}
114
+	if pluginDriver, err := lookupPlugin(name, home, options); err == nil {
115
+		return pluginDriver, nil
116
+	}
114 117
 	logrus.Errorf("Failed to GetDriver graph %s %s", name, home)
115 118
 	return nil, ErrNotSupported
116 119
 }
117 120
new file mode 100644
... ...
@@ -0,0 +1,33 @@
0
+// +build experimental
1
+// +build daemon
2
+
3
+package graphdriver
4
+
5
+import (
6
+	"fmt"
7
+	"io"
8
+
9
+	"github.com/docker/docker/pkg/plugins"
10
+)
11
+
12
+type pluginClient interface {
13
+	// Call calls the specified method with the specified arguments for the plugin.
14
+	Call(string, interface{}, interface{}) error
15
+	// Stream calls the specified method with the specified arguments for the plugin and returns the response IO stream
16
+	Stream(string, interface{}) (io.ReadCloser, error)
17
+	// SendFile calls the specified method, and passes through the IO stream
18
+	SendFile(string, io.Reader, interface{}) error
19
+}
20
+
21
+func lookupPlugin(name, home string, opts []string) (Driver, error) {
22
+	pl, err := plugins.Get(name, "GraphDriver")
23
+	if err != nil {
24
+		return nil, fmt.Errorf("Error looking up graphdriver plugin %s: %v", name, err)
25
+	}
26
+	return newPluginDriver(name, home, opts, pl.Client)
27
+}
28
+
29
+func newPluginDriver(name, home string, opts []string, c pluginClient) (Driver, error) {
30
+	proxy := &graphDriverProxy{name, c}
31
+	return proxy, proxy.Init(home, opts)
32
+}
0 33
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+// +build !experimental
1
+
2
+package graphdriver
3
+
4
+func lookupPlugin(name, home string, opts []string) (Driver, error) {
5
+	return nil, ErrNotSupported
6
+}
0 7
new file mode 100644
... ...
@@ -0,0 +1,210 @@
0
+// +build experimental
1
+// +build daemon
2
+
3
+package graphdriver
4
+
5
+import (
6
+	"errors"
7
+	"fmt"
8
+
9
+	"github.com/docker/docker/pkg/archive"
10
+)
11
+
12
+type graphDriverProxy struct {
13
+	name   string
14
+	client pluginClient
15
+}
16
+
17
+type graphDriverRequest struct {
18
+	ID         string `json:",omitempty"`
19
+	Parent     string `json:",omitempty"`
20
+	MountLabel string `json:",omitempty"`
21
+}
22
+
23
+type graphDriverResponse struct {
24
+	Err      string            `json:",omitempty"`
25
+	Dir      string            `json:",omitempty"`
26
+	Exists   bool              `json:",omitempty"`
27
+	Status   [][2]string       `json:",omitempty"`
28
+	Changes  []archive.Change  `json:",omitempty"`
29
+	Size     int64             `json:",omitempty"`
30
+	Metadata map[string]string `json:",omitempty"`
31
+}
32
+
33
+type graphDriverInitRequest struct {
34
+	Home string
35
+	Opts []string
36
+}
37
+
38
+func (d *graphDriverProxy) Init(home string, opts []string) error {
39
+	args := &graphDriverInitRequest{
40
+		Home: home,
41
+		Opts: opts,
42
+	}
43
+	var ret graphDriverResponse
44
+	if err := d.client.Call("GraphDriver.Init", args, &ret); err != nil {
45
+		return err
46
+	}
47
+	if ret.Err != "" {
48
+		return errors.New(ret.Err)
49
+	}
50
+	return nil
51
+}
52
+
53
+func (d *graphDriverProxy) String() string {
54
+	return d.name
55
+}
56
+
57
+func (d *graphDriverProxy) Create(id, parent string) error {
58
+	args := &graphDriverRequest{
59
+		ID:     id,
60
+		Parent: parent,
61
+	}
62
+	var ret graphDriverResponse
63
+	if err := d.client.Call("GraphDriver.Create", args, &ret); err != nil {
64
+		return err
65
+	}
66
+	if ret.Err != "" {
67
+		return errors.New(ret.Err)
68
+	}
69
+	return nil
70
+}
71
+
72
+func (d *graphDriverProxy) Remove(id string) error {
73
+	args := &graphDriverRequest{ID: id}
74
+	var ret graphDriverResponse
75
+	if err := d.client.Call("GraphDriver.Remove", args, &ret); err != nil {
76
+		return err
77
+	}
78
+	if ret.Err != "" {
79
+		return errors.New(ret.Err)
80
+	}
81
+	return nil
82
+}
83
+
84
+func (d *graphDriverProxy) Get(id, mountLabel string) (string, error) {
85
+	args := &graphDriverRequest{
86
+		ID:         id,
87
+		MountLabel: mountLabel,
88
+	}
89
+	var ret graphDriverResponse
90
+	if err := d.client.Call("GraphDriver.Get", args, &ret); err != nil {
91
+		return "", err
92
+	}
93
+	var err error
94
+	if ret.Err != "" {
95
+		err = errors.New(ret.Err)
96
+	}
97
+	return ret.Dir, err
98
+}
99
+
100
+func (d *graphDriverProxy) Put(id string) error {
101
+	args := &graphDriverRequest{ID: id}
102
+	var ret graphDriverResponse
103
+	if err := d.client.Call("GraphDriver.Put", args, &ret); err != nil {
104
+		return err
105
+	}
106
+	if ret.Err != "" {
107
+		return errors.New(ret.Err)
108
+	}
109
+	return nil
110
+}
111
+
112
+func (d *graphDriverProxy) Exists(id string) bool {
113
+	args := &graphDriverRequest{ID: id}
114
+	var ret graphDriverResponse
115
+	if err := d.client.Call("GraphDriver.Exists", args, &ret); err != nil {
116
+		return false
117
+	}
118
+	return ret.Exists
119
+}
120
+
121
+func (d *graphDriverProxy) Status() [][2]string {
122
+	args := &graphDriverRequest{}
123
+	var ret graphDriverResponse
124
+	if err := d.client.Call("GraphDriver.Status", args, &ret); err != nil {
125
+		return nil
126
+	}
127
+	return ret.Status
128
+}
129
+
130
+func (d *graphDriverProxy) GetMetadata(id string) (map[string]string, error) {
131
+	args := &graphDriverRequest{
132
+		ID: id,
133
+	}
134
+	var ret graphDriverResponse
135
+	if err := d.client.Call("GraphDriver.GetMetadata", args, &ret); err != nil {
136
+		return nil, err
137
+	}
138
+	if ret.Err != "" {
139
+		return nil, errors.New(ret.Err)
140
+	}
141
+	return ret.Metadata, nil
142
+}
143
+
144
+func (d *graphDriverProxy) Cleanup() error {
145
+	args := &graphDriverRequest{}
146
+	var ret graphDriverResponse
147
+	if err := d.client.Call("GraphDriver.Cleanup", args, &ret); err != nil {
148
+		return nil
149
+	}
150
+	if ret.Err != "" {
151
+		return errors.New(ret.Err)
152
+	}
153
+	return nil
154
+}
155
+
156
+func (d *graphDriverProxy) Diff(id, parent string) (archive.Archive, error) {
157
+	args := &graphDriverRequest{
158
+		ID:     id,
159
+		Parent: parent,
160
+	}
161
+	body, err := d.client.Stream("GraphDriver.Diff", args)
162
+	if err != nil {
163
+		body.Close()
164
+		return nil, err
165
+	}
166
+	return archive.Archive(body), nil
167
+}
168
+
169
+func (d *graphDriverProxy) Changes(id, parent string) ([]archive.Change, error) {
170
+	args := &graphDriverRequest{
171
+		ID:     id,
172
+		Parent: parent,
173
+	}
174
+	var ret graphDriverResponse
175
+	if err := d.client.Call("GraphDriver.Changes", args, &ret); err != nil {
176
+		return nil, err
177
+	}
178
+	if ret.Err != "" {
179
+		return nil, errors.New(ret.Err)
180
+	}
181
+
182
+	return ret.Changes, nil
183
+}
184
+
185
+func (d *graphDriverProxy) ApplyDiff(id, parent string, diff archive.Reader) (int64, error) {
186
+	var ret graphDriverResponse
187
+	if err := d.client.SendFile(fmt.Sprintf("GraphDriver.ApplyDiff?id=%s&parent=%s", id, parent), diff, &ret); err != nil {
188
+		return -1, err
189
+	}
190
+	if ret.Err != "" {
191
+		return -1, errors.New(ret.Err)
192
+	}
193
+	return ret.Size, nil
194
+}
195
+
196
+func (d *graphDriverProxy) DiffSize(id, parent string) (int64, error) {
197
+	args := &graphDriverRequest{
198
+		ID:     id,
199
+		Parent: parent,
200
+	}
201
+	var ret graphDriverResponse
202
+	if err := d.client.Call("GraphDriver.DiffSize", args, &ret); err != nil {
203
+		return -1, err
204
+	}
205
+	if ret.Err != "" {
206
+		return -1, errors.New(ret.Err)
207
+	}
208
+	return ret.Size, nil
209
+}
0 210
new file mode 100644
... ...
@@ -0,0 +1,321 @@
0
+# Experimental: Docker graph driver plugins
1
+
2
+Docker graph driver plugins enable admins to use an external/out-of-process
3
+graph driver for use with Docker engine. This is an alternative to using the
4
+built-in storage drivers, such as aufs/overlay/devicemapper/btrfs.
5
+
6
+A graph driver plugin is used for image and container fs storage, as such
7
+the plugin must be started and available for connections prior to Docker Engine
8
+being started.
9
+
10
+# Write a graph driver plugin
11
+
12
+See the [plugin documentation](/docs/extend/plugins.md) for detailed information
13
+on the underlying plugin protocol.
14
+
15
+
16
+## Graph Driver plugin protocol
17
+
18
+If a plugin registers itself as a `GraphDriver` when activated, then it is
19
+expected to provide the rootfs for containers as well as image layer storage.
20
+
21
+### /GraphDriver.Init
22
+
23
+**Request**:
24
+```
25
+{
26
+  "Home": "/graph/home/path",
27
+  "Opts": []
28
+}
29
+```
30
+
31
+Initialize the graph driver plugin with a home directory and array of options.
32
+Plugins are not required to accept these options as the Docker Engine does not
33
+require that the plugin use this path or options, they are only being passed
34
+through from the user.
35
+
36
+**Response**:
37
+```
38
+{
39
+  "Err": null
40
+}
41
+```
42
+
43
+Respond with a string error if an error occurred.
44
+
45
+
46
+### /GraphDriver.Create
47
+
48
+**Request**:
49
+```
50
+{
51
+  "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187",
52
+  "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142"
53
+}
54
+```
55
+
56
+Create a new, empty, filesystem layer with the specified `ID` and `Parent`.
57
+`Parent` may be an empty string, which would indicate that there is no parent
58
+layer.
59
+
60
+**Response**:
61
+```
62
+{
63
+  "Err: null
64
+}
65
+```
66
+
67
+Respond with a string error if an error occurred.
68
+
69
+
70
+### /GraphDriver.Remove
71
+
72
+**Request**:
73
+```
74
+{
75
+  "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187"
76
+}
77
+```
78
+
79
+Remove the filesystem layer with this given `ID`.
80
+
81
+**Response**:
82
+```
83
+{
84
+  "Err: null
85
+}
86
+```
87
+
88
+Respond with a string error if an error occurred.
89
+
90
+### /GraphDriver.Get
91
+
92
+**Request**:
93
+```
94
+{
95
+  "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187"
96
+  "MountLabel": ""
97
+}
98
+```
99
+
100
+Get the mountpoint for the layered filesystem referred to by the given `ID`.
101
+
102
+**Response**:
103
+```
104
+{
105
+  "Dir": "/var/mygraph/46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187",
106
+  "Err": ""
107
+}
108
+```
109
+
110
+Respond with the absolute path to the mounted layered filesystem.
111
+Respond with a string error if an error occurred.
112
+
113
+### /GraphDriver.Put
114
+
115
+**Request**:
116
+```
117
+{
118
+  "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187"
119
+}
120
+```
121
+
122
+Release the system resources for the specified `ID`, such as unmounting the
123
+filesystem layer.
124
+
125
+**Response**:
126
+```
127
+{
128
+  "Err: null
129
+}
130
+```
131
+
132
+Respond with a string error if an error occurred.
133
+
134
+### /GraphDriver.Exists
135
+
136
+**Request**:
137
+```
138
+{
139
+  "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187"
140
+}
141
+```
142
+
143
+Determine if a filesystem layer with the specified `ID` exists.
144
+
145
+**Response**:
146
+```
147
+{
148
+  "Exists": true
149
+}
150
+```
151
+
152
+Respond with a boolean for whether or not the filesystem layer with the specified
153
+`ID` exists.
154
+
155
+### /GraphDriver.Status
156
+
157
+**Request**:
158
+```
159
+{}
160
+```
161
+
162
+Get low-level diagnostic information about the graph driver.
163
+
164
+**Response**:
165
+```
166
+{
167
+  "Status": [[]]
168
+}
169
+```
170
+
171
+Respond with a 2-D array with key/value pairs for the underlying status
172
+information.
173
+
174
+
175
+### /GraphDriver.GetMetadata
176
+
177
+**Request**:
178
+```
179
+{
180
+  "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187"
181
+}
182
+```
183
+
184
+Get low-level diagnostic information about the layered filesystem with the
185
+with the specified `ID`
186
+
187
+**Response**:
188
+```
189
+{
190
+  "Metadata": {},
191
+  "Err": null
192
+}
193
+```
194
+
195
+Respond with a set of key/value pairs containing the low-level diagnostic
196
+information about the layered filesystem.
197
+Respond with a string error if an error occurred.
198
+
199
+### /GraphDriver.Cleanup
200
+
201
+**Request**:
202
+```
203
+{}
204
+```
205
+
206
+Perform neccessary tasks to release resources help by the plugin, for example
207
+unmounting all the layered file systems.
208
+
209
+**Response**:
210
+```
211
+{
212
+  "Err: null
213
+}
214
+```
215
+
216
+Respond with a string error if an error occurred.
217
+
218
+
219
+### /GraphDriver.Diff
220
+
221
+**Request**:
222
+```
223
+{
224
+  "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187",
225
+  "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142"
226
+}
227
+```
228
+
229
+Get an archive of the changes between the filesystem layers specified by the `ID`
230
+and `Parent`. `Parent` may be an empty string, in which case there is no parent.
231
+
232
+**Response**:
233
+```
234
+{{ TAR STREAM }}
235
+```
236
+
237
+### /GraphDriver.Changes
238
+
239
+**Request**:
240
+```
241
+{
242
+  "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187",
243
+  "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142"
244
+}
245
+```
246
+
247
+Get a list of changes between the filesystem layers specified by the `ID` and
248
+`Parent`. `Parent` may be an empty string, in which case there is no parent.
249
+
250
+**Response**:
251
+```
252
+{
253
+  "Changes": [{}],
254
+  "Err": null
255
+}
256
+```
257
+
258
+Responds with a list of changes. The structure of a change is:
259
+```
260
+  "Path": "/some/path",
261
+  "Kind": 0,
262
+```
263
+
264
+Where teh `Path` is the filesystem path within the layered filesystem that is
265
+changed and `Kind` is an integer specifying the type of change that occurred:
266
+
267
+- 0 - Modified
268
+- 1 - Added
269
+- 2 - Deleted
270
+
271
+Respond with a string error if an error occurred.
272
+
273
+### /GraphDriver.ApplyDiff
274
+
275
+**Request**:
276
+```
277
+{{ TAR STREAM }}
278
+```
279
+
280
+Extract the changeset from the given diff into the layer with the specified `ID`
281
+and `Parent`
282
+
283
+**Query Parameters**:
284
+
285
+- id (required)- the `ID` of the new filesystem layer to extract the diff to
286
+- parent (required)- the `Parent` of the given `ID`
287
+
288
+**Response**:
289
+```
290
+{
291
+  "Size": 512366,
292
+  "Err": null
293
+}
294
+```
295
+
296
+Respond with the size of the new layer in bytes.
297
+Respond with a string error if an error occurred.
298
+
299
+### /GraphDriver.DiffSize
300
+
301
+**Request**:
302
+```
303
+{
304
+  "ID": "46fe8644f2572fd1e505364f7581e0c9dbc7f14640bd1fb6ce97714fb6fc5187",
305
+  "Parent": "2cd9c322cb78a55e8212aa3ea8425a4180236d7106938ec921d0935a4b8ca142"
306
+}
307
+```
308
+
309
+Calculate the changes between the specified `ID`
310
+
311
+**Response**:
312
+```
313
+{
314
+  "Size": 512366,
315
+  "Err": null
316
+}
317
+```
318
+
319
+Respond with the size changes between the specified `ID` and `Parent`
320
+Respond with a string error if an error occurred.
... ...
@@ -1,12 +1,22 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"testing"
5 6
 
7
+	"github.com/docker/docker/pkg/reexec"
6 8
 	"github.com/go-check/check"
7 9
 )
8 10
 
9 11
 func Test(t *testing.T) {
12
+	reexec.Init() // This is required for external graphdriver tests
13
+
14
+	if !isLocalDaemon {
15
+		fmt.Println("INFO: Testing against a remote daemon")
16
+	} else {
17
+		fmt.Println("INFO: Testing against a local daemon")
18
+	}
19
+
10 20
 	check.TestingT(t)
11 21
 }
12 22
 
13 23
new file mode 100644
... ...
@@ -0,0 +1,348 @@
0
+// +build experimental
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
+	"strings"
14
+
15
+	"github.com/docker/docker/daemon/graphdriver"
16
+	"github.com/docker/docker/daemon/graphdriver/vfs"
17
+	"github.com/docker/docker/pkg/archive"
18
+	"github.com/go-check/check"
19
+)
20
+
21
+func init() {
22
+	check.Suite(&DockerExternalGraphdriverSuite{
23
+		ds: &DockerSuite{},
24
+	})
25
+}
26
+
27
+type DockerExternalGraphdriverSuite struct {
28
+	server *httptest.Server
29
+	ds     *DockerSuite
30
+	d      *Daemon
31
+	ec     *graphEventsCounter
32
+}
33
+
34
+type graphEventsCounter struct {
35
+	activations int
36
+	creations   int
37
+	removals    int
38
+	gets        int
39
+	puts        int
40
+	stats       int
41
+	cleanups    int
42
+	exists      int
43
+	init        int
44
+	metadata    int
45
+	diff        int
46
+	applydiff   int
47
+	changes     int
48
+	diffsize    int
49
+}
50
+
51
+func (s *DockerExternalGraphdriverSuite) SetUpTest(c *check.C) {
52
+	s.d = NewDaemon(c)
53
+	s.ec = &graphEventsCounter{}
54
+}
55
+
56
+func (s *DockerExternalGraphdriverSuite) TearDownTest(c *check.C) {
57
+	s.d.Stop()
58
+	s.ds.TearDownTest(c)
59
+}
60
+
61
+func (s *DockerExternalGraphdriverSuite) SetUpSuite(c *check.C) {
62
+	mux := http.NewServeMux()
63
+	s.server = httptest.NewServer(mux)
64
+
65
+	type graphDriverRequest struct {
66
+		ID         string `json:",omitempty"`
67
+		Parent     string `json:",omitempty"`
68
+		MountLabel string `json:",omitempty"`
69
+	}
70
+
71
+	type graphDriverResponse struct {
72
+		Err      error             `json:",omitempty"`
73
+		Dir      string            `json:",omitempty"`
74
+		Exists   bool              `json:",omitempty"`
75
+		Status   [][2]string       `json:",omitempty"`
76
+		Metadata map[string]string `json:",omitempty"`
77
+		Changes  []archive.Change  `json:",omitempty"`
78
+		Size     int64             `json:",omitempty"`
79
+	}
80
+
81
+	respond := func(w http.ResponseWriter, data interface{}) {
82
+		w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json")
83
+		switch t := data.(type) {
84
+		case error:
85
+			fmt.Fprintln(w, fmt.Sprintf(`{"Err": %s}`, t.Error()))
86
+		case string:
87
+			fmt.Fprintln(w, t)
88
+		default:
89
+			json.NewEncoder(w).Encode(&data)
90
+		}
91
+	}
92
+
93
+	base, err := ioutil.TempDir("", "external-graph-test")
94
+	c.Assert(err, check.IsNil)
95
+	vfsProto, err := vfs.Init(base, []string{})
96
+	if err != nil {
97
+		c.Fatalf("error initializing graph driver: %v", err)
98
+	}
99
+	driver := graphdriver.NaiveDiffDriver(vfsProto)
100
+
101
+	mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
102
+		s.ec.activations++
103
+		respond(w, `{"Implements": ["GraphDriver"]}`)
104
+	})
105
+
106
+	mux.HandleFunc("/GraphDriver.Init", func(w http.ResponseWriter, r *http.Request) {
107
+		s.ec.init++
108
+		respond(w, "{}")
109
+	})
110
+
111
+	mux.HandleFunc("/GraphDriver.Create", func(w http.ResponseWriter, r *http.Request) {
112
+		s.ec.creations++
113
+
114
+		var req graphDriverRequest
115
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
116
+			http.Error(w, err.Error(), 500)
117
+			return
118
+		}
119
+		if err := driver.Create(req.ID, req.Parent); err != nil {
120
+			respond(w, err)
121
+			return
122
+		}
123
+		respond(w, "{}")
124
+	})
125
+
126
+	mux.HandleFunc("/GraphDriver.Remove", func(w http.ResponseWriter, r *http.Request) {
127
+		s.ec.removals++
128
+
129
+		var req graphDriverRequest
130
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
131
+			http.Error(w, err.Error(), 500)
132
+			return
133
+		}
134
+
135
+		if err := driver.Remove(req.ID); err != nil {
136
+			respond(w, err)
137
+			return
138
+		}
139
+		respond(w, "{}")
140
+	})
141
+
142
+	mux.HandleFunc("/GraphDriver.Get", func(w http.ResponseWriter, r *http.Request) {
143
+		s.ec.gets++
144
+
145
+		var req graphDriverRequest
146
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
147
+			http.Error(w, err.Error(), 500)
148
+		}
149
+
150
+		dir, err := driver.Get(req.ID, req.MountLabel)
151
+		if err != nil {
152
+			respond(w, err)
153
+			return
154
+		}
155
+		respond(w, &graphDriverResponse{Dir: dir})
156
+	})
157
+
158
+	mux.HandleFunc("/GraphDriver.Put", func(w http.ResponseWriter, r *http.Request) {
159
+		s.ec.puts++
160
+
161
+		var req graphDriverRequest
162
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
163
+			http.Error(w, err.Error(), 500)
164
+			return
165
+		}
166
+
167
+		if err := driver.Put(req.ID); err != nil {
168
+			respond(w, err)
169
+			return
170
+		}
171
+		respond(w, "{}")
172
+	})
173
+
174
+	mux.HandleFunc("/GraphDriver.Exists", func(w http.ResponseWriter, r *http.Request) {
175
+		s.ec.exists++
176
+
177
+		var req graphDriverRequest
178
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
179
+			http.Error(w, err.Error(), 500)
180
+			return
181
+		}
182
+		respond(w, &graphDriverResponse{Exists: driver.Exists(req.ID)})
183
+	})
184
+
185
+	mux.HandleFunc("/GraphDriver.Status", func(w http.ResponseWriter, r *http.Request) {
186
+		s.ec.stats++
187
+		respond(w, `{"Status":{}}`)
188
+	})
189
+
190
+	mux.HandleFunc("/GraphDriver.Cleanup", func(w http.ResponseWriter, r *http.Request) {
191
+		s.ec.cleanups++
192
+		err := driver.Cleanup()
193
+		if err != nil {
194
+			respond(w, err)
195
+			return
196
+		}
197
+		respond(w, `{}`)
198
+	})
199
+
200
+	mux.HandleFunc("/GraphDriver.GetMetadata", func(w http.ResponseWriter, r *http.Request) {
201
+		s.ec.metadata++
202
+
203
+		var req graphDriverRequest
204
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
205
+			http.Error(w, err.Error(), 500)
206
+			return
207
+		}
208
+
209
+		data, err := driver.GetMetadata(req.ID)
210
+		if err != nil {
211
+			respond(w, err)
212
+			return
213
+		}
214
+		respond(w, &graphDriverResponse{Metadata: data})
215
+	})
216
+
217
+	mux.HandleFunc("/GraphDriver.Diff", func(w http.ResponseWriter, r *http.Request) {
218
+		s.ec.diff++
219
+
220
+		var req graphDriverRequest
221
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
222
+			http.Error(w, err.Error(), 500)
223
+			return
224
+		}
225
+
226
+		diff, err := driver.Diff(req.ID, req.Parent)
227
+		if err != nil {
228
+			respond(w, err)
229
+			return
230
+		}
231
+		io.Copy(w, diff)
232
+	})
233
+
234
+	mux.HandleFunc("/GraphDriver.Changes", func(w http.ResponseWriter, r *http.Request) {
235
+		s.ec.changes++
236
+		var req graphDriverRequest
237
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
238
+			http.Error(w, err.Error(), 500)
239
+			return
240
+		}
241
+
242
+		changes, err := driver.Changes(req.ID, req.Parent)
243
+		if err != nil {
244
+			respond(w, err)
245
+			return
246
+		}
247
+		respond(w, &graphDriverResponse{Changes: changes})
248
+	})
249
+
250
+	mux.HandleFunc("/GraphDriver.ApplyDiff", func(w http.ResponseWriter, r *http.Request) {
251
+		s.ec.applydiff++
252
+		id := r.URL.Query().Get("id")
253
+		parent := r.URL.Query().Get("parent")
254
+
255
+		size, err := driver.ApplyDiff(id, parent, r.Body)
256
+		if err != nil {
257
+			respond(w, err)
258
+			return
259
+		}
260
+		respond(w, &graphDriverResponse{Size: size})
261
+	})
262
+
263
+	mux.HandleFunc("/GraphDriver.DiffSize", func(w http.ResponseWriter, r *http.Request) {
264
+		s.ec.diffsize++
265
+
266
+		var req graphDriverRequest
267
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
268
+			http.Error(w, err.Error(), 500)
269
+			return
270
+		}
271
+
272
+		size, err := driver.DiffSize(req.ID, req.Parent)
273
+		if err != nil {
274
+			respond(w, err)
275
+			return
276
+		}
277
+		respond(w, &graphDriverResponse{Size: size})
278
+	})
279
+
280
+	if err := os.MkdirAll("/etc/docker/plugins", 0755); err != nil {
281
+		c.Fatal(err)
282
+	}
283
+
284
+	if err := ioutil.WriteFile("/etc/docker/plugins/test-external-graph-driver.spec", []byte(s.server.URL), 0644); err != nil {
285
+		c.Fatal(err)
286
+	}
287
+}
288
+
289
+func (s *DockerExternalGraphdriverSuite) TearDownSuite(c *check.C) {
290
+	s.server.Close()
291
+
292
+	if err := os.RemoveAll("/etc/docker/plugins"); err != nil {
293
+		c.Fatal(err)
294
+	}
295
+}
296
+
297
+func (s *DockerExternalGraphdriverSuite) TestExternalGraphDriver(c *check.C) {
298
+	c.Assert(s.d.StartWithBusybox("-s", "test-external-graph-driver"), check.IsNil)
299
+
300
+	out, err := s.d.Cmd("run", "-d", "--name=graphtest", "busybox", "sh", "-c", "echo hello > /hello")
301
+	c.Assert(err, check.IsNil, check.Commentf(out))
302
+
303
+	err = s.d.Restart("-s", "test-external-graph-driver")
304
+
305
+	out, err = s.d.Cmd("inspect", "--format='{{.GraphDriver.Name}}'", "graphtest")
306
+	c.Assert(err, check.IsNil, check.Commentf(out))
307
+	c.Assert(strings.TrimSpace(out), check.Equals, "test-external-graph-driver")
308
+
309
+	out, err = s.d.Cmd("diff", "graphtest")
310
+	c.Assert(err, check.IsNil, check.Commentf(out))
311
+	c.Assert(strings.Contains(out, "A /hello"), check.Equals, true)
312
+
313
+	out, err = s.d.Cmd("rm", "-f", "graphtest")
314
+	c.Assert(err, check.IsNil, check.Commentf(out))
315
+
316
+	out, err = s.d.Cmd("info")
317
+	c.Assert(err, check.IsNil, check.Commentf(out))
318
+
319
+	err = s.d.Stop()
320
+	c.Assert(err, check.IsNil)
321
+
322
+	c.Assert(s.ec.activations, check.Equals, 2)
323
+	c.Assert(s.ec.init, check.Equals, 2)
324
+	c.Assert(s.ec.creations >= 1, check.Equals, true)
325
+	c.Assert(s.ec.removals >= 1, check.Equals, true)
326
+	c.Assert(s.ec.gets >= 1, check.Equals, true)
327
+	c.Assert(s.ec.puts >= 1, check.Equals, true)
328
+	c.Assert(s.ec.stats, check.Equals, 1)
329
+	c.Assert(s.ec.cleanups, check.Equals, 2)
330
+	c.Assert(s.ec.exists >= 1, check.Equals, true)
331
+	c.Assert(s.ec.applydiff >= 1, check.Equals, true)
332
+	c.Assert(s.ec.changes, check.Equals, 1)
333
+	c.Assert(s.ec.diffsize, check.Equals, 0)
334
+	c.Assert(s.ec.diff, check.Equals, 0)
335
+	c.Assert(s.ec.metadata, check.Equals, 1)
336
+}
337
+
338
+func (s *DockerExternalGraphdriverSuite) TestExternalGraphDriverPull(c *check.C) {
339
+	testRequires(c, Network)
340
+	c.Assert(s.d.Start(), check.IsNil)
341
+
342
+	out, err := s.d.Cmd("pull", "busybox:latest")
343
+	c.Assert(err, check.IsNil, check.Commentf(out))
344
+
345
+	out, err = s.d.Cmd("run", "-d", "busybox", "top")
346
+	c.Assert(err, check.IsNil, check.Commentf(out))
347
+}
... ...
@@ -68,11 +68,8 @@ func init() {
68 68
 	// Similarly, it will be perfectly valid to also run CLI tests from
69 69
 	// a Linux CLI (built with the daemon tag) against a Windows daemon.
70 70
 	if len(os.Getenv("DOCKER_REMOTE_DAEMON")) > 0 {
71
-		fmt.Println("INFO: Testing against a remote daemon")
72 71
 		isLocalDaemon = false
73 72
 	} else {
74
-		fmt.Println("INFO: Testing against a local daemon")
75 73
 		isLocalDaemon = true
76 74
 	}
77
-
78 75
 }
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"bytes"
5 5
 	"encoding/json"
6 6
 	"fmt"
7
+	"io"
7 8
 	"io/ioutil"
8 9
 	"net/http"
9 10
 	"strings"
... ...
@@ -52,19 +53,41 @@ type Client struct {
52 52
 // Call calls the specified method with the specified arguments for the plugin.
53 53
 // It will retry for 30 seconds if a failure occurs when calling.
54 54
 func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) error {
55
-	return c.callWithRetry(serviceMethod, args, ret, true)
55
+	var buf bytes.Buffer
56
+	if err := json.NewEncoder(&buf).Encode(args); err != nil {
57
+		return err
58
+	}
59
+	body, err := c.callWithRetry(serviceMethod, &buf, true)
60
+	if err != nil {
61
+		return err
62
+	}
63
+	defer body.Close()
64
+	return json.NewDecoder(body).Decode(&ret)
56 65
 }
57 66
 
58
-func (c *Client) callWithRetry(serviceMethod string, args interface{}, ret interface{}, retry bool) error {
67
+// Stream calls the specified method with the specified arguments for the plugin and returns the response body
68
+func (c *Client) Stream(serviceMethod string, args interface{}) (io.ReadCloser, error) {
59 69
 	var buf bytes.Buffer
60 70
 	if err := json.NewEncoder(&buf).Encode(args); err != nil {
61
-		return err
71
+		return nil, err
62 72
 	}
73
+	return c.callWithRetry(serviceMethod, &buf, true)
74
+}
63 75
 
64
-	req, err := http.NewRequest("POST", "/"+serviceMethod, &buf)
76
+// SendFile calls the specified method, and passes through the IO stream
77
+func (c *Client) SendFile(serviceMethod string, data io.Reader, ret interface{}) error {
78
+	body, err := c.callWithRetry(serviceMethod, data, true)
65 79
 	if err != nil {
66 80
 		return err
67 81
 	}
82
+	return json.NewDecoder(body).Decode(&ret)
83
+}
84
+
85
+func (c *Client) callWithRetry(serviceMethod string, data io.Reader, retry bool) (io.ReadCloser, error) {
86
+	req, err := http.NewRequest("POST", "/"+serviceMethod, data)
87
+	if err != nil {
88
+		return nil, err
89
+	}
68 90
 	req.Header.Add("Accept", versionMimetype)
69 91
 	req.URL.Scheme = "http"
70 92
 	req.URL.Host = c.addr
... ...
@@ -76,12 +99,12 @@ func (c *Client) callWithRetry(serviceMethod string, args interface{}, ret inter
76 76
 		resp, err := c.http.Do(req)
77 77
 		if err != nil {
78 78
 			if !retry {
79
-				return err
79
+				return nil, err
80 80
 			}
81 81
 
82 82
 			timeOff := backoff(retries)
83 83
 			if abort(start, timeOff) {
84
-				return err
84
+				return nil, err
85 85
 			}
86 86
 			retries++
87 87
 			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
89 89
 			continue
90 90
 		}
91 91
 
92
-		defer resp.Body.Close()
93 92
 		if resp.StatusCode != http.StatusOK {
94 93
 			remoteErr, err := ioutil.ReadAll(resp.Body)
95 94
 			if err != nil {
96
-				return &remoteError{err.Error(), serviceMethod}
95
+				return nil, &remoteError{err.Error(), serviceMethod}
97 96
 			}
98
-			return &remoteError{string(remoteErr), serviceMethod}
97
+			return nil, &remoteError{string(remoteErr), serviceMethod}
99 98
 		}
100
-
101
-		return json.NewDecoder(resp.Body).Decode(&ret)
99
+		return resp.Body, nil
102 100
 	}
103 101
 }
104 102
 
... ...
@@ -30,7 +30,7 @@ func teardownRemotePluginServer() {
30 30
 
31 31
 func TestFailedConnection(t *testing.T) {
32 32
 	c, _ := NewClient("tcp://127.0.0.1:1", tlsconfig.Options{InsecureSkipVerify: true})
33
-	err := c.callWithRetry("Service.Method", nil, nil, false)
33
+	_, err := c.callWithRetry("Service.Method", nil, false)
34 34
 	if err == nil {
35 35
 		t.Fatal("Unexpected successful connection")
36 36
 	}