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:
parent
389b806945
commit
333ac3a3eb
11 changed files with 174 additions and 74 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
22
utils/tcp.go
22
utils/tcp.go
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue