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>
| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,12 @@ |
| 0 |
+// +build !experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/spf13/cobra" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// NewPluginCommand returns a cobra command for `plugin` subcommands |
|
| 10 |
+func NewPluginCommand(cmd *cobra.Command, dockerCli *client.DockerCli) {
|
|
| 11 |
+} |
| 0 | 12 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,36 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/spf13/cobra" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// NewPluginCommand returns a cobra command for `plugin` subcommands |
|
| 13 |
+func NewPluginCommand(rootCmd *cobra.Command, dockerCli *client.DockerCli) {
|
|
| 14 |
+ cmd := &cobra.Command{
|
|
| 15 |
+ Use: "plugin", |
|
| 16 |
+ Short: "Manage Docker plugins", |
|
| 17 |
+ Args: cli.NoArgs, |
|
| 18 |
+ Run: func(cmd *cobra.Command, args []string) {
|
|
| 19 |
+ fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) |
|
| 20 |
+ }, |
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ cmd.AddCommand( |
|
| 24 |
+ newDisableCommand(dockerCli), |
|
| 25 |
+ newEnableCommand(dockerCli), |
|
| 26 |
+ newInspectCommand(dockerCli), |
|
| 27 |
+ newInstallCommand(dockerCli), |
|
| 28 |
+ newListCommand(dockerCli), |
|
| 29 |
+ newRemoveCommand(dockerCli), |
|
| 30 |
+ newSetCommand(dockerCli), |
|
| 31 |
+ newPushCommand(dockerCli), |
|
| 32 |
+ ) |
|
| 33 |
+ |
|
| 34 |
+ rootCmd.AddCommand(cmd) |
|
| 35 |
+} |
| 0 | 36 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,23 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/cli" |
|
| 7 |
+ "github.com/spf13/cobra" |
|
| 8 |
+ "golang.org/x/net/context" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+func newDisableCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 12 |
+ cmd := &cobra.Command{
|
|
| 13 |
+ Use: "disable", |
|
| 14 |
+ Short: "Disable a plugin", |
|
| 15 |
+ Args: cli.ExactArgs(1), |
|
| 16 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 17 |
+ return dockerCli.Client().PluginDisable(context.Background(), args[0]) |
|
| 18 |
+ }, |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ return cmd |
|
| 22 |
+} |
| 0 | 23 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,23 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/cli" |
|
| 7 |
+ "github.com/spf13/cobra" |
|
| 8 |
+ "golang.org/x/net/context" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+func newEnableCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 12 |
+ cmd := &cobra.Command{
|
|
| 13 |
+ Use: "enable", |
|
| 14 |
+ Short: "Enable a plugin", |
|
| 15 |
+ Args: cli.ExactArgs(1), |
|
| 16 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 17 |
+ return dockerCli.Client().PluginEnable(context.Background(), args[0]) |
|
| 18 |
+ }, |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ return cmd |
|
| 22 |
+} |
| 0 | 23 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,39 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/spf13/cobra" |
|
| 10 |
+ "golang.org/x/net/context" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 14 |
+ cmd := &cobra.Command{
|
|
| 15 |
+ Use: "inspect", |
|
| 16 |
+ Short: "Inspect a plugin", |
|
| 17 |
+ Args: cli.ExactArgs(1), |
|
| 18 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 19 |
+ return runInspect(dockerCli, args[0]) |
|
| 20 |
+ }, |
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ return cmd |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+func runInspect(dockerCli *client.DockerCli, name string) error {
|
|
| 27 |
+ p, err := dockerCli.Client().PluginInspect(context.Background(), name) |
|
| 28 |
+ if err != nil {
|
|
| 29 |
+ return err |
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ b, err := json.MarshalIndent(p, "", "\t") |
|
| 33 |
+ if err != nil {
|
|
| 34 |
+ return err |
|
| 35 |
+ } |
|
| 36 |
+ _, err = dockerCli.Out().Write(b) |
|
| 37 |
+ return err |
|
| 38 |
+} |
| 0 | 39 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,51 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/docker/docker/reference" |
|
| 10 |
+ "github.com/docker/docker/registry" |
|
| 11 |
+ "github.com/spf13/cobra" |
|
| 12 |
+ "golang.org/x/net/context" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+func newInstallCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 16 |
+ cmd := &cobra.Command{
|
|
| 17 |
+ Use: "install", |
|
| 18 |
+ Short: "Install a plugin", |
|
| 19 |
+ Args: cli.RequiresMinArgs(1), // TODO: allow for set args |
|
| 20 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 21 |
+ return runInstall(dockerCli, args[0], args[1:]) |
|
| 22 |
+ }, |
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ return cmd |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func runInstall(dockerCli *client.DockerCli, name string, args []string) error {
|
|
| 29 |
+ named, err := reference.ParseNamed(name) // FIXME: validate |
|
| 30 |
+ if err != nil {
|
|
| 31 |
+ return err |
|
| 32 |
+ } |
|
| 33 |
+ named = reference.WithDefaultTag(named) |
|
| 34 |
+ ref, ok := named.(reference.NamedTagged) |
|
| 35 |
+ if !ok {
|
|
| 36 |
+ return fmt.Errorf("invalid name: %s", named.String())
|
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ ctx := context.Background() |
|
| 40 |
+ |
|
| 41 |
+ repoInfo, err := registry.ParseRepositoryInfo(named) |
|
| 42 |
+ authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) |
|
| 43 |
+ |
|
| 44 |
+ encodedAuth, err := client.EncodeAuthToBase64(authConfig) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ return err |
|
| 47 |
+ } |
|
| 48 |
+ // TODO: pass acceptAllPermissions and noEnable flag |
|
| 49 |
+ return dockerCli.Client().PluginInstall(ctx, ref.String(), encodedAuth, false, false, dockerCli.In(), dockerCli.Out()) |
|
| 50 |
+} |
| 0 | 51 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,44 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "text/tabwriter" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api/client" |
|
| 9 |
+ "github.com/docker/docker/cli" |
|
| 10 |
+ "github.com/spf13/cobra" |
|
| 11 |
+ "golang.org/x/net/context" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 15 |
+ cmd := &cobra.Command{
|
|
| 16 |
+ Use: "ls", |
|
| 17 |
+ Short: "List plugins", |
|
| 18 |
+ Aliases: []string{"list"},
|
|
| 19 |
+ Args: cli.ExactArgs(0), |
|
| 20 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 21 |
+ return runList(dockerCli) |
|
| 22 |
+ }, |
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ return cmd |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func runList(dockerCli *client.DockerCli) error {
|
|
| 29 |
+ plugins, err := dockerCli.Client().PluginList(context.Background()) |
|
| 30 |
+ if err != nil {
|
|
| 31 |
+ return err |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) |
|
| 35 |
+ fmt.Fprintf(w, "NAME \tTAG \tACTIVE") |
|
| 36 |
+ fmt.Fprintf(w, "\n") |
|
| 37 |
+ |
|
| 38 |
+ for _, p := range plugins {
|
|
| 39 |
+ fmt.Fprintf(w, "%s\t%s\t%v\n", p.Name, p.Tag, p.Active) |
|
| 40 |
+ } |
|
| 41 |
+ w.Flush() |
|
| 42 |
+ return nil |
|
| 43 |
+} |
| 0 | 44 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,50 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "golang.org/x/net/context" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/api/client" |
|
| 10 |
+ "github.com/docker/docker/cli" |
|
| 11 |
+ "github.com/docker/docker/reference" |
|
| 12 |
+ "github.com/docker/docker/registry" |
|
| 13 |
+ "github.com/spf13/cobra" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+func newPushCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 17 |
+ cmd := &cobra.Command{
|
|
| 18 |
+ Use: "push", |
|
| 19 |
+ Short: "Push a plugin", |
|
| 20 |
+ Args: cli.ExactArgs(1), |
|
| 21 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 22 |
+ return runPush(dockerCli, args[0]) |
|
| 23 |
+ }, |
|
| 24 |
+ } |
|
| 25 |
+ return cmd |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func runPush(dockerCli *client.DockerCli, name string) error {
|
|
| 29 |
+ named, err := reference.ParseNamed(name) // FIXME: validate |
|
| 30 |
+ if err != nil {
|
|
| 31 |
+ return err |
|
| 32 |
+ } |
|
| 33 |
+ named = reference.WithDefaultTag(named) |
|
| 34 |
+ ref, ok := named.(reference.NamedTagged) |
|
| 35 |
+ if !ok {
|
|
| 36 |
+ return fmt.Errorf("invalid name: %s", named.String())
|
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ ctx := context.Background() |
|
| 40 |
+ |
|
| 41 |
+ repoInfo, err := registry.ParseRepositoryInfo(named) |
|
| 42 |
+ authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) |
|
| 43 |
+ |
|
| 44 |
+ encodedAuth, err := client.EncodeAuthToBase64(authConfig) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ return err |
|
| 47 |
+ } |
|
| 48 |
+ return dockerCli.Client().PluginPush(ctx, ref.String(), encodedAuth) |
|
| 49 |
+} |
| 0 | 50 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,43 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/spf13/cobra" |
|
| 10 |
+ "golang.org/x/net/context" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 14 |
+ cmd := &cobra.Command{
|
|
| 15 |
+ Use: "rm", |
|
| 16 |
+ Short: "Remove a plugin", |
|
| 17 |
+ Aliases: []string{"remove"},
|
|
| 18 |
+ Args: cli.RequiresMinArgs(1), |
|
| 19 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 20 |
+ return runRemove(dockerCli, args) |
|
| 21 |
+ }, |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ return cmd |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+func runRemove(dockerCli *client.DockerCli, names []string) error {
|
|
| 28 |
+ var errs cli.Errors |
|
| 29 |
+ for _, name := range names {
|
|
| 30 |
+ // TODO: pass names to api instead of making multiple api calls |
|
| 31 |
+ if err := dockerCli.Client().PluginRemove(context.Background(), name); err != nil {
|
|
| 32 |
+ errs = append(errs, err) |
|
| 33 |
+ continue |
|
| 34 |
+ } |
|
| 35 |
+ fmt.Fprintln(dockerCli.Out(), name) |
|
| 36 |
+ } |
|
| 37 |
+ // Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value. |
|
| 38 |
+ if errs != nil {
|
|
| 39 |
+ return errs |
|
| 40 |
+ } |
|
| 41 |
+ return nil |
|
| 42 |
+} |
| 0 | 43 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,28 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "golang.org/x/net/context" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/spf13/cobra" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func newSetCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 13 |
+ cmd := &cobra.Command{
|
|
| 14 |
+ Use: "set", |
|
| 15 |
+ Short: "Change settings for a plugin", |
|
| 16 |
+ Args: cli.RequiresMinArgs(2), |
|
| 17 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 18 |
+ return runSet(dockerCli, args[0], args[1:]) |
|
| 19 |
+ }, |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ return cmd |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+func runSet(dockerCli *client.DockerCli, name string, args []string) error {
|
|
| 26 |
+ return dockerCli.Client().PluginSet(context.Background(), name, args) |
|
| 27 |
+} |
| 0 | 28 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,21 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "net/http" |
|
| 6 |
+ |
|
| 7 |
+ enginetypes "github.com/docker/engine-api/types" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// Backend for Plugin |
|
| 11 |
+type Backend interface {
|
|
| 12 |
+ Disable(name string) error |
|
| 13 |
+ Enable(name string) error |
|
| 14 |
+ List() ([]enginetypes.Plugin, error) |
|
| 15 |
+ Inspect(name string) (enginetypes.Plugin, error) |
|
| 16 |
+ Remove(name string) error |
|
| 17 |
+ Set(name string, args []string) error |
|
| 18 |
+ Pull(name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error) |
|
| 19 |
+ Push(name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) error |
|
| 20 |
+} |
| 0 | 21 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,23 @@ |
| 0 |
+package plugin |
|
| 1 |
+ |
|
| 2 |
+import "github.com/docker/docker/api/server/router" |
|
| 3 |
+ |
|
| 4 |
+// pluginRouter is a router to talk with the plugin controller |
|
| 5 |
+type pluginRouter struct {
|
|
| 6 |
+ backend Backend |
|
| 7 |
+ routes []router.Route |
|
| 8 |
+} |
|
| 9 |
+ |
|
| 10 |
+// NewRouter initializes a new plugin router |
|
| 11 |
+func NewRouter(b Backend) router.Router {
|
|
| 12 |
+ r := &pluginRouter{
|
|
| 13 |
+ backend: b, |
|
| 14 |
+ } |
|
| 15 |
+ r.initRoutes() |
|
| 16 |
+ return r |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+// Routes returns the available routers to the plugin controller |
|
| 20 |
+func (r *pluginRouter) Routes() []router.Route {
|
|
| 21 |
+ return r.routes |
|
| 22 |
+} |
| 0 | 23 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,20 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "github.com/docker/docker/api/server/router" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+func (r *pluginRouter) initRoutes() {
|
|
| 9 |
+ r.routes = []router.Route{
|
|
| 10 |
+ router.NewGetRoute("/plugins", r.listPlugins),
|
|
| 11 |
+ router.NewGetRoute("/plugins/{name:.*}", r.inspectPlugin),
|
|
| 12 |
+ router.NewDeleteRoute("/plugins/{name:.*}", r.removePlugin),
|
|
| 13 |
+ router.NewPostRoute("/plugins/{name:.*}/enable", r.enablePlugin), // PATCH?
|
|
| 14 |
+ router.NewPostRoute("/plugins/{name:.*}/disable", r.disablePlugin),
|
|
| 15 |
+ router.NewPostRoute("/plugins/pull", r.pullPlugin),
|
|
| 16 |
+ router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin),
|
|
| 17 |
+ router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin),
|
|
| 18 |
+ } |
|
| 19 |
+} |
| 0 | 9 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,103 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "encoding/base64" |
|
| 6 |
+ "encoding/json" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "strings" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/api/server/httputils" |
|
| 11 |
+ "github.com/docker/engine-api/types" |
|
| 12 |
+ "golang.org/x/net/context" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 16 |
+ if err := httputils.ParseForm(r); err != nil {
|
|
| 17 |
+ return err |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ metaHeaders := map[string][]string{}
|
|
| 21 |
+ for k, v := range r.Header {
|
|
| 22 |
+ if strings.HasPrefix(k, "X-Meta-") {
|
|
| 23 |
+ metaHeaders[k] = v |
|
| 24 |
+ } |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ // Get X-Registry-Auth |
|
| 28 |
+ authEncoded := r.Header.Get("X-Registry-Auth")
|
|
| 29 |
+ authConfig := &types.AuthConfig{}
|
|
| 30 |
+ if authEncoded != "" {
|
|
| 31 |
+ authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) |
|
| 32 |
+ if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
|
|
| 33 |
+ authConfig = &types.AuthConfig{}
|
|
| 34 |
+ } |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ privileges, err := pr.backend.Pull(r.FormValue("name"), metaHeaders, authConfig)
|
|
| 38 |
+ if err != nil {
|
|
| 39 |
+ return err |
|
| 40 |
+ } |
|
| 41 |
+ return httputils.WriteJSON(w, http.StatusOK, privileges) |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+func (pr *pluginRouter) enablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 45 |
+ return pr.backend.Enable(vars["name"]) |
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+func (pr *pluginRouter) disablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 49 |
+ return pr.backend.Disable(vars["name"]) |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+func (pr *pluginRouter) removePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 53 |
+ return pr.backend.Remove(vars["name"]) |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+func (pr *pluginRouter) pushPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 57 |
+ if err := httputils.ParseForm(r); err != nil {
|
|
| 58 |
+ return err |
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ metaHeaders := map[string][]string{}
|
|
| 62 |
+ for k, v := range r.Header {
|
|
| 63 |
+ if strings.HasPrefix(k, "X-Meta-") {
|
|
| 64 |
+ metaHeaders[k] = v |
|
| 65 |
+ } |
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ // Get X-Registry-Auth |
|
| 69 |
+ authEncoded := r.Header.Get("X-Registry-Auth")
|
|
| 70 |
+ authConfig := &types.AuthConfig{}
|
|
| 71 |
+ if authEncoded != "" {
|
|
| 72 |
+ authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) |
|
| 73 |
+ if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
|
|
| 74 |
+ authConfig = &types.AuthConfig{}
|
|
| 75 |
+ } |
|
| 76 |
+ } |
|
| 77 |
+ return pr.backend.Push(vars["name"], metaHeaders, authConfig) |
|
| 78 |
+} |
|
| 79 |
+ |
|
| 80 |
+func (pr *pluginRouter) setPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 81 |
+ var args []string |
|
| 82 |
+ if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
|
|
| 83 |
+ return err |
|
| 84 |
+ } |
|
| 85 |
+ return pr.backend.Set(vars["name"], args) |
|
| 86 |
+} |
|
| 87 |
+ |
|
| 88 |
+func (pr *pluginRouter) listPlugins(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 89 |
+ l, err := pr.backend.List() |
|
| 90 |
+ if err != nil {
|
|
| 91 |
+ return err |
|
| 92 |
+ } |
|
| 93 |
+ return httputils.WriteJSON(w, http.StatusOK, l) |
|
| 94 |
+} |
|
| 95 |
+ |
|
| 96 |
+func (pr *pluginRouter) inspectPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 97 |
+ result, err := pr.backend.Inspect(vars["name"]) |
|
| 98 |
+ if err != nil {
|
|
| 99 |
+ return err |
|
| 100 |
+ } |
|
| 101 |
+ return httputils.WriteJSON(w, http.StatusOK, result) |
|
| 102 |
+} |
| ... | ... |
@@ -6,6 +6,7 @@ import ( |
| 6 | 6 |
"github.com/docker/docker/api/client/image" |
| 7 | 7 |
"github.com/docker/docker/api/client/network" |
| 8 | 8 |
"github.com/docker/docker/api/client/node" |
| 9 |
+ "github.com/docker/docker/api/client/plugin" |
|
| 9 | 10 |
"github.com/docker/docker/api/client/registry" |
| 10 | 11 |
"github.com/docker/docker/api/client/service" |
| 11 | 12 |
"github.com/docker/docker/api/client/swarm" |
| ... | ... |
@@ -81,6 +82,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
|
| 81 | 81 |
system.NewVersionCommand(dockerCli), |
| 82 | 82 |
volume.NewVolumeCommand(dockerCli), |
| 83 | 83 |
) |
| 84 |
+ plugin.NewPluginCommand(rootCmd, dockerCli) |
|
| 84 | 85 |
|
| 85 | 86 |
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
|
| 86 | 87 |
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
|
| 87 | 88 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,21 @@ |
| 0 |
+package cli |
|
| 1 |
+ |
|
| 2 |
+import "bytes" |
|
| 3 |
+ |
|
| 4 |
+// Errors is a list of errors. |
|
| 5 |
+// Useful in a loop if you don't want to return the error right away and you want to display after the loop, |
|
| 6 |
+// all the errors that happened during the loop. |
|
| 7 |
+type Errors []error |
|
| 8 |
+ |
|
| 9 |
+func (errs Errors) Error() string {
|
|
| 10 |
+ if len(errs) < 1 {
|
|
| 11 |
+ return "" |
|
| 12 |
+ } |
|
| 13 |
+ var buf bytes.Buffer |
|
| 14 |
+ buf.WriteString(errs[0].Error()) |
|
| 15 |
+ for _, err := range errs[1:] {
|
|
| 16 |
+ buf.WriteString(", ")
|
|
| 17 |
+ buf.WriteString(err.Error()) |
|
| 18 |
+ } |
|
| 19 |
+ return buf.String() |
|
| 20 |
+} |
| ... | ... |
@@ -262,6 +262,10 @@ func (cli *DaemonCli) start() (err error) {
|
| 262 | 262 |
<-stopc // wait for daemonCli.start() to return |
| 263 | 263 |
}) |
| 264 | 264 |
|
| 265 |
+ if err := pluginInit(cli.Config, containerdRemote, registryService); err != nil {
|
|
| 266 |
+ return err |
|
| 267 |
+ } |
|
| 268 |
+ |
|
| 265 | 269 |
d, err := daemon.NewDaemon(cli.Config, registryService, containerdRemote) |
| 266 | 270 |
if err != nil {
|
| 267 | 271 |
return fmt.Errorf("Error starting daemon: %v", err)
|
| ... | ... |
@@ -418,6 +422,7 @@ func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) {
|
| 418 | 418 |
if d.NetworkControllerEnabled() {
|
| 419 | 419 |
routers = append(routers, network.NewRouter(d, c)) |
| 420 | 420 |
} |
| 421 |
+ routers = addExperimentalRouters(routers) |
|
| 421 | 422 |
|
| 422 | 423 |
s.InitRouter(utils.IsDebugEnabled(), routers...) |
| 423 | 424 |
} |
| ... | ... |
@@ -1,8 +1,8 @@ |
| 1 |
+// +build linux |
|
| 2 |
+ |
|
| 1 | 3 |
package main |
| 2 | 4 |
|
| 3 |
-import ( |
|
| 4 |
- systemdDaemon "github.com/coreos/go-systemd/daemon" |
|
| 5 |
-) |
|
| 5 |
+import systemdDaemon "github.com/coreos/go-systemd/daemon" |
|
| 6 | 6 |
|
| 7 | 7 |
// notifySystem sends a message to the host when the server is ready to be used |
| 8 | 8 |
func notifySystem() {
|
| 9 | 9 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,13 @@ |
| 0 |
+// +build !experimental !linux |
|
| 1 |
+ |
|
| 2 |
+package main |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "github.com/docker/docker/daemon" |
|
| 6 |
+ "github.com/docker/docker/libcontainerd" |
|
| 7 |
+ "github.com/docker/docker/registry" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+func pluginInit(config *daemon.Config, remote libcontainerd.Remote, rs registry.Service) error {
|
|
| 11 |
+ return nil |
|
| 12 |
+} |
| 0 | 13 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,14 @@ |
| 0 |
+// +build linux,experimental |
|
| 1 |
+ |
|
| 2 |
+package main |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "github.com/docker/docker/daemon" |
|
| 6 |
+ "github.com/docker/docker/libcontainerd" |
|
| 7 |
+ "github.com/docker/docker/plugin" |
|
| 8 |
+ "github.com/docker/docker/registry" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+func pluginInit(config *daemon.Config, remote libcontainerd.Remote, rs registry.Service) error {
|
|
| 12 |
+ return plugin.Init(config.Root, config.ExecRoot, remote, rs) |
|
| 13 |
+} |
| 0 | 9 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,13 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package main |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "github.com/docker/docker/api/server/router" |
|
| 6 |
+ pluginrouter "github.com/docker/docker/api/server/router/plugin" |
|
| 7 |
+ "github.com/docker/docker/plugin" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+func addExperimentalRouters(routers []router.Router) []router.Router {
|
|
| 11 |
+ return append(routers, pluginrouter.NewRouter(plugin.GetManager())) |
|
| 12 |
+} |
| ... | ... |
@@ -486,7 +486,7 @@ func NewDaemon(config *Config, registryService registry.Service, containerdRemot |
| 486 | 486 |
} |
| 487 | 487 |
|
| 488 | 488 |
// Configure the volumes driver |
| 489 |
- volStore, err := configureVolumes(config, rootUID, rootGID) |
|
| 489 |
+ volStore, err := d.configureVolumes(rootUID, rootGID) |
|
| 490 | 490 |
if err != nil {
|
| 491 | 491 |
return nil, err |
| 492 | 492 |
} |
| ... | ... |
@@ -768,8 +768,8 @@ func setDefaultMtu(config *Config) {
|
| 768 | 768 |
config.Mtu = defaultNetworkMtu |
| 769 | 769 |
} |
| 770 | 770 |
|
| 771 |
-func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore, error) {
|
|
| 772 |
- volumesDriver, err := local.New(config.Root, rootUID, rootGID) |
|
| 771 |
+func (daemon *Daemon) configureVolumes(rootUID, rootGID int) (*store.VolumeStore, error) {
|
|
| 772 |
+ volumesDriver, err := local.New(daemon.configStore.Root, rootUID, rootGID) |
|
| 773 | 773 |
if err != nil {
|
| 774 | 774 |
return nil, err |
| 775 | 775 |
} |
| ... | ... |
@@ -777,7 +777,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore, |
| 777 | 777 |
if !volumedrivers.Register(volumesDriver, volumesDriver.Name()) {
|
| 778 | 778 |
return nil, fmt.Errorf("local volume driver could not be registered")
|
| 779 | 779 |
} |
| 780 |
- return store.New(config.Root) |
|
| 780 |
+ return store.New(daemon.configStore.Root) |
|
| 781 | 781 |
} |
| 782 | 782 |
|
| 783 | 783 |
// IsShuttingDown tells whether the daemon is shutting down or not |
| ... | ... |
@@ -23,7 +23,7 @@ func lookupPlugin(name, home string, opts []string) (Driver, error) {
|
| 23 | 23 |
if err != nil {
|
| 24 | 24 |
return nil, fmt.Errorf("Error looking up graphdriver plugin %s: %v", name, err)
|
| 25 | 25 |
} |
| 26 |
- return newPluginDriver(name, home, opts, pl.Client) |
|
| 26 |
+ return newPluginDriver(name, home, opts, pl.Client()) |
|
| 27 | 27 |
} |
| 28 | 28 |
|
| 29 | 29 |
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 |
| 84 | 84 |
} |
| 85 | 85 |
|
| 86 | 86 |
// makes sure name is not empty or `scratch` |
| 87 |
- if err := validateRepoName(repoInfo.Name()); err != nil {
|
|
| 87 |
+ if err := ValidateRepoName(repoInfo.Name()); err != nil {
|
|
| 88 | 88 |
return err |
| 89 | 89 |
} |
| 90 | 90 |
|
| ... | ... |
@@ -193,8 +193,8 @@ func writeStatus(requestedTag string, out progress.Output, layersDownloaded bool |
| 193 | 193 |
} |
| 194 | 194 |
} |
| 195 | 195 |
|
| 196 |
-// validateRepoName validates the name of a repository. |
|
| 197 |
-func validateRepoName(name string) error {
|
|
| 196 |
+// ValidateRepoName validates the name of a repository. |
|
| 197 |
+func ValidateRepoName(name string) error {
|
|
| 198 | 198 |
if name == "" {
|
| 199 | 199 |
return fmt.Errorf("Repository name can't be empty")
|
| 200 | 200 |
} |
| ... | ... |
@@ -77,7 +77,7 @@ func (s *DockerExternalGraphdriverSuite) setUpPluginViaJSONFile(c *check.C) {
|
| 77 | 77 |
mux := http.NewServeMux() |
| 78 | 78 |
s.jserver = httptest.NewServer(mux) |
| 79 | 79 |
|
| 80 |
- p := plugins.Plugin{Name: "json-external-graph-driver", Addr: s.jserver.URL}
|
|
| 80 |
+ p := plugins.NewLocalPlugin("json-external-graph-driver", s.jserver.URL)
|
|
| 81 | 81 |
b, err := json.Marshal(p) |
| 82 | 82 |
c.Assert(err, check.IsNil) |
| 83 | 83 |
|
| ... | ... |
@@ -203,18 +203,17 @@ func TestResponseModifierOverride(t *testing.T) {
|
| 203 | 203 |
|
| 204 | 204 |
// createTestPlugin creates a new sample authorization plugin |
| 205 | 205 |
func createTestPlugin(t *testing.T) *authorizationPlugin {
|
| 206 |
- plugin := &plugins.Plugin{Name: "authz"}
|
|
| 207 | 206 |
pwd, err := os.Getwd() |
| 208 | 207 |
if err != nil {
|
| 209 | 208 |
t.Fatal(err) |
| 210 | 209 |
} |
| 211 | 210 |
|
| 212 |
- plugin.Client, err = plugins.NewClient("unix:///"+path.Join(pwd, pluginAddress), tlsconfig.Options{InsecureSkipVerify: true})
|
|
| 211 |
+ client, err := plugins.NewClient("unix:///"+path.Join(pwd, pluginAddress), &tlsconfig.Options{InsecureSkipVerify: true})
|
|
| 213 | 212 |
if err != nil {
|
| 214 | 213 |
t.Fatalf("Failed to create client %v", err)
|
| 215 | 214 |
} |
| 216 | 215 |
|
| 217 |
- return &authorizationPlugin{name: "plugin", plugin: plugin}
|
|
| 216 |
+ return &authorizationPlugin{name: "plugin", plugin: client}
|
|
| 218 | 217 |
} |
| 219 | 218 |
|
| 220 | 219 |
// AuthZPluginTestServer is a simple server that implements the authZ plugin interface |
| ... | ... |
@@ -35,7 +35,7 @@ func NewPlugins(names []string) []Plugin {
|
| 35 | 35 |
|
| 36 | 36 |
// authorizationPlugin is an internal adapter to docker plugin system |
| 37 | 37 |
type authorizationPlugin struct {
|
| 38 |
- plugin *plugins.Plugin |
|
| 38 |
+ plugin *plugins.Client |
|
| 39 | 39 |
name string |
| 40 | 40 |
once sync.Once |
| 41 | 41 |
} |
| ... | ... |
@@ -54,7 +54,7 @@ func (a *authorizationPlugin) AuthZRequest(authReq *Request) (*Response, error) |
| 54 | 54 |
} |
| 55 | 55 |
|
| 56 | 56 |
authRes := &Response{}
|
| 57 |
- if err := a.plugin.Client.Call(AuthZApiRequest, authReq, authRes); err != nil {
|
|
| 57 |
+ if err := a.plugin.Call(AuthZApiRequest, authReq, authRes); err != nil {
|
|
| 58 | 58 |
return nil, err |
| 59 | 59 |
} |
| 60 | 60 |
|
| ... | ... |
@@ -67,7 +67,7 @@ func (a *authorizationPlugin) AuthZResponse(authReq *Request) (*Response, error) |
| 67 | 67 |
} |
| 68 | 68 |
|
| 69 | 69 |
authRes := &Response{}
|
| 70 |
- if err := a.plugin.Client.Call(AuthZApiResponse, authReq, authRes); err != nil {
|
|
| 70 |
+ if err := a.plugin.Call(AuthZApiResponse, authReq, authRes); err != nil {
|
|
| 71 | 71 |
return nil, err |
| 72 | 72 |
} |
| 73 | 73 |
|
| ... | ... |
@@ -80,7 +80,12 @@ func (a *authorizationPlugin) initPlugin() error {
|
| 80 | 80 |
var err error |
| 81 | 81 |
a.once.Do(func() {
|
| 82 | 82 |
if a.plugin == nil {
|
| 83 |
- a.plugin, err = plugins.Get(a.name, AuthZApiImplements) |
|
| 83 |
+ plugin, e := plugins.Get(a.name, AuthZApiImplements) |
|
| 84 |
+ if e != nil {
|
|
| 85 |
+ err = e |
|
| 86 |
+ return |
|
| 87 |
+ } |
|
| 88 |
+ a.plugin = plugin.Client() |
|
| 84 | 89 |
} |
| 85 | 90 |
}) |
| 86 | 91 |
return err |
| ... | ... |
@@ -20,14 +20,16 @@ const ( |
| 20 | 20 |
) |
| 21 | 21 |
|
| 22 | 22 |
// NewClient creates a new plugin client (http). |
| 23 |
-func NewClient(addr string, tlsConfig tlsconfig.Options) (*Client, error) {
|
|
| 23 |
+func NewClient(addr string, tlsConfig *tlsconfig.Options) (*Client, error) {
|
|
| 24 | 24 |
tr := &http.Transport{}
|
| 25 | 25 |
|
| 26 |
- c, err := tlsconfig.Client(tlsConfig) |
|
| 27 |
- if err != nil {
|
|
| 28 |
- return nil, err |
|
| 26 |
+ if tlsConfig != nil {
|
|
| 27 |
+ c, err := tlsconfig.Client(*tlsConfig) |
|
| 28 |
+ if err != nil {
|
|
| 29 |
+ return nil, err |
|
| 30 |
+ } |
|
| 31 |
+ tr.TLSClientConfig = c |
|
| 29 | 32 |
} |
| 30 |
- tr.TLSClientConfig = c |
|
| 31 | 33 |
|
| 32 | 34 |
u, err := url.Parse(addr) |
| 33 | 35 |
if err != nil {
|
| ... | ... |
@@ -31,7 +31,7 @@ func teardownRemotePluginServer() {
|
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 | 33 |
func TestFailedConnection(t *testing.T) {
|
| 34 |
- c, _ := NewClient("tcp://127.0.0.1:1", tlsconfig.Options{InsecureSkipVerify: true})
|
|
| 34 |
+ c, _ := NewClient("tcp://127.0.0.1:1", &tlsconfig.Options{InsecureSkipVerify: true})
|
|
| 35 | 35 |
_, err := c.callWithRetry("Service.Method", nil, false)
|
| 36 | 36 |
if err == nil {
|
| 37 | 37 |
t.Fatal("Unexpected successful connection")
|
| ... | ... |
@@ -55,7 +55,7 @@ func TestEchoInputOutput(t *testing.T) {
|
| 55 | 55 |
io.Copy(w, r.Body) |
| 56 | 56 |
}) |
| 57 | 57 |
|
| 58 |
- c, _ := NewClient(addr, tlsconfig.Options{InsecureSkipVerify: true})
|
|
| 58 |
+ c, _ := NewClient(addr, &tlsconfig.Options{InsecureSkipVerify: true})
|
|
| 59 | 59 |
var output Manifest |
| 60 | 60 |
err := c.Call("Test.Echo", m, &output)
|
| 61 | 61 |
if err != nil {
|
| ... | ... |
@@ -64,7 +64,7 @@ func (l *localRegistry) Plugin(name string) (*Plugin, error) {
|
| 64 | 64 |
|
| 65 | 65 |
for _, p := range socketpaths {
|
| 66 | 66 |
if fi, err := os.Stat(p); err == nil && fi.Mode()&os.ModeSocket != 0 {
|
| 67 |
- return newLocalPlugin(name, "unix://"+p), nil |
|
| 67 |
+ return NewLocalPlugin(name, "unix://"+p), nil |
|
| 68 | 68 |
} |
| 69 | 69 |
} |
| 70 | 70 |
|
| ... | ... |
@@ -101,7 +101,7 @@ func readPluginInfo(name, path string) (*Plugin, error) {
|
| 101 | 101 |
return nil, fmt.Errorf("Unknown protocol")
|
| 102 | 102 |
} |
| 103 | 103 |
|
| 104 |
- return newLocalPlugin(name, addr), nil |
|
| 104 |
+ return NewLocalPlugin(name, addr), nil |
|
| 105 | 105 |
} |
| 106 | 106 |
|
| 107 | 107 |
func readPluginJSONInfo(name, path string) (*Plugin, error) {
|
| ... | ... |
@@ -115,7 +115,7 @@ func readPluginJSONInfo(name, path string) (*Plugin, error) {
|
| 115 | 115 |
if err := json.NewDecoder(f).Decode(&p); err != nil {
|
| 116 | 116 |
return nil, err |
| 117 | 117 |
} |
| 118 |
- p.Name = name |
|
| 118 |
+ p.name = name |
|
| 119 | 119 |
if len(p.TLSConfig.CAFile) == 0 {
|
| 120 | 120 |
p.TLSConfig.InsecureSkipVerify = true |
| 121 | 121 |
} |
| ... | ... |
@@ -58,7 +58,7 @@ func TestFileSpecPlugin(t *testing.T) {
|
| 58 | 58 |
t.Fatal(err) |
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 |
- if p.Name != c.name {
|
|
| 61 |
+ if p.name != c.name {
|
|
| 62 | 62 |
t.Fatalf("Expected plugin `%s`, got %s\n", c.name, p.Name)
|
| 63 | 63 |
} |
| 64 | 64 |
|
| ... | ... |
@@ -97,7 +97,7 @@ func TestFileJSONSpecPlugin(t *testing.T) {
|
| 97 | 97 |
t.Fatal(err) |
| 98 | 98 |
} |
| 99 | 99 |
|
| 100 |
- if plugin.Name != "example" {
|
|
| 100 |
+ if plugin.name != "example" {
|
|
| 101 | 101 |
t.Fatalf("Expected plugin `plugin-example`, got %s\n", plugin.Name)
|
| 102 | 102 |
} |
| 103 | 103 |
|
| ... | ... |
@@ -55,13 +55,13 @@ type Manifest struct {
|
| 55 | 55 |
// Plugin is the definition of a docker plugin. |
| 56 | 56 |
type Plugin struct {
|
| 57 | 57 |
// Name of the plugin |
| 58 |
- Name string `json:"-"` |
|
| 58 |
+ name string |
|
| 59 | 59 |
// Address of the plugin |
| 60 | 60 |
Addr string |
| 61 | 61 |
// TLS configuration of the plugin |
| 62 |
- TLSConfig tlsconfig.Options |
|
| 62 |
+ TLSConfig *tlsconfig.Options |
|
| 63 | 63 |
// Client attached to the plugin |
| 64 |
- Client *Client `json:"-"` |
|
| 64 |
+ client *Client |
|
| 65 | 65 |
// Manifest of the plugin (see above) |
| 66 | 66 |
Manifest *Manifest `json:"-"` |
| 67 | 67 |
|
| ... | ... |
@@ -73,11 +73,23 @@ type Plugin struct {
|
| 73 | 73 |
activateWait *sync.Cond |
| 74 | 74 |
} |
| 75 | 75 |
|
| 76 |
-func newLocalPlugin(name, addr string) *Plugin {
|
|
| 76 |
+// Name returns the name of the plugin. |
|
| 77 |
+func (p *Plugin) Name() string {
|
|
| 78 |
+ return p.name |
|
| 79 |
+} |
|
| 80 |
+ |
|
| 81 |
+// Client returns a ready-to-use plugin client that can be used to communicate with the plugin. |
|
| 82 |
+func (p *Plugin) Client() *Client {
|
|
| 83 |
+ return p.client |
|
| 84 |
+} |
|
| 85 |
+ |
|
| 86 |
+// NewLocalPlugin creates a new local plugin. |
|
| 87 |
+func NewLocalPlugin(name, addr string) *Plugin {
|
|
| 77 | 88 |
return &Plugin{
|
| 78 |
- Name: name, |
|
| 79 |
- Addr: addr, |
|
| 80 |
- TLSConfig: tlsconfig.Options{InsecureSkipVerify: true},
|
|
| 89 |
+ name: name, |
|
| 90 |
+ Addr: addr, |
|
| 91 |
+ // TODO: change to nil |
|
| 92 |
+ TLSConfig: &tlsconfig.Options{InsecureSkipVerify: true},
|
|
| 81 | 93 |
activateWait: sync.NewCond(&sync.Mutex{}),
|
| 82 | 94 |
} |
| 83 | 95 |
} |
| ... | ... |
@@ -102,10 +114,10 @@ func (p *Plugin) activateWithLock() error {
|
| 102 | 102 |
if err != nil {
|
| 103 | 103 |
return err |
| 104 | 104 |
} |
| 105 |
- p.Client = c |
|
| 105 |
+ p.client = c |
|
| 106 | 106 |
|
| 107 | 107 |
m := new(Manifest) |
| 108 |
- if err = p.Client.Call("Plugin.Activate", nil, m); err != nil {
|
|
| 108 |
+ if err = p.client.Call("Plugin.Activate", nil, m); err != nil {
|
|
| 109 | 109 |
return err |
| 110 | 110 |
} |
| 111 | 111 |
|
| ... | ... |
@@ -116,7 +128,7 @@ func (p *Plugin) activateWithLock() error {
|
| 116 | 116 |
if !handled {
|
| 117 | 117 |
continue |
| 118 | 118 |
} |
| 119 |
- handler(p.Name, p.Client) |
|
| 119 |
+ handler(p.name, p.client) |
|
| 120 | 120 |
} |
| 121 | 121 |
return nil |
| 122 | 122 |
} |
| 123 | 123 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,139 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ "os" |
|
| 8 |
+ "path/filepath" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/Sirupsen/logrus" |
|
| 11 |
+ "github.com/docker/docker/pkg/archive" |
|
| 12 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 13 |
+ "github.com/docker/docker/plugin/distribution" |
|
| 14 |
+ "github.com/docker/docker/reference" |
|
| 15 |
+ "github.com/docker/engine-api/types" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+// Disable deactivates a plugin, which implies that they cannot be used by containers. |
|
| 19 |
+func (pm *Manager) Disable(name string) error {
|
|
| 20 |
+ p, err := pm.get(name) |
|
| 21 |
+ if err != nil {
|
|
| 22 |
+ return err |
|
| 23 |
+ } |
|
| 24 |
+ return pm.disable(p) |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+// Enable activates a plugin, which implies that they are ready to be used by containers. |
|
| 28 |
+func (pm *Manager) Enable(name string) error {
|
|
| 29 |
+ p, err := pm.get(name) |
|
| 30 |
+ if err != nil {
|
|
| 31 |
+ return err |
|
| 32 |
+ } |
|
| 33 |
+ return pm.enable(p) |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+// Inspect examines a plugin manifest |
|
| 37 |
+func (pm *Manager) Inspect(name string) (tp types.Plugin, err error) {
|
|
| 38 |
+ p, err := pm.get(name) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return tp, err |
|
| 41 |
+ } |
|
| 42 |
+ return p.p, nil |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+// Pull pulls a plugin and enables it. |
|
| 46 |
+func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.AuthConfig) (types.PluginPrivileges, error) {
|
|
| 47 |
+ ref, err := reference.ParseNamed(name) |
|
| 48 |
+ if err != nil {
|
|
| 49 |
+ logrus.Debugf("error in reference.ParseNamed: %v", err)
|
|
| 50 |
+ return nil, err |
|
| 51 |
+ } |
|
| 52 |
+ name = ref.String() |
|
| 53 |
+ |
|
| 54 |
+ if p, _ := pm.get(name); p != nil {
|
|
| 55 |
+ logrus.Debugf("plugin already exists")
|
|
| 56 |
+ return nil, fmt.Errorf("%s exists", name)
|
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ pluginID := stringid.GenerateNonCryptoID() |
|
| 60 |
+ |
|
| 61 |
+ if err := os.MkdirAll(filepath.Join(pm.libRoot, pluginID), 0755); err != nil {
|
|
| 62 |
+ logrus.Debugf("error in MkdirAll: %v", err)
|
|
| 63 |
+ return nil, err |
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ pd, err := distribution.Pull(name, pm.registryService, metaHeader, authConfig) |
|
| 67 |
+ if err != nil {
|
|
| 68 |
+ logrus.Debugf("error in distribution.Pull(): %v", err)
|
|
| 69 |
+ return nil, err |
|
| 70 |
+ } |
|
| 71 |
+ |
|
| 72 |
+ if err := distribution.WritePullData(pd, filepath.Join(pm.libRoot, pluginID), true); err != nil {
|
|
| 73 |
+ logrus.Debugf("error in distribution.WritePullData(): %v", err)
|
|
| 74 |
+ return nil, err |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ p := pm.newPlugin(ref, pluginID) |
|
| 78 |
+ if ref, ok := ref.(reference.NamedTagged); ok {
|
|
| 79 |
+ p.p.Tag = ref.Tag() |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ if err := pm.initPlugin(p); err != nil {
|
|
| 83 |
+ return nil, err |
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ pm.Lock() |
|
| 87 |
+ pm.plugins[pluginID] = p |
|
| 88 |
+ pm.nameToID[name] = pluginID |
|
| 89 |
+ pm.save() |
|
| 90 |
+ pm.Unlock() |
|
| 91 |
+ |
|
| 92 |
+ return computePrivileges(&p.p.Manifest), nil |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// List displays the list of plugins and associated metadata. |
|
| 96 |
+func (pm *Manager) List() ([]types.Plugin, error) {
|
|
| 97 |
+ out := make([]types.Plugin, 0, len(pm.plugins)) |
|
| 98 |
+ for _, p := range pm.plugins {
|
|
| 99 |
+ out = append(out, p.p) |
|
| 100 |
+ } |
|
| 101 |
+ return out, nil |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+// Push pushes a plugin to the store. |
|
| 105 |
+func (pm *Manager) Push(name string, metaHeader http.Header, authConfig *types.AuthConfig) error {
|
|
| 106 |
+ p, err := pm.get(name) |
|
| 107 |
+ dest := filepath.Join(pm.libRoot, p.p.ID) |
|
| 108 |
+ config, err := os.Open(filepath.Join(dest, "manifest.json")) |
|
| 109 |
+ if err != nil {
|
|
| 110 |
+ return err |
|
| 111 |
+ } |
|
| 112 |
+ rootfs, err := archive.Tar(filepath.Join(dest, "rootfs"), archive.Gzip) |
|
| 113 |
+ if err != nil {
|
|
| 114 |
+ return err |
|
| 115 |
+ } |
|
| 116 |
+ _, err = distribution.Push(name, pm.registryService, metaHeader, authConfig, config, rootfs) |
|
| 117 |
+ // XXX: Ignore returning digest for now. |
|
| 118 |
+ // Since digest needs to be written to the ProgressWriter. |
|
| 119 |
+ return nil |
|
| 120 |
+} |
|
| 121 |
+ |
|
| 122 |
+// Remove deletes plugin's root directory. |
|
| 123 |
+func (pm *Manager) Remove(name string) error {
|
|
| 124 |
+ p, err := pm.get(name) |
|
| 125 |
+ if err != nil {
|
|
| 126 |
+ return err |
|
| 127 |
+ } |
|
| 128 |
+ return pm.remove(p) |
|
| 129 |
+} |
|
| 130 |
+ |
|
| 131 |
+// Set sets plugin args |
|
| 132 |
+func (pm *Manager) Set(name string, args []string) error {
|
|
| 133 |
+ p, err := pm.get(name) |
|
| 134 |
+ if err != nil {
|
|
| 135 |
+ return err |
|
| 136 |
+ } |
|
| 137 |
+ return pm.set(p, args) |
|
| 138 |
+} |
| 0 | 139 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,208 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package distribution |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "fmt" |
|
| 7 |
+ "io" |
|
| 8 |
+ "io/ioutil" |
|
| 9 |
+ "net/http" |
|
| 10 |
+ "os" |
|
| 11 |
+ "path/filepath" |
|
| 12 |
+ |
|
| 13 |
+ "github.com/Sirupsen/logrus" |
|
| 14 |
+ "github.com/docker/distribution" |
|
| 15 |
+ "github.com/docker/distribution/manifest/schema2" |
|
| 16 |
+ dockerdist "github.com/docker/docker/distribution" |
|
| 17 |
+ archive "github.com/docker/docker/pkg/chrootarchive" |
|
| 18 |
+ "github.com/docker/docker/reference" |
|
| 19 |
+ "github.com/docker/docker/registry" |
|
| 20 |
+ "github.com/docker/engine-api/types" |
|
| 21 |
+ "golang.org/x/net/context" |
|
| 22 |
+) |
|
| 23 |
+ |
|
| 24 |
+// PullData is the plugin manifest and the rootfs |
|
| 25 |
+type PullData interface {
|
|
| 26 |
+ Config() ([]byte, error) |
|
| 27 |
+ Layer() (io.ReadCloser, error) |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+type pullData struct {
|
|
| 31 |
+ repository distribution.Repository |
|
| 32 |
+ manifest schema2.Manifest |
|
| 33 |
+ index int |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+func (pd *pullData) Config() ([]byte, error) {
|
|
| 37 |
+ blobs := pd.repository.Blobs(context.Background()) |
|
| 38 |
+ config, err := blobs.Get(context.Background(), pd.manifest.Config.Digest) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return nil, err |
|
| 41 |
+ } |
|
| 42 |
+ // validate |
|
| 43 |
+ var p types.Plugin |
|
| 44 |
+ if err := json.Unmarshal(config, &p); err != nil {
|
|
| 45 |
+ return nil, err |
|
| 46 |
+ } |
|
| 47 |
+ return config, nil |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+func (pd *pullData) Layer() (io.ReadCloser, error) {
|
|
| 51 |
+ if pd.index >= len(pd.manifest.Layers) {
|
|
| 52 |
+ return nil, io.EOF |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ blobs := pd.repository.Blobs(context.Background()) |
|
| 56 |
+ rsc, err := blobs.Open(context.Background(), pd.manifest.Layers[pd.index].Digest) |
|
| 57 |
+ if err != nil {
|
|
| 58 |
+ return nil, err |
|
| 59 |
+ } |
|
| 60 |
+ pd.index++ |
|
| 61 |
+ return rsc, nil |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// Pull downloads the plugin from Store |
|
| 65 |
+func Pull(name string, rs registry.Service, metaheader http.Header, authConfig *types.AuthConfig) (PullData, error) {
|
|
| 66 |
+ ref, err := reference.ParseNamed(name) |
|
| 67 |
+ if err != nil {
|
|
| 68 |
+ logrus.Debugf("pull.go: error in ParseNamed: %v", err)
|
|
| 69 |
+ return nil, err |
|
| 70 |
+ } |
|
| 71 |
+ |
|
| 72 |
+ repoInfo, err := rs.ResolveRepository(ref) |
|
| 73 |
+ if err != nil {
|
|
| 74 |
+ logrus.Debugf("pull.go: error in ResolveRepository: %v", err)
|
|
| 75 |
+ return nil, err |
|
| 76 |
+ } |
|
| 77 |
+ |
|
| 78 |
+ if err := dockerdist.ValidateRepoName(repoInfo.Name()); err != nil {
|
|
| 79 |
+ logrus.Debugf("pull.go: error in ValidateRepoName: %v", err)
|
|
| 80 |
+ return nil, err |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ endpoints, err := rs.LookupPullEndpoints(repoInfo.Hostname()) |
|
| 84 |
+ if err != nil {
|
|
| 85 |
+ logrus.Debugf("pull.go: error in LookupPullEndpoints: %v", err)
|
|
| 86 |
+ return nil, err |
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ var confirmedV2 bool |
|
| 90 |
+ var repository distribution.Repository |
|
| 91 |
+ |
|
| 92 |
+ for _, endpoint := range endpoints {
|
|
| 93 |
+ if confirmedV2 && endpoint.Version == registry.APIVersion1 {
|
|
| 94 |
+ logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
|
|
| 95 |
+ continue |
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ // TODO: reuse contexts |
|
| 99 |
+ repository, confirmedV2, err = dockerdist.NewV2Repository(context.Background(), repoInfo, endpoint, metaheader, authConfig, "pull") |
|
| 100 |
+ if err != nil {
|
|
| 101 |
+ logrus.Debugf("pull.go: error in NewV2Repository: %v", err)
|
|
| 102 |
+ return nil, err |
|
| 103 |
+ } |
|
| 104 |
+ if !confirmedV2 {
|
|
| 105 |
+ logrus.Debugf("pull.go: !confirmedV2")
|
|
| 106 |
+ return nil, ErrUnSupportedRegistry |
|
| 107 |
+ } |
|
| 108 |
+ logrus.Debugf("Trying to pull %s from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
|
|
| 109 |
+ break |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ tag := DefaultTag |
|
| 113 |
+ if ref, ok := ref.(reference.NamedTagged); ok {
|
|
| 114 |
+ tag = ref.Tag() |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ // tags := repository.Tags(context.Background()) |
|
| 118 |
+ // desc, err := tags.Get(context.Background(), tag) |
|
| 119 |
+ // if err != nil {
|
|
| 120 |
+ // return nil, err |
|
| 121 |
+ // } |
|
| 122 |
+ // |
|
| 123 |
+ msv, err := repository.Manifests(context.Background()) |
|
| 124 |
+ if err != nil {
|
|
| 125 |
+ logrus.Debugf("pull.go: error in repository.Manifests: %v", err)
|
|
| 126 |
+ return nil, err |
|
| 127 |
+ } |
|
| 128 |
+ manifest, err := msv.Get(context.Background(), "", distribution.WithTag(tag)) |
|
| 129 |
+ if err != nil {
|
|
| 130 |
+ // TODO: change 401 to 404 |
|
| 131 |
+ logrus.Debugf("pull.go: error in msv.Get(): %v", err)
|
|
| 132 |
+ return nil, err |
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+ _, pl, err := manifest.Payload() |
|
| 136 |
+ if err != nil {
|
|
| 137 |
+ logrus.Debugf("pull.go: error in manifest.Payload(): %v", err)
|
|
| 138 |
+ return nil, err |
|
| 139 |
+ } |
|
| 140 |
+ var m schema2.Manifest |
|
| 141 |
+ if err := json.Unmarshal(pl, &m); err != nil {
|
|
| 142 |
+ logrus.Debugf("pull.go: error in json.Unmarshal(): %v", err)
|
|
| 143 |
+ return nil, err |
|
| 144 |
+ } |
|
| 145 |
+ |
|
| 146 |
+ pd := &pullData{
|
|
| 147 |
+ repository: repository, |
|
| 148 |
+ manifest: m, |
|
| 149 |
+ } |
|
| 150 |
+ |
|
| 151 |
+ logrus.Debugf("manifest: %s", pl)
|
|
| 152 |
+ return pd, nil |
|
| 153 |
+} |
|
| 154 |
+ |
|
| 155 |
+// WritePullData extracts manifest and rootfs to the disk. |
|
| 156 |
+func WritePullData(pd PullData, dest string, extract bool) error {
|
|
| 157 |
+ config, err := pd.Config() |
|
| 158 |
+ if err != nil {
|
|
| 159 |
+ return err |
|
| 160 |
+ } |
|
| 161 |
+ var p types.Plugin |
|
| 162 |
+ if err := json.Unmarshal(config, &p); err != nil {
|
|
| 163 |
+ return err |
|
| 164 |
+ } |
|
| 165 |
+ logrus.Debugf("%#v", p)
|
|
| 166 |
+ |
|
| 167 |
+ if err := os.MkdirAll(dest, 0700); err != nil {
|
|
| 168 |
+ return err |
|
| 169 |
+ } |
|
| 170 |
+ |
|
| 171 |
+ if extract {
|
|
| 172 |
+ if err := ioutil.WriteFile(filepath.Join(dest, "manifest.json"), config, 0600); err != nil {
|
|
| 173 |
+ return err |
|
| 174 |
+ } |
|
| 175 |
+ |
|
| 176 |
+ if err := os.MkdirAll(filepath.Join(dest, "rootfs"), 0700); err != nil {
|
|
| 177 |
+ return err |
|
| 178 |
+ } |
|
| 179 |
+ } |
|
| 180 |
+ |
|
| 181 |
+ for i := 0; ; i++ {
|
|
| 182 |
+ l, err := pd.Layer() |
|
| 183 |
+ if err == io.EOF {
|
|
| 184 |
+ break |
|
| 185 |
+ } |
|
| 186 |
+ if err != nil {
|
|
| 187 |
+ return err |
|
| 188 |
+ } |
|
| 189 |
+ |
|
| 190 |
+ if !extract {
|
|
| 191 |
+ f, err := os.Create(filepath.Join(dest, fmt.Sprintf("layer%d.tar", i)))
|
|
| 192 |
+ if err != nil {
|
|
| 193 |
+ return err |
|
| 194 |
+ } |
|
| 195 |
+ io.Copy(f, l) |
|
| 196 |
+ l.Close() |
|
| 197 |
+ f.Close() |
|
| 198 |
+ continue |
|
| 199 |
+ } |
|
| 200 |
+ |
|
| 201 |
+ if _, err := archive.ApplyLayer(filepath.Join(dest, "rootfs"), l); err != nil {
|
|
| 202 |
+ return err |
|
| 203 |
+ } |
|
| 204 |
+ |
|
| 205 |
+ } |
|
| 206 |
+ return nil |
|
| 207 |
+} |
| 0 | 208 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,134 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package distribution |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "crypto/sha256" |
|
| 6 |
+ "io" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/Sirupsen/logrus" |
|
| 10 |
+ "github.com/docker/distribution" |
|
| 11 |
+ "github.com/docker/distribution/digest" |
|
| 12 |
+ "github.com/docker/distribution/manifest/schema2" |
|
| 13 |
+ dockerdist "github.com/docker/docker/distribution" |
|
| 14 |
+ "github.com/docker/docker/reference" |
|
| 15 |
+ "github.com/docker/docker/registry" |
|
| 16 |
+ "github.com/docker/engine-api/types" |
|
| 17 |
+ "golang.org/x/net/context" |
|
| 18 |
+) |
|
| 19 |
+ |
|
| 20 |
+// Push pushes a plugin to a registry. |
|
| 21 |
+func Push(name string, rs registry.Service, metaHeader http.Header, authConfig *types.AuthConfig, config io.ReadCloser, layers io.ReadCloser) (digest.Digest, error) {
|
|
| 22 |
+ ref, err := reference.ParseNamed(name) |
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return "", err |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ repoInfo, err := rs.ResolveRepository(ref) |
|
| 28 |
+ if err != nil {
|
|
| 29 |
+ return "", err |
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ if err := dockerdist.ValidateRepoName(repoInfo.Name()); err != nil {
|
|
| 33 |
+ return "", err |
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ endpoints, err := rs.LookupPushEndpoints(repoInfo.Hostname()) |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ return "", err |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ var confirmedV2 bool |
|
| 42 |
+ var repository distribution.Repository |
|
| 43 |
+ for _, endpoint := range endpoints {
|
|
| 44 |
+ if confirmedV2 && endpoint.Version == registry.APIVersion1 {
|
|
| 45 |
+ logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
|
|
| 46 |
+ continue |
|
| 47 |
+ } |
|
| 48 |
+ repository, confirmedV2, err = dockerdist.NewV2Repository(context.Background(), repoInfo, endpoint, metaHeader, authConfig, "push", "pull") |
|
| 49 |
+ if err != nil {
|
|
| 50 |
+ return "", err |
|
| 51 |
+ } |
|
| 52 |
+ if !confirmedV2 {
|
|
| 53 |
+ return "", ErrUnSupportedRegistry |
|
| 54 |
+ } |
|
| 55 |
+ logrus.Debugf("Trying to push %s to %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
|
|
| 56 |
+ // This means that we found an endpoint. and we are ready to push |
|
| 57 |
+ break |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ // Returns a reference to the repository's blob service. |
|
| 61 |
+ blobs := repository.Blobs(context.Background()) |
|
| 62 |
+ |
|
| 63 |
+ // Descriptor = {mediaType, size, digest}
|
|
| 64 |
+ var descs []distribution.Descriptor |
|
| 65 |
+ |
|
| 66 |
+ for i, f := range []io.ReadCloser{config, layers} {
|
|
| 67 |
+ bw, err := blobs.Create(context.Background()) |
|
| 68 |
+ if err != nil {
|
|
| 69 |
+ logrus.Debugf("Error in blobs.Create: %v", err)
|
|
| 70 |
+ return "", err |
|
| 71 |
+ } |
|
| 72 |
+ h := sha256.New() |
|
| 73 |
+ r := io.TeeReader(f, h) |
|
| 74 |
+ _, err = io.Copy(bw, r) |
|
| 75 |
+ if err != nil {
|
|
| 76 |
+ logrus.Debugf("Error in io.Copy: %v", err)
|
|
| 77 |
+ return "", err |
|
| 78 |
+ } |
|
| 79 |
+ f.Close() |
|
| 80 |
+ mt := MediaTypeLayer |
|
| 81 |
+ if i == 0 {
|
|
| 82 |
+ mt = MediaTypeConfig |
|
| 83 |
+ } |
|
| 84 |
+ // Commit completes the write process to the BlobService. |
|
| 85 |
+ // The descriptor arg to Commit is called the "provisional" descriptor and |
|
| 86 |
+ // used for validation. |
|
| 87 |
+ // The returned descriptor should be the one used. Its called the "Canonical" |
|
| 88 |
+ // descriptor. |
|
| 89 |
+ desc, err := bw.Commit(context.Background(), distribution.Descriptor{
|
|
| 90 |
+ MediaType: mt, |
|
| 91 |
+ // XXX: What about the Size? |
|
| 92 |
+ Digest: digest.NewDigest("sha256", h),
|
|
| 93 |
+ }) |
|
| 94 |
+ if err != nil {
|
|
| 95 |
+ logrus.Debugf("Error in bw.Commit: %v", err)
|
|
| 96 |
+ return "", err |
|
| 97 |
+ } |
|
| 98 |
+ // The canonical descriptor is set the mediatype again, just in case. |
|
| 99 |
+ // Dont touch the digest or the size here. |
|
| 100 |
+ desc.MediaType = mt |
|
| 101 |
+ logrus.Debugf("pushed blob: %s %s", desc.MediaType, desc.Digest)
|
|
| 102 |
+ descs = append(descs, desc) |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ // XXX: schema2.Versioned needs a MediaType as well. |
|
| 106 |
+ // "application/vnd.docker.distribution.manifest.v2+json" |
|
| 107 |
+ m, err := schema2.FromStruct(schema2.Manifest{Versioned: schema2.SchemaVersion, Config: descs[0], Layers: descs[1:]})
|
|
| 108 |
+ if err != nil {
|
|
| 109 |
+ logrus.Debugf("error in schema2.FromStruct: %v", err)
|
|
| 110 |
+ return "", err |
|
| 111 |
+ } |
|
| 112 |
+ |
|
| 113 |
+ msv, err := repository.Manifests(context.Background()) |
|
| 114 |
+ if err != nil {
|
|
| 115 |
+ logrus.Debugf("error in repository.Manifests: %v", err)
|
|
| 116 |
+ return "", err |
|
| 117 |
+ } |
|
| 118 |
+ |
|
| 119 |
+ _, pl, err := m.Payload() |
|
| 120 |
+ if err != nil {
|
|
| 121 |
+ logrus.Debugf("error in m.Payload: %v", err)
|
|
| 122 |
+ return "", err |
|
| 123 |
+ } |
|
| 124 |
+ |
|
| 125 |
+ logrus.Debugf("Pushed manifest: %s", pl)
|
|
| 126 |
+ |
|
| 127 |
+ tag := DefaultTag |
|
| 128 |
+ if tagged, ok := ref.(reference.NamedTagged); ok {
|
|
| 129 |
+ tag = tagged.Tag() |
|
| 130 |
+ } |
|
| 131 |
+ |
|
| 132 |
+ return msv.Put(context.Background(), m, distribution.WithTag(tag)) |
|
| 133 |
+} |
| 0 | 134 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,16 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package distribution |
|
| 3 |
+ |
|
| 4 |
+import "errors" |
|
| 5 |
+ |
|
| 6 |
+// ErrUnSupportedRegistry indicates that the registry does not support v2 protocol |
|
| 7 |
+var ErrUnSupportedRegistry = errors.New("Only V2 repositories are supported for plugin distribution")
|
|
| 8 |
+ |
|
| 9 |
+// Plugin related media types |
|
| 10 |
+const ( |
|
| 11 |
+ MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" |
|
| 12 |
+ MediaTypeConfig = "application/vnd.docker.plugin.v0+json" |
|
| 13 |
+ MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip" |
|
| 14 |
+ DefaultTag = "latest" |
|
| 15 |
+) |
| 0 | 16 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,9 @@ |
| 0 |
+package plugin |
|
| 1 |
+ |
|
| 2 |
+import "github.com/docker/docker/pkg/plugins" |
|
| 3 |
+ |
|
| 4 |
+// Plugin represents a plugin. It is used to abstract from an older plugin architecture (in pkg/plugins). |
|
| 5 |
+type Plugin interface {
|
|
| 6 |
+ Client() *plugins.Client |
|
| 7 |
+ Name() string |
|
| 8 |
+} |
| 0 | 9 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,23 @@ |
| 0 |
+// +build !experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import "github.com/docker/docker/pkg/plugins" |
|
| 5 |
+ |
|
| 6 |
+// FindWithCapability returns a list of plugins matching the given capability. |
|
| 7 |
+func FindWithCapability(capability string) ([]Plugin, error) {
|
|
| 8 |
+ pl, err := plugins.GetAll(capability) |
|
| 9 |
+ if err != nil {
|
|
| 10 |
+ return nil, err |
|
| 11 |
+ } |
|
| 12 |
+ result := make([]Plugin, len(pl)) |
|
| 13 |
+ for i, p := range pl {
|
|
| 14 |
+ result[i] = p |
|
| 15 |
+ } |
|
| 16 |
+ return result, nil |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+// LookupWithCapability returns a plugin matching the given name and capability. |
|
| 20 |
+func LookupWithCapability(name, capability string) (Plugin, error) {
|
|
| 21 |
+ return plugins.Get(name, capability) |
|
| 22 |
+} |
| 0 | 23 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,384 @@ |
| 0 |
+// +build experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "errors" |
|
| 7 |
+ "fmt" |
|
| 8 |
+ "io" |
|
| 9 |
+ "os" |
|
| 10 |
+ "path/filepath" |
|
| 11 |
+ "strings" |
|
| 12 |
+ "sync" |
|
| 13 |
+ |
|
| 14 |
+ "github.com/Sirupsen/logrus" |
|
| 15 |
+ "github.com/docker/docker/libcontainerd" |
|
| 16 |
+ "github.com/docker/docker/pkg/ioutils" |
|
| 17 |
+ "github.com/docker/docker/pkg/plugins" |
|
| 18 |
+ "github.com/docker/docker/reference" |
|
| 19 |
+ "github.com/docker/docker/registry" |
|
| 20 |
+ "github.com/docker/docker/restartmanager" |
|
| 21 |
+ "github.com/docker/engine-api/types" |
|
| 22 |
+) |
|
| 23 |
+ |
|
| 24 |
+const ( |
|
| 25 |
+ defaultPluginRuntimeDestination = "/run/docker/plugins" |
|
| 26 |
+ defaultPluginStateDestination = "/state" |
|
| 27 |
+) |
|
| 28 |
+ |
|
| 29 |
+var manager *Manager |
|
| 30 |
+ |
|
| 31 |
+// ErrNotFound indicates that a plugin was not found locally. |
|
| 32 |
+type ErrNotFound string |
|
| 33 |
+ |
|
| 34 |
+func (name ErrNotFound) Error() string { return fmt.Sprintf("plugin %q not found", string(name)) }
|
|
| 35 |
+ |
|
| 36 |
+// ErrInadequateCapability indicates that a plugin was found but did not have the requested capability. |
|
| 37 |
+type ErrInadequateCapability struct {
|
|
| 38 |
+ name string |
|
| 39 |
+ capability string |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func (e ErrInadequateCapability) Error() string {
|
|
| 43 |
+ return fmt.Sprintf("plugin %q found, but not with %q capability", e.name, e.capability)
|
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+type plugin struct {
|
|
| 47 |
+ //sync.RWMutex TODO |
|
| 48 |
+ p types.Plugin |
|
| 49 |
+ client *plugins.Client |
|
| 50 |
+ restartManager restartmanager.RestartManager |
|
| 51 |
+ stateSourcePath string |
|
| 52 |
+ runtimeSourcePath string |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+func (p *plugin) Client() *plugins.Client {
|
|
| 56 |
+ return p.client |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+func (p *plugin) Name() string {
|
|
| 60 |
+ return p.p.Name |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+func (pm *Manager) newPlugin(ref reference.Named, id string) *plugin {
|
|
| 64 |
+ p := &plugin{
|
|
| 65 |
+ p: types.Plugin{
|
|
| 66 |
+ Name: ref.Name(), |
|
| 67 |
+ ID: id, |
|
| 68 |
+ }, |
|
| 69 |
+ stateSourcePath: filepath.Join(pm.libRoot, id, "state"), |
|
| 70 |
+ runtimeSourcePath: filepath.Join(pm.runRoot, id), |
|
| 71 |
+ } |
|
| 72 |
+ if ref, ok := ref.(reference.NamedTagged); ok {
|
|
| 73 |
+ p.p.Tag = ref.Tag() |
|
| 74 |
+ } |
|
| 75 |
+ return p |
|
| 76 |
+} |
|
| 77 |
+ |
|
| 78 |
+// TODO: figure out why save() doesn't json encode *plugin object |
|
| 79 |
+type pluginMap map[string]*plugin |
|
| 80 |
+ |
|
| 81 |
+// Manager controls the plugin subsystem. |
|
| 82 |
+type Manager struct {
|
|
| 83 |
+ sync.RWMutex |
|
| 84 |
+ libRoot string |
|
| 85 |
+ runRoot string |
|
| 86 |
+ plugins pluginMap // TODO: figure out why save() doesn't json encode *plugin object |
|
| 87 |
+ nameToID map[string]string |
|
| 88 |
+ handlers map[string]func(string, *plugins.Client) |
|
| 89 |
+ containerdClient libcontainerd.Client |
|
| 90 |
+ registryService registry.Service |
|
| 91 |
+ handleLegacy bool |
|
| 92 |
+} |
|
| 93 |
+ |
|
| 94 |
+// GetManager returns the singleton plugin Manager |
|
| 95 |
+func GetManager() *Manager {
|
|
| 96 |
+ return manager |
|
| 97 |
+} |
|
| 98 |
+ |
|
| 99 |
+// Init (was NewManager) instantiates the singleton Manager. |
|
| 100 |
+// TODO: revert this to NewManager once we get rid of all the singletons. |
|
| 101 |
+func Init(root, execRoot string, remote libcontainerd.Remote, rs registry.Service) (err error) {
|
|
| 102 |
+ if manager != nil {
|
|
| 103 |
+ return nil |
|
| 104 |
+ } |
|
| 105 |
+ |
|
| 106 |
+ root = filepath.Join(root, "plugins") |
|
| 107 |
+ execRoot = filepath.Join(execRoot, "plugins") |
|
| 108 |
+ for _, dir := range []string{root, execRoot} {
|
|
| 109 |
+ if err := os.MkdirAll(dir, 0700); err != nil {
|
|
| 110 |
+ return err |
|
| 111 |
+ } |
|
| 112 |
+ } |
|
| 113 |
+ |
|
| 114 |
+ manager = &Manager{
|
|
| 115 |
+ libRoot: root, |
|
| 116 |
+ runRoot: execRoot, |
|
| 117 |
+ plugins: make(map[string]*plugin), |
|
| 118 |
+ nameToID: make(map[string]string), |
|
| 119 |
+ handlers: make(map[string]func(string, *plugins.Client)), |
|
| 120 |
+ registryService: rs, |
|
| 121 |
+ handleLegacy: true, |
|
| 122 |
+ } |
|
| 123 |
+ if err := os.MkdirAll(manager.runRoot, 0700); err != nil {
|
|
| 124 |
+ return err |
|
| 125 |
+ } |
|
| 126 |
+ if err := manager.init(); err != nil {
|
|
| 127 |
+ return err |
|
| 128 |
+ } |
|
| 129 |
+ manager.containerdClient, err = remote.Client(manager) |
|
| 130 |
+ if err != nil {
|
|
| 131 |
+ return err |
|
| 132 |
+ } |
|
| 133 |
+ return nil |
|
| 134 |
+} |
|
| 135 |
+ |
|
| 136 |
+// Handle sets a callback for a given capability. The callback will be called for every plugin with a given capability. |
|
| 137 |
+// TODO: append instead of set? |
|
| 138 |
+func Handle(capability string, callback func(string, *plugins.Client)) {
|
|
| 139 |
+ pluginType := fmt.Sprintf("docker.%s/1", strings.ToLower(capability))
|
|
| 140 |
+ manager.handlers[pluginType] = callback |
|
| 141 |
+ if manager.handleLegacy {
|
|
| 142 |
+ plugins.Handle(capability, callback) |
|
| 143 |
+ } |
|
| 144 |
+} |
|
| 145 |
+ |
|
| 146 |
+func (pm *Manager) get(name string) (*plugin, error) {
|
|
| 147 |
+ pm.RLock() |
|
| 148 |
+ id, nameOk := pm.nameToID[name] |
|
| 149 |
+ p, idOk := pm.plugins[id] |
|
| 150 |
+ pm.RUnlock() |
|
| 151 |
+ if !nameOk || !idOk {
|
|
| 152 |
+ return nil, ErrNotFound(name) |
|
| 153 |
+ } |
|
| 154 |
+ return p, nil |
|
| 155 |
+} |
|
| 156 |
+ |
|
| 157 |
+// FindWithCapability returns a list of plugins matching the given capability. |
|
| 158 |
+func FindWithCapability(capability string) ([]Plugin, error) {
|
|
| 159 |
+ handleLegacy := true |
|
| 160 |
+ result := make([]Plugin, 0, 1) |
|
| 161 |
+ if manager != nil {
|
|
| 162 |
+ handleLegacy = manager.handleLegacy |
|
| 163 |
+ manager.RLock() |
|
| 164 |
+ defer manager.RUnlock() |
|
| 165 |
+ pluginLoop: |
|
| 166 |
+ for _, p := range manager.plugins {
|
|
| 167 |
+ for _, typ := range p.p.Manifest.Interface.Types {
|
|
| 168 |
+ if typ.Capability != capability || typ.Prefix != "docker" {
|
|
| 169 |
+ continue pluginLoop |
|
| 170 |
+ } |
|
| 171 |
+ } |
|
| 172 |
+ result = append(result, p) |
|
| 173 |
+ } |
|
| 174 |
+ } |
|
| 175 |
+ if handleLegacy {
|
|
| 176 |
+ pl, err := plugins.GetAll(capability) |
|
| 177 |
+ if err != nil {
|
|
| 178 |
+ return nil, fmt.Errorf("legacy plugin: %v", err)
|
|
| 179 |
+ } |
|
| 180 |
+ for _, p := range pl {
|
|
| 181 |
+ if _, ok := manager.nameToID[p.Name()]; !ok {
|
|
| 182 |
+ result = append(result, p) |
|
| 183 |
+ } |
|
| 184 |
+ } |
|
| 185 |
+ } |
|
| 186 |
+ return result, nil |
|
| 187 |
+} |
|
| 188 |
+ |
|
| 189 |
+// LookupWithCapability returns a plugin matching the given name and capability. |
|
| 190 |
+func LookupWithCapability(name, capability string) (Plugin, error) {
|
|
| 191 |
+ var ( |
|
| 192 |
+ p *plugin |
|
| 193 |
+ err error |
|
| 194 |
+ ) |
|
| 195 |
+ handleLegacy := true |
|
| 196 |
+ if manager != nil {
|
|
| 197 |
+ p, err = manager.get(name) |
|
| 198 |
+ if err != nil {
|
|
| 199 |
+ if _, ok := err.(ErrNotFound); !ok {
|
|
| 200 |
+ return nil, err |
|
| 201 |
+ } |
|
| 202 |
+ handleLegacy = manager.handleLegacy |
|
| 203 |
+ } else {
|
|
| 204 |
+ handleLegacy = false |
|
| 205 |
+ } |
|
| 206 |
+ } |
|
| 207 |
+ if handleLegacy {
|
|
| 208 |
+ p, err := plugins.Get(name, capability) |
|
| 209 |
+ if err != nil {
|
|
| 210 |
+ return nil, fmt.Errorf("legacy plugin: %v", err)
|
|
| 211 |
+ } |
|
| 212 |
+ return p, nil |
|
| 213 |
+ } else if err != nil {
|
|
| 214 |
+ return nil, err |
|
| 215 |
+ } |
|
| 216 |
+ |
|
| 217 |
+ capability = strings.ToLower(capability) |
|
| 218 |
+ for _, typ := range p.p.Manifest.Interface.Types {
|
|
| 219 |
+ if typ.Capability == capability && typ.Prefix == "docker" {
|
|
| 220 |
+ return p, nil |
|
| 221 |
+ } |
|
| 222 |
+ } |
|
| 223 |
+ return nil, ErrInadequateCapability{name, capability}
|
|
| 224 |
+} |
|
| 225 |
+ |
|
| 226 |
+// StateChanged updates daemon inter... |
|
| 227 |
+func (pm *Manager) StateChanged(id string, e libcontainerd.StateInfo) error {
|
|
| 228 |
+ logrus.Debugf("plugin statechanged %s %#v", id, e)
|
|
| 229 |
+ |
|
| 230 |
+ return nil |
|
| 231 |
+} |
|
| 232 |
+ |
|
| 233 |
+// AttachStreams attaches io streams to the plugin |
|
| 234 |
+func (pm *Manager) AttachStreams(id string, iop libcontainerd.IOPipe) error {
|
|
| 235 |
+ iop.Stdin.Close() |
|
| 236 |
+ |
|
| 237 |
+ logger := logrus.New() |
|
| 238 |
+ logger.Hooks.Add(logHook{id})
|
|
| 239 |
+ // TODO: cache writer per id |
|
| 240 |
+ w := logger.Writer() |
|
| 241 |
+ go func() {
|
|
| 242 |
+ io.Copy(w, iop.Stdout) |
|
| 243 |
+ }() |
|
| 244 |
+ go func() {
|
|
| 245 |
+ // TODO: update logrus and use logger.WriterLevel |
|
| 246 |
+ io.Copy(w, iop.Stderr) |
|
| 247 |
+ }() |
|
| 248 |
+ return nil |
|
| 249 |
+} |
|
| 250 |
+ |
|
| 251 |
+func (pm *Manager) init() error {
|
|
| 252 |
+ dt, err := os.Open(filepath.Join(pm.libRoot, "plugins.json")) |
|
| 253 |
+ if err != nil {
|
|
| 254 |
+ if os.IsNotExist(err) {
|
|
| 255 |
+ return nil |
|
| 256 |
+ } |
|
| 257 |
+ return err |
|
| 258 |
+ } |
|
| 259 |
+ // TODO: Populate pm.plugins |
|
| 260 |
+ if err := json.NewDecoder(dt).Decode(&pm.nameToID); err != nil {
|
|
| 261 |
+ return err |
|
| 262 |
+ } |
|
| 263 |
+ // FIXME: validate, restore |
|
| 264 |
+ |
|
| 265 |
+ return nil |
|
| 266 |
+} |
|
| 267 |
+ |
|
| 268 |
+func (pm *Manager) initPlugin(p *plugin) error {
|
|
| 269 |
+ dt, err := os.Open(filepath.Join(pm.libRoot, p.p.ID, "manifest.json")) |
|
| 270 |
+ if err != nil {
|
|
| 271 |
+ return err |
|
| 272 |
+ } |
|
| 273 |
+ err = json.NewDecoder(dt).Decode(&p.p.Manifest) |
|
| 274 |
+ dt.Close() |
|
| 275 |
+ if err != nil {
|
|
| 276 |
+ return err |
|
| 277 |
+ } |
|
| 278 |
+ |
|
| 279 |
+ p.p.Config.Mounts = make([]types.PluginMount, len(p.p.Manifest.Mounts)) |
|
| 280 |
+ for i, mount := range p.p.Manifest.Mounts {
|
|
| 281 |
+ p.p.Config.Mounts[i] = mount |
|
| 282 |
+ } |
|
| 283 |
+ p.p.Config.Env = make([]string, 0, len(p.p.Manifest.Env)) |
|
| 284 |
+ for _, env := range p.p.Manifest.Env {
|
|
| 285 |
+ if env.Value != nil {
|
|
| 286 |
+ p.p.Config.Env = append(p.p.Config.Env, fmt.Sprintf("%s=%s", env.Name, *env.Value))
|
|
| 287 |
+ } |
|
| 288 |
+ } |
|
| 289 |
+ copy(p.p.Config.Args, p.p.Manifest.Args.Value) |
|
| 290 |
+ |
|
| 291 |
+ f, err := os.Create(filepath.Join(pm.libRoot, p.p.ID, "plugin-config.json")) |
|
| 292 |
+ if err != nil {
|
|
| 293 |
+ return err |
|
| 294 |
+ } |
|
| 295 |
+ err = json.NewEncoder(f).Encode(&p.p.Config) |
|
| 296 |
+ f.Close() |
|
| 297 |
+ return err |
|
| 298 |
+} |
|
| 299 |
+ |
|
| 300 |
+func (pm *Manager) remove(p *plugin) error {
|
|
| 301 |
+ if p.p.Active {
|
|
| 302 |
+ return fmt.Errorf("plugin %s is active", p.p.Name)
|
|
| 303 |
+ } |
|
| 304 |
+ pm.Lock() // fixme: lock single record |
|
| 305 |
+ defer pm.Unlock() |
|
| 306 |
+ os.RemoveAll(p.stateSourcePath) |
|
| 307 |
+ delete(pm.plugins, p.p.Name) |
|
| 308 |
+ pm.save() |
|
| 309 |
+ return nil |
|
| 310 |
+} |
|
| 311 |
+ |
|
| 312 |
+func (pm *Manager) set(p *plugin, args []string) error {
|
|
| 313 |
+ m := make(map[string]string, len(args)) |
|
| 314 |
+ for _, arg := range args {
|
|
| 315 |
+ i := strings.Index(arg, "=") |
|
| 316 |
+ if i < 0 {
|
|
| 317 |
+ return fmt.Errorf("No equal sign '=' found in %s", arg)
|
|
| 318 |
+ } |
|
| 319 |
+ m[arg[:i]] = arg[i+1:] |
|
| 320 |
+ } |
|
| 321 |
+ return errors.New("not implemented")
|
|
| 322 |
+} |
|
| 323 |
+ |
|
| 324 |
+// fixme: not safe |
|
| 325 |
+func (pm *Manager) save() error {
|
|
| 326 |
+ filePath := filepath.Join(pm.libRoot, "plugins.json") |
|
| 327 |
+ |
|
| 328 |
+ jsonData, err := json.Marshal(pm.nameToID) |
|
| 329 |
+ if err != nil {
|
|
| 330 |
+ logrus.Debugf("Error in json.Marshal: %v", err)
|
|
| 331 |
+ return err |
|
| 332 |
+ } |
|
| 333 |
+ ioutils.AtomicWriteFile(filePath, jsonData, 0600) |
|
| 334 |
+ return nil |
|
| 335 |
+} |
|
| 336 |
+ |
|
| 337 |
+type logHook struct{ id string }
|
|
| 338 |
+ |
|
| 339 |
+func (logHook) Levels() []logrus.Level {
|
|
| 340 |
+ return logrus.AllLevels |
|
| 341 |
+} |
|
| 342 |
+ |
|
| 343 |
+func (l logHook) Fire(entry *logrus.Entry) error {
|
|
| 344 |
+ entry.Data = logrus.Fields{"plugin": l.id}
|
|
| 345 |
+ return nil |
|
| 346 |
+} |
|
| 347 |
+ |
|
| 348 |
+func computePrivileges(m *types.PluginManifest) types.PluginPrivileges {
|
|
| 349 |
+ var privileges types.PluginPrivileges |
|
| 350 |
+ if m.Network.Type != "null" && m.Network.Type != "bridge" {
|
|
| 351 |
+ privileges = append(privileges, types.PluginPrivilege{
|
|
| 352 |
+ Name: "network", |
|
| 353 |
+ Description: "", |
|
| 354 |
+ Value: []string{m.Network.Type},
|
|
| 355 |
+ }) |
|
| 356 |
+ } |
|
| 357 |
+ for _, mount := range m.Mounts {
|
|
| 358 |
+ if mount.Source != nil {
|
|
| 359 |
+ privileges = append(privileges, types.PluginPrivilege{
|
|
| 360 |
+ Name: "mount", |
|
| 361 |
+ Description: "", |
|
| 362 |
+ Value: []string{*mount.Source},
|
|
| 363 |
+ }) |
|
| 364 |
+ } |
|
| 365 |
+ } |
|
| 366 |
+ for _, device := range m.Devices {
|
|
| 367 |
+ if device.Path != nil {
|
|
| 368 |
+ privileges = append(privileges, types.PluginPrivilege{
|
|
| 369 |
+ Name: "device", |
|
| 370 |
+ Description: "", |
|
| 371 |
+ Value: []string{*device.Path},
|
|
| 372 |
+ }) |
|
| 373 |
+ } |
|
| 374 |
+ } |
|
| 375 |
+ if len(m.Capabilities) > 0 {
|
|
| 376 |
+ privileges = append(privileges, types.PluginPrivilege{
|
|
| 377 |
+ Name: "capabilities", |
|
| 378 |
+ Description: "", |
|
| 379 |
+ Value: m.Capabilities, |
|
| 380 |
+ }) |
|
| 381 |
+ } |
|
| 382 |
+ return privileges |
|
| 383 |
+} |
| 0 | 384 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,126 @@ |
| 0 |
+// +build linux,experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "os" |
|
| 6 |
+ "path/filepath" |
|
| 7 |
+ "syscall" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/Sirupsen/logrus" |
|
| 10 |
+ "github.com/docker/docker/libcontainerd" |
|
| 11 |
+ "github.com/docker/docker/oci" |
|
| 12 |
+ "github.com/docker/docker/pkg/plugins" |
|
| 13 |
+ "github.com/docker/docker/pkg/system" |
|
| 14 |
+ "github.com/docker/docker/restartmanager" |
|
| 15 |
+ "github.com/docker/engine-api/types" |
|
| 16 |
+ "github.com/docker/engine-api/types/container" |
|
| 17 |
+ "github.com/opencontainers/specs/specs-go" |
|
| 18 |
+) |
|
| 19 |
+ |
|
| 20 |
+func (pm *Manager) enable(p *plugin) error {
|
|
| 21 |
+ spec, err := pm.initSpec(p) |
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ return err |
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ p.restartManager = restartmanager.New(container.RestartPolicy{Name: "always"}, 0)
|
|
| 27 |
+ if err := pm.containerdClient.Create(p.p.ID, libcontainerd.Spec(*spec), libcontainerd.WithRestartManager(p.restartManager)); err != nil { // POC-only
|
|
| 28 |
+ return err |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ socket := p.p.Manifest.Interface.Socket |
|
| 32 |
+ p.client, err = plugins.NewClient("unix://"+filepath.Join(p.runtimeSourcePath, socket), nil)
|
|
| 33 |
+ if err != nil {
|
|
| 34 |
+ return err |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ //TODO: check net.Dial |
|
| 38 |
+ |
|
| 39 |
+ pm.Lock() // fixme: lock single record |
|
| 40 |
+ p.p.Active = true |
|
| 41 |
+ pm.save() |
|
| 42 |
+ pm.Unlock() |
|
| 43 |
+ |
|
| 44 |
+ for _, typ := range p.p.Manifest.Interface.Types {
|
|
| 45 |
+ if handler := pm.handlers[typ.String()]; handler != nil {
|
|
| 46 |
+ handler(p.Name(), p.Client()) |
|
| 47 |
+ } |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ return nil |
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+func (pm *Manager) initSpec(p *plugin) (*specs.Spec, error) {
|
|
| 54 |
+ s := oci.DefaultSpec() |
|
| 55 |
+ |
|
| 56 |
+ rootfs := filepath.Join(pm.libRoot, p.p.ID, "rootfs") |
|
| 57 |
+ s.Root = specs.Root{
|
|
| 58 |
+ Path: rootfs, |
|
| 59 |
+ Readonly: false, // TODO: all plugins should be readonly? settable in manifest? |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ mounts := append(p.p.Config.Mounts, types.PluginMount{
|
|
| 63 |
+ Source: &p.runtimeSourcePath, |
|
| 64 |
+ Destination: defaultPluginRuntimeDestination, |
|
| 65 |
+ Type: "bind", |
|
| 66 |
+ Options: []string{"rbind", "rshared"},
|
|
| 67 |
+ }, types.PluginMount{
|
|
| 68 |
+ Source: &p.stateSourcePath, |
|
| 69 |
+ Destination: defaultPluginStateDestination, |
|
| 70 |
+ Type: "bind", |
|
| 71 |
+ Options: []string{"rbind", "rshared"},
|
|
| 72 |
+ }) |
|
| 73 |
+ for _, mount := range mounts {
|
|
| 74 |
+ m := specs.Mount{
|
|
| 75 |
+ Destination: mount.Destination, |
|
| 76 |
+ Type: mount.Type, |
|
| 77 |
+ Options: mount.Options, |
|
| 78 |
+ } |
|
| 79 |
+ // TODO: if nil, then it's required and user didn't set it |
|
| 80 |
+ if mount.Source != nil {
|
|
| 81 |
+ m.Source = *mount.Source |
|
| 82 |
+ } |
|
| 83 |
+ if m.Source != "" && m.Type == "bind" {
|
|
| 84 |
+ fi, err := os.Lstat(filepath.Join(rootfs, string(os.PathSeparator), m.Destination)) // TODO: followsymlinks |
|
| 85 |
+ if err != nil {
|
|
| 86 |
+ return nil, err |
|
| 87 |
+ } |
|
| 88 |
+ if fi.IsDir() {
|
|
| 89 |
+ if err := os.MkdirAll(m.Source, 0700); err != nil {
|
|
| 90 |
+ return nil, err |
|
| 91 |
+ } |
|
| 92 |
+ } |
|
| 93 |
+ } |
|
| 94 |
+ s.Mounts = append(s.Mounts, m) |
|
| 95 |
+ } |
|
| 96 |
+ |
|
| 97 |
+ envs := make([]string, 1, len(p.p.Config.Env)+1) |
|
| 98 |
+ envs[0] = "PATH=" + system.DefaultPathEnv |
|
| 99 |
+ envs = append(envs, p.p.Config.Env...) |
|
| 100 |
+ |
|
| 101 |
+ args := append(p.p.Manifest.Entrypoint, p.p.Config.Args...) |
|
| 102 |
+ s.Process = specs.Process{
|
|
| 103 |
+ Terminal: false, |
|
| 104 |
+ Args: args, |
|
| 105 |
+ Cwd: "/", // TODO: add in manifest? |
|
| 106 |
+ Env: envs, |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ return &s, nil |
|
| 110 |
+} |
|
| 111 |
+ |
|
| 112 |
+func (pm *Manager) disable(p *plugin) error {
|
|
| 113 |
+ if err := p.restartManager.Cancel(); err != nil {
|
|
| 114 |
+ logrus.Error(err) |
|
| 115 |
+ } |
|
| 116 |
+ if err := pm.containerdClient.Signal(p.p.ID, int(syscall.SIGKILL)); err != nil {
|
|
| 117 |
+ logrus.Error(err) |
|
| 118 |
+ } |
|
| 119 |
+ os.RemoveAll(p.runtimeSourcePath) |
|
| 120 |
+ pm.Lock() // fixme: lock single record |
|
| 121 |
+ defer pm.Unlock() |
|
| 122 |
+ p.p.Active = false |
|
| 123 |
+ pm.save() |
|
| 124 |
+ return nil |
|
| 125 |
+} |
| 0 | 126 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,21 @@ |
| 0 |
+// +build windows,experimental |
|
| 1 |
+ |
|
| 2 |
+package plugin |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/opencontainers/specs/specs-go" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+func (pm *Manager) enable(p *plugin) error {
|
|
| 11 |
+ return fmt.Errorf("Not implemented")
|
|
| 12 |
+} |
|
| 13 |
+ |
|
| 14 |
+func (pm *Manager) initSpec(p *plugin) (*specs.Spec, error) {
|
|
| 15 |
+ return nil, fmt.Errorf("Not implemented")
|
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+func (pm *Manager) disable(p *plugin) error {
|
|
| 19 |
+ return fmt.Errorf("Not implemented")
|
|
| 20 |
+} |
| ... | ... |
@@ -7,7 +7,7 @@ import ( |
| 7 | 7 |
"sync" |
| 8 | 8 |
|
| 9 | 9 |
"github.com/docker/docker/pkg/locker" |
| 10 |
- "github.com/docker/docker/pkg/plugins" |
|
| 10 |
+ "github.com/docker/docker/plugin" |
|
| 11 | 11 |
"github.com/docker/docker/volume" |
| 12 | 12 |
) |
| 13 | 13 |
|
| ... | ... |
@@ -88,10 +88,10 @@ func Unregister(name string) bool {
|
| 88 | 88 |
return true |
| 89 | 89 |
} |
| 90 | 90 |
|
| 91 |
-// Lookup returns the driver associated with the given name. If a |
|
| 91 |
+// lookup returns the driver associated with the given name. If a |
|
| 92 | 92 |
// driver with the given name has not been registered it checks if |
| 93 | 93 |
// there is a VolumeDriver plugin available with the given name. |
| 94 |
-func Lookup(name string) (volume.Driver, error) {
|
|
| 94 |
+func lookup(name string) (volume.Driver, error) {
|
|
| 95 | 95 |
drivers.driverLock.Lock(name) |
| 96 | 96 |
defer drivers.driverLock.Unlock(name) |
| 97 | 97 |
|
| ... | ... |
@@ -102,7 +102,7 @@ func Lookup(name string) (volume.Driver, error) {
|
| 102 | 102 |
return ext, nil |
| 103 | 103 |
} |
| 104 | 104 |
|
| 105 |
- pl, err := plugins.Get(name, extName) |
|
| 105 |
+ p, err := plugin.LookupWithCapability(name, extName) |
|
| 106 | 106 |
if err != nil {
|
| 107 | 107 |
return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err)
|
| 108 | 108 |
} |
| ... | ... |
@@ -113,7 +113,7 @@ func Lookup(name string) (volume.Driver, error) {
|
| 113 | 113 |
return ext, nil |
| 114 | 114 |
} |
| 115 | 115 |
|
| 116 |
- d := NewVolumeDriver(name, pl.Client) |
|
| 116 |
+ d := NewVolumeDriver(name, p.Client()) |
|
| 117 | 117 |
if err := validateDriver(d); err != nil {
|
| 118 | 118 |
return nil, err |
| 119 | 119 |
} |
| ... | ... |
@@ -136,7 +136,7 @@ func GetDriver(name string) (volume.Driver, error) {
|
| 136 | 136 |
if name == "" {
|
| 137 | 137 |
name = volume.DefaultDriverName |
| 138 | 138 |
} |
| 139 |
- return Lookup(name) |
|
| 139 |
+ return lookup(name) |
|
| 140 | 140 |
} |
| 141 | 141 |
|
| 142 | 142 |
// GetDriverList returns list of volume drivers registered. |
| ... | ... |
@@ -153,9 +153,9 @@ func GetDriverList() []string {
|
| 153 | 153 |
|
| 154 | 154 |
// GetAllDrivers lists all the registered drivers |
| 155 | 155 |
func GetAllDrivers() ([]volume.Driver, error) {
|
| 156 |
- plugins, err := plugins.GetAll(extName) |
|
| 156 |
+ plugins, err := plugin.FindWithCapability(extName) |
|
| 157 | 157 |
if err != nil {
|
| 158 |
- return nil, err |
|
| 158 |
+ return nil, fmt.Errorf("error listing plugins: %v", err)
|
|
| 159 | 159 |
} |
| 160 | 160 |
var ds []volume.Driver |
| 161 | 161 |
|
| ... | ... |
@@ -167,13 +167,14 @@ func GetAllDrivers() ([]volume.Driver, error) {
|
| 167 | 167 |
} |
| 168 | 168 |
|
| 169 | 169 |
for _, p := range plugins {
|
| 170 |
- ext, ok := drivers.extensions[p.Name] |
|
| 170 |
+ name := p.Name() |
|
| 171 |
+ ext, ok := drivers.extensions[name] |
|
| 171 | 172 |
if ok {
|
| 172 | 173 |
continue |
| 173 | 174 |
} |
| 174 | 175 |
|
| 175 |
- ext = NewVolumeDriver(p.Name, p.Client) |
|
| 176 |
- drivers.extensions[p.Name] = ext |
|
| 176 |
+ ext = NewVolumeDriver(name, p.Client()) |
|
| 177 |
+ drivers.extensions[name] = ext |
|
| 177 | 178 |
ds = append(ds, ext) |
| 178 | 179 |
} |
| 179 | 180 |
return ds, nil |
| ... | ... |
@@ -58,7 +58,7 @@ func TestVolumeRequestError(t *testing.T) {
|
| 58 | 58 |
}) |
| 59 | 59 |
|
| 60 | 60 |
u, _ := url.Parse(server.URL) |
| 61 |
- client, err := plugins.NewClient("tcp://"+u.Host, tlsconfig.Options{InsecureSkipVerify: true})
|
|
| 61 |
+ client, err := plugins.NewClient("tcp://"+u.Host, &tlsconfig.Options{InsecureSkipVerify: true})
|
|
| 62 | 62 |
if err != nil {
|
| 63 | 63 |
t.Fatal(err) |
| 64 | 64 |
} |
| ... | ... |
@@ -157,15 +157,16 @@ func (s *VolumeStore) List() ([]volume.Volume, []string, error) {
|
| 157 | 157 |
|
| 158 | 158 |
// list goes through each volume driver and asks for its list of volumes. |
| 159 | 159 |
func (s *VolumeStore) list() ([]volume.Volume, []string, error) {
|
| 160 |
- drivers, err := volumedrivers.GetAllDrivers() |
|
| 161 |
- if err != nil {
|
|
| 162 |
- return nil, nil, err |
|
| 163 |
- } |
|
| 164 | 160 |
var ( |
| 165 | 161 |
ls []volume.Volume |
| 166 | 162 |
warnings []string |
| 167 | 163 |
) |
| 168 | 164 |
|
| 165 |
+ drivers, err := volumedrivers.GetAllDrivers() |
|
| 166 |
+ if err != nil {
|
|
| 167 |
+ return nil, nil, err |
|
| 168 |
+ } |
|
| 169 |
+ |
|
| 169 | 170 |
type vols struct {
|
| 170 | 171 |
vols []volume.Volume |
| 171 | 172 |
err error |