Browse code

Plugins JSON spec.

Allow full configuration of external plugins via a JSON document.

Signed-off-by: David Calavera <david.calavera@gmail.com>

David Calavera authored on 2015/05/28 07:21:18
Showing 11 changed files
... ...
@@ -15,8 +15,8 @@ import (
15 15
 	"github.com/docker/docker/cliconfig"
16 16
 	"github.com/docker/docker/pkg/homedir"
17 17
 	flag "github.com/docker/docker/pkg/mflag"
18
+	"github.com/docker/docker/pkg/sockets"
18 19
 	"github.com/docker/docker/pkg/term"
19
-	"github.com/docker/docker/utils"
20 20
 )
21 21
 
22 22
 // DockerCli represents the docker command line client.
... ...
@@ -210,7 +210,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, keyFile string, proto, a
210 210
 	tr := &http.Transport{
211 211
 		TLSClientConfig: tlsConfig,
212 212
 	}
213
-	utils.ConfigureTCPTransport(tr, proto, addr)
213
+	sockets.ConfigureTCPTransport(tr, proto, addr)
214 214
 
215 215
 	configFile, e := cliconfig.Load(filepath.Join(homedir.Get(), ".docker"))
216 216
 	if e != nil {
... ...
@@ -26,18 +26,35 @@ containers is recommended.
26 26
 Docker discovers plugins by looking for them in the plugin directory whenever a
27 27
 user or container tries to use one by name.
28 28
 
29
-There are two types of files which can be put in the plugin directory.
29
+There are three types of files which can be put in the plugin directory.
30 30
 
31 31
 * `.sock` files are UNIX domain sockets.
32 32
 * `.spec` files are text files containing a URL, such as `unix:///other.sock`.
33
+* `.json` files are text files containing a full json specification for the plugin.
33 34
 
34 35
 The name of the file (excluding the extension) determines the plugin name.
35 36
 
36 37
 For example, the `flocker` plugin might create a UNIX socket at
37 38
 `/usr/share/docker/plugins/flocker.sock`.
38 39
 
39
-Plugins must be run locally on the same machine as the Docker daemon.  UNIX
40
-domain sockets are strongly encouraged for security reasons.
40
+### JSON specification
41
+
42
+This is the JSON format for a plugin:
43
+
44
+```json
45
+{
46
+  "Name": "plugin-example",
47
+  "Addr": "https://example.com/docker/plugin",
48
+  "TLSConfig": {
49
+    "InsecureSkipVerify": false,
50
+    "CAFile": "/usr/shared/docker/certs/example-ca.pem",
51
+    "CertFile": "/usr/shared/docker/certs/example-cert.pem",
52
+    "KeyFile": "/usr/shared/docker/certs/example-key.pem",
53
+  }
54
+}
55
+```
56
+
57
+The `TLSConfig` field is optional and TLS will only be verified if this configuration is present.
41 58
 
42 59
 ## Plugin lifecycle
43 60
 
... ...
@@ -41,7 +41,6 @@ type DockerExternalVolumeSuite struct {
41 41
 func (s *DockerExternalVolumeSuite) SetUpTest(c *check.C) {
42 42
 	s.d = NewDaemon(c)
43 43
 	s.ec = &eventCounter{}
44
-
45 44
 }
46 45
 
47 46
 func (s *DockerExternalVolumeSuite) TearDownTest(c *check.C) {
... ...
@@ -5,12 +5,13 @@ import (
5 5
 	"encoding/json"
6 6
 	"fmt"
7 7
 	"io/ioutil"
8
-	"net"
9 8
 	"net/http"
10 9
 	"strings"
11 10
 	"time"
12 11
 
13 12
 	"github.com/Sirupsen/logrus"
13
+	"github.com/docker/docker/pkg/sockets"
14
+	"github.com/docker/docker/pkg/tlsconfig"
14 15
 )
15 16
 
16 17
 const (
... ...
@@ -18,11 +19,18 @@ const (
18 18
 	defaultTimeOut  = 30
19 19
 )
20 20
 
21
-func NewClient(addr string) *Client {
21
+func NewClient(addr string, tlsConfig tlsconfig.Options) (*Client, error) {
22 22
 	tr := &http.Transport{}
23
+
24
+	c, err := tlsconfig.Client(tlsConfig)
25
+	if err != nil {
26
+		return nil, err
27
+	}
28
+	tr.TLSClientConfig = c
29
+
23 30
 	protoAndAddr := strings.Split(addr, "://")
24
-	configureTCPTransport(tr, protoAndAddr[0], protoAndAddr[1])
25
-	return &Client{&http.Client{Transport: tr}, protoAndAddr[1]}
31
+	sockets.ConfigureTCPTransport(tr, protoAndAddr[0], protoAndAddr[1])
32
+	return &Client{&http.Client{Transport: tr}, protoAndAddr[1]}, nil
26 33
 }
27 34
 
28 35
 type Client struct {
... ...
@@ -96,18 +104,3 @@ func backoff(retries int) time.Duration {
96 96
 func abort(start time.Time, timeOff time.Duration) bool {
97 97
 	return timeOff+time.Since(start) > time.Duration(defaultTimeOut)*time.Second
98 98
 }
99
-
100
-func configureTCPTransport(tr *http.Transport, proto, addr string) {
101
-	// Why 32? See https://github.com/docker/docker/pull/8035.
102
-	timeout := 32 * time.Second
103
-	if proto == "unix" {
104
-		// No need for compression in local communications.
105
-		tr.DisableCompression = true
106
-		tr.Dial = func(_, _ string) (net.Conn, error) {
107
-			return net.DialTimeout(proto, addr, timeout)
108
-		}
109
-	} else {
110
-		tr.Proxy = http.ProxyFromEnvironment
111
-		tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
112
-	}
113
-}
... ...
@@ -7,6 +7,8 @@ import (
7 7
 	"reflect"
8 8
 	"testing"
9 9
 	"time"
10
+
11
+	"github.com/docker/docker/pkg/tlsconfig"
10 12
 )
11 13
 
12 14
 var (
... ...
@@ -27,7 +29,7 @@ func teardownRemotePluginServer() {
27 27
 }
28 28
 
29 29
 func TestFailedConnection(t *testing.T) {
30
-	c := NewClient("tcp://127.0.0.1:1")
30
+	c, _ := NewClient("tcp://127.0.0.1:1", tlsconfig.Options{InsecureSkipVerify: true})
31 31
 	err := c.callWithRetry("Service.Method", nil, nil, false)
32 32
 	if err == nil {
33 33
 		t.Fatal("Unexpected successful connection")
... ...
@@ -51,7 +53,7 @@ func TestEchoInputOutput(t *testing.T) {
51 51
 		io.Copy(w, r.Body)
52 52
 	})
53 53
 
54
-	c := NewClient(addr)
54
+	c, _ := NewClient(addr, tlsconfig.Options{InsecureSkipVerify: true})
55 55
 	var output Manifest
56 56
 	err := c.Call("Test.Echo", m, &output)
57 57
 	if err != nil {
... ...
@@ -1,6 +1,7 @@
1 1
 package plugins
2 2
 
3 3
 import (
4
+	"encoding/json"
4 5
 	"errors"
5 6
 	"fmt"
6 7
 	"io/ioutil"
... ...
@@ -37,25 +38,25 @@ func (l *LocalRegistry) Plugin(name string) (*Plugin, error) {
37 37
 	filepath := filepath.Join(l.path, name)
38 38
 	specpath := filepath + ".spec"
39 39
 	if fi, err := os.Stat(specpath); err == nil {
40
-		return readPluginInfo(specpath, fi)
40
+		return readPluginSpecInfo(specpath, fi)
41 41
 	}
42
+
42 43
 	socketpath := filepath + ".sock"
43 44
 	if fi, err := os.Stat(socketpath); err == nil {
44
-		return readPluginInfo(socketpath, fi)
45
+		return readPluginSocketInfo(socketpath, fi)
46
+	}
47
+
48
+	jsonpath := filepath + ".json"
49
+	if _, err := os.Stat(jsonpath); err == nil {
50
+		return readPluginJSONInfo(name, jsonpath)
45 51
 	}
52
+
46 53
 	return nil, ErrNotFound
47 54
 }
48 55
 
49
-func readPluginInfo(path string, fi os.FileInfo) (*Plugin, error) {
56
+func readPluginSpecInfo(path string, fi os.FileInfo) (*Plugin, error) {
50 57
 	name := strings.Split(fi.Name(), ".")[0]
51 58
 
52
-	if fi.Mode()&os.ModeSocket != 0 {
53
-		return &Plugin{
54
-			Name: name,
55
-			Addr: "unix://" + path,
56
-		}, nil
57
-	}
58
-
59 59
 	content, err := ioutil.ReadFile(path)
60 60
 	if err != nil {
61 61
 		return nil, err
... ...
@@ -71,8 +72,34 @@ func readPluginInfo(path string, fi os.FileInfo) (*Plugin, error) {
71 71
 		return nil, fmt.Errorf("Unknown protocol")
72 72
 	}
73 73
 
74
-	return &Plugin{
75
-		Name: name,
76
-		Addr: addr,
77
-	}, nil
74
+	return newLocalPlugin(name, addr), nil
75
+}
76
+
77
+func readPluginSocketInfo(path string, fi os.FileInfo) (*Plugin, error) {
78
+	name := strings.Split(fi.Name(), ".")[0]
79
+
80
+	if fi.Mode()&os.ModeSocket == 0 {
81
+		return nil, fmt.Errorf("%s is not a socket", path)
82
+	}
83
+
84
+	return newLocalPlugin(name, "unix://"+path), nil
85
+}
86
+
87
+func readPluginJSONInfo(name, path string) (*Plugin, error) {
88
+	f, err := os.Open(path)
89
+	if err != nil {
90
+		return nil, err
91
+	}
92
+	defer f.Close()
93
+
94
+	var p Plugin
95
+	if err := json.NewDecoder(f).Decode(&p); err != nil {
96
+		return nil, err
97
+	}
98
+	p.Name = name
99
+	if len(p.TLSConfig.CAFile) == 0 {
100
+		p.TLSConfig.InsecureSkipVerify = true
101
+	}
102
+
103
+	return &p, nil
78 104
 }
... ...
@@ -61,7 +61,7 @@ func TestLocalSocket(t *testing.T) {
61 61
 }
62 62
 
63 63
 func TestFileSpecPlugin(t *testing.T) {
64
-	tmpdir, err := ioutil.TempDir("", "docker-test")
64
+	tmpdir, err := ioutil.TempDir("", "docker-test-")
65 65
 	if err != nil {
66 66
 		t.Fatal(err)
67 67
 	}
... ...
@@ -102,3 +102,51 @@ func TestFileSpecPlugin(t *testing.T) {
102 102
 		}
103 103
 	}
104 104
 }
105
+
106
+func TestFileJSONSpecPlugin(t *testing.T) {
107
+	tmpdir, err := ioutil.TempDir("", "docker-test-")
108
+	if err != nil {
109
+		t.Fatal(err)
110
+	}
111
+
112
+	p := filepath.Join(tmpdir, "example.json")
113
+	spec := `{
114
+  "Name": "plugin-example",
115
+  "Addr": "https://example.com/docker/plugin",
116
+  "TLSConfig": {
117
+    "CAFile": "/usr/shared/docker/certs/example-ca.pem",
118
+    "CertFile": "/usr/shared/docker/certs/example-cert.pem",
119
+    "KeyFile": "/usr/shared/docker/certs/example-key.pem"
120
+	}
121
+}`
122
+
123
+	if err = ioutil.WriteFile(p, []byte(spec), 0644); err != nil {
124
+		t.Fatal(err)
125
+	}
126
+
127
+	r := newLocalRegistry(tmpdir)
128
+	plugin, err := r.Plugin("example")
129
+	if err != nil {
130
+		t.Fatal(err)
131
+	}
132
+
133
+	if plugin.Name != "example" {
134
+		t.Fatalf("Expected plugin `plugin-example`, got %s\n", plugin.Name)
135
+	}
136
+
137
+	if plugin.Addr != "https://example.com/docker/plugin" {
138
+		t.Fatalf("Expected plugin addr `https://example.com/docker/plugin`, got %s\n", plugin.Addr)
139
+	}
140
+
141
+	if plugin.TLSConfig.CAFile != "/usr/shared/docker/certs/example-ca.pem" {
142
+		t.Fatalf("Expected plugin CA `/usr/shared/docker/certs/example-ca.pem`, got %s\n", plugin.TLSConfig.CAFile)
143
+	}
144
+
145
+	if plugin.TLSConfig.CertFile != "/usr/shared/docker/certs/example-cert.pem" {
146
+		t.Fatalf("Expected plugin Certificate `/usr/shared/docker/certs/example-cert.pem`, got %s\n", plugin.TLSConfig.CertFile)
147
+	}
148
+
149
+	if plugin.TLSConfig.KeyFile != "/usr/shared/docker/certs/example-key.pem" {
150
+		t.Fatalf("Expected plugin Key `/usr/shared/docker/certs/example-key.pem`, got %s\n", plugin.TLSConfig.KeyFile)
151
+	}
152
+}
... ...
@@ -5,6 +5,7 @@ import (
5 5
 	"sync"
6 6
 
7 7
 	"github.com/Sirupsen/logrus"
8
+	"github.com/docker/docker/pkg/tlsconfig"
8 9
 )
9 10
 
10 11
 var (
... ...
@@ -26,22 +27,36 @@ type Manifest struct {
26 26
 }
27 27
 
28 28
 type Plugin struct {
29
-	Name     string
30
-	Addr     string
31
-	Client   *Client
32
-	Manifest *Manifest
29
+	Name      string `json:"-"`
30
+	Addr      string
31
+	TLSConfig tlsconfig.Options
32
+	Client    *Client   `json:"-"`
33
+	Manifest  *Manifest `json:"-"`
34
+}
35
+
36
+func newLocalPlugin(name, addr string) *Plugin {
37
+	return &Plugin{
38
+		Name:      name,
39
+		Addr:      addr,
40
+		TLSConfig: tlsconfig.Options{InsecureSkipVerify: true},
41
+	}
33 42
 }
34 43
 
35 44
 func (p *Plugin) activate() error {
36
-	m := new(Manifest)
37
-	p.Client = NewClient(p.Addr)
38
-	err := p.Client.Call("Plugin.Activate", nil, m)
45
+	c, err := NewClient(p.Addr, p.TLSConfig)
39 46
 	if err != nil {
40 47
 		return err
41 48
 	}
49
+	p.Client = c
50
+
51
+	m := new(Manifest)
52
+	if err = p.Client.Call("Plugin.Activate", nil, m); err != nil {
53
+		return err
54
+	}
42 55
 
43 56
 	logrus.Debugf("%s's manifest: %v", p.Name, m)
44 57
 	p.Manifest = m
58
+
45 59
 	for _, iface := range m.Implements {
46 60
 		handler, handled := extpointHandlers[iface]
47 61
 		if !handled {
... ...
@@ -3,6 +3,8 @@ package sockets
3 3
 import (
4 4
 	"crypto/tls"
5 5
 	"net"
6
+	"net/http"
7
+	"time"
6 8
 
7 9
 	"github.com/docker/docker/pkg/listenbuffer"
8 10
 )
... ...
@@ -18,3 +20,18 @@ func NewTcpSocket(addr string, tlsConfig *tls.Config, activate <-chan struct{})
18 18
 	}
19 19
 	return l, nil
20 20
 }
21
+
22
+func ConfigureTCPTransport(tr *http.Transport, proto, addr string) {
23
+	// Why 32? See https://github.com/docker/docker/pull/8035.
24
+	timeout := 32 * time.Second
25
+	if proto == "unix" {
26
+		// No need for compression in local communications.
27
+		tr.DisableCompression = true
28
+		tr.Dial = func(_, _ string) (net.Conn, error) {
29
+			return net.DialTimeout(proto, addr, timeout)
30
+		}
31
+	} else {
32
+		tr.Proxy = http.ProxyFromEnvironment
33
+		tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
34
+	}
35
+}
21 36
deleted file mode 100644
... ...
@@ -1,22 +0,0 @@
1
-package utils
2
-
3
-import (
4
-	"net"
5
-	"net/http"
6
-	"time"
7
-)
8
-
9
-func ConfigureTCPTransport(tr *http.Transport, proto, addr string) {
10
-	// Why 32? See https://github.com/docker/docker/pull/8035.
11
-	timeout := 32 * time.Second
12
-	if proto == "unix" {
13
-		// No need for compression in local communications.
14
-		tr.DisableCompression = true
15
-		tr.Dial = func(_, _ string) (net.Conn, error) {
16
-			return net.DialTimeout(proto, addr, timeout)
17
-		}
18
-	} else {
19
-		tr.Proxy = http.ProxyFromEnvironment
20
-		tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
21
-	}
22
-}
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"testing"
10 10
 
11 11
 	"github.com/docker/docker/pkg/plugins"
12
+	"github.com/docker/docker/pkg/tlsconfig"
12 13
 )
13 14
 
14 15
 func TestVolumeRequestError(t *testing.T) {
... ...
@@ -42,11 +43,14 @@ func TestVolumeRequestError(t *testing.T) {
42 42
 	})
43 43
 
44 44
 	u, _ := url.Parse(server.URL)
45
-	client := plugins.NewClient("tcp://" + u.Host)
45
+	client, err := plugins.NewClient("tcp://"+u.Host, tlsconfig.Options{InsecureSkipVerify: true})
46
+	if err != nil {
47
+		t.Fatal(err)
48
+	}
49
+
46 50
 	driver := volumeDriverProxy{client}
47 51
 
48
-	err := driver.Create("volume")
49
-	if err == nil {
52
+	if err = driver.Create("volume"); err == nil {
50 53
 		t.Fatal("Expected error, was nil")
51 54
 	}
52 55