1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Plugins JSON spec.

Allow full configuration of external plugins via a JSON document.

Signed-off-by: David Calavera <david.calavera@gmail.com>
This commit is contained in:
David Calavera 2015-05-27 15:21:18 -07:00
parent 389b806945
commit 333ac3a3eb
11 changed files with 174 additions and 74 deletions

View file

@ -15,8 +15,8 @@ import (
"github.com/docker/docker/cliconfig" "github.com/docker/docker/cliconfig"
"github.com/docker/docker/pkg/homedir" "github.com/docker/docker/pkg/homedir"
flag "github.com/docker/docker/pkg/mflag" flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/sockets"
"github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/term"
"github.com/docker/docker/utils"
) )
// DockerCli represents the docker command line client. // DockerCli represents the docker command line client.
@ -210,7 +210,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, keyFile string, proto, a
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
} }
utils.ConfigureTCPTransport(tr, proto, addr) sockets.ConfigureTCPTransport(tr, proto, addr)
configFile, e := cliconfig.Load(filepath.Join(homedir.Get(), ".docker")) configFile, e := cliconfig.Load(filepath.Join(homedir.Get(), ".docker"))
if e != nil { if e != nil {

View file

@ -26,18 +26,35 @@ containers is recommended.
Docker discovers plugins by looking for them in the plugin directory whenever a Docker discovers plugins by looking for them in the plugin directory whenever a
user or container tries to use one by name. user or container tries to use one by name.
There are two types of files which can be put in the plugin directory. There are three types of files which can be put in the plugin directory.
* `.sock` files are UNIX domain sockets. * `.sock` files are UNIX domain sockets.
* `.spec` files are text files containing a URL, such as `unix:///other.sock`. * `.spec` files are text files containing a URL, such as `unix:///other.sock`.
* `.json` files are text files containing a full json specification for the plugin.
The name of the file (excluding the extension) determines the plugin name. The name of the file (excluding the extension) determines the plugin name.
For example, the `flocker` plugin might create a UNIX socket at For example, the `flocker` plugin might create a UNIX socket at
`/usr/share/docker/plugins/flocker.sock`. `/usr/share/docker/plugins/flocker.sock`.
Plugins must be run locally on the same machine as the Docker daemon. UNIX ### JSON specification
domain sockets are strongly encouraged for security reasons.
This is the JSON format for a plugin:
```json
{
"Name": "plugin-example",
"Addr": "https://example.com/docker/plugin",
"TLSConfig": {
"InsecureSkipVerify": false,
"CAFile": "/usr/shared/docker/certs/example-ca.pem",
"CertFile": "/usr/shared/docker/certs/example-cert.pem",
"KeyFile": "/usr/shared/docker/certs/example-key.pem",
}
}
```
The `TLSConfig` field is optional and TLS will only be verified if this configuration is present.
## Plugin lifecycle ## Plugin lifecycle

View file

@ -41,7 +41,6 @@ type DockerExternalVolumeSuite struct {
func (s *DockerExternalVolumeSuite) SetUpTest(c *check.C) { func (s *DockerExternalVolumeSuite) SetUpTest(c *check.C) {
s.d = NewDaemon(c) s.d = NewDaemon(c)
s.ec = &eventCounter{} s.ec = &eventCounter{}
} }
func (s *DockerExternalVolumeSuite) TearDownTest(c *check.C) { func (s *DockerExternalVolumeSuite) TearDownTest(c *check.C) {

View file

@ -5,12 +5,13 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/sockets"
"github.com/docker/docker/pkg/tlsconfig"
) )
const ( const (
@ -18,11 +19,18 @@ const (
defaultTimeOut = 30 defaultTimeOut = 30
) )
func NewClient(addr string) *Client { func NewClient(addr string, tlsConfig tlsconfig.Options) (*Client, error) {
tr := &http.Transport{} tr := &http.Transport{}
c, err := tlsconfig.Client(tlsConfig)
if err != nil {
return nil, err
}
tr.TLSClientConfig = c
protoAndAddr := strings.Split(addr, "://") protoAndAddr := strings.Split(addr, "://")
configureTCPTransport(tr, protoAndAddr[0], protoAndAddr[1]) sockets.ConfigureTCPTransport(tr, protoAndAddr[0], protoAndAddr[1])
return &Client{&http.Client{Transport: tr}, protoAndAddr[1]} return &Client{&http.Client{Transport: tr}, protoAndAddr[1]}, nil
} }
type Client struct { type Client struct {
@ -96,18 +104,3 @@ func backoff(retries int) time.Duration {
func abort(start time.Time, timeOff time.Duration) bool { func abort(start time.Time, timeOff time.Duration) bool {
return timeOff+time.Since(start) > time.Duration(defaultTimeOut)*time.Second return timeOff+time.Since(start) > time.Duration(defaultTimeOut)*time.Second
} }
func configureTCPTransport(tr *http.Transport, proto, addr string) {
// Why 32? See https://github.com/docker/docker/pull/8035.
timeout := 32 * time.Second
if proto == "unix" {
// No need for compression in local communications.
tr.DisableCompression = true
tr.Dial = func(_, _ string) (net.Conn, error) {
return net.DialTimeout(proto, addr, timeout)
}
} else {
tr.Proxy = http.ProxyFromEnvironment
tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
}
}

View file

@ -7,6 +7,8 @@ import (
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/docker/docker/pkg/tlsconfig"
) )
var ( var (
@ -27,7 +29,7 @@ func teardownRemotePluginServer() {
} }
func TestFailedConnection(t *testing.T) { func TestFailedConnection(t *testing.T) {
c := NewClient("tcp://127.0.0.1:1") 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, nil, false)
if err == nil { if err == nil {
t.Fatal("Unexpected successful connection") t.Fatal("Unexpected successful connection")
@ -51,7 +53,7 @@ func TestEchoInputOutput(t *testing.T) {
io.Copy(w, r.Body) io.Copy(w, r.Body)
}) })
c := NewClient(addr) c, _ := NewClient(addr, tlsconfig.Options{InsecureSkipVerify: true})
var output Manifest var output Manifest
err := c.Call("Test.Echo", m, &output) err := c.Call("Test.Echo", m, &output)
if err != nil { if err != nil {

View file

@ -1,6 +1,7 @@
package plugins package plugins
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -37,25 +38,25 @@ func (l *LocalRegistry) Plugin(name string) (*Plugin, error) {
filepath := filepath.Join(l.path, name) filepath := filepath.Join(l.path, name)
specpath := filepath + ".spec" specpath := filepath + ".spec"
if fi, err := os.Stat(specpath); err == nil { if fi, err := os.Stat(specpath); err == nil {
return readPluginInfo(specpath, fi) return readPluginSpecInfo(specpath, fi)
} }
socketpath := filepath + ".sock" socketpath := filepath + ".sock"
if fi, err := os.Stat(socketpath); err == nil { if fi, err := os.Stat(socketpath); err == nil {
return readPluginInfo(socketpath, fi) return readPluginSocketInfo(socketpath, fi)
} }
jsonpath := filepath + ".json"
if _, err := os.Stat(jsonpath); err == nil {
return readPluginJSONInfo(name, jsonpath)
}
return nil, ErrNotFound return nil, ErrNotFound
} }
func readPluginInfo(path string, fi os.FileInfo) (*Plugin, error) { func readPluginSpecInfo(path string, fi os.FileInfo) (*Plugin, error) {
name := strings.Split(fi.Name(), ".")[0] name := strings.Split(fi.Name(), ".")[0]
if fi.Mode()&os.ModeSocket != 0 {
return &Plugin{
Name: name,
Addr: "unix://" + path,
}, nil
}
content, err := ioutil.ReadFile(path) content, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
@ -71,8 +72,34 @@ func readPluginInfo(path string, fi os.FileInfo) (*Plugin, error) {
return nil, fmt.Errorf("Unknown protocol") return nil, fmt.Errorf("Unknown protocol")
} }
return &Plugin{ return newLocalPlugin(name, addr), nil
Name: name, }
Addr: addr,
}, nil func readPluginSocketInfo(path string, fi os.FileInfo) (*Plugin, error) {
name := strings.Split(fi.Name(), ".")[0]
if fi.Mode()&os.ModeSocket == 0 {
return nil, fmt.Errorf("%s is not a socket", path)
}
return newLocalPlugin(name, "unix://"+path), nil
}
func readPluginJSONInfo(name, path string) (*Plugin, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var p Plugin
if err := json.NewDecoder(f).Decode(&p); err != nil {
return nil, err
}
p.Name = name
if len(p.TLSConfig.CAFile) == 0 {
p.TLSConfig.InsecureSkipVerify = true
}
return &p, nil
} }

View file

@ -61,7 +61,7 @@ func TestLocalSocket(t *testing.T) {
} }
func TestFileSpecPlugin(t *testing.T) { func TestFileSpecPlugin(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "docker-test") tmpdir, err := ioutil.TempDir("", "docker-test-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -102,3 +102,51 @@ func TestFileSpecPlugin(t *testing.T) {
} }
} }
} }
func TestFileJSONSpecPlugin(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "docker-test-")
if err != nil {
t.Fatal(err)
}
p := filepath.Join(tmpdir, "example.json")
spec := `{
"Name": "plugin-example",
"Addr": "https://example.com/docker/plugin",
"TLSConfig": {
"CAFile": "/usr/shared/docker/certs/example-ca.pem",
"CertFile": "/usr/shared/docker/certs/example-cert.pem",
"KeyFile": "/usr/shared/docker/certs/example-key.pem"
}
}`
if err = ioutil.WriteFile(p, []byte(spec), 0644); err != nil {
t.Fatal(err)
}
r := newLocalRegistry(tmpdir)
plugin, err := r.Plugin("example")
if err != nil {
t.Fatal(err)
}
if plugin.Name != "example" {
t.Fatalf("Expected plugin `plugin-example`, got %s\n", plugin.Name)
}
if plugin.Addr != "https://example.com/docker/plugin" {
t.Fatalf("Expected plugin addr `https://example.com/docker/plugin`, got %s\n", plugin.Addr)
}
if plugin.TLSConfig.CAFile != "/usr/shared/docker/certs/example-ca.pem" {
t.Fatalf("Expected plugin CA `/usr/shared/docker/certs/example-ca.pem`, got %s\n", plugin.TLSConfig.CAFile)
}
if plugin.TLSConfig.CertFile != "/usr/shared/docker/certs/example-cert.pem" {
t.Fatalf("Expected plugin Certificate `/usr/shared/docker/certs/example-cert.pem`, got %s\n", plugin.TLSConfig.CertFile)
}
if plugin.TLSConfig.KeyFile != "/usr/shared/docker/certs/example-key.pem" {
t.Fatalf("Expected plugin Key `/usr/shared/docker/certs/example-key.pem`, got %s\n", plugin.TLSConfig.KeyFile)
}
}

View file

@ -5,6 +5,7 @@ import (
"sync" "sync"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/tlsconfig"
) )
var ( var (
@ -26,22 +27,36 @@ type Manifest struct {
} }
type Plugin struct { type Plugin struct {
Name string Name string `json:"-"`
Addr string Addr string
Client *Client TLSConfig tlsconfig.Options
Manifest *Manifest Client *Client `json:"-"`
Manifest *Manifest `json:"-"`
}
func newLocalPlugin(name, addr string) *Plugin {
return &Plugin{
Name: name,
Addr: addr,
TLSConfig: tlsconfig.Options{InsecureSkipVerify: true},
}
} }
func (p *Plugin) activate() error { func (p *Plugin) activate() error {
m := new(Manifest) c, err := NewClient(p.Addr, p.TLSConfig)
p.Client = NewClient(p.Addr)
err := p.Client.Call("Plugin.Activate", nil, m)
if err != nil { if err != nil {
return err return err
} }
p.Client = c
m := new(Manifest)
if err = p.Client.Call("Plugin.Activate", nil, m); err != nil {
return err
}
logrus.Debugf("%s's manifest: %v", p.Name, m) logrus.Debugf("%s's manifest: %v", p.Name, m)
p.Manifest = m p.Manifest = m
for _, iface := range m.Implements { for _, iface := range m.Implements {
handler, handled := extpointHandlers[iface] handler, handled := extpointHandlers[iface]
if !handled { if !handled {

View file

@ -3,6 +3,8 @@ package sockets
import ( import (
"crypto/tls" "crypto/tls"
"net" "net"
"net/http"
"time"
"github.com/docker/docker/pkg/listenbuffer" "github.com/docker/docker/pkg/listenbuffer"
) )
@ -18,3 +20,18 @@ func NewTcpSocket(addr string, tlsConfig *tls.Config, activate <-chan struct{})
} }
return l, nil return l, nil
} }
func ConfigureTCPTransport(tr *http.Transport, proto, addr string) {
// Why 32? See https://github.com/docker/docker/pull/8035.
timeout := 32 * time.Second
if proto == "unix" {
// No need for compression in local communications.
tr.DisableCompression = true
tr.Dial = func(_, _ string) (net.Conn, error) {
return net.DialTimeout(proto, addr, timeout)
}
} else {
tr.Proxy = http.ProxyFromEnvironment
tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
}
}

View file

@ -1,22 +0,0 @@
package utils
import (
"net"
"net/http"
"time"
)
func ConfigureTCPTransport(tr *http.Transport, proto, addr string) {
// Why 32? See https://github.com/docker/docker/pull/8035.
timeout := 32 * time.Second
if proto == "unix" {
// No need for compression in local communications.
tr.DisableCompression = true
tr.Dial = func(_, _ string) (net.Conn, error) {
return net.DialTimeout(proto, addr, timeout)
}
} else {
tr.Proxy = http.ProxyFromEnvironment
tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
}
}

View file

@ -9,6 +9,7 @@ import (
"testing" "testing"
"github.com/docker/docker/pkg/plugins" "github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/pkg/tlsconfig"
) )
func TestVolumeRequestError(t *testing.T) { func TestVolumeRequestError(t *testing.T) {
@ -42,11 +43,14 @@ func TestVolumeRequestError(t *testing.T) {
}) })
u, _ := url.Parse(server.URL) u, _ := url.Parse(server.URL)
client := plugins.NewClient("tcp://" + u.Host) client, err := plugins.NewClient("tcp://"+u.Host, tlsconfig.Options{InsecureSkipVerify: true})
if err != nil {
t.Fatal(err)
}
driver := volumeDriverProxy{client} driver := volumeDriverProxy{client}
err := driver.Create("volume") if err = driver.Create("volume"); err == nil {
if err == nil {
t.Fatal("Expected error, was nil") t.Fatal("Expected error, was nil")
} }