mirror of https://github.com/docker/docs.git
				
				
				
			plugins: experimental support for new plugin management
This patch introduces a new experimental engine-level plugin management with a new API and command line. Plugins can be distributed via a Docker registry, and their lifecycle is managed by the engine. This makes plugins a first-class construct. For more background, have a look at issue #20363. Documentation is in a separate commit. If you want to understand how the new plugin system works, you can start by reading the documentation. Note: backwards compatibility with existing plugins is maintained, albeit they won't benefit from the advantages of the new system. Signed-off-by: Tibor Vass <tibor@docker.com> Signed-off-by: Anusha Ragunathan <anusha@docker.com>
This commit is contained in:
		
							parent
							
								
									e5b7d36e99
								
							
						
					
					
						commit
						f37117045c
					
				|  | @ -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) { | ||||
| } | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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()) | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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), | ||||
| 	} | ||||
| } | ||||
|  | @ -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{} | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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() | ||||
| } | ||||
|  | @ -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...) | ||||
| } | ||||
|  |  | |||
|  | @ -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() { | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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())) | ||||
| } | ||||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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") | ||||
| 	} | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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 | ||||
| 	} | ||||
|  |  | |||
|  | @ -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) | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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)) | ||||
| } | ||||
|  | @ -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" | ||||
| ) | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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") | ||||
| } | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue