diff --git a/api/client/cli.go b/api/client/cli.go index 84574fd3ce..a53a0ba3e5 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -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 { diff --git a/experimental/plugin_api.md b/experimental/plugin_api.md index 9309b28468..f38db9cdaf 100644 --- a/experimental/plugin_api.md +++ b/experimental/plugin_api.md @@ -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 diff --git a/integration-cli/docker_cli_start_volume_driver_unix_test.go b/integration-cli/docker_cli_start_volume_driver_unix_test.go index fe94defb5a..2a952620fa 100644 --- a/integration-cli/docker_cli_start_volume_driver_unix_test.go +++ b/integration-cli/docker_cli_start_volume_driver_unix_test.go @@ -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) { diff --git a/pkg/plugins/client.go b/pkg/plugins/client.go index 890fd7c268..dab9be0efa 100644 --- a/pkg/plugins/client.go +++ b/pkg/plugins/client.go @@ -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 - } -} diff --git a/pkg/plugins/client_test.go b/pkg/plugins/client_test.go index 0f7cd34dfa..6a2c96f713 100644 --- a/pkg/plugins/client_test.go +++ b/pkg/plugins/client_test.go @@ -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 { diff --git a/pkg/plugins/discovery.go b/pkg/plugins/discovery.go index 3a42ba6d17..0160fd5d87 100644 --- a/pkg/plugins/discovery.go +++ b/pkg/plugins/discovery.go @@ -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 } diff --git a/pkg/plugins/discovery_test.go b/pkg/plugins/discovery_test.go index a818dc3f3c..e6001c51c7 100644 --- a/pkg/plugins/discovery_test.go +++ b/pkg/plugins/discovery_test.go @@ -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) + } +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 47519486bd..642e9c8b53 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -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 { diff --git a/pkg/sockets/tcp_socket.go b/pkg/sockets/tcp_socket.go index 658ee23dd7..746270c32e 100644 --- a/pkg/sockets/tcp_socket.go +++ b/pkg/sockets/tcp_socket.go @@ -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 + } +} diff --git a/utils/tcp.go b/utils/tcp.go deleted file mode 100644 index 75980ff69a..0000000000 --- a/utils/tcp.go +++ /dev/null @@ -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 - } -} diff --git a/volume/drivers/proxy_test.go b/volume/drivers/proxy_test.go index c679eca1c4..cadf8c0d4e 100644 --- a/volume/drivers/proxy_test.go +++ b/volume/drivers/proxy_test.go @@ -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") }