Przeglądaj źródła

Merge pull request #23446 from tiborvass/plugins-experimental

Plugins: experimental support for new plugin management
Brian Goff 9 lat temu
rodzic
commit
6ed921dc38
67 zmienionych plików z 2497 dodań i 66 usunięć
  1. 12 0
      api/client/plugin/cmd.go
  2. 36 0
      api/client/plugin/cmd_experimental.go
  3. 23 0
      api/client/plugin/disable.go
  4. 23 0
      api/client/plugin/enable.go
  5. 39 0
      api/client/plugin/inspect.go
  6. 51 0
      api/client/plugin/install.go
  7. 44 0
      api/client/plugin/list.go
  8. 50 0
      api/client/plugin/push.go
  9. 43 0
      api/client/plugin/remove.go
  10. 28 0
      api/client/plugin/set.go
  11. 21 0
      api/server/router/plugin/backend.go
  12. 23 0
      api/server/router/plugin/plugin.go
  13. 20 0
      api/server/router/plugin/plugin_experimental.go
  14. 9 0
      api/server/router/plugin/plugin_regular.go
  15. 103 0
      api/server/router/plugin/plugin_routes.go
  16. 2 0
      cli/cobraadaptor/adaptor.go
  17. 21 0
      cli/error.go
  18. 5 0
      cmd/dockerd/daemon.go
  19. 3 3
      cmd/dockerd/daemon_linux.go
  20. 13 0
      cmd/dockerd/daemon_no_plugin_support.go
  21. 14 0
      cmd/dockerd/daemon_plugin_support.go
  22. 9 0
      cmd/dockerd/routes.go
  23. 13 0
      cmd/dockerd/routes_experimental.go
  24. 4 4
      daemon/daemon.go
  25. 1 1
      daemon/graphdriver/plugin.go
  26. 3 3
      distribution/pull.go
  27. 52 0
      docs/reference/commandline/plugin_disable.md
  28. 52 0
      docs/reference/commandline/plugin_enable.md
  29. 135 0
      docs/reference/commandline/plugin_inspect.md
  30. 51 0
      docs/reference/commandline/plugin_install.md
  31. 40 0
      docs/reference/commandline/plugin_ls.md
  32. 41 0
      docs/reference/commandline/plugin_rm.md
  33. 1 1
      hack/vendor.sh
  34. 1 1
      integration-cli/docker_cli_external_graphdriver_unix_test.go
  35. 2 3
      pkg/authorization/authz_unix_test.go
  36. 9 4
      pkg/authorization/plugin.go
  37. 7 5
      pkg/plugins/client.go
  38. 2 2
      pkg/plugins/client_test.go
  39. 3 3
      pkg/plugins/discovery.go
  40. 2 2
      pkg/plugins/discovery_test.go
  41. 1 1
      pkg/plugins/discovery_unix_test.go
  42. 22 10
      pkg/plugins/plugins.go
  43. 139 0
      plugin/backend.go
  44. 208 0
      plugin/distribution/pull.go
  45. 134 0
      plugin/distribution/push.go
  46. 16 0
      plugin/distribution/types.go
  47. 9 0
      plugin/interface.go
  48. 23 0
      plugin/legacy.go
  49. 384 0
      plugin/manager.go
  50. 126 0
      plugin/manager_linux.go
  51. 21 0
      plugin/manager_windows.go
  52. 15 0
      vendor/src/github.com/docker/engine-api/client/errors.go
  53. 3 7
      vendor/src/github.com/docker/engine-api/client/interface.go
  54. 26 0
      vendor/src/github.com/docker/engine-api/client/interface_experimental.go
  55. 11 0
      vendor/src/github.com/docker/engine-api/client/interface_stable.go
  56. 14 0
      vendor/src/github.com/docker/engine-api/client/plugin_disable.go
  57. 14 0
      vendor/src/github.com/docker/engine-api/client/plugin_enable.go
  58. 22 0
      vendor/src/github.com/docker/engine-api/client/plugin_inspect.go
  59. 54 0
      vendor/src/github.com/docker/engine-api/client/plugin_install.go
  60. 23 0
      vendor/src/github.com/docker/engine-api/client/plugin_list.go
  61. 15 0
      vendor/src/github.com/docker/engine-api/client/plugin_push.go
  62. 14 0
      vendor/src/github.com/docker/engine-api/client/plugin_remove.go
  63. 14 0
      vendor/src/github.com/docker/engine-api/client/plugin_set.go
  64. 160 0
      vendor/src/github.com/docker/engine-api/types/plugin.go
  65. 12 11
      volume/drivers/extpoint.go
  66. 1 1
      volume/drivers/proxy_test.go
  67. 5 4
      volume/store/store.go

+ 12 - 0
api/client/plugin/cmd.go

@@ -0,0 +1,12 @@
+// +build !experimental
+
+package plugin
+
+import (
+	"github.com/docker/docker/api/client"
+	"github.com/spf13/cobra"
+)
+
+// NewPluginCommand returns a cobra command for `plugin` subcommands
+func NewPluginCommand(cmd *cobra.Command, dockerCli *client.DockerCli) {
+}

+ 36 - 0
api/client/plugin/cmd_experimental.go

@@ -0,0 +1,36 @@
+// +build experimental
+
+package plugin
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+)
+
+// NewPluginCommand returns a cobra command for `plugin` subcommands
+func NewPluginCommand(rootCmd *cobra.Command, dockerCli *client.DockerCli) {
+	cmd := &cobra.Command{
+		Use:   "plugin",
+		Short: "Manage Docker plugins",
+		Args:  cli.NoArgs,
+		Run: func(cmd *cobra.Command, args []string) {
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
+		},
+	}
+
+	cmd.AddCommand(
+		newDisableCommand(dockerCli),
+		newEnableCommand(dockerCli),
+		newInspectCommand(dockerCli),
+		newInstallCommand(dockerCli),
+		newListCommand(dockerCli),
+		newRemoveCommand(dockerCli),
+		newSetCommand(dockerCli),
+		newPushCommand(dockerCli),
+	)
+
+	rootCmd.AddCommand(cmd)
+}

+ 23 - 0
api/client/plugin/disable.go

@@ -0,0 +1,23 @@
+// +build experimental
+
+package plugin
+
+import (
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+func newDisableCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "disable",
+		Short: "Disable a plugin",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return dockerCli.Client().PluginDisable(context.Background(), args[0])
+		},
+	}
+
+	return cmd
+}

+ 23 - 0
api/client/plugin/enable.go

@@ -0,0 +1,23 @@
+// +build experimental
+
+package plugin
+
+import (
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+func newEnableCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "enable",
+		Short: "Enable a plugin",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return dockerCli.Client().PluginEnable(context.Background(), args[0])
+		},
+	}
+
+	return cmd
+}

+ 39 - 0
api/client/plugin/inspect.go

@@ -0,0 +1,39 @@
+// +build experimental
+
+package plugin
+
+import (
+	"encoding/json"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "inspect",
+		Short: "Inspect a plugin",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runInspect(dockerCli, args[0])
+		},
+	}
+
+	return cmd
+}
+
+func runInspect(dockerCli *client.DockerCli, name string) error {
+	p, err := dockerCli.Client().PluginInspect(context.Background(), name)
+	if err != nil {
+		return err
+	}
+
+	b, err := json.MarshalIndent(p, "", "\t")
+	if err != nil {
+		return err
+	}
+	_, err = dockerCli.Out().Write(b)
+	return err
+}

+ 51 - 0
api/client/plugin/install.go

@@ -0,0 +1,51 @@
+// +build experimental
+
+package plugin
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/reference"
+	"github.com/docker/docker/registry"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+func newInstallCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "install",
+		Short: "Install a plugin",
+		Args:  cli.RequiresMinArgs(1), // TODO: allow for set args
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runInstall(dockerCli, args[0], args[1:])
+		},
+	}
+
+	return cmd
+}
+
+func runInstall(dockerCli *client.DockerCli, name string, args []string) error {
+	named, err := reference.ParseNamed(name) // FIXME: validate
+	if err != nil {
+		return err
+	}
+	named = reference.WithDefaultTag(named)
+	ref, ok := named.(reference.NamedTagged)
+	if !ok {
+		return fmt.Errorf("invalid name: %s", named.String())
+	}
+
+	ctx := context.Background()
+
+	repoInfo, err := registry.ParseRepositoryInfo(named)
+	authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
+
+	encodedAuth, err := client.EncodeAuthToBase64(authConfig)
+	if err != nil {
+		return err
+	}
+	// TODO: pass acceptAllPermissions and noEnable flag
+	return dockerCli.Client().PluginInstall(ctx, ref.String(), encodedAuth, false, false, dockerCli.In(), dockerCli.Out())
+}

+ 44 - 0
api/client/plugin/list.go

@@ -0,0 +1,44 @@
+// +build experimental
+
+package plugin
+
+import (
+	"fmt"
+	"text/tabwriter"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "ls",
+		Short:   "List plugins",
+		Aliases: []string{"list"},
+		Args:    cli.ExactArgs(0),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runList(dockerCli)
+		},
+	}
+
+	return cmd
+}
+
+func runList(dockerCli *client.DockerCli) error {
+	plugins, err := dockerCli.Client().PluginList(context.Background())
+	if err != nil {
+		return err
+	}
+
+	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
+	fmt.Fprintf(w, "NAME \tTAG \tACTIVE")
+	fmt.Fprintf(w, "\n")
+
+	for _, p := range plugins {
+		fmt.Fprintf(w, "%s\t%s\t%v\n", p.Name, p.Tag, p.Active)
+	}
+	w.Flush()
+	return nil
+}

+ 50 - 0
api/client/plugin/push.go

@@ -0,0 +1,50 @@
+// +build experimental
+
+package plugin
+
+import (
+	"fmt"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/reference"
+	"github.com/docker/docker/registry"
+	"github.com/spf13/cobra"
+)
+
+func newPushCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "push",
+		Short: "Push a plugin",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runPush(dockerCli, args[0])
+		},
+	}
+	return cmd
+}
+
+func runPush(dockerCli *client.DockerCli, name string) error {
+	named, err := reference.ParseNamed(name) // FIXME: validate
+	if err != nil {
+		return err
+	}
+	named = reference.WithDefaultTag(named)
+	ref, ok := named.(reference.NamedTagged)
+	if !ok {
+		return fmt.Errorf("invalid name: %s", named.String())
+	}
+
+	ctx := context.Background()
+
+	repoInfo, err := registry.ParseRepositoryInfo(named)
+	authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
+
+	encodedAuth, err := client.EncodeAuthToBase64(authConfig)
+	if err != nil {
+		return err
+	}
+	return dockerCli.Client().PluginPush(ctx, ref.String(), encodedAuth)
+}

+ 43 - 0
api/client/plugin/remove.go

@@ -0,0 +1,43 @@
+// +build experimental
+
+package plugin
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "rm",
+		Short:   "Remove a plugin",
+		Aliases: []string{"remove"},
+		Args:    cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runRemove(dockerCli, args)
+		},
+	}
+
+	return cmd
+}
+
+func runRemove(dockerCli *client.DockerCli, names []string) error {
+	var errs cli.Errors
+	for _, name := range names {
+		// TODO: pass names to api instead of making multiple api calls
+		if err := dockerCli.Client().PluginRemove(context.Background(), name); err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		fmt.Fprintln(dockerCli.Out(), name)
+	}
+	// Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value.
+	if errs != nil {
+		return errs
+	}
+	return nil
+}

+ 28 - 0
api/client/plugin/set.go

