Skip to content

Commit

Permalink
Plugins JSON spec.
Browse files Browse the repository at this point in the history
Allow full configuration of external plugins via a JSON document.

Signed-off-by: David Calavera <[email protected]>
  • Loading branch information
calavera committed Jun 29, 2015
1 parent 389b806 commit 333ac3a
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 74 deletions.
4 changes: 2 additions & 2 deletions api/client/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/pkg/homedir"
flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/sockets"
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/utils"
)

// DockerCli represents the docker command line client.
Expand Down Expand Up @@ -210,7 +210,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, keyFile string, proto, a
tr := &http.Transport{
TLSClientConfig: tlsConfig,
}
utils.ConfigureTCPTransport(tr, proto, addr)
sockets.ConfigureTCPTransport(tr, proto, addr)

configFile, e := cliconfig.Load(filepath.Join(homedir.Get(), ".docker"))
if e != nil {
Expand Down
23 changes: 20 additions & 3 deletions experimental/plugin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,35 @@ containers is recommended.
Docker discovers plugins by looking for them in the plugin directory whenever a
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.
* `.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.

For example, the `flocker` plugin might create a UNIX socket at
`/usr/share/docker/plugins/flocker.sock`.

Plugins must be run locally on the same machine as the Docker daemon. UNIX
domain sockets are strongly encouraged for security reasons.
### JSON specification

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ type DockerExternalVolumeSuite struct {
func (s *DockerExternalVolumeSuite) SetUpTest(c *check.C) {
s.d = NewDaemon(c)
s.ec = &eventCounter{}

}

func (s *DockerExternalVolumeSuite) TearDownTest(c *check.C) {
Expand Down
31 changes: 12 additions & 19 deletions pkg/plugins/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,32 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"

"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/sockets"
"github.com/docker/docker/pkg/tlsconfig"
)

const (
versionMimetype = "application/vnd.docker.plugins.v1+json"
defaultTimeOut = 30
)

func NewClient(addr string) *Client {
func NewClient(addr string, tlsConfig tlsconfig.Options) (*Client, error) {
tr := &http.Transport{}

c, err := tlsconfig.Client(tlsConfig)
if err != nil {
return nil, err
}
tr.TLSClientConfig = c

protoAndAddr := strings.Split(addr, "://")
configureTCPTransport(tr, protoAndAddr[0], protoAndAddr[1])
return &Client{&http.Client{Transport: tr}, protoAndAddr[1]}
sockets.ConfigureTCPTransport(tr, protoAndAddr[0], protoAndAddr[1])
return &Client{&http.Client{Transport: tr}, protoAndAddr[1]}, nil
}

type Client struct {
Expand Down Expand Up @@ -96,18 +104,3 @@ func backoff(retries int) time.Duration {
func abort(start time.Time, timeOff time.Duration) bool {
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
}
}
6 changes: 4 additions & 2 deletions pkg/plugins/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"reflect"
"testing"
"time"

"github.com/docker/docker/pkg/tlsconfig"
)

var (
Expand All @@ -27,7 +29,7 @@ func teardownRemotePluginServer() {
}

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)
if err == nil {
t.Fatal("Unexpected successful connection")
Expand All @@ -51,7 +53,7 @@ func TestEchoInputOutput(t *testing.T) {
io.Copy(w, r.Body)
})

c := NewClient(addr)
c, _ := NewClient(addr, tlsconfig.Options{InsecureSkipVerify: true})
var output Manifest
err := c.Call("Test.Echo", m, &output)
if err != nil {
Expand Down
55 changes: 41 additions & 14 deletions pkg/plugins/discovery.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package plugins

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -37,25 +38,25 @@ func (l *LocalRegistry) Plugin(name string) (*Plugin, error) {
filepath := filepath.Join(l.path, name)
specpath := filepath + ".spec"
if fi, err := os.Stat(specpath); err == nil {
return readPluginInfo(specpath, fi)
return readPluginSpecInfo(specpath, fi)
}

socketpath := filepath + ".sock"
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
}

func readPluginInfo(path string, fi os.FileInfo) (*Plugin, error) {
func readPluginSpecInfo(path string, fi os.FileInfo) (*Plugin, error) {
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)
if err != nil {
return nil, err
Expand All @@ -71,8 +72,34 @@ func readPluginInfo(path string, fi os.FileInfo) (*Plugin, error) {
return nil, fmt.Errorf("Unknown protocol")
}

return &Plugin{
Name: name,
Addr: addr,
}, nil
return newLocalPlugin(name, 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
}
50 changes: 49 additions & 1 deletion pkg/plugins/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestLocalSocket(t *testing.T) {
}

func TestFileSpecPlugin(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "docker-test")
tmpdir, err := ioutil.TempDir("", "docker-test-")
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -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)
}
}
29 changes: 22 additions & 7 deletions pkg/plugins/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"sync"

"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/tlsconfig"
)

var (
Expand All @@ -26,22 +27,36 @@ type Manifest struct {
}

type Plugin struct {
Name string
Addr string
Client *Client
Manifest *Manifest
Name string `json:"-"`
Addr string
TLSConfig tlsconfig.Options
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 {
m := new(Manifest)
p.Client = NewClient(p.Addr)
err := p.Client.Call("Plugin.Activate", nil, m)
c, err := NewClient(p.Addr, p.TLSConfig)
if err != nil {
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)
p.Manifest = m

for _, iface := range m.Implements {
handler, handled := extpointHandlers[iface]
if !handled {
Expand Down
17 changes: 17 additions & 0 deletions pkg/sockets/tcp_socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package sockets
import (
"crypto/tls"
"net"
"net/http"
"time"

"github.com/docker/docker/pkg/listenbuffer"
)
Expand All @@ -18,3 +20,18 @@ func NewTcpSocket(addr string, tlsConfig *tls.Config, activate <-chan struct{})
}
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 changes: 0 additions & 22 deletions utils/tcp.go

This file was deleted.

Loading

0 comments on commit 333ac3a

Please sign in to comment.