plugins: experimental support for new plugin management

This patch introduces a new experimental engine-level plugin management
with a new API and command line. Plugins can be distributed via a Docker
registry, and their lifecycle is managed by the engine.
This makes plugins a first-class construct.

For more background, have a look at issue #20363.

Documentation is in a separate commit. If you want to understand how the
new plugin system works, you can start by reading the documentation.

Note: backwards compatibility with existing plugins is maintained,
albeit they won't benefit from the advantages of the new system.

Signed-off-by: Tibor Vass <tibor@docker.com>
Signed-off-by: Anusha Ragunathan <anusha@docker.com>
This commit is contained in:
Tibor Vass 2016-05-16 11:50:55 -04:00
parent e5b7d36e99
commit f37117045c
47 changed files with 1740 additions and 58 deletions

12
api/client/plugin/cmd.go Normal file
View File

@ -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) {
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

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

44
api/client/plugin/list.go Normal file
View File

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

50
api/client/plugin/push.go Normal file
View File

@ -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)
}

View File

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

28
api/client/plugin/set.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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{}

View File

@ -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)
}

View File

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

21
cli/error.go Normal file
View File

@ -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()
}

View File

@ -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...)
}

View File

@ -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() {

View File

@ -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
}

View File

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

9
cmd/dockerd/routes.go Normal file
View File

@ -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
}

View File

@ -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()))
}

View File

@ -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

View File

@ -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) {

View File

@ -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")
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

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

139
plugin/backend.go Normal file
View File

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

208
plugin/distribution/pull.go Normal file
View File

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

134
plugin/distribution/push.go Normal file
View File

@ -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))
}

View File

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

9
plugin/interface.go Normal file
View File

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

23
plugin/legacy.go Normal file
View File

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

384
plugin/manager.go Normal file
View File

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

126
plugin/manager_linux.go Normal file
View File

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

21
plugin/manager_windows.go Normal file
View File

@ -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")
}

View File

@ -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

View File

@ -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)
}

View File

@ -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