@@ -0,0 +1,28 @@
+// +build experimental
+
+package plugin
+
+import (
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+)
+
+func newSetCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "set",
+		Short: "Change settings for a plugin",
+		Args:  cli.RequiresMinArgs(2),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runSet(dockerCli, args[0], args[1:])
+		},
+	}
+
+	return cmd
+}
+
+func runSet(dockerCli *client.DockerCli, name string, args []string) error {
+	return dockerCli.Client().PluginSet(context.Background(), name, args)
+}

+ 21 - 0
api/server/router/plugin/backend.go

@@ -0,0 +1,21 @@
+// +build experimental
+
+package plugin
+
+import (
+	"net/http"
+
+	enginetypes "github.com/docker/engine-api/types"
+)
+
+// Backend for Plugin
+type Backend interface {
+	Disable(name string) error
+	Enable(name string) error
+	List() ([]enginetypes.Plugin, error)
+	Inspect(name string) (enginetypes.Plugin, error)
+	Remove(name string) error
+	Set(name string, args []string) error
+	Pull(name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error)
+	Push(name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) error
+}

+ 23 - 0
api/server/router/plugin/plugin.go

@@ -0,0 +1,23 @@
+package plugin
+
+import "github.com/docker/docker/api/server/router"
+
+// pluginRouter is a router to talk with the plugin controller
+type pluginRouter struct {
+	backend Backend
+	routes  []router.Route
+}
+
+// NewRouter initializes a new plugin router
+func NewRouter(b Backend) router.Router {
+	r := &pluginRouter{
+		backend: b,
+	}
+	r.initRoutes()
+	return r
+}
+
+// Routes returns the available routers to the plugin controller
+func (r *pluginRouter) Routes() []router.Route {
+	return r.routes
+}

+ 20 - 0
api/server/router/plugin/plugin_experimental.go

@@ -0,0 +1,20 @@
+// +build experimental
+
+package plugin
+
+import (
+	"github.com/docker/docker/api/server/router"
+)
+
+func (r *pluginRouter) initRoutes() {
+	r.routes = []router.Route{
+		router.NewGetRoute("/plugins", r.listPlugins),
+		router.NewGetRoute("/plugins/{name:.*}", r.inspectPlugin),
+		router.NewDeleteRoute("/plugins/{name:.*}", r.removePlugin),
+		router.NewPostRoute("/plugins/{name:.*}/enable", r.enablePlugin), // PATCH?
+		router.NewPostRoute("/plugins/{name:.*}/disable", r.disablePlugin),
+		router.NewPostRoute("/plugins/pull", r.pullPlugin),
+		router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin),
+		router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin),
+	}
+}

+ 9 - 0
api/server/router/plugin/plugin_regular.go

@@ -0,0 +1,9 @@
+// +build !experimental
+
+package plugin
+
+func (r *pluginRouter) initRoutes() {}
+
+// Backend is empty so that the package can compile in non-experimental
+// (Needed by volume driver)
+type Backend interface{}

+ 103 - 0
api/server/router/plugin/plugin_routes.go

@@ -0,0 +1,103 @@
+// +build experimental
+
+package plugin
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"net/http"
+	"strings"
+
+	"github.com/docker/docker/api/server/httputils"
+	"github.com/docker/engine-api/types"
+	"golang.org/x/net/context"
+)
+
+func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+
+	metaHeaders := map[string][]string{}
+	for k, v := range r.Header {
+		if strings.HasPrefix(k, "X-Meta-") {
+			metaHeaders[k] = v
+		}
+	}
+
+	// Get X-Registry-Auth
+	authEncoded := r.Header.Get("X-Registry-Auth")
+	authConfig := &types.AuthConfig{}
+	if authEncoded != "" {
+		authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
+		if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
+			authConfig = &types.AuthConfig{}
+		}
+	}
+
+	privileges, err := pr.backend.Pull(r.FormValue("name"), metaHeaders, authConfig)
+	if err != nil {
+		return err
+	}
+	return httputils.WriteJSON(w, http.StatusOK, privileges)
+}
+
+func (pr *pluginRouter) enablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	return pr.backend.Enable(vars["name"])
+}
+
+func (pr *pluginRouter) disablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	return pr.backend.Disable(vars["name"])
+}
+
+func (pr *pluginRouter) removePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	return pr.backend.Remove(vars["name"])
+}
+
+func (pr *pluginRouter) pushPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+
+	metaHeaders := map[string][]string{}
+	for k, v := range r.Header {
+		if strings.HasPrefix(k, "X-Meta-") {
+			metaHeaders[k] = v
+		}
+	}
+
+	// Get X-Registry-Auth
+	authEncoded := r.Header.Get("X-Registry-Auth")
+	authConfig := &types.AuthConfig{}
+	if authEncoded != "" {
+		authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
+		if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
+			authConfig = &types.AuthConfig{}
+		}
+	}
+	return pr.backend.Push(vars["name"], metaHeaders, authConfig)
+}
+
+func (pr *pluginRouter) setPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var args []string
+	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
+		return err
+	}
+	return pr.backend.Set(vars["name"], args)
+}
+
+func (pr *pluginRouter) listPlugins(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	l, err := pr.backend.List()
+	if err != nil {
+		return err
+	}
+	return httputils.WriteJSON(w, http.StatusOK, l)
+}
+
+func (pr *pluginRouter) inspectPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	result, err := pr.backend.Inspect(vars["name"])
+	if err != nil {
+		return err
+	}
+	return httputils.WriteJSON(w, http.StatusOK, result)
+}

+ 2 - 0
cli/cobraadaptor/adaptor.go

@@ -6,6 +6,7 @@ import (
 	"github.com/docker/docker/api/client/image"
 	"github.com/docker/docker/api/client/network"
 	"github.com/docker/docker/api/client/node"
+	"github.com/docker/docker/api/client/plugin"
 	"github.com/docker/docker/api/client/registry"
 	"github.com/docker/docker/api/client/service"
 	"github.com/docker/docker/api/client/swarm"
@@ -81,6 +82,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
 		system.NewVersionCommand(dockerCli),
 		volume.NewVolumeCommand(dockerCli),
 	)
+	plugin.NewPluginCommand(rootCmd, dockerCli)
 
 	rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
 	rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")

+ 21 - 0
cli/error.go

@@ -0,0 +1,21 @@
+package cli
+
+import "bytes"
+
+// Errors is a list of errors.
+// Useful in a loop if you don't want to return the error right away and you want to display after the loop,
+// all the errors that happened during the loop.
+type Errors []error
+
+func (errs Errors) Error() string {
+	if len(errs) < 1 {
+		return ""
+	}
+	var buf bytes.Buffer
+	buf.WriteString(errs[0].Error())
+	for _, err := range errs[1:] {
+		buf.WriteString(", ")
+		buf.WriteString(err.Error())
+	}
+	return buf.String()
+}

+ 5 - 0
cmd/dockerd/daemon.go

@@ -262,6 +262,10 @@ func (cli *DaemonCli) start() (err error) {
 		<-stopc // wait for daemonCli.start() to return
 	})
 
+	if err := pluginInit(cli.Config, containerdRemote, registryService); err != nil {
+		return err
+	}
+
 	d, err := daemon.NewDaemon(cli.Config, registryService, containerdRemote)
 	if err != nil {
 		return fmt.Errorf("Error starting daemon: %v", err)
@@ -418,6 +422,7 @@ func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) {
 	if d.NetworkControllerEnabled() {
 		routers = append(routers, network.NewRouter(d, c))
 	}
+	routers = addExperimentalRouters(routers)
 
 	s.InitRouter(utils.IsDebugEnabled(), routers...)
 }

+ 3 - 3
cmd/dockerd/daemon_linux.go

@@ -1,8 +1,8 @@
+// +build linux
+
 package main
 
