Merge pull request #13524 from calavera/plugin_json_spec

Plugins JSON spec.
This commit is contained in:
Brian Goff 2015-06-30 15:44:48 -04:00
commit f13b40f6e7
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/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.
@ -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 {

View file

@ -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

View file

@ -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) {

View file

@ -5,12 +5,13 @@ 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 (
@ -18,11 +19,18 @@ const (
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 {
@ -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
}
}

View file

@ -7,6 +7,8 @@ import (
"reflect"
"testing"
"time"
"github.com/docker/docker/pkg/tlsconfig"
)
var (
@ -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")
@ -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 {

View file

@ -1,6 +1,7 @@
package plugins
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
@ -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
@ -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
}

View file

@ -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)
}
@ -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"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/tlsconfig"
)
var (
@ -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 {

View file

@ -3,6 +3,8 @@ package sockets
import (
"crypto/tls"
"net"
"net/http"
"time"
"github.com/docker/docker/pkg/listenbuffer"
)
@ -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
}
}

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"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/pkg/tlsconfig"
)
func TestVolumeRequestError(t *testing.T) {
@ -42,11 +43,14 @@ func TestVolumeRequestError(t *testing.T) {
})
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}
err := driver.Create("volume")
if err == nil {
if err = driver.Create("volume"); err == nil {
t.Fatal("Expected error, was nil")
}