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 b4f6e2c8b7
commit 7312c4b8bb
6 changed files with 145 additions and 43 deletions

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