-import (
-	systemdDaemon "github.com/coreos/go-systemd/daemon"
-)
+import systemdDaemon "github.com/coreos/go-systemd/daemon"
 
 // notifySystem sends a message to the host when the server is ready to be used
 func notifySystem() {

+ 13 - 0
cmd/dockerd/daemon_no_plugin_support.go

@@ -0,0 +1,13 @@
+// +build !experimental !linux
+
+package main
+
+import (
+	"github.com/docker/docker/daemon"
+	"github.com/docker/docker/libcontainerd"
+	"github.com/docker/docker/registry"
+)
+
+func pluginInit(config *daemon.Config, remote libcontainerd.Remote, rs registry.Service) error {
+	return nil
+}

+ 14 - 0
cmd/dockerd/daemon_plugin_support.go

@@ -0,0 +1,14 @@
+// +build linux,experimental
+
+package main
+
+import (
+	"github.com/docker/docker/daemon"
+	"github.com/docker/docker/libcontainerd"
+	"github.com/docker/docker/plugin"
+	"github.com/docker/docker/registry"
+)
+
+func pluginInit(config *daemon.Config, remote libcontainerd.Remote, rs registry.Service) error {
+	return plugin.Init(config.Root, config.ExecRoot, remote, rs)
+}

+ 9 - 0
cmd/dockerd/routes.go

@@ -0,0 +1,9 @@
+// +build !experimental
+
+package main
+
+import "github.com/docker/docker/api/server/router"
+
+func addExperimentalRouters(routers []router.Router) []router.Router {
+	return routers
+}

+ 13 - 0
cmd/dockerd/routes_experimental.go

@@ -0,0 +1,13 @@
+// +build experimental
+
+package main
+
+import (
+	"github.com/docker/docker/api/server/router"
+	pluginrouter "github.com/docker/docker/api/server/router/plugin"
+	"github.com/docker/docker/plugin"
+)
+
+func addExperimentalRouters(routers []router.Router) []router.Router {
+	return append(routers, pluginrouter.NewRouter(plugin.GetManager()))
+}

+ 4 - 4
daemon/daemon.go

@@ -486,7 +486,7 @@ func NewDaemon(config *Config, registryService registry.Service, containerdRemot
 	}
 
 	// Configure the volumes driver
-	volStore, err := configureVolumes(config, rootUID, rootGID)
+	volStore, err := d.configureVolumes(rootUID, rootGID)
 	if err != nil {
 		return nil, err
 	}
@@ -768,8 +768,8 @@ func setDefaultMtu(config *Config) {
 	config.Mtu = defaultNetworkMtu
 }
 
-func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore, error) {
-	volumesDriver, err := local.New(config.Root, rootUID, rootGID)
+func (daemon *Daemon) configureVolumes(rootUID, rootGID int) (*store.VolumeStore, error) {
+	volumesDriver, err := local.New(daemon.configStore.Root, rootUID, rootGID)
 	if err != nil {
 		return nil, err
 	}
@@ -777,7 +777,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
 	if !volumedrivers.Register(volumesDriver, volumesDriver.Name()) {
 		return nil, fmt.Errorf("local volume driver could not be registered")
 	}
-	return store.New(config.Root)
+	return store.New(daemon.configStore.Root)
 }
 
 // IsShuttingDown tells whether the daemon is shutting down or not

+ 1 - 1
daemon/graphdriver/plugin.go

@@ -23,7 +23,7 @@ func lookupPlugin(name, home string, opts []string) (Driver, error) {
 	if err != nil {
 		return nil, fmt.Errorf("Error looking up graphdriver plugin %s: %v", name, err)
 	}
-	return newPluginDriver(name, home, opts, pl.Client)
+	return newPluginDriver(name, home, opts, pl.Client())
 }
 
 func newPluginDriver(name, home string, opts []string, c pluginClient) (Driver, error) {

+ 3 - 3
distribution/pull.go

@@ -84,7 +84,7 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
 	}
 
 	// makes sure name is not empty or `scratch`
-	if err := validateRepoName(repoInfo.Name()); err != nil {
+	if err := ValidateRepoName(repoInfo.Name()); err != nil {
 		return err
 	}
 
@@ -193,8 +193,8 @@ func writeStatus(requestedTag string, out progress.Output, layersDownloaded bool
 	}
 }
 
-// validateRepoName validates the name of a repository.
-func validateRepoName(name string) error {
+// ValidateRepoName validates the name of a repository.
+func ValidateRepoName(name string) error {
 	if name == "" {
 		return fmt.Errorf("Repository name can't be empty")
 	}

+ 52 - 0
docs/reference/commandline/plugin_disable.md

@@ -0,0 +1,52 @@
+<!--[metadata]>
++++
+title = "plugin disable"
+description = "the plugin disable command description and usage"
+keywords = ["plugin, disable"]
+[menu.main]
+parent = "smn_cli"
+advisory = "experimental"
++++
+<![end-metadata]-->
+
+# plugin disable (experimental)
+
+    Usage: docker plugin disable PLUGIN
+
+    Disable a plugin
+
+      --help             Print usage
+
+Disables a plugin. The plugin must be installed before it can be disabled,
+see [`docker plugin install`](plugin_install.md).
+
+
+The following example shows that the `no-remove` plugin is currently installed
+and active:
+
+```bash
+$ docker plugin ls
+NAME        	            TAG			ACTIVE
+tiborvass/no-remove	    latest		true
+```
+To disable the plugin, use the following command:
+
+```bash
+$ docker plugin disable tiborvass/no-remove:latest
+```
+
+After the plugin is disabled, it appears as "inactive" in the list of plugins:
+
+```bash
+$ docker plugin ls
+NAME			VERSION		ACTIVE
+tiborvass/no-remove	latest		false
+```
+
+## Related information
+
+* [plugin ls](plugin_ls.md)
+* [plugin enable](plugin_enable.md)
+* [plugin inspect](plugin_inspect.md)
+* [plugin install](plugin_install.md)
+* [plugin rm](plugin_rm.md)

+ 52 - 0
docs/reference/commandline/plugin_enable.md

@@ -0,0 +1,52 @@
+<!--[metadata]>
++++
+title = "plugin enable"
+description = "the plugin enable command description and usage"
+keywords = ["plugin, enable"]
+[menu.main]
+parent = "smn_cli"
+advisory = "experimental"
++++
+<![end-metadata]-->
+
+# plugin enable (experimental)
+
+    Usage: docker plugin enable PLUGIN
+
+    Enable a plugin
+
+      --help             Print usage
+
+Enables a plugin. The plugin must be installed before it can be enabled,
+see [`docker plugin install`](plugin_install.md).
+
+
+The following example shows that the `no-remove` plugin is currently installed,
+but disabled ("inactive"):
+
+```bash
+$ docker plugin ls
+NAME                	VERSION             ACTIVE
+tiborvass/no-remove	latest              false
+```
+To enable the plugin, use the following command:
+
+```bash
+$ docker plugin enable tiborvass/no-remove:latest
+```
+
+After the plugin is enabled, it appears as "active" in the list of plugins:
+
+```bash
+$ docker plugin ls
+NAME                	VERSION             ACTIVE
+tiborvass/no-remove	latest              true
+```
+
+## Related information
+
+* [plugin ls](plugin_ls.md)
+* [plugin disable](plugin_disable.md)
+* [plugin inspect](plugin_inspect.md)
+* [plugin install](plugin_install.md)
+* [plugin rm](plugin_rm.md)

+ 135 - 0
docs/reference/commandline/plugin_inspect.md

@@ -0,0 +1,135 @@
+<!--[metadata]>
++++
+title = "plugin inspect"
+description = "The plugin inspect command description and usage"
+keywords = ["plugin, inspect"]
+[menu.main]
+parent = "smn_cli"
+advisory = "experimental"
++++
+<![end-metadata]-->
+
+# plugin inspect (experimental)
+
+    Usage: docker plugin inspect PLUGIN
+
+    Return low-level information about a plugin
+
+      --help              Print usage
+
+
+Returns information about a plugin. By default, this command renders all results
+in a JSON array.
+
+Example output:
+
+```bash
+$ docker plugin inspect tiborvass/no-remove:latest
+```
+```JSON
+{
+    "Manifest": {
+        "ManifestVersion": "",
+        "Description": "A test plugin for Docker",
+        "Documentation": "https://docs.docker.com/engine/extend/plugins/",
+        "Entrypoint": [
+            "plugin-no-remove",
+            "/data"
+        ],
+        "Interface": {
+            "Types": [
+                "docker.volumedriver/1.0"
+            ],
+            "Socket": "plugins.sock"
+        },
+        "Network": {
+            "Type": "host"
+        },
+        "Capabilities": null,
+        "Mounts": [
+            {
+                "Name": "",
+                "Description": "",
+                "Settable": false,
+                "Source": "/data",
+                "Destination": "/data",
+                "Type": "bind",
+                "Options": [
+                    "shared",
+                    "rbind"
+                ]
+            },
+            {
+                "Name": "",
+                "Description": "",
+                "Settable": false,
+                "Source": null,
+                "Destination": "/foobar",
+                "Type": "tmpfs",
+                "Options": null
+            }
+        ],
+        "Devices": [
+            {
+                "Name": "device",
+                "Description": "a host device to mount",
+                "Settable": false,
+                "Path": null
+            }
+        ],
+        "Env": [
+            {
+                "Name": "DEBUG",
+                "Description": "If set, prints debug messages",
+                "Settable": false,
+                "Value": null
+            }
+        ],
+        "Args": [
+            {
+                "Name": "arg1",
+                "Description": "a command line argument",
+                "Settable": false,
+                "Value": null
+            }
+        ]
+    },
+    "Config": {
+        "Mounts": [
+            {
+                "Source": "/data",
+                "Destination": "/data",
+                "Type": "bind",
+                "Options": [
+                    "shared",
+                    "rbind"
+                ]
+            },
+            {
+                "Source": null,
+                "Destination": "/foobar",
+                "Type": "tmpfs",
+                "Options": null
+            }
+        ],
+        "Env": [],
+        "Args": [],
+        "Devices": null
+    },
+    "Active": true,
+    "Name": "tiborvass/no-remove",
+    "Tag": "latest",
+    "ID": "ac9d36b664921d61813254f7e9946f10e3cadbb676346539f1705fcaf039c01f"
+}
+```
+(output formatted for readability)
+
+
+
+## Related information
+
+* [plugin ls](plugin_ls.md)
+* [plugin enable](plugin_enable.md)
+* [plugin disable](plugin_disable.md)
+* [plugin install](plugin_install.md)
+* [plugin rm](plugin_rm.md)

+ 51 - 0
docs/reference/commandline/plugin_install.md

@@ -0,0 +1,51 @@
+<!--[metadata]>
++++
+title = "plugin install"
+description = "the plugin install command description and usage"
+keywords = ["plugin, install"]
+[menu.main]
+parent = "smn_cli"
+advisory = "experimental"
++++
+<![end-metadata]-->
+
+# plugin install (experimental)
+
+    Usage: docker plugin install PLUGIN
+
+    Install a plugin
+
+      --help             Print usage
+
+Installs and enables a plugin. Docker looks first for the plugin on your Docker
+host. If the plugin does not exist locally, then the plugin is pulled from
+Docker Hub.
+
+
+The following example installs `no-remove` plugin. Install consists of pulling the
+plugin from Docker Hub, prompting the user to accept the list of privileges that
+the plugin needs and enabling the plugin.
+
+```bash
+$ docker plugin install tiborvass/no-remove
+Plugin "tiborvass/no-remove:latest" requested the following privileges:
+ - Networking: host
+ - Mounting host path: /data
+Do you grant the above permissions? [y/N] y
+```
+
+After the plugin is installed, it appears in the list of plugins:
+
+```bash
+$ docker plugin ls
+NAME                	VERSION             ACTIVE
+tiborvass/no-remove   latest              true
+```
+
+## Related information
+
+* [plugin ls](plugin_ls.md)
+* [plugin enable](plugin_enable.md)
+* [plugin disable](plugin_disable.md)
+* [plugin inspect](plugin_inspect.md)
+* [plugin rm](plugin_rm.md)

+ 40 - 0
docs/reference/commandline/plugin_ls.md

@@ -0,0 +1,40 @@
+<!--[metadata]>
++++
+title = "plugin ls"
+description = "The plugin ls command description and usage"
+keywords = ["plugin, list"]
+[menu.main]
+parent = "smn_cli"
+advisory = "experimental"
++++
+<![end-metadata]-->
+
+# plugin ls (experimental)
+
+    Usage: docker plugin ls
+
+    List plugins
+
+      --help   Print usage
+
+    Aliases:
+      ls, list
+
+Lists all the plugins that are currently installed. You can install plugins
+using the [`docker plugin install`](plugin_install.md) command.
+
+Example output:
+
+```bash
+$ docker plugin ls
+NAME                	VERSION             ACTIVE
+tiborvass/no-remove	latest              true
+```
+
+## Related information
+
+* [plugin enable](plugin_enable.md)
+* [plugin disable](plugin_disable.md)
+* [plugin inspect](plugin_inspect.md)
+* [plugin install](plugin_install.md)
+* [plugin rm](plugin_rm.md)

+ 41 - 0
docs/reference/commandline/plugin_rm.md

@@ -0,0 +1,41 @@
+<!--[metadata]>
++++
+title = "plugin rm"
+description = "the plugin rm command description and usage"
+keywords = ["plugin, rm"]
+[menu.main]
+parent = "smn_cli"
+advisory = "experimental"
++++
+<![end-metadata]-->
+
+# plugin rm (experimental)
+
+    Usage: docker plugin rm PLUGIN
+
+    Remove a plugin
+
+      --help             Print usage
+
+    Aliases:
+      rm, remove
+
+Removes a plugin. You cannot remove a plugin if it is active, you must disable
+a plugin using the [`docker plugin disable`](plugin_disable.md) before removing
+it.
+
+The following example disables and removes the `no-remove:latest` plugin;
+
+```bash
+$ docker plugin disable tiborvass/no-remove:latest
+$ docker plugin rm tiborvass/no-remove:latest
+no-remove:latest
+```
+
+## Related information
+
+* [plugin ls](plugin_ls.md)
+* [plugin enable](plugin_enable.md)
+* [plugin disable](plugin_disable.md)
+* [plugin inspect](plugin_inspect.md)
+* [plugin install](plugin_install.md)

+ 1 - 1
hack/vendor.sh

@@ -60,7 +60,7 @@ clone git golang.org/x/net 2beffdc2e92c8a3027590f898fe88f69af48a3f8 https://gith
 clone git golang.org/x/sys eb2c74142fd19a79b3f237334c7384d5167b1b46 https://github.com/golang/sys.git
 clone git github.com/docker/go-units 651fc226e7441360384da338d0fd37f2440ffbe3
 clone git github.com/docker/go-connections fa2850ff103453a9ad190da0df0af134f0314b3d
-clone git github.com/docker/engine-api 6b2f24f16a7f1598635b6a99dbe38ec8a5eccaf8
+clone git github.com/docker/engine-api f3b5ad20d4576de14c96603db522dec530d03f62
 clone git github.com/RackSec/srslog 259aed10dfa74ea2961eddd1d9847619f6e98837
 clone git github.com/imdario/mergo 0.2.1
 

+ 1 - 1
integration-cli/docker_cli_external_graphdriver_unix_test.go

@@ -77,7 +77,7 @@ func (s *DockerExternalGraphdriverSuite) setUpPluginViaJSONFile(c *check.C) {
 	mux := http.NewServeMux()
 	s.jserver = httptest.NewServer(mux)
 
-	p := plugins.Plugin{Name: "json-external-graph-driver", Addr: s.jserver.URL}
+	p := plugins.NewLocalPlugin("json-external-graph-driver", s.jserver.URL)
 	b, err := json.Marshal(p)
 	c.Assert(err, check.IsNil)
 

+ 2 - 3
pkg/authorization/authz_unix_test.go

@@ -203,18 +203,17 @@ func TestResponseModifierOverride(t *testing.T) {
 
 // createTestPlugin creates a new sample authorization plugin
 func createTestPlugin(t *testing.T) *authorizationPlugin {
-	plugin := &plugins.Plugin{Name: "authz"}
 	pwd, err := os.Getwd()
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	plugin.Client, err = plugins.NewClient("unix:///"+path.Join(pwd, pluginAddress), tlsconfig.Options{InsecureSkipVerify: true})
+	client, err := plugins.NewClient("unix:///"+path.Join(pwd, pluginAddress), &tlsconfig.Options{InsecureSkipVerify: true})
 	if err != nil {
 		t.Fatalf("Failed to create client %v", err)
 	}
 
-	return &authorizationPlugin{name: "plugin", plugin: plugin}
+	return &authorizationPlugin{name: "plugin", plugin: client}
 }
 
 // AuthZPluginTestServer is a simple server that implements the authZ plugin interface

+ 9 - 4
pkg/authorization/plugin.go

@@ -35,7 +35,7 @@ func NewPlugins(names []string) []Plugin {
 
 // authorizationPlugin is an internal adapter to docker plugin system
 type authorizationPlugin struct {
-	plugin *plugins.Plugin
+	plugin *plugins.Client
 	name   string
 	once   sync.Once
 }
@@ -54,7 +54,7 @@ func (a *authorizationPlugin) AuthZRequest(authReq *Request) (*Response, error)
 	}
 
 	authRes := &Response{}
-	if err := a.plugin.Client.Call(AuthZApiRequest, authReq, authRes); err != nil {
+	if err := a.plugin.Call(AuthZApiRequest, authReq, authRes); err != nil {
 		return nil, err
 	}
 
@@ -67,7 +67,7 @@ func (a *authorizationPlugin) AuthZResponse(authReq *Request) (*Response, error)
 	}
 
 	authRes := &Response{}
-	if err := a.plugin.Client.Call(AuthZApiResponse, authReq, authRes); err != nil {
+	if err := a.plugin.Call(AuthZApiResponse, authReq, authRes); err != nil {
 		return nil, err
 	}
 
@@ -80,7 +80,12 @@ func (a *authorizationPlugin) initPlugin() error {
 	var err error
 	a.once.Do(func() {
 		if a.plugin == nil {
-			a.plugin, err = plugins.Get(a.name, AuthZApiImplements)
+			plugin, e := plugins.Get(a.name, AuthZApiImplements)
+			if e != nil {
+				err = e
+				return
+			}
+			a.plugin = plugin.Client()
 		}
 	})
 	return err

+ 7 - 5
pkg/plugins/client.go

@@ -20,14 +20,16 @@ const (
 )
 
 // NewClient creates a new plugin client (http).
-func NewClient(addr string, tlsConfig tlsconfig.Options) (*Client, error) {
+func NewClient(addr string, tlsConfig *tlsconfig.Options) (*Client, error) {
 	tr := &http.Transport{}
 
-	c, err := tlsconfig.Client(tlsConfig)
-	if err != nil {
-		return nil, err
+	if tlsConfig != nil {
+		c, err := tlsconfig.Client(*tlsConfig)
+		if err != nil {
+			return nil, err
+		}
+		tr.TLSClientConfig = c
 	}
-	tr.TLSClientConfig = c
 
 	u, err := url.Parse(addr)
 	if err != nil {

+ 2 - 2
pkg/plugins/client_test.go

@@ -31,7 +31,7 @@ func teardownRemotePluginServer() {
 }
 
 func TestFailedConnection(t *testing.T) {
-	c, _ := NewClient("tcp://127.0.0.1:1", tlsconfig.Options{InsecureSkipVerify: true})
+	c, _ := NewClient("tcp://127.0.0.1:1", &tlsconfig.Options{InsecureSkipVerify: true})
 	_, err := c.callWithRetry("Service.Method", nil, false)
 	if err == nil {
 		t.Fatal("Unexpected successful connection")
@@ -55,7 +55,7 @@ func TestEchoInputOutput(t *testing.T) {
 		io.Copy(w, r.Body)
 	})
 
-	c, _ := NewClient(addr, tlsconfig.Options{InsecureSkipVerify: true})
+	c, _ := NewClient(addr, &tlsconfig.Options{InsecureSkipVerify: true})
 	var output Manifest
 	err := c.Call("Test.Echo", m, &output)
 	if err != nil {

+ 3 - 3
pkg/plugins/discovery.go

@@ -64,7 +64,7 @@ func (l *localRegistry) Plugin(name string) (*Plugin, error) {
 
 	for _, p := range socketpaths {
 		if fi, err := os.Stat(p); err == nil && fi.Mode()&os.ModeSocket != 0 {
-			return newLocalPlugin(name, "unix://"+p), nil
+			return NewLocalPlugin(name, "unix://"+p), nil
 		}
 	}
 
@@ -101,7 +101,7 @@ func readPluginInfo(name, path string) (*Plugin, error) {
 		return nil, fmt.Errorf("Unknown protocol")
 	}
 
-	return newLocalPlugin(name, addr), nil
+	return NewLocalPlugin(name, addr), nil
 }
 
 func readPluginJSONInfo(name, path string) (*Plugin, error) {
@@ -115,7 +115,7 @@ func readPluginJSONInfo(name, path string) (*Plugin, error) {
 	if err := json.NewDecoder(f).Decode(&p); err != nil {
 		return nil, err
 	}
-	p.Name = name
+	p.name = name
 	if len(p.TLSConfig.CAFile) == 0 {
 		p.TLSConfig.InsecureSkipVerify = true
 	}

+ 2 - 2
pkg/plugins/discovery_test.go

@@ -58,7 +58,7 @@ func TestFileSpecPlugin(t *testing.T) {
 			t.Fatal(err)
 		}
 
-		if p.Name != c.name {
+		if p.name != c.name {
 			t.Fatalf("Expected plugin `%s`, got %s\n", c.name, p.Name)
 		}
 
@@ -97,7 +97,7 @@ func TestFileJSONSpecPlugin(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	if plugin.Name != "example" {
+	if plugin.name != "example" {
 		t.Fatalf("Expected plugin `plugin-example`, got %s\n", plugin.Name)
 	}
 

+ 1 - 1
pkg/plugins/discovery_unix_test.go

@@ -45,7 +45,7 @@ func TestLocalSocket(t *testing.T) {
 			t.Fatalf("Expected %v, was %v\n", p, pp)
 		}
 
-		if p.Name != "echo" {
+		if p.name != "echo" {
 			t.Fatalf("Expected plugin `echo`, got %s\n", p.Name)
 		}
 

+ 22 - 10
pkg/plugins/plugins.go

@@ -55,13 +55,13 @@ type Manifest struct {
 // Plugin is the definition of a docker plugin.
 type Plugin struct {
 	// Name of the plugin
-	Name string `json:"-"`
+	name string
 	// Address of the plugin
 	Addr string
 	// TLS configuration of the plugin
-	TLSConfig tlsconfig.Options
+	TLSConfig *tlsconfig.Options
 	// Client attached to the plugin
-	Client *Client `json:"-"`
+	client *Client
 	// Manifest of the plugin (see above)
 	Manifest *Manifest `json:"-"`
 
@@ -73,11 +73,23 @@ type Plugin struct {
 	activateWait *sync.Cond
 }
 
-func newLocalPlugin(name, addr string) *Plugin {
+// Name returns the name of the plugin.
+func (p *Plugin) Name() string {
+	return p.name
+}
+
+// Client returns a ready-to-use plugin client that can be used to communicate with the plugin.
+func (p *Plugin) Client() *Client {
+	return p.client
+}
+
+// NewLocalPlugin creates a new local plugin.
+func NewLocalPlugin(name, addr string) *Plugin {
 	return &Plugin{
-		Name:         name,
-		Addr:         addr,
-		TLSConfig:    tlsconfig.Options{InsecureSkipVerify: true},
+		name: name,
+		Addr: addr,
+		// TODO: change to nil
+		TLSConfig:    &tlsconfig.Options{InsecureSkipVerify: true},
 		activateWait: sync.NewCond(&sync.Mutex{}),
 	}
 }
@@ -102,10 +114,10 @@ func (p *Plugin) activateWithLock() error {
 	if err != nil {
 		return err
 	}
-	p.Client = c
+	p.client = c
 
 	m := new(Manifest)
-	if err = p.Client.Call("Plugin.Activate", nil, m); err != nil {
+	if err = p.client.Call("Plugin.Activate", nil, m); err != nil {
 		return err
 	}
 
@@ -116,7 +128,7 @@ func (p *Plugin) activateWithLock() error {
 		if !handled {
 			continue
 		}
-		handler(p.Name, p.Client)
+		handler(p.name, p.client)
 	}
 	return nil
 }

+ 139 - 0
plugin/backend.go

@@ -0,0 +1,139 @@
+// +build experimental
+
+package plugin
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"path/filepath"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker/pkg/archive"
+	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/docker/plugin/distribution"
+	"github.com/docker/docker/reference"
+	"github.com/docker/engine-api/types"
+)
+
+// Disable deactivates a plugin, which implies that they cannot be used by containers.
+func (pm *Manager) Disable(name string) error {
+	p, err := pm.get(name)
+	if err != nil {
+		return err
+	}
+	return pm.disable(p)
+}
+
+// Enable activates a plugin, which implies that they are ready to be used by containers.
+func (pm *Manager) Enable(name string) error {
+	p, err := pm.get(name)
+	if err != nil {
+		return err
+	}
+	return pm.enable(p)
+}
+
+// Inspect examines a plugin manifest
+func (pm *Manager) Inspect(name string) (tp types.Plugin, err error) {
+	p, err := pm.get(name)
+	if err != nil {
+		return tp, err
+	}
+	return p.p, nil
+}
+
+// Pull pulls a plugin and enables it.
+func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.AuthConfig) (types.PluginPrivileges, error) {
+	ref, err := reference.ParseNamed(name)
+	if err != nil {
+		logrus.Debugf("error in reference.ParseNamed: %v", err)
+		return nil, err
+	}
+	name = ref.String()
+
+	if p, _ := pm.get(name); p != nil {
+		logrus.Debugf("plugin already exists")
+		return nil, fmt.Errorf("%s exists", name)
+	}
+
+	pluginID := stringid.GenerateNonCryptoID()
+
+	if err := os.MkdirAll(filepath.Join(pm.libRoot, pluginID), 0755); err != nil {
+		logrus.Debugf("error in MkdirAll: %v", err)
+		return nil, err
+	}
+
+	pd, err := distribution.Pull(name, pm.registryService, metaHeader, authConfig)
+	if err != nil {
+		logrus.Debugf("error in distribution.Pull(): %v", err)
+		return nil, err
+	}
+
+	if err := distribution.WritePullData(pd, filepath.Join(pm.libRoot, pluginID), true); err != nil {
+		logrus.Debugf("error in distribution.WritePullData(): %v", err)
+		return nil, err
+	}
+
+	p := pm.newPlugin(ref, pluginID)
+	if ref, ok := ref.(reference.NamedTagged); ok {
+		p.p.Tag = ref.Tag()
+	}
+
+	if err := pm.initPlugin(p); err != nil {
+		return nil, err
+	}
+
+	pm.Lock()
+	pm.plugins[pluginID] = p
+	pm.nameToID[name] = pluginID
+	pm.save()
+	pm.Unlock()
+
+	return computePrivileges(&p.p.Manifest), nil
+}
+
+// List displays the list of plugins and associated metadata.
+func (pm *Manager) List() ([]types.Plugin, error) {
+	out := make([]types.Plugin, 0, len(pm.plugins))
+	for _, p := range pm.plugins {
+		out = append(out, p.p)
+	}
+	return out, nil
+}
+
+// Push pushes a plugin to the store.
+func (pm *Manager) Push(name string, metaHeader http.Header, authConfig *types.AuthConfig) error {
+	p, err := pm.get(name)
+	dest := filepath.Join(pm.libRoot, p.p.ID)
+	config, err := os.Open(filepath.Join(dest, "manifest.json"))
+	if err != nil {
+		return err
+	}
+	rootfs, err := archive.Tar(filepath.Join(dest, "rootfs"), archive.Gzip)
+	if err != nil {
+		return err
+	}
+	_, err = distribution.Push(name, pm.registryService, metaHeader, authConfig, config, rootfs)
+	// XXX: Ignore returning digest for now.
+	// Since digest needs to be written to the ProgressWriter.
+	return nil
+}
+
+// Remove deletes plugin's root directory.
+func (pm *Manager) Remove(name string) error {
+	p, err := pm.get(name)
+	if err != nil {
+		return err
+	}
+	return pm.remove(p)
+}
+
+// Set sets plugin args
+func (pm *Manager) Set(name string, args []string) error {
+	p, err := pm.get(name)
+	if err != nil {
+		return err
+	}
+	return pm.set(p, args)
+}

+ 208 - 0
plugin/distribution/pull.go

@@ -0,0 +1,208 @@
+// +build experimental
+
+package distribution
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/distribution"
+	"github.com/docker/distribution/manifest/schema2"
+	dockerdist "github.com/docker/docker/distribution"
+	archive "github.com/docker/docker/pkg/chrootarchive"
+	"github.com/docker/docker/reference"
+	"github.com/docker/docker/registry"
+	"github.com/docker/engine-api/types"
+	"golang.org/x/net/context"
+)
+
+// PullData is the plugin manifest and the rootfs
+type PullData interface {
+	Config() ([]byte, error)
+	Layer() (io.ReadCloser, error)
+}
+
+type pullData struct {
+	repository distribution.Repository
+	manifest   schema2.Manifest
+	index      int
+}
+
+func (pd *pullData) Config() ([]byte, error) {
+	blobs := pd.repository.Blobs(context.Background())
+	config, err := blobs.Get(context.Background(), pd.manifest.Config.Digest)
+	if err != nil {
+		return nil, err
+	}
+	// validate
+	var p types.Plugin
+	if err := json.Unmarshal(config, &p); err != nil {
+		return nil, err
+	}
+	return config, nil
+}
+
+func (pd *pullData) Layer() (io.ReadCloser, error) {
+	if pd.index >= len(pd.manifest.Layers) {
+		return nil, io.EOF
+	}
+
+	blobs := pd.repository.Blobs(context.Background())
+	rsc, err := blobs.Open(context.Background(), pd.manifest.Layers[pd.index].Digest)
+	if err != nil {
+		return nil, err
+	}
+	pd.index++
+	return rsc, nil
+}
+
+// Pull downloads the plugin from Store
+func Pull(name string, rs registry.Service, metaheader http.Header, authConfig *types.AuthConfig) (PullData, error) {
+	ref, err := reference.ParseNamed(name)
+	if err != nil {
+		logrus.Debugf("pull.go: error in ParseNamed: %v", err)
+		return nil, err
+	}
+
+	repoInfo, err := rs.ResolveRepository(ref)
+	if err != nil {
+		logrus.Debugf("pull.go: error in ResolveRepository: %v", err)
+		return nil, err
+	}
+
+	if err := dockerdist.ValidateRepoName(repoInfo.Name()); err != nil {
+		logrus.Debugf("pull.go: error in ValidateRepoName: %v", err)
+		return nil, err
+	}
+
+	endpoints, err := rs.LookupPullEndpoints(repoInfo.Hostname())
+	if err != nil {
+		logrus.Debugf("pull.go: error in LookupPullEndpoints: %v", err)
+		return nil, err
+	}
+
+	var confirmedV2 bool
+	var repository distribution.Repository
+
+	for _, endpoint := range endpoints {
+		if confirmedV2 && endpoint.Version == registry.APIVersion1 {
+			logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
+			continue
+		}
+
+		// TODO: reuse contexts
+		repository, confirmedV2, err = dockerdist.NewV2Repository(context.Background(), repoInfo, endpoint, metaheader, authConfig, "pull")
+		if err != nil {
+			logrus.Debugf("pull.go: error in NewV2Repository: %v", err)
+			return nil, err
+		}
+		if !confirmedV2 {
+			logrus.Debugf("pull.go: !confirmedV2")
+			return nil, ErrUnSupportedRegistry
+		}
+		logrus.Debugf("Trying to pull %s from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
+		break
+	}
+
+	tag := DefaultTag
+	if ref, ok := ref.(reference.NamedTagged); ok {
+		tag = ref.Tag()
+	}
+
+	// tags := repository.Tags(context.Background())
+	// desc, err := tags.Get(context.Background(), tag)
+	// 	if err != nil {
+	// 		return nil, err
+	// 	}
+	//
+	msv, err := repository.Manifests(context.Background())
+	if err != nil {
+		logrus.Debugf("pull.go: error in repository.Manifests: %v", err)
+		return nil, err
+	}
+	manifest, err := msv.Get(context.Background(), "", distribution.WithTag(tag))
+	if err != nil {
+		// TODO: change 401 to 404
+		logrus.Debugf("pull.go: error in msv.Get(): %v", err)
+		return nil, err
+	}
+
+	_, pl, err := manifest.Payload()
+	if err != nil {
+		logrus.Debugf("pull.go: error in manifest.Payload(): %v", err)
+		return nil, err
+	}
+	var m schema2.Manifest
+	if err := json.Unmarshal(pl, &m); err != nil {
+		logrus.Debugf("pull.go: error in json.Unmarshal(): %v", err)
+		return nil, err
+	}
+
+	pd := &pullData{
+		repository: repository,
+		manifest:   m,
+	}
+
+	logrus.Debugf("manifest: %s", pl)
+	return pd, nil
+}
+
+// WritePullData extracts manifest and rootfs to the disk.
+func WritePullData(pd PullData, dest string, extract bool) error {
+	config, err := pd.Config()
+	if err != nil {
+		return err
+	}
+	var p types.Plugin
+	if err := json.Unmarshal(config, &p); err != nil {
+		return err
+	}
+	logrus.Debugf("%#v", p)
+
+	if err := os.MkdirAll(dest, 0700); err != nil {
+		return err
+	}
+
+	if extract {
+		if err := ioutil.WriteFile(filepath.Join(dest, "manifest.json"), config, 0600); err != nil {
+			return err
+		}
+
+		if err := os.MkdirAll(filepath.Join(dest, "rootfs"), 0700); err != nil {
+			return err
+		}
+	}
+
+	for i := 0; ; i++ {
+		l, err := pd.Layer()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return err
+		}
+
+		if !extract {
+			f, err := os.Create(filepath.Join(dest, fmt.Sprintf("layer%d.tar", i)))
+			if err != nil {
+				return err
+			}
+			io.Copy(f, l)
+			l.Close()
+			f.Close()
+			continue
+		}
+
+		if _, err := archive.ApplyLayer(filepath.Join(dest, "rootfs"), l); err != nil {
+			return err
+		}
+
+	}
+	return nil
+}

+ 134 - 0
plugin/distribution/push.go

@@ -0,0 +1,134 @@
+// +build experimental
+
+package distribution
+
+import (
+	"crypto/sha256"
+	"io"
+	"net/http"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/distribution"
+	"github.com/docker/distribution/digest"
+	"github.com/docker/distribution/manifest/schema2"
+	dockerdist "github.com/docker/docker/distribution"
+	"github.com/docker/docker/reference"
+	"github.com/docker/docker/registry"
+	"github.com/docker/engine-api/types"
+	"golang.org/x/net/context"
+)
+
+// Push pushes a plugin to a registry.
+func Push(name string, rs registry.Service, metaHeader http.Header, authConfig *types.AuthConfig, config io.ReadCloser, layers io.ReadCloser) (digest.Digest, error) {
+	ref, err := reference.ParseNamed(name)
+	if err != nil {
+		return "", err
+	}
+
+	repoInfo, err := rs.ResolveRepository(ref)
+	if err != nil {
+		return "", err
+	}
+
+	if err := dockerdist.ValidateRepoName(repoInfo.Name()); err != nil {
+		return "", err
+	}
+
+	endpoints, err := rs.LookupPushEndpoints(repoInfo.Hostname())
+	if err != nil {
+		return "", err
+	}
+
+	var confirmedV2 bool
+	var repository distribution.Repository
+	for _, endpoint := range endpoints {
+		if confirmedV2 && endpoint.Version == registry.APIVersion1 {
+			logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
+			continue
+		}
+		repository, confirmedV2, err = dockerdist.NewV2Repository(context.Background(), repoInfo, endpoint, metaHeader, authConfig, "push", "pull")
+		if err != nil {
+			return "", err
+		}
+		if !confirmedV2 {
+			return "", ErrUnSupportedRegistry
+		}
+		logrus.Debugf("Trying to push %s to %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
+		// This means that we found an endpoint. and we are ready to push
+		break
+	}
+
+	// Returns a reference to the repository's blob service.
+	blobs := repository.Blobs(context.Background())
+
+	// Descriptor = {mediaType, size, digest}
+	var descs []distribution.Descriptor
+
+	for i, f := range []io.ReadCloser{config, layers} {
+		bw, err := blobs.Create(context.Background())
+		if err != nil {
+			logrus.Debugf("Error in blobs.Create: %v", err)
+			return "", err
+		}
+		h := sha256.New()
+		r := io.TeeReader(f, h)
+		_, err = io.Copy(bw, r)
+		if err != nil {
+			logrus.Debugf("Error in io.Copy: %v", err)
+			return "", err
+		}
+		f.Close()
+		mt := MediaTypeLayer
+		if i == 0 {
+			mt = MediaTypeConfig
+		}
+		// Commit completes the write process to the BlobService.
+		// The descriptor arg to Commit is called the "provisional" descriptor and
+		// used for validation.
+		// The returned descriptor should be the one used. Its called the "Canonical"
+		// descriptor.
+		desc, err := bw.Commit(context.Background(), distribution.Descriptor{
+			MediaType: mt,
+			// XXX: What about the Size?
+			Digest: digest.NewDigest("sha256", h),
+		})
+		if err != nil {
+			logrus.Debugf("Error in bw.Commit: %v", err)
+			return "", err
+		}
+		// The canonical descriptor is set the mediatype again, just in case.
+		// Dont touch the digest or the size here.
+		desc.MediaType = mt
+		logrus.Debugf("pushed blob: %s %s", desc.MediaType, desc.Digest)
+		descs = append(descs, desc)
+	}
+
+	// XXX: schema2.Versioned needs a MediaType as well.
+	// "application/vnd.docker.distribution.manifest.v2+json"
+	m, err := schema2.FromStruct(schema2.Manifest{Versioned: schema2.SchemaVersion, Config: descs[0], Layers: descs[1:]})
+	if err != nil {
+		logrus.Debugf("error in schema2.FromStruct: %v", err)
+		return "", err
+	}
+
+	msv, err := repository.Manifests(context.Background())
+	if err != nil {
+		logrus.Debugf("error in repository.Manifests: %v", err)
+		return "", err
+	}
+
+	_, pl, err := m.Payload()
+	if err != nil {
+		logrus.Debugf("error in m.Payload: %v", err)
+		return "", err
+	}
+
+	logrus.Debugf("Pushed manifest: %s", pl)
+
+	tag := DefaultTag
+	if tagged, ok := ref.(reference.NamedTagged); ok {
+		tag = tagged.Tag()
+	}
+
+	return msv.Put(context.Background(), m, distribution.WithTag(tag))
+}

+ 16 - 0
plugin/distribution/types.go

@@ -0,0 +1,16 @@
+// +build experimental
+
+package distribution
+
+import "errors"
+
+// ErrUnSupportedRegistry indicates that the registry does not support v2 protocol
+var ErrUnSupportedRegistry = errors.New("Only V2 repositories are supported for plugin distribution")
+
+// Plugin related media types
+const (
+	MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"
+	MediaTypeConfig   = "application/vnd.docker.plugin.v0+json"
+	MediaTypeLayer    = "application/vnd.docker.image.rootfs.diff.tar.gzip"
+	DefaultTag        = "latest"
+)

+ 9 - 0
plugin/interface.go

@@ -0,0 +1,9 @@
+package plugin
+
+import "github.com/docker/docker/pkg/plugins"
+
+// Plugin represents a plugin. It is used to abstract from an older plugin architecture (in pkg/plugins).
+type Plugin interface {
+	Client() *plugins.Client
+	Name() string
+}

+ 23 - 0
plugin/legacy.go

@@ -0,0 +1,23 @@
+// +build !experimental
+
+package plugin
+
+import "github.com/docker/docker/pkg/plugins"
+
+// FindWithCapability returns a list of plugins matching the given capability.
+func FindWithCapability(capability string) ([]Plugin, error) {
+	pl, err := plugins.GetAll(capability)
+	if err != nil {
+		return nil, err
+	}
+	result := make([]Plugin, len(pl))
+	for i, p := range pl {
+		result[i] = p
+	}
+	return result, nil
+}
+
+// LookupWithCapability returns a plugin matching the given name and capability.
+func LookupWithCapability(name, capability string) (Plugin, error) {
+	return plugins.Get(name, capability)
+}

+ 384 - 0
plugin/manager.go

@@ -0,0 +1,384 @@
+// +build experimental
+
+package plugin
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker/libcontainerd"
+	"github.com/docker/docker/pkg/ioutils"
+	"github.com/docker/docker/pkg/plugins"
+	"github.com/docker/docker/reference"
+	"github.com/docker/docker/registry"
+	"github.com/docker/docker/restartmanager"
+	"github.com/docker/engine-api/types"
+)
+
+const (
+	defaultPluginRuntimeDestination = "/run/docker/plugins"
+	defaultPluginStateDestination   = "/state"
+)
+
+var manager *Manager
+
+// ErrNotFound indicates that a plugin was not found locally.
+type ErrNotFound string
+
+func (name ErrNotFound) Error() string { return fmt.Sprintf("plugin %q not found", string(name)) }
+
+// ErrInadequateCapability indicates that a plugin was found but did not have the requested capability.
+type ErrInadequateCapability struct {
+	name       string
+	capability string
+}
+
+func (e ErrInadequateCapability) Error() string {
+	return fmt.Sprintf("plugin %q found, but not with %q capability", e.name, e.capability)
+}
+
+type plugin struct {
+	//sync.RWMutex TODO
+	p                 types.Plugin
+	client            *plugins.Client
+	restartManager    restartmanager.RestartManager
+	stateSourcePath   string
+	runtimeSourcePath string
+}
+
+func (p *plugin) Client() *plugins.Client {
+	return p.client
+}
+
+func (p *plugin) Name() string {
+	return p.p.Name
+}
+
+func (pm *Manager) newPlugin(ref reference.Named, id string) *plugin {
+	p := &plugin{
+		p: types.Plugin{
+			Name: ref.Name(),
+			ID:   id,
+		},
+		stateSourcePath:   filepath.Join(pm.libRoot, id, "state"),
+		runtimeSourcePath: filepath.Join(pm.runRoot, id),
+	}
+	if ref, ok := ref.(reference.NamedTagged); ok {
+		p.p.Tag = ref.Tag()
+	}
+	return p
+}
+
+// TODO: figure out why save() doesn't json encode *plugin object
+type pluginMap map[string]*plugin
+
+// Manager controls the plugin subsystem.
+type Manager struct {
+	sync.RWMutex
+	libRoot          string
+	runRoot          string
+	plugins          pluginMap // TODO: figure out why save() doesn't json encode *plugin object
+	nameToID         map[string]string
+	handlers         map[string]func(string, *plugins.Client)
+	containerdClient libcontainerd.Client
+	registryService  registry.Service
+	handleLegacy     bool
+}
+
+// GetManager returns the singleton plugin Manager
+func GetManager() *Manager {
+	return manager
+}
+
+// Init (was NewManager) instantiates the singleton Manager.
+// TODO: revert this to NewManager once we get rid of all the singletons.
+func Init(root, execRoot string, remote libcontainerd.Remote, rs registry.Service) (err error) {
+	if manager != nil {
+		return nil
+	}
+
+	root = filepath.Join(root, "plugins")
+	execRoot = filepath.Join(execRoot, "plugins")
+	for _, dir := range []string{root, execRoot} {
+		if err := os.MkdirAll(dir, 0700); err != nil {
+			return err
+		}
+	}
+
+	manager = &Manager{
+		libRoot:         root,
+		runRoot:         execRoot,
+		plugins:         make(map[string]*plugin),
+		nameToID:        make(map[string]string),
+		handlers:        make(map[string]func(string, *plugins.Client)),
+		registryService: rs,
+		handleLegacy:    true,
+	}
+	if err := os.MkdirAll(manager.runRoot, 0700); err != nil {
+		return err
+	}
+	if err := manager.init(); err != nil {
+		return err
+	}
+	manager.containerdClient, err = remote.Client(manager)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// Handle sets a callback for a given capability. The callback will be called for every plugin with a given capability.
+// TODO: append instead of set?
+func Handle(capability string, callback func(string, *plugins.Client)) {
+	pluginType := fmt.Sprintf("docker.%s/1", strings.ToLower(capability))
+	manager.handlers[pluginType] = callback
+	if manager.handleLegacy {
+		plugins.Handle(capability, callback)
+	}
+}
+
+func (pm *Manager) get(name string) (*plugin, error) {
+	pm.RLock()
+	id, nameOk := pm.nameToID[name]
+	p, idOk := pm.plugins[id]
+	pm.RUnlock()
+	if !nameOk || !idOk {
+		return nil, ErrNotFound(name)
+	}
+	return p, nil
+}
+
+// FindWithCapability returns a list of plugins matching the given capability.
+func FindWithCapability(capability string) ([]Plugin, error) {
+	handleLegacy := true
+	result := make([]Plugin, 0, 1)
+	if manager != nil {
+		handleLegacy = manager.handleLegacy
+		manager.RLock()
+		defer manager.RUnlock()
+	pluginLoop:
+		for _, p := range manager.plugins {
+			for _, typ := range p.p.Manifest.Interface.Types {
+				if typ.Capability != capability || typ.Prefix != "docker" {
+					continue pluginLoop
+				}
+			}
+			result = append(result, p)
+		}
+	}
+	if handleLegacy {
+		pl, err := plugins.GetAll(capability)
+		if err != nil {
+			return nil, fmt.Errorf("legacy plugin: %v", err)
+		}
+		for _, p := range pl {
+			if _, ok := manager.nameToID[p.Name()]; !ok {
+				result = append(result, p)
+			}
+		}
+	}
+	return result, nil
+}
+
+// LookupWithCapability returns a plugin matching the given name and capability.
+func LookupWithCapability(name, capability string) (Plugin, error) {
+	var (
+		p   *plugin
+		err error
+	)
+	handleLegacy := true
+	if manager != nil {
+		p, err = manager.get(name)
+		if err != nil {
+			if _, ok := err.(ErrNotFound); !ok {
+				return nil, err
+			}
+			handleLegacy = manager.handleLegacy
+		} else {
+			handleLegacy = false
+		}
+	}
+	if handleLegacy {
+		p, err := plugins.Get(name, capability)
+		if err != nil {
+			return nil, fmt.Errorf("legacy plugin: %v", err)
+		}
+		return p, nil
+	} else if err != nil {
+		return nil, err
+	}
+
+	capability = strings.ToLower(capability)
+	for _, typ := range p.p.Manifest.Interface.Types {
+		if typ.Capability == capability && typ.Prefix == "docker" {
+			return p, nil
+		}
+	}
+	return nil, ErrInadequateCapability{name, capability}
+}
+
+// StateChanged updates daemon inter...
+func (pm *Manager) StateChanged(id string, e libcontainerd.StateInfo) error {
+	logrus.Debugf("plugin statechanged %s %#v", id, e)
+
+	return nil
+}
+
+// AttachStreams attaches io streams to the plugin
+func (pm *Manager) AttachStreams(id string, iop libcontainerd.IOPipe) error {
+	iop.Stdin.Close()
+
+	logger := logrus.New()
+	logger.Hooks.Add(logHook{id})
+	// TODO: cache writer per id
+	w := logger.Writer()
+	go func() {
+		io.Copy(w, iop.Stdout)
+	}()
+	go func() {
+		// TODO: update logrus and use logger.WriterLevel
+		io.Copy(w, iop.Stderr)
+	}()
+	return nil
+}
+
+func (pm *Manager) init() error {
+	dt, err := os.Open(filepath.Join(pm.libRoot, "plugins.json"))
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil
+		}
+		return err
+	}
+	// TODO: Populate pm.plugins
+	if err := json.NewDecoder(dt).Decode(&pm.nameToID); err != nil {
+		return err
+	}
+	// FIXME: validate, restore
+
+	return nil
+}
+
+func (pm *Manager) initPlugin(p *plugin) error {
+	dt, err := os.Open(filepath.Join(pm.libRoot, p.p.ID, "manifest.json"))
+	if err != nil {
+		return err
+	}
+	err = json.NewDecoder(dt).Decode(&p.p.Manifest)
+	dt.Close()
+	if err != nil {
+		return err
+	}
+
+	p.p.Config.Mounts = make([]types.PluginMount, len(p.p.Manifest.Mounts))
+	for i, mount := range p.p.Manifest.Mounts {
+		p.p.Config.Mounts[i] = mount
+	}
+	p.p.Config.Env = make([]string, 0, len(p.p.Manifest.Env))
+	for _, env := range p.p.Manifest.Env {
+		if env.Value != nil {
+			p.p.Config.Env = append(p.p.Config.Env, fmt.Sprintf("%s=%s", env.Name, *env.Value))
+		}
+	}
+	copy(p.p.Config.Args, p.p.Manifest.Args.Value)
+
+	f, err := os.Create(filepath.Join(pm.libRoot, p.p.ID, "plugin-config.json"))
+	if err != nil {
+		return err
+	}
+	err = json.NewEncoder(f).Encode(&p.p.Config)
+	f.Close()
+	return err
+}
+
+func (pm *Manager) remove(p *plugin) error {
+	if p.p.Active {
+		return fmt.Errorf("plugin %s is active", p.p.Name)
+	}
+	pm.Lock() // fixme: lock single record
+	defer pm.Unlock()
+	os.RemoveAll(p.stateSourcePath)
+	delete(pm.plugins, p.p.Name)
+	pm.save()
+	return nil
+}
+
+func (pm *Manager) set(p *plugin, args []string) error {
+	m := make(map[string]string, len(args))
+	for _, arg := range args {
+		i := strings.Index(arg, "=")
+		if i < 0 {
+			return fmt.Errorf("No equal sign '=' found in %s", arg)
+		}
+		m[arg[:i]] = arg[i+1:]
+	}
+	return errors.New("not implemented")
+}
+
+// fixme: not safe
+func (pm *Manager) save() error {
+	filePath := filepath.Join(pm.libRoot, "plugins.json")
+
+	jsonData, err := json.Marshal(pm.nameToID)
+	if err != nil {
+		logrus.Debugf("Error in json.Marshal: %v", err)
+		return err
+	}
+	ioutils.AtomicWriteFile(filePath, jsonData, 0600)
+	return nil
+}
+
+type logHook struct{ id string }
+
+func (logHook) Levels() []logrus.Level {
+	return logrus.AllLevels
+}
+
+func (l logHook) Fire(entry *logrus.Entry) error {
+	entry.Data = logrus.Fields{"plugin": l.id}
+	return nil
+}
+
+func computePrivileges(m *types.PluginManifest) types.PluginPrivileges {
+	var privileges types.PluginPrivileges
+	if m.Network.Type != "null" && m.Network.Type != "bridge" {
+		privileges = append(privileges, types.PluginPrivilege{
+			Name:        "network",
+			Description: "",
+			Value:       []string{m.Network.Type},
+		})
+	}
+	for _, mount := range m.Mounts {
+		if mount.Source != nil {
+			privileges = append(privileges, types.PluginPrivilege{
+				Name:        "mount",
+				Description: "",
+				Value:       []string{*mount.Source},
+			})
+		}
+	}
+	for _, device := range m.Devices {
+		if device.Path != nil {
+			privileges = append(privileges, types.PluginPrivilege{
+				Name:        "device",
+				Description: "",
+				Value:       []string{*device.Path},
+			})
+		}
+	}
+	if len(m.Capabilities) > 0 {
+		privileges = append(privileges, types.PluginPrivilege{
+			Name:        "capabilities",
+			Description: "",
+			Value:       m.Capabilities,
+		})
+	}
+	return privileges
+}

+ 126 - 0
plugin/manager_linux.go

@@ -0,0 +1,126 @@
+// +build linux,experimental
+
+package plugin
+
+import (
+	"os"
+	"path/filepath"
+	"syscall"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker/libcontainerd"
+	"github.com/docker/docker/oci"
+	"github.com/docker/docker/pkg/plugins"
+	"github.com/docker/docker/pkg/system"
+	"github.com/docker/docker/restartmanager"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/engine-api/types/container"
+	"github.com/opencontainers/specs/specs-go"
+)
+
+func (pm *Manager) enable(p *plugin) error {
+	spec, err := pm.initSpec(p)
+	if err != nil {
+		return err
+	}
+
+	p.restartManager = restartmanager.New(container.RestartPolicy{Name: "always"}, 0)
+	if err := pm.containerdClient.Create(p.p.ID, libcontainerd.Spec(*spec), libcontainerd.WithRestartManager(p.restartManager)); err != nil { // POC-only
+		return err
+	}
+
+	socket := p.p.Manifest.Interface.Socket
+	p.client, err = plugins.NewClient("unix://"+filepath.Join(p.runtimeSourcePath, socket), nil)
+	if err != nil {
+		return err
+	}
+
+	//TODO: check net.Dial
+
+	pm.Lock() // fixme: lock single record
+	p.p.Active = true
+	pm.save()
+	pm.Unlock()
+
+	for _, typ := range p.p.Manifest.Interface.Types {
+		if handler := pm.handlers[typ.String()]; handler != nil {
+			handler(p.Name(), p.Client())
+		}
+	}
+
+	return nil
+}
+
+func (pm *Manager) initSpec(p *plugin) (*specs.Spec, error) {
+	s := oci.DefaultSpec()
+
+	rootfs := filepath.Join(pm.libRoot, p.p.ID, "rootfs")
+	s.Root = specs.Root{
+		Path:     rootfs,
+		Readonly: false, // TODO: all plugins should be readonly? settable in manifest?
+	}
+
+	mounts := append(p.p.Config.Mounts, types.PluginMount{
+		Source:      &p.runtimeSourcePath,
+		Destination: defaultPluginRuntimeDestination,
+		Type:        "bind",
+		Options:     []string{"rbind", "rshared"},
+	}, types.PluginMount{
+		Source:      &p.stateSourcePath,
+		Destination: defaultPluginStateDestination,
+		Type:        "bind",
+		Options:     []string{"rbind", "rshared"},
+	})
+	for _, mount := range mounts {
+		m := specs.Mount{
+			Destination: mount.Destination,
+			Type:        mount.Type,
+			Options:     mount.Options,
+		}
+		// TODO: if nil, then it's required and user didn't set it
+		if mount.Source != nil {
+			m.Source = *mount.Source
+		}
+		if m.Source != "" && m.Type == "bind" {
+			fi, err := os.Lstat(filepath.Join(rootfs, string(os.PathSeparator), m.Destination)) // TODO: followsymlinks
+			if err != nil {
+				return nil, err
+			}
+			if fi.IsDir() {
+				if err := os.MkdirAll(m.Source, 0700); err != nil {
+					return nil, err
+				}
+			}
+		}
+		s.Mounts = append(s.Mounts, m)
+	}
+
+	envs := make([]string, 1, len(p.p.Config.Env)+1)
+	envs[0] = "PATH=" + system.DefaultPathEnv
+	envs = append(envs, p.p.Config.Env...)
+
+	args := append(p.p.Manifest.Entrypoint, p.p.Config.Args...)
+	s.Process = specs.Process{
+		Terminal: false,
+		Args:     args,
+		Cwd:      "/", // TODO: add in manifest?
+		Env:      envs,
+	}
+
+	return &s, nil
+}
+
+func (pm *Manager) disable(p *plugin) error {
+	if err := p.restartManager.Cancel(); err != nil {
+		logrus.Error(err)
+	}
+	if err := pm.containerdClient.Signal(p.p.ID, int(syscall.SIGKILL)); err != nil {
+		logrus.Error(err)
+	}
+	os.RemoveAll(p.runtimeSourcePath)
+	pm.Lock() // fixme: lock single record
+	defer pm.Unlock()
+	p.p.Active = false
+	pm.save()
+	return nil
+}

+ 21 - 0
plugin/manager_windows.go

@@ -0,0 +1,21 @@
+// +build windows,experimental
+
+package plugin
+
+import (
+	"fmt"
+
+	"github.com/opencontainers/specs/specs-go"
+)
+
+func (pm *Manager) enable(p *plugin) error {
+	return fmt.Errorf("Not implemented")
+}
+
+func (pm *Manager) initSpec(p *plugin) (*specs.Spec, error) {
+	return nil, fmt.Errorf("Not implemented")
+}
+
+func (pm *Manager) disable(p *plugin) error {
+	return fmt.Errorf("Not implemented")
+}

+ 15 - 0
vendor/src/github.com/docker/engine-api/client/errors.go

@@ -171,3 +171,18 @@ func IsErrTaskNotFound(err error) bool {
 	_, ok := err.(taskNotFoundError)
 	return ok
 }
+
+type pluginPermissionDenied struct {
+	name string
+}
+
+func (e pluginPermissionDenied) Error() string {
+	return "Permission denied while installing plugin " + e.name
+}
+
+// IsErrPluginPermissionDenied returns true if the error is caused
+// when a user denies a plugin's permissions
+func IsErrPluginPermissionDenied(err error) bool {
+	_, ok := err.(pluginPermissionDenied)
+	return ok
+}

+ 3 - 7
vendor/src/github.com/docker/engine-api/client/interface.go

@@ -4,18 +4,17 @@ import (
 	"io"
 	"time"
 
-	"golang.org/x/net/context"
-
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types/container"
 	"github.com/docker/engine-api/types/filters"
 	"github.com/docker/engine-api/types/network"
 	"github.com/docker/engine-api/types/registry"
 	"github.com/docker/engine-api/types/swarm"
+	"golang.org/x/net/context"
 )
 
-// APIClient is an interface that clients that talk with a docker server must implement.
-type APIClient interface {
+// CommonAPIClient is the common methods between stable and experimental versions of APIClient.
+type CommonAPIClient interface {
 	ClientVersion() string
 	CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error
 	CheckpointDelete(ctx context.Context, container string, checkpointID string) error
@@ -97,6 +96,3 @@ type APIClient interface {
 	VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error)
 	VolumeRemove(ctx context.Context, volumeID string) error
 }
-
-// Ensure that Client always implements APIClient.
-var _ APIClient = &Client{}

+ 26 - 0
vendor/src/github.com/docker/engine-api/client/interface_experimental.go

@@ -0,0 +1,26 @@
+// +build experimental
+
+package client
+
+import (
+	"io"
+
+	"github.com/docker/engine-api/types"
+	"golang.org/x/net/context"
+)
+
+// APIClient is an interface that clients that talk with a docker server must implement.
+type APIClient interface {
+	CommonAPIClient
+	PluginList(ctx context.Context) (types.PluginsListResponse, error)
+	PluginRemove(ctx context.Context, name string) error
+	PluginEnable(ctx context.Context, name string) error
+	PluginDisable(ctx context.Context, name string) error
+	PluginInstall(ctx context.Context, name, registryAuth string, acceptAllPermissions, noEnable bool, in io.ReadCloser, out io.Writer) error
+	PluginPush(ctx context.Context, name string, registryAuth string) error
+	PluginSet(ctx context.Context, name string, args []string) error
+	PluginInspect(ctx context.Context, name string) (*types.Plugin, error)
+}
+
+// Ensure that Client always implements APIClient.
+var _ APIClient = &Client{}

+ 11 - 0
vendor/src/github.com/docker/engine-api/client/interface_stable.go

@@ -0,0 +1,11 @@
+// +build !experimental
+
+package client
+
+// APIClient is an interface that clients that talk with a docker server must implement.
+type APIClient interface {
+	CommonAPIClient
+}
+
+// Ensure that Client always implements APIClient.
+var _ APIClient = &Client{}

+ 14 - 0
vendor/src/github.com/docker/engine-api/client/plugin_disable.go

@@ -0,0 +1,14 @@
+// +build experimental
+
+package client
+
+import (
+	"golang.org/x/net/context"
+)
+
+// PluginDisable disables a plugin
+func (cli *Client) PluginDisable(ctx context.Context, name string) error {
+	resp, err := cli.post(ctx, "/plugins/"+name+"/disable", nil, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 14 - 0
vendor/src/github.com/docker/engine-api/client/plugin_enable.go

@@ -0,0 +1,14 @@
+// +build experimental
+
+package client
+
+import (
+	"golang.org/x/net/context"
+)
+
+// PluginEnable enables a plugin
+func (cli *Client) PluginEnable(ctx context.Context, name string) error {
+	resp, err := cli.post(ctx, "/plugins/"+name+"/enable", nil, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 22 - 0
vendor/src/github.com/docker/engine-api/client/plugin_inspect.go

@@ -0,0 +1,22 @@
+// +build experimental
+
+package client
+
+import (
+	"encoding/json"
+
+	"github.com/docker/engine-api/types"
+	"golang.org/x/net/context"
+)
+
+// PluginInspect inspects an existing plugin
+func (cli *Client) PluginInspect(ctx context.Context, name string) (*types.Plugin, error) {
+	var p types.Plugin
+	resp, err := cli.get(ctx, "/plugins/"+name, nil, nil)
+	if err != nil {
+		return nil, err
+	}
+	err = json.NewDecoder(resp.body).Decode(&p)
+	ensureReaderClosed(resp)
+	return &p, err
+}

+ 54 - 0
vendor/src/github.com/docker/engine-api/client/plugin_install.go

@@ -0,0 +1,54 @@
+// +build experimental
+
+package client
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/url"
+	"strings"
+
+	"github.com/docker/engine-api/types"
+	"golang.org/x/net/context"
+)
+
+// PluginInstall installs a plugin
+func (cli *Client) PluginInstall(ctx context.Context, name, registryAuth string, acceptAllPermissions, noEnable bool, in io.ReadCloser, out io.Writer) error {
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
+	resp, err := cli.post(ctx, "/plugins/pull", url.Values{"name": []string{name}}, nil, headers)
+	if err != nil {
+		ensureReaderClosed(resp)
+		return err
+	}
+	var privileges types.PluginPrivileges
+	if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
+		return err
+	}
+	ensureReaderClosed(resp)
+
+	if !acceptAllPermissions && len(privileges) > 0 {
+
+		fmt.Fprintf(out, "Plugin %q requested the following privileges:\n", name)
+		for _, privilege := range privileges {
+			fmt.Fprintf(out, " - %s: %v\n", privilege.Name, privilege.Value)
+		}
+
+		fmt.Fprint(out, "Do you grant the above permissions? [y/N] ")
+		reader := bufio.NewReader(in)
+		line, _, err := reader.ReadLine()
+		if err != nil {
+			return err
+		}
+		if strings.ToLower(string(line)) != "y" {
+			resp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil)
+			ensureReaderClosed(resp)
+			return pluginPermissionDenied{name}
+		}
+	}
+	if noEnable {
+		return nil
+	}
+	return cli.PluginEnable(ctx, name)
+}

+ 23 - 0
vendor/src/github.com/docker/engine-api/client/plugin_list.go

@@ -0,0 +1,23 @@
+// +build experimental
+
+package client
+
+import (
+	"encoding/json"
+
+	"github.com/docker/engine-api/types"
+	"golang.org/x/net/context"
+)
+
+// PluginList returns the installed plugins
+func (cli *Client) PluginList(ctx context.Context) (types.PluginsListResponse, error) {
+	var plugins types.PluginsListResponse
+	resp, err := cli.get(ctx, "/plugins", nil, nil)
+	if err != nil {
+		return plugins, err
+	}
+
+	err = json.NewDecoder(resp.body).Decode(&plugins)
+	ensureReaderClosed(resp)
+	return plugins, err
+}

+ 15 - 0
vendor/src/github.com/docker/engine-api/client/plugin_push.go

@@ -0,0 +1,15 @@
+// +build experimental
+
+package client
+
+import (
+	"golang.org/x/net/context"
+)
+
+// PluginPush pushes a plugin to a registry
+func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) error {
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
+	resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, headers)
+	ensureReaderClosed(resp)
+	return err
+}

+ 14 - 0
vendor/src/github.com/docker/engine-api/client/plugin_remove.go

@@ -0,0 +1,14 @@
+// +build experimental
+
+package client
+
+import (
+	"golang.org/x/net/context"
+)
+
+// PluginRemove removes a plugin
+func (cli *Client) PluginRemove(ctx context.Context, name string) error {
+	resp, err := cli.delete(ctx, "/plugins/"+name, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 14 - 0
vendor/src/github.com/docker/engine-api/client/plugin_set.go

@@ -0,0 +1,14 @@
+// +build experimental
+
+package client
+
+import (
+	"golang.org/x/net/context"
+)
+
+// PluginSet modifies settings for an existing plugin
+func (cli *Client) PluginSet(ctx context.Context, name string, args []string) error {
+	resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, args, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 160 - 0
vendor/src/github.com/docker/engine-api/types/plugin.go

@@ -0,0 +1,160 @@
+// +build experimental
+
+package types
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+// PluginConfig represents the values of settings potentially modifiable by a user
+type PluginConfig struct {
+	Mounts  []PluginMount
+	Env     []string
+	Args    []string
+	Devices []PluginDevice
+}
+
+// Plugin represents a Docker plugin for the remote API
+type Plugin struct {
+	ID       string `json:"Id,omitempty"`
+	Name     string
+	Tag      string
+	Active   bool
+	Config   PluginConfig
+	Manifest PluginManifest
+}
+
+// PluginsListResponse contains the response for the remote API
+type PluginsListResponse []*Plugin
+
+const (
+	authzDriver   = "AuthzDriver"
+	graphDriver   = "GraphDriver"
+	ipamDriver    = "IpamDriver"
+	networkDriver = "NetworkDriver"
+	volumeDriver  = "VolumeDriver"
+)
+
+// PluginInterfaceType represents a type that a plugin implements.
+type PluginInterfaceType struct {
+	Prefix     string // This is always "docker"
+	Capability string // Capability should be validated against the above list.
+	Version    string // Plugin API version. Depends on the capability
+}
+
+// UnmarshalJSON implements json.Unmarshaler for PluginInterfaceType
+func (t *PluginInterfaceType) UnmarshalJSON(p []byte) error {
+	versionIndex := len(p)
+	prefixIndex := 0
+	if len(p) < 2 || p[0] != '"' || p[len(p)-1] != '"' {
+		return fmt.Errorf("%q is not a plugin interface type", p)
+	}
+	p = p[1 : len(p)-1]
+loop:
+	for i, b := range p {
+		switch b {
+		case '.':
+			prefixIndex = i
+		case '/':
+			versionIndex = i
+			break loop
+		}
+	}
+	t.Prefix = string(p[:prefixIndex])
+	t.Capability = string(p[prefixIndex+1 : versionIndex])
+	if versionIndex < len(p) {
+		t.Version = string(p[versionIndex+1:])
+	}
+	return nil
+}
+
+// MarshalJSON implements json.Marshaler for PluginInterfaceType
+func (t *PluginInterfaceType) MarshalJSON() ([]byte, error) {
+	return json.Marshal(t.String())
+}
+
+// String implements fmt.Stringer for PluginInterfaceType
+func (t PluginInterfaceType) String() string {
+	return fmt.Sprintf("%s.%s/%s", t.Prefix, t.Capability, t.Version)
+}
+
+// PluginInterface describes the interface between Docker and plugin
+type PluginInterface struct {
+	Types  []PluginInterfaceType
+	Socket string
+}
+
+// PluginSetting is to be embedded in other structs, if they are supposed to be
+// modifiable by the user.
+type PluginSetting struct {
+	Name        string
+	Description string
+	Settable    []string
+}
+
+// PluginNetwork represents the network configuration for a plugin
+type PluginNetwork struct {
+	Type string
+}
+
+// PluginMount represents the mount configuration for a plugin
+type PluginMount struct {
+	PluginSetting
+	Source      *string
+	Destination string
+	Type        string
+	Options     []string
+}
+
+// PluginEnv represents an environment variable for a plugin
+type PluginEnv struct {
+	PluginSetting
+	Value *string
+}
+
+// PluginArgs represents the command line arguments for a plugin
+type PluginArgs struct {
+	PluginSetting
+	Value []string
+}
+
+// PluginDevice represents a device for a plugin
+type PluginDevice struct {
+	PluginSetting
+	Path *string
+}
+
+// PluginUser represents the user for the plugin's process
+type PluginUser struct {
+	UID uint32 `json:"Uid,omitempty"`
+	GID uint32 `json:"Gid,omitempty"`
+}
+
+// PluginManifest represents the manifest of a plugin
+type PluginManifest struct {
+	ManifestVersion string
+	Description     string
+	Documentation   string
+	Interface       PluginInterface
+	Entrypoint      []string
+	Workdir         string
+	User            PluginUser `json:",omitempty"`
+	Network         PluginNetwork
+	Capabilities    []string
+	Mounts          []PluginMount
+	Devices         []PluginDevice
+	Env             []PluginEnv
+	Args            PluginArgs
+}
+
+// PluginPrivilege describes a permission the user has to accept
+// upon installing a plugin.
+type PluginPrivilege struct {
+	Name        string
+	Description string
+	Value       []string
+}
+
+// PluginPrivileges is a list of PluginPrivilege
+type PluginPrivileges []PluginPrivilege

+ 12 - 11
volume/drivers/extpoint.go

@@ -7,7 +7,7 @@ import (
 	"sync"
 
 	"github.com/docker/docker/pkg/locker"
-	"github.com/docker/docker/pkg/plugins"
+	"github.com/docker/docker/plugin"
 	"github.com/docker/docker/volume"
 )
 
@@ -88,10 +88,10 @@ func Unregister(name string) bool {
 	return true
 }
 
-// Lookup returns the driver associated with the given name. If a
+// lookup returns the driver associated with the given name. If a
 // driver with the given name has not been registered it checks if
 // there is a VolumeDriver plugin available with the given name.
-func Lookup(name string) (volume.Driver, error) {
+func lookup(name string) (volume.Driver, error) {
 	drivers.driverLock.Lock(name)
 	defer drivers.driverLock.Unlock(name)
 
@@ -102,7 +102,7 @@ func Lookup(name string) (volume.Driver, error) {
 		return ext, nil
 	}
 
-	pl, err := plugins.Get(name, extName)
+	p, err := plugin.LookupWithCapability(name, extName)
 	if err != nil {
 		return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err)
 	}
@@ -113,7 +113,7 @@ func Lookup(name string) (volume.Driver, error) {
 		return ext, nil
 	}
 
-	d := NewVolumeDriver(name, pl.Client)
+	d := NewVolumeDriver(name, p.Client())
 	if err := validateDriver(d); err != nil {
 		return nil, err
 	}
@@ -136,7 +136,7 @@ func GetDriver(name string) (volume.Driver, error) {
 	if name == "" {
 		name = volume.DefaultDriverName
 	}
-	return Lookup(name)
+	return lookup(name)
 }
 
 // GetDriverList returns list of volume drivers registered.
@@ -153,9 +153,9 @@ func GetDriverList() []string {
 
 // GetAllDrivers lists all the registered drivers
 func GetAllDrivers() ([]volume.Driver, error) {
-	plugins, err := plugins.GetAll(extName)
+	plugins, err := plugin.FindWithCapability(extName)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("error listing plugins: %v", err)
 	}
 	var ds []volume.Driver
 
@@ -167,13 +167,14 @@ func GetAllDrivers() ([]volume.Driver, error) {
 	}
 
 	for _, p := range plugins {
-		ext, ok := drivers.extensions[p.Name]
+		name := p.Name()
+		ext, ok := drivers.extensions[name]
 		if ok {
 			continue
 		}
 
-		ext = NewVolumeDriver(p.Name, p.Client)
-		drivers.extensions[p.Name] = ext
+		ext = NewVolumeDriver(name, p.Client())
+		drivers.extensions[name] = ext
 		ds = append(ds, ext)
 	}
 	return ds, nil

+ 1 - 1
volume/drivers/proxy_test.go

@@ -58,7 +58,7 @@ func TestVolumeRequestError(t *testing.T) {
 	})
 
 	u, _ := url.Parse(server.URL)
-	client, err := plugins.NewClient("tcp://"+u.Host, tlsconfig.Options{InsecureSkipVerify: true})
+	client, err := plugins.NewClient("tcp://"+u.Host, &tlsconfig.Options{InsecureSkipVerify: true})
 	if err != nil {
 		t.Fatal(err)
 	}

+ 5 - 4
volume/store/store.go

@@ -157,15 +157,16 @@ func (s *VolumeStore) List() ([]volume.Volume, []string, error) {
 
 // list goes through each volume driver and asks for its list of volumes.
 func (s *VolumeStore) list() ([]volume.Volume, []string, error) {
-	drivers, err := volumedrivers.GetAllDrivers()
-	if err != nil {
-		return nil, nil, err
-	}
 	var (
 		ls       []volume.Volume
 		warnings []string
 	)
 
+	drivers, err := volumedrivers.GetAllDrivers()
+	if err != nil {
+		return nil, nil, err
+	}
+
 	type vols struct {
 		vols       []volume.Volume
 		err        error