diff --git a/api/client/plugin/cmd.go b/api/client/plugin/cmd.go new file mode 100644 index 0000000000..f9ecc519f0 --- /dev/null +++ b/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) { +} diff --git a/api/client/plugin/cmd_experimental.go b/api/client/plugin/cmd_experimental.go new file mode 100644 index 0000000000..6c991937fe --- /dev/null +++ b/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) +} diff --git a/api/client/plugin/disable.go b/api/client/plugin/disable.go new file mode 100644 index 0000000000..6d0ff696c6 --- /dev/null +++ b/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 +} diff --git a/api/client/plugin/enable.go b/api/client/plugin/enable.go new file mode 100644 index 0000000000..2a05b4a943 --- /dev/null +++ b/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 +} diff --git a/api/client/plugin/inspect.go b/api/client/plugin/inspect.go new file mode 100644 index 0000000000..f68ad40da7 --- /dev/null +++ b/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 +} diff --git a/api/client/plugin/install.go b/api/client/plugin/install.go new file mode 100644 index 0000000000..dd38bd27d3 --- /dev/null +++ b/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()) +} diff --git a/api/client/plugin/list.go b/api/client/plugin/list.go new file mode 100644 index 0000000000..d6c788ca7c --- /dev/null +++ b/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 +} diff --git a/api/client/plugin/push.go b/api/client/plugin/push.go new file mode 100644 index 0000000000..c650eb6f89 --- /dev/null +++ b/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) +} diff --git a/api/client/plugin/remove.go b/api/client/plugin/remove.go new file mode 100644 index 0000000000..9acd79e4a2 --- /dev/null +++ b/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 +} diff --git a/api/client/plugin/set.go b/api/client/plugin/set.go new file mode 100644 index 0000000000..5bb7bfe165 --- /dev/null +++ b/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) +} diff --git a/api/server/router/plugin/backend.go b/api/server/router/plugin/backend.go new file mode 100644 index 0000000000..0eb4f5b8f1 --- /dev/null +++ b/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 +} diff --git a/api/server/router/plugin/plugin.go b/api/server/router/plugin/plugin.go new file mode 100644 index 0000000000..999ba6b746 --- /dev/null +++ b/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 +} diff --git a/api/server/router/plugin/plugin_experimental.go b/api/server/router/plugin/plugin_experimental.go new file mode 100644 index 0000000000..4e437ee65e --- /dev/null +++ b/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), + } +} diff --git a/api/server/router/plugin/plugin_regular.go b/api/server/router/plugin/plugin_regular.go new file mode 100644 index 0000000000..f987faecd2 --- /dev/null +++ b/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{} diff --git a/api/server/router/plugin/plugin_routes.go b/api/server/router/plugin/plugin_routes.go new file mode 100644 index 0000000000..dfdde72483 --- /dev/null +++ b/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) +} diff --git a/cli/cobraadaptor/adaptor.go b/cli/cobraadaptor/adaptor.go index 55538dd98f..a87c75206a 100644 --- a/cli/cobraadaptor/adaptor.go +++ b/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") diff --git a/cli/error.go b/cli/error.go new file mode 100644 index 0000000000..902d1b6e49 --- /dev/null +++ b/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() +} diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index 2717ee3132..3713321db4 100644 --- a/cmd/dockerd/daemon.go +++ b/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...) } diff --git a/cmd/dockerd/daemon_linux.go b/cmd/dockerd/daemon_linux.go index 93a38943b9..a556daa187 100644 --- a/cmd/dockerd/daemon_linux.go +++ b/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() { diff --git a/cmd/dockerd/daemon_no_plugin_support.go b/cmd/dockerd/daemon_no_plugin_support.go new file mode 100644 index 0000000000..17f249190d --- /dev/null +++ b/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 +} diff --git a/cmd/dockerd/daemon_plugin_support.go b/cmd/dockerd/daemon_plugin_support.go new file mode 100644 index 0000000000..56a4f85dfd --- /dev/null +++ b/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) +} diff --git a/cmd/dockerd/routes.go b/cmd/dockerd/routes.go new file mode 100644 index 0000000000..65b97bd8c2 --- /dev/null +++ b/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 +} diff --git a/cmd/dockerd/routes_experimental.go b/cmd/dockerd/routes_experimental.go new file mode 100644 index 0000000000..665df9499a --- /dev/null +++ b/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())) +} diff --git a/daemon/daemon.go b/daemon/daemon.go index 0c34c35359..9472ac7da0 100644 --- a/daemon/daemon.go +++ b/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 diff --git a/daemon/graphdriver/plugin.go b/daemon/graphdriver/plugin.go index d63161b074..9f172b72d4 100644 --- a/daemon/graphdriver/plugin.go +++ b/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) { diff --git a/distribution/pull.go b/distribution/pull.go index 4fbf17fc4b..d54acff4f3 100644 --- a/distribution/pull.go +++ b/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") } diff --git a/integration-cli/docker_cli_external_graphdriver_unix_test.go b/integration-cli/docker_cli_external_graphdriver_unix_test.go index 014b85c284..2e1f04a8a5 100644 --- a/integration-cli/docker_cli_external_graphdriver_unix_test.go +++ b/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) diff --git a/pkg/authorization/authz_unix_test.go b/pkg/authorization/authz_unix_test.go index 7ba2e696a6..e13303f7a5 100644 --- a/pkg/authorization/authz_unix_test.go +++ b/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 diff --git a/pkg/authorization/plugin.go b/pkg/authorization/plugin.go index 940699d9a3..fc5c7efb4b 100644 --- a/pkg/authorization/plugin.go +++ b/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 diff --git a/pkg/plugins/client.go b/pkg/plugins/client.go index 18c332986f..a778677f7c 100644 --- a/pkg/plugins/client.go +++ b/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 { diff --git a/pkg/plugins/client_test.go b/pkg/plugins/client_test.go index 3fa2ff46ad..9faad86a15 100644 --- a/pkg/plugins/client_test.go +++ b/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 { diff --git a/pkg/plugins/discovery.go b/pkg/plugins/discovery.go index 9dc64194f2..2077f2abc5 100644 --- a/pkg/plugins/discovery.go +++ b/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 } diff --git a/pkg/plugins/discovery_test.go b/pkg/plugins/discovery_test.go index 2e8dc704eb..f74090ee21 100644 --- a/pkg/plugins/discovery_test.go +++ b/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) } diff --git a/pkg/plugins/discovery_unix_test.go b/pkg/plugins/discovery_unix_test.go index 8166ae00b7..53e02d2858 100644 --- a/pkg/plugins/discovery_unix_test.go +++ b/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) } diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index b83b5ae61d..9cda7fcd4c 100644 --- a/pkg/plugins/plugins.go +++ b/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 } diff --git a/plugin/backend.go b/plugin/backend.go new file mode 100644 index 0000000000..541f06c936 --- /dev/null +++ b/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) +} diff --git a/plugin/distribution/pull.go b/plugin/distribution/pull.go new file mode 100644 index 0000000000..5dcc907fbe --- /dev/null +++ b/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 +} diff --git a/plugin/distribution/push.go b/plugin/distribution/push.go new file mode 100644 index 0000000000..5041c00520 --- /dev/null +++ b/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)) +} diff --git a/plugin/distribution/types.go b/plugin/distribution/types.go new file mode 100644 index 0000000000..e1c05b76ff --- /dev/null +++ b/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" +) diff --git a/plugin/interface.go b/plugin/interface.go new file mode 100644 index 0000000000..4a3ed64df2 --- /dev/null +++ b/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 +} diff --git a/plugin/legacy.go b/plugin/legacy.go new file mode 100644 index 0000000000..8ea4c0da96 --- /dev/null +++ b/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) +} diff --git a/plugin/manager.go b/plugin/manager.go new file mode 100644 index 0000000000..8d298453a6 --- /dev/null +++ b/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 +} diff --git a/plugin/manager_linux.go b/plugin/manager_linux.go new file mode 100644 index 0000000000..b5051dad1a --- /dev/null +++ b/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 +} diff --git a/plugin/manager_windows.go b/plugin/manager_windows.go new file mode 100644 index 0000000000..055a732884 --- /dev/null +++ b/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") +} diff --git a/volume/drivers/extpoint.go b/volume/drivers/extpoint.go index ebcf26cc34..537a25f472 100644 --- a/volume/drivers/extpoint.go +++ b/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 diff --git a/volume/drivers/proxy_test.go b/volume/drivers/proxy_test.go index 455f8d3fc9..b78c46a036 100644 --- a/volume/drivers/proxy_test.go +++ b/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) } diff --git a/volume/store/store.go b/volume/store/store.go index 962c735b36..acb049c760 100644 --- a/volume/store/store.go +++ b/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