From e5b7d36e9954ffd8c199c0fcfaa0b9f4bfdeb196 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Mon, 13 Jun 2016 22:01:08 -0700 Subject: [PATCH 1/3] plugins: vendor engine-api to import new plugin types Signed-off-by: Tibor Vass --- hack/vendor.sh | 2 +- .../docker/engine-api/client/errors.go | 15 ++ .../docker/engine-api/client/interface.go | 10 +- .../client/interface_experimental.go | 26 +++ .../engine-api/client/interface_stable.go | 11 ++ .../engine-api/client/plugin_disable.go | 14 ++ .../docker/engine-api/client/plugin_enable.go | 14 ++ .../engine-api/client/plugin_inspect.go | 22 +++ .../engine-api/client/plugin_install.go | 54 ++++++ .../docker/engine-api/client/plugin_list.go | 23 +++ .../docker/engine-api/client/plugin_push.go | 15 ++ .../docker/engine-api/client/plugin_remove.go | 14 ++ .../docker/engine-api/client/plugin_set.go | 14 ++ .../docker/engine-api/types/plugin.go | 160 ++++++++++++++++++ 14 files changed, 386 insertions(+), 8 deletions(-) create mode 100644 vendor/src/github.com/docker/engine-api/client/interface_experimental.go create mode 100644 vendor/src/github.com/docker/engine-api/client/interface_stable.go create mode 100644 vendor/src/github.com/docker/engine-api/client/plugin_disable.go create mode 100644 vendor/src/github.com/docker/engine-api/client/plugin_enable.go create mode 100644 vendor/src/github.com/docker/engine-api/client/plugin_inspect.go create mode 100644 vendor/src/github.com/docker/engine-api/client/plugin_install.go create mode 100644 vendor/src/github.com/docker/engine-api/client/plugin_list.go create mode 100644 vendor/src/github.com/docker/engine-api/client/plugin_push.go create mode 100644 vendor/src/github.com/docker/engine-api/client/plugin_remove.go create mode 100644 vendor/src/github.com/docker/engine-api/client/plugin_set.go create mode 100644 vendor/src/github.com/docker/engine-api/types/plugin.go diff --git a/hack/vendor.sh b/hack/vendor.sh index e2ee7a08f8..5301e16289 100755 --- a/hack/vendor.sh +++ b/hack/vendor.sh @@ -60,7 +60,7 @@ clone git golang.org/x/net 2beffdc2e92c8a3027590f898fe88f69af48a3f8 https://gith clone git golang.org/x/sys eb2c74142fd19a79b3f237334c7384d5167b1b46 https://github.com/golang/sys.git clone git github.com/docker/go-units 651fc226e7441360384da338d0fd37f2440ffbe3 clone git github.com/docker/go-connections fa2850ff103453a9ad190da0df0af134f0314b3d -clone git github.com/docker/engine-api 6b2f24f16a7f1598635b6a99dbe38ec8a5eccaf8 +clone git github.com/docker/engine-api f3b5ad20d4576de14c96603db522dec530d03f62 clone git github.com/RackSec/srslog 259aed10dfa74ea2961eddd1d9847619f6e98837 clone git github.com/imdario/mergo 0.2.1 diff --git a/vendor/src/github.com/docker/engine-api/client/errors.go b/vendor/src/github.com/docker/engine-api/client/errors.go index be9f595ef8..6f6a78f97d 100644 --- a/vendor/src/github.com/docker/engine-api/client/errors.go +++ b/vendor/src/github.com/docker/engine-api/client/errors.go @@ -171,3 +171,18 @@ func IsErrTaskNotFound(err error) bool { _, ok := err.(taskNotFoundError) return ok } + +type pluginPermissionDenied struct { + name string +} + +func (e pluginPermissionDenied) Error() string { + return "Permission denied while installing plugin " + e.name +} + +// IsErrPluginPermissionDenied returns true if the error is caused +// when a user denies a plugin's permissions +func IsErrPluginPermissionDenied(err error) bool { + _, ok := err.(pluginPermissionDenied) + return ok +} diff --git a/vendor/src/github.com/docker/engine-api/client/interface.go b/vendor/src/github.com/docker/engine-api/client/interface.go index d64619899e..95d775561f 100644 --- a/vendor/src/github.com/docker/engine-api/client/interface.go +++ b/vendor/src/github.com/docker/engine-api/client/interface.go @@ -4,18 +4,17 @@ import ( "io" "time" - "golang.org/x/net/context" - "github.com/docker/engine-api/types" "github.com/docker/engine-api/types/container" "github.com/docker/engine-api/types/filters" "github.com/docker/engine-api/types/network" "github.com/docker/engine-api/types/registry" "github.com/docker/engine-api/types/swarm" + "golang.org/x/net/context" ) -// APIClient is an interface that clients that talk with a docker server must implement. -type APIClient interface { +// CommonAPIClient is the common methods between stable and experimental versions of APIClient. +type CommonAPIClient interface { ClientVersion() string CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error CheckpointDelete(ctx context.Context, container string, checkpointID string) error @@ -97,6 +96,3 @@ type APIClient interface { VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) VolumeRemove(ctx context.Context, volumeID string) error } - -// Ensure that Client always implements APIClient. -var _ APIClient = &Client{} diff --git a/vendor/src/github.com/docker/engine-api/client/interface_experimental.go b/vendor/src/github.com/docker/engine-api/client/interface_experimental.go new file mode 100644 index 0000000000..c968e530bf --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/interface_experimental.go @@ -0,0 +1,26 @@ +// +build experimental + +package client + +import ( + "io" + + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +// APIClient is an interface that clients that talk with a docker server must implement. +type APIClient interface { + CommonAPIClient + PluginList(ctx context.Context) (types.PluginsListResponse, error) + PluginRemove(ctx context.Context, name string) error + PluginEnable(ctx context.Context, name string) error + PluginDisable(ctx context.Context, name string) error + PluginInstall(ctx context.Context, name, registryAuth string, acceptAllPermissions, noEnable bool, in io.ReadCloser, out io.Writer) error + PluginPush(ctx context.Context, name string, registryAuth string) error + PluginSet(ctx context.Context, name string, args []string) error + PluginInspect(ctx context.Context, name string) (*types.Plugin, error) +} + +// Ensure that Client always implements APIClient. +var _ APIClient = &Client{} diff --git a/vendor/src/github.com/docker/engine-api/client/interface_stable.go b/vendor/src/github.com/docker/engine-api/client/interface_stable.go new file mode 100644 index 0000000000..496f522d51 --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/interface_stable.go @@ -0,0 +1,11 @@ +// +build !experimental + +package client + +// APIClient is an interface that clients that talk with a docker server must implement. +type APIClient interface { + CommonAPIClient +} + +// Ensure that Client always implements APIClient. +var _ APIClient = &Client{} diff --git a/vendor/src/github.com/docker/engine-api/client/plugin_disable.go b/vendor/src/github.com/docker/engine-api/client/plugin_disable.go new file mode 100644 index 0000000000..893fc6e823 --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/plugin_disable.go @@ -0,0 +1,14 @@ +// +build experimental + +package client + +import ( + "golang.org/x/net/context" +) + +// PluginDisable disables a plugin +func (cli *Client) PluginDisable(ctx context.Context, name string) error { + resp, err := cli.post(ctx, "/plugins/"+name+"/disable", nil, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/vendor/src/github.com/docker/engine-api/client/plugin_enable.go b/vendor/src/github.com/docker/engine-api/client/plugin_enable.go new file mode 100644 index 0000000000..84422abc79 --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/plugin_enable.go @@ -0,0 +1,14 @@ +// +build experimental + +package client + +import ( + "golang.org/x/net/context" +) + +// PluginEnable enables a plugin +func (cli *Client) PluginEnable(ctx context.Context, name string) error { + resp, err := cli.post(ctx, "/plugins/"+name+"/enable", nil, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/vendor/src/github.com/docker/engine-api/client/plugin_inspect.go b/vendor/src/github.com/docker/engine-api/client/plugin_inspect.go new file mode 100644 index 0000000000..b4bcc20069 --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/plugin_inspect.go @@ -0,0 +1,22 @@ +// +build experimental + +package client + +import ( + "encoding/json" + + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +// PluginInspect inspects an existing plugin +func (cli *Client) PluginInspect(ctx context.Context, name string) (*types.Plugin, error) { + var p types.Plugin + resp, err := cli.get(ctx, "/plugins/"+name, nil, nil) + if err != nil { + return nil, err + } + err = json.NewDecoder(resp.body).Decode(&p) + ensureReaderClosed(resp) + return &p, err +} diff --git a/vendor/src/github.com/docker/engine-api/client/plugin_install.go b/vendor/src/github.com/docker/engine-api/client/plugin_install.go new file mode 100644 index 0000000000..9005e81a56 --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/plugin_install.go @@ -0,0 +1,54 @@ +// +build experimental + +package client + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "net/url" + "strings" + + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +// PluginInstall installs a plugin +func (cli *Client) PluginInstall(ctx context.Context, name, registryAuth string, acceptAllPermissions, noEnable bool, in io.ReadCloser, out io.Writer) error { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + resp, err := cli.post(ctx, "/plugins/pull", url.Values{"name": []string{name}}, nil, headers) + if err != nil { + ensureReaderClosed(resp) + return err + } + var privileges types.PluginPrivileges + if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { + return err + } + ensureReaderClosed(resp) + + if !acceptAllPermissions && len(privileges) > 0 { + + fmt.Fprintf(out, "Plugin %q requested the following privileges:\n", name) + for _, privilege := range privileges { + fmt.Fprintf(out, " - %s: %v\n", privilege.Name, privilege.Value) + } + + fmt.Fprint(out, "Do you grant the above permissions? [y/N] ") + reader := bufio.NewReader(in) + line, _, err := reader.ReadLine() + if err != nil { + return err + } + if strings.ToLower(string(line)) != "y" { + resp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) + ensureReaderClosed(resp) + return pluginPermissionDenied{name} + } + } + if noEnable { + return nil + } + return cli.PluginEnable(ctx, name) +} diff --git a/vendor/src/github.com/docker/engine-api/client/plugin_list.go b/vendor/src/github.com/docker/engine-api/client/plugin_list.go new file mode 100644 index 0000000000..7f2e2f21f3 --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/plugin_list.go @@ -0,0 +1,23 @@ +// +build experimental + +package client + +import ( + "encoding/json" + + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +// PluginList returns the installed plugins +func (cli *Client) PluginList(ctx context.Context) (types.PluginsListResponse, error) { + var plugins types.PluginsListResponse + resp, err := cli.get(ctx, "/plugins", nil, nil) + if err != nil { + return plugins, err + } + + err = json.NewDecoder(resp.body).Decode(&plugins) + ensureReaderClosed(resp) + return plugins, err +} diff --git a/vendor/src/github.com/docker/engine-api/client/plugin_push.go b/vendor/src/github.com/docker/engine-api/client/plugin_push.go new file mode 100644 index 0000000000..3afea5ed79 --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/plugin_push.go @@ -0,0 +1,15 @@ +// +build experimental + +package client + +import ( + "golang.org/x/net/context" +) + +// PluginPush pushes a plugin to a registry +func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) error { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, headers) + ensureReaderClosed(resp) + return err +} diff --git a/vendor/src/github.com/docker/engine-api/client/plugin_remove.go b/vendor/src/github.com/docker/engine-api/client/plugin_remove.go new file mode 100644 index 0000000000..baf666556b --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/plugin_remove.go @@ -0,0 +1,14 @@ +// +build experimental + +package client + +import ( + "golang.org/x/net/context" +) + +// PluginRemove removes a plugin +func (cli *Client) PluginRemove(ctx context.Context, name string) error { + resp, err := cli.delete(ctx, "/plugins/"+name, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/vendor/src/github.com/docker/engine-api/client/plugin_set.go b/vendor/src/github.com/docker/engine-api/client/plugin_set.go new file mode 100644 index 0000000000..fb40f38b22 --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/client/plugin_set.go @@ -0,0 +1,14 @@ +// +build experimental + +package client + +import ( + "golang.org/x/net/context" +) + +// PluginSet modifies settings for an existing plugin +func (cli *Client) PluginSet(ctx context.Context, name string, args []string) error { + resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, args, nil) + ensureReaderClosed(resp) + return err +} diff --git a/vendor/src/github.com/docker/engine-api/types/plugin.go b/vendor/src/github.com/docker/engine-api/types/plugin.go new file mode 100644 index 0000000000..f9df9efe05 --- /dev/null +++ b/vendor/src/github.com/docker/engine-api/types/plugin.go @@ -0,0 +1,160 @@ +// +build experimental + +package types + +import ( + "encoding/json" + "fmt" +) + +// PluginConfig represents the values of settings potentially modifiable by a user +type PluginConfig struct { + Mounts []PluginMount + Env []string + Args []string + Devices []PluginDevice +} + +// Plugin represents a Docker plugin for the remote API +type Plugin struct { + ID string `json:"Id,omitempty"` + Name string + Tag string + Active bool + Config PluginConfig + Manifest PluginManifest +} + +// PluginsListResponse contains the response for the remote API +type PluginsListResponse []*Plugin + +const ( + authzDriver = "AuthzDriver" + graphDriver = "GraphDriver" + ipamDriver = "IpamDriver" + networkDriver = "NetworkDriver" + volumeDriver = "VolumeDriver" +) + +// PluginInterfaceType represents a type that a plugin implements. +type PluginInterfaceType struct { + Prefix string // This is always "docker" + Capability string // Capability should be validated against the above list. + Version string // Plugin API version. Depends on the capability +} + +// UnmarshalJSON implements json.Unmarshaler for PluginInterfaceType +func (t *PluginInterfaceType) UnmarshalJSON(p []byte) error { + versionIndex := len(p) + prefixIndex := 0 + if len(p) < 2 || p[0] != '"' || p[len(p)-1] != '"' { + return fmt.Errorf("%q is not a plugin interface type", p) + } + p = p[1 : len(p)-1] +loop: + for i, b := range p { + switch b { + case '.': + prefixIndex = i + case '/': + versionIndex = i + break loop + } + } + t.Prefix = string(p[:prefixIndex]) + t.Capability = string(p[prefixIndex+1 : versionIndex]) + if versionIndex < len(p) { + t.Version = string(p[versionIndex+1:]) + } + return nil +} + +// MarshalJSON implements json.Marshaler for PluginInterfaceType +func (t *PluginInterfaceType) MarshalJSON() ([]byte, error) { + return json.Marshal(t.String()) +} + +// String implements fmt.Stringer for PluginInterfaceType +func (t PluginInterfaceType) String() string { + return fmt.Sprintf("%s.%s/%s", t.Prefix, t.Capability, t.Version) +} + +// PluginInterface describes the interface between Docker and plugin +type PluginInterface struct { + Types []PluginInterfaceType + Socket string +} + +// PluginSetting is to be embedded in other structs, if they are supposed to be +// modifiable by the user. +type PluginSetting struct { + Name string + Description string + Settable []string +} + +// PluginNetwork represents the network configuration for a plugin +type PluginNetwork struct { + Type string +} + +// PluginMount represents the mount configuration for a plugin +type PluginMount struct { + PluginSetting + Source *string + Destination string + Type string + Options []string +} + +// PluginEnv represents an environment variable for a plugin +type PluginEnv struct { + PluginSetting + Value *string +} + +// PluginArgs represents the command line arguments for a plugin +type PluginArgs struct { + PluginSetting + Value []string +} + +// PluginDevice represents a device for a plugin +type PluginDevice struct { + PluginSetting + Path *string +} + +// PluginUser represents the user for the plugin's process +type PluginUser struct { + UID uint32 `json:"Uid,omitempty"` + GID uint32 `json:"Gid,omitempty"` +} + +// PluginManifest represents the manifest of a plugin +type PluginManifest struct { + ManifestVersion string + Description string + Documentation string + Interface PluginInterface + Entrypoint []string + Workdir string + User PluginUser `json:",omitempty"` + Network PluginNetwork + Capabilities []string + Mounts []PluginMount + Devices []PluginDevice + Env []PluginEnv + Args PluginArgs +} + +// PluginPrivilege describes a permission the user has to accept +// upon installing a plugin. +type PluginPrivilege struct { + Name string + Description string + Value []string +} + +// PluginPrivileges is a list of PluginPrivilege +type PluginPrivileges []PluginPrivilege From f37117045c5398fd3dca8016ea8ca0cb47e7312b Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Mon, 16 May 2016 11:50:55 -0400 Subject: [PATCH 2/3] plugins: experimental support for new plugin management This patch introduces a new experimental engine-level plugin management with a new API and command line. Plugins can be distributed via a Docker registry, and their lifecycle is managed by the engine. This makes plugins a first-class construct. For more background, have a look at issue #20363. Documentation is in a separate commit. If you want to understand how the new plugin system works, you can start by reading the documentation. Note: backwards compatibility with existing plugins is maintained, albeit they won't benefit from the advantages of the new system. Signed-off-by: Tibor Vass Signed-off-by: Anusha Ragunathan --- api/client/plugin/cmd.go | 12 + api/client/plugin/cmd_experimental.go | 36 ++ api/client/plugin/disable.go | 23 ++ api/client/plugin/enable.go | 23 ++ api/client/plugin/inspect.go | 39 ++ api/client/plugin/install.go | 51 +++ api/client/plugin/list.go | 44 ++ api/client/plugin/push.go | 50 +++ api/client/plugin/remove.go | 43 ++ api/client/plugin/set.go | 28 ++ api/server/router/plugin/backend.go | 21 + api/server/router/plugin/plugin.go | 23 ++ .../router/plugin/plugin_experimental.go | 20 + api/server/router/plugin/plugin_regular.go | 9 + api/server/router/plugin/plugin_routes.go | 103 +++++ cli/cobraadaptor/adaptor.go | 2 + cli/error.go | 21 + cmd/dockerd/daemon.go | 5 + cmd/dockerd/daemon_linux.go | 6 +- cmd/dockerd/daemon_no_plugin_support.go | 13 + cmd/dockerd/daemon_plugin_support.go | 14 + cmd/dockerd/routes.go | 9 + cmd/dockerd/routes_experimental.go | 13 + daemon/daemon.go | 8 +- daemon/graphdriver/plugin.go | 2 +- distribution/pull.go | 6 +- ...cker_cli_external_graphdriver_unix_test.go | 2 +- pkg/authorization/authz_unix_test.go | 5 +- pkg/authorization/plugin.go | 13 +- pkg/plugins/client.go | 12 +- pkg/plugins/client_test.go | 4 +- pkg/plugins/discovery.go | 6 +- pkg/plugins/discovery_test.go | 4 +- pkg/plugins/discovery_unix_test.go | 2 +- pkg/plugins/plugins.go | 32 +- plugin/backend.go | 139 +++++++ plugin/distribution/pull.go | 208 ++++++++++ plugin/distribution/push.go | 134 ++++++ plugin/distribution/types.go | 16 + plugin/interface.go | 9 + plugin/legacy.go | 23 ++ plugin/manager.go | 384 ++++++++++++++++++ plugin/manager_linux.go | 126 ++++++ plugin/manager_windows.go | 21 + volume/drivers/extpoint.go | 23 +- volume/drivers/proxy_test.go | 2 +- volume/store/store.go | 9 +- 47 files changed, 1740 insertions(+), 58 deletions(-) create mode 100644 api/client/plugin/cmd.go create mode 100644 api/client/plugin/cmd_experimental.go create mode 100644 api/client/plugin/disable.go create mode 100644 api/client/plugin/enable.go create mode 100644 api/client/plugin/inspect.go create mode 100644 api/client/plugin/install.go create mode 100644 api/client/plugin/list.go create mode 100644 api/client/plugin/push.go create mode 100644 api/client/plugin/remove.go create mode 100644 api/client/plugin/set.go create mode 100644 api/server/router/plugin/backend.go create mode 100644 api/server/router/plugin/plugin.go create mode 100644 api/server/router/plugin/plugin_experimental.go create mode 100644 api/server/router/plugin/plugin_regular.go create mode 100644 api/server/router/plugin/plugin_routes.go create mode 100644 cli/error.go create mode 100644 cmd/dockerd/daemon_no_plugin_support.go create mode 100644 cmd/dockerd/daemon_plugin_support.go create mode 100644 cmd/dockerd/routes.go create mode 100644 cmd/dockerd/routes_experimental.go create mode 100644 plugin/backend.go create mode 100644 plugin/distribution/pull.go create mode 100644 plugin/distribution/push.go create mode 100644 plugin/distribution/types.go create mode 100644 plugin/interface.go create mode 100644 plugin/legacy.go create mode 100644 plugin/manager.go create mode 100644 plugin/manager_linux.go create mode 100644 plugin/manager_windows.go 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 From e79873c27c2b3f404db02682bb4f11b5a046602e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 9 Jun 2016 23:57:15 +0200 Subject: [PATCH 3/3] docker plugin commandline reference Signed-off-by: Sebastiaan van Stijn --- docs/reference/commandline/plugin_disable.md | 52 +++++++ docs/reference/commandline/plugin_enable.md | 52 +++++++ docs/reference/commandline/plugin_inspect.md | 135 +++++++++++++++++++ docs/reference/commandline/plugin_install.md | 51 +++++++ docs/reference/commandline/plugin_ls.md | 40 ++++++ docs/reference/commandline/plugin_rm.md | 41 ++++++ 6 files changed, 371 insertions(+) create mode 100644 docs/reference/commandline/plugin_disable.md create mode 100644 docs/reference/commandline/plugin_enable.md create mode 100644 docs/reference/commandline/plugin_inspect.md create mode 100644 docs/reference/commandline/plugin_install.md create mode 100644 docs/reference/commandline/plugin_ls.md create mode 100644 docs/reference/commandline/plugin_rm.md diff --git a/docs/reference/commandline/plugin_disable.md b/docs/reference/commandline/plugin_disable.md new file mode 100644 index 0000000000..44f78af8c4 --- /dev/null +++ b/docs/reference/commandline/plugin_disable.md @@ -0,0 +1,52 @@ + + +# plugin disable (experimental) + + Usage: docker plugin disable PLUGIN + + Disable a plugin + + --help Print usage + +Disables a plugin. The plugin must be installed before it can be disabled, +see [`docker plugin install`](plugin_install.md). + + +The following example shows that the `no-remove` plugin is currently installed +and active: + +```bash +$ docker plugin ls +NAME TAG ACTIVE +tiborvass/no-remove latest true +``` +To disable the plugin, use the following command: + +```bash +$ docker plugin disable tiborvass/no-remove:latest +``` + +After the plugin is disabled, it appears as "inactive" in the list of plugins: + +```bash +$ docker plugin ls +NAME VERSION ACTIVE +tiborvass/no-remove latest false +``` + +## Related information + +* [plugin ls](plugin_ls.md) +* [plugin enable](plugin_enable.md) +* [plugin inspect](plugin_inspect.md) +* [plugin install](plugin_install.md) +* [plugin rm](plugin_rm.md) diff --git a/docs/reference/commandline/plugin_enable.md b/docs/reference/commandline/plugin_enable.md new file mode 100644 index 0000000000..2bd577fd04 --- /dev/null +++ b/docs/reference/commandline/plugin_enable.md @@ -0,0 +1,52 @@ + + +# plugin enable (experimental) + + Usage: docker plugin enable PLUGIN + + Enable a plugin + + --help Print usage + +Enables a plugin. The plugin must be installed before it can be enabled, +see [`docker plugin install`](plugin_install.md). + + +The following example shows that the `no-remove` plugin is currently installed, +but disabled ("inactive"): + +```bash +$ docker plugin ls +NAME VERSION ACTIVE +tiborvass/no-remove latest false +``` +To enable the plugin, use the following command: + +```bash +$ docker plugin enable tiborvass/no-remove:latest +``` + +After the plugin is enabled, it appears as "active" in the list of plugins: + +```bash +$ docker plugin ls +NAME VERSION ACTIVE +tiborvass/no-remove latest true +``` + +## Related information + +* [plugin ls](plugin_ls.md) +* [plugin disable](plugin_disable.md) +* [plugin inspect](plugin_inspect.md) +* [plugin install](plugin_install.md) +* [plugin rm](plugin_rm.md) diff --git a/docs/reference/commandline/plugin_inspect.md b/docs/reference/commandline/plugin_inspect.md new file mode 100644 index 0000000000..e0282ea294 --- /dev/null +++ b/docs/reference/commandline/plugin_inspect.md @@ -0,0 +1,135 @@ + + +# plugin inspect (experimental) + + Usage: docker plugin inspect PLUGIN + + Return low-level information about a plugin + + --help Print usage + + +Returns information about a plugin. By default, this command renders all results +in a JSON array. + +Example output: + +```bash +$ docker plugin inspect tiborvass/no-remove:latest +``` +```JSON +{ + "Manifest": { + "ManifestVersion": "", + "Description": "A test plugin for Docker", + "Documentation": "https://docs.docker.com/engine/extend/plugins/", + "Entrypoint": [ + "plugin-no-remove", + "/data" + ], + "Interface": { + "Types": [ + "docker.volumedriver/1.0" + ], + "Socket": "plugins.sock" + }, + "Network": { + "Type": "host" + }, + "Capabilities": null, + "Mounts": [ + { + "Name": "", + "Description": "", + "Settable": false, + "Source": "/data", + "Destination": "/data", + "Type": "bind", + "Options": [ + "shared", + "rbind" + ] + }, + { + "Name": "", + "Description": "", + "Settable": false, + "Source": null, + "Destination": "/foobar", + "Type": "tmpfs", + "Options": null + } + ], + "Devices": [ + { + "Name": "device", + "Description": "a host device to mount", + "Settable": false, + "Path": null + } + ], + "Env": [ + { + "Name": "DEBUG", + "Description": "If set, prints debug messages", + "Settable": false, + "Value": null + } + ], + "Args": [ + { + "Name": "arg1", + "Description": "a command line argument", + "Settable": false, + "Value": null + } + ] + }, + "Config": { + "Mounts": [ + { + "Source": "/data", + "Destination": "/data", + "Type": "bind", + "Options": [ + "shared", + "rbind" + ] + }, + { + "Source": null, + "Destination": "/foobar", + "Type": "tmpfs", + "Options": null + } + ], + "Env": [], + "Args": [], + "Devices": null + }, + "Active": true, + "Name": "tiborvass/no-remove", + "Tag": "latest", + "ID": "ac9d36b664921d61813254f7e9946f10e3cadbb676346539f1705fcaf039c01f" +} +``` +(output formatted for readability) + + + +## Related information + +* [plugin ls](plugin_ls.md) +* [plugin enable](plugin_enable.md) +* [plugin disable](plugin_disable.md) +* [plugin install](plugin_install.md) +* [plugin rm](plugin_rm.md) diff --git a/docs/reference/commandline/plugin_install.md b/docs/reference/commandline/plugin_install.md new file mode 100644 index 0000000000..276af5a09f --- /dev/null +++ b/docs/reference/commandline/plugin_install.md @@ -0,0 +1,51 @@ + + +# plugin install (experimental) + + Usage: docker plugin install PLUGIN + + Install a plugin + + --help Print usage + +Installs and enables a plugin. Docker looks first for the plugin on your Docker +host. If the plugin does not exist locally, then the plugin is pulled from +Docker Hub. + + +The following example installs `no-remove` plugin. Install consists of pulling the +plugin from Docker Hub, prompting the user to accept the list of privileges that +the plugin needs and enabling the plugin. + +```bash +$ docker plugin install tiborvass/no-remove +Plugin "tiborvass/no-remove:latest" requested the following privileges: + - Networking: host + - Mounting host path: /data +Do you grant the above permissions? [y/N] y +``` + +After the plugin is installed, it appears in the list of plugins: + +```bash +$ docker plugin ls +NAME VERSION ACTIVE +tiborvass/no-remove latest true +``` + +## Related information + +* [plugin ls](plugin_ls.md) +* [plugin enable](plugin_enable.md) +* [plugin disable](plugin_disable.md) +* [plugin inspect](plugin_inspect.md) +* [plugin rm](plugin_rm.md) diff --git a/docs/reference/commandline/plugin_ls.md b/docs/reference/commandline/plugin_ls.md new file mode 100644 index 0000000000..ea36368ed9 --- /dev/null +++ b/docs/reference/commandline/plugin_ls.md @@ -0,0 +1,40 @@ + + +# plugin ls (experimental) + + Usage: docker plugin ls + + List plugins + + --help Print usage + + Aliases: + ls, list + +Lists all the plugins that are currently installed. You can install plugins +using the [`docker plugin install`](plugin_install.md) command. + +Example output: + +```bash +$ docker plugin ls +NAME VERSION ACTIVE +tiborvass/no-remove latest true +``` + +## Related information + +* [plugin enable](plugin_enable.md) +* [plugin disable](plugin_disable.md) +* [plugin inspect](plugin_inspect.md) +* [plugin install](plugin_install.md) +* [plugin rm](plugin_rm.md) diff --git a/docs/reference/commandline/plugin_rm.md b/docs/reference/commandline/plugin_rm.md new file mode 100644 index 0000000000..a5dcf9e7cc --- /dev/null +++ b/docs/reference/commandline/plugin_rm.md @@ -0,0 +1,41 @@ + + +# plugin rm (experimental) + + Usage: docker plugin rm PLUGIN + + Remove a plugin + + --help Print usage + + Aliases: + rm, remove + +Removes a plugin. You cannot remove a plugin if it is active, you must disable +a plugin using the [`docker plugin disable`](plugin_disable.md) before removing +it. + +The following example disables and removes the `no-remove:latest` plugin; + +```bash +$ docker plugin disable tiborvass/no-remove:latest +$ docker plugin rm tiborvass/no-remove:latest +no-remove:latest +``` + +## Related information + +* [plugin ls](plugin_ls.md) +* [plugin enable](plugin_enable.md) +* [plugin disable](plugin_disable.md) +* [plugin inspect](plugin_inspect.md) +* [plugin install](plugin_install.md)