Allow full configuration of external plugins via a JSON document.
Signed-off-by: David Calavera <david.calavera@gmail.com>
| ... | ... |
@@ -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 |
|
| ... | ... |
@@ -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 |
|