From 5b6e1bc9e551ba56b44c3f409fd5d48f8a54ce1d Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Tue, 4 Oct 2016 12:01:19 -0700 Subject: [PATCH] Add plugin create functionality. Signed-off-by: Anusha Ragunathan --- api/server/router/plugin/backend.go | 3 + api/server/router/plugin/plugin.go | 1 + api/server/router/plugin/plugin_routes.go | 15 +++ api/swagger.yaml | 27 ++++ api/types/client.go | 5 + cli/command/plugin/cmd.go | 1 + cli/command/plugin/create.go | 125 ++++++++++++++++++ client/interface_experimental.go | 3 + client/plugin_create.go | 26 ++++ docs/reference/api/docker_remote_api_v1.25.md | 38 ++++++ docs/reference/commandline/plugin_create.md | 57 ++++++++ docs/reference/commandline/plugin_disable.md | 1 + docs/reference/commandline/plugin_enable.md | 1 + docs/reference/commandline/plugin_inspect.md | 1 + docs/reference/commandline/plugin_install.md | 1 + docs/reference/commandline/plugin_ls.md | 1 + docs/reference/commandline/plugin_rm.md | 1 + docs/reference/commandline/plugin_set.md | 1 + plugin/backend.go | 37 ++++++ 19 files changed, 345 insertions(+) create mode 100644 cli/command/plugin/create.go create mode 100644 client/plugin_create.go create mode 100644 docs/reference/commandline/plugin_create.md mode change 100755 => 100644 docs/reference/commandline/plugin_inspect.md diff --git a/api/server/router/plugin/backend.go b/api/server/router/plugin/backend.go index 53be49b12a..acbb1e408c 100644 --- a/api/server/router/plugin/backend.go +++ b/api/server/router/plugin/backend.go @@ -1,9 +1,11 @@ package plugin import ( + "io" "net/http" enginetypes "github.com/docker/docker/api/types" + "golang.org/x/net/context" ) // Backend for Plugin @@ -16,4 +18,5 @@ type Backend interface { 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 + CreateFromContext(ctx context.Context, tarCtx io.Reader, options *enginetypes.PluginCreateOptions) error } diff --git a/api/server/router/plugin/plugin.go b/api/server/router/plugin/plugin.go index c10510a4db..15bdcb3cd5 100644 --- a/api/server/router/plugin/plugin.go +++ b/api/server/router/plugin/plugin.go @@ -32,5 +32,6 @@ func (r *pluginRouter) initRoutes() { router.NewPostRoute("/plugins/pull", r.pullPlugin), router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin), router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin), + router.NewPostRoute("/plugins/create", r.createPlugin), } } diff --git a/api/server/router/plugin/plugin_routes.go b/api/server/router/plugin/plugin_routes.go index 2a62c2adc6..1af8dad6c6 100644 --- a/api/server/router/plugin/plugin_routes.go +++ b/api/server/router/plugin/plugin_routes.go @@ -40,6 +40,21 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r return httputils.WriteJSON(w, http.StatusOK, privileges) } +func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + options := &types.PluginCreateOptions{ + RepoName: r.FormValue("name")} + + if err := pr.backend.CreateFromContext(ctx, r.Body, options); err != nil { + return err + } + w.WriteHeader(http.StatusNoContent) + return nil +} + func (pr *pluginRouter) enablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { return pr.backend.Enable(vars["name"]) } diff --git a/api/swagger.yaml b/api/swagger.yaml index bd173b3000..740b994c8e 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -6089,6 +6089,33 @@ paths: type: "string" tags: - "Plugins" + /plugins/create: + post: + summary: "Create a plugin" + operationId: "PostPluginsCreate" + consumes: + - "application/x-tar" + responses: + 204: + description: "no error" + 500: + description: "server error" + schema: + $ref: "#/definitions/ErrorResponse" + parameters: + - name: "name" + in: "query" + description: "The name of the plugin. The `:latest` tag is optional, and is the default if omitted." + required: true + type: "string" + - name: "tarContext" + in: "body" + description: "Path to tar containing plugin rootfs and manifest" + schema: + type: "string" + format: "binary" + tags: + - "Plugins" /nodes: get: summary: "List nodes" diff --git a/api/types/client.go b/api/types/client.go index 8eaecf5c81..e22c5d4db4 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -338,3 +338,8 @@ type PluginInstallOptions struct { PrivilegeFunc RequestPrivilegeFunc AcceptPermissionsFunc func(PluginPrivileges) (bool, error) } + +// PluginCreateOptions hold all options to plugin create. +type PluginCreateOptions struct { + RepoName string +} diff --git a/cli/command/plugin/cmd.go b/cli/command/plugin/cmd.go index c78f43a8d4..2bb165db19 100644 --- a/cli/command/plugin/cmd.go +++ b/cli/command/plugin/cmd.go @@ -29,6 +29,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { newRemoveCommand(dockerCli), newSetCommand(dockerCli), newPushCommand(dockerCli), + newCreateCommand(dockerCli), ) return cmd } diff --git a/cli/command/plugin/create.go b/cli/command/plugin/create.go new file mode 100644 index 0000000000..3b18ed3750 --- /dev/null +++ b/cli/command/plugin/create.go @@ -0,0 +1,125 @@ +package plugin + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/reference" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +// validateTag checks if the given repoName can be resolved. +func validateTag(rawRepo string) error { + _, err := reference.ParseNamed(rawRepo) + + return err +} + +// validateManifest ensures that a valid manifest.json is available in the given path +func validateManifest(path string) error { + dt, err := os.Open(filepath.Join(path, "manifest.json")) + if err != nil { + return err + } + + m := types.PluginManifest{} + err = json.NewDecoder(dt).Decode(&m) + dt.Close() + + return err +} + +// validateContextDir validates the given dir and returns abs path on success. +func validateContextDir(contextDir string) (string, error) { + absContextDir, err := filepath.Abs(contextDir) + + stat, err := os.Lstat(absContextDir) + if err != nil { + return "", err + } + + if !stat.IsDir() { + return "", fmt.Errorf("context must be a directory") + } + + return absContextDir, nil +} + +type pluginCreateOptions struct { + repoName string + context string + compress bool +} + +func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + options := pluginCreateOptions{} + + cmd := &cobra.Command{ + Use: "create [OPTIONS] reponame[:tag] PATH-TO-ROOTFS (rootfs + manifest.json)", + Short: "Create a plugin from a rootfs and manifest", + Args: cli.RequiresMinArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + options.repoName = args[0] + options.context = args[1] + return runCreate(dockerCli, options) + }, + } + + flags := cmd.Flags() + + flags.BoolVar(&options.compress, "compress", false, "Compress the context using gzip") + + return cmd +} + +func runCreate(dockerCli *command.DockerCli, options pluginCreateOptions) error { + var ( + createCtx io.ReadCloser + err error + ) + + if err := validateTag(options.repoName); err != nil { + return err + } + + absContextDir, err := validateContextDir(options.context) + if err != nil { + return err + } + + if err := validateManifest(options.context); err != nil { + return err + } + + compression := archive.Uncompressed + if options.compress { + logrus.Debugf("compression enabled") + compression = archive.Gzip + } + + createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{ + Compression: compression, + }) + + if err != nil { + return err + } + + ctx := context.Background() + + createOptions := types.PluginCreateOptions{RepoName: options.repoName} + if err = dockerCli.Client().PluginCreate(ctx, createCtx, createOptions); err != nil { + return err + } + fmt.Fprintln(dockerCli.Out(), options.repoName) + return nil +} diff --git a/client/interface_experimental.go b/client/interface_experimental.go index 4f5cf853b8..709b5d8ffb 100644 --- a/client/interface_experimental.go +++ b/client/interface_experimental.go @@ -1,6 +1,8 @@ package client import ( + "io" + "github.com/docker/docker/api/types" "golang.org/x/net/context" ) @@ -27,4 +29,5 @@ type PluginAPIClient interface { PluginPush(ctx context.Context, name string, registryAuth string) error PluginSet(ctx context.Context, name string, args []string) error PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) + PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error } diff --git a/client/plugin_create.go b/client/plugin_create.go new file mode 100644 index 0000000000..a660ba5733 --- /dev/null +++ b/client/plugin_create.go @@ -0,0 +1,26 @@ +package client + +import ( + "io" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// PluginCreate creates a plugin +func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error { + headers := http.Header(make(map[string][]string)) + headers.Set("Content-Type", "application/tar") + + query := url.Values{} + query.Set("name", createOptions.RepoName) + + resp, err := cli.postRaw(ctx, "/plugins/create", query, createContext, headers) + if err != nil { + return err + } + ensureReaderClosed(resp) + return err +} diff --git a/docs/reference/api/docker_remote_api_v1.25.md b/docs/reference/api/docker_remote_api_v1.25.md index 544390c971..742443df7f 100644 --- a/docs/reference/api/docker_remote_api_v1.25.md +++ b/docs/reference/api/docker_remote_api_v1.25.md @@ -4401,6 +4401,44 @@ Content-Type: text/plain; charset=utf-8 - **404** - plugin not installed - **500** - plugin is active +### Create a plugin + +`POST /v1.25/plugins/create?name=(plugin name)` + +Create a plugin + +**Example request**: + +To create a plugin named `plugin` + +``` +POST /v1.25/plugins/create?name=plugin:latest HTTP/1.1 +Content-Type: application/x-tar + +{% raw %} +{{ TAR STREAM }} +{% endraw %} +``` + +The `:latest` tag is optional, and is used as default if omitted. + +**Example response**: + +``` +HTTP/1.1 204 No Content +Content-Length: 0 +Content-Type: text/plain; charset=utf-8 +``` + +**Query parameters**: + +- **name** - A name and optional tag to apply for the plugin in the `name:tag format`. If you omit the `tag` the default `:latest` value is assumed. + +**Status codes**: + +- **204** - no error +- **500** - server error + + +```markdown +Usage: docker plugin create [OPTIONS] reponame[:tag] PATH-TO-ROOTFS + +create a plugin from the given PATH-TO-ROOTFS, which contains the plugin's root filesystem and the manifest file, manifest.json + +Options: + --compress Compress the context using gzip + --help Print usage +``` + +Creates a plugin. Before creating the plugin, prepare the plugin's root filesystem as well as +the manifest.json (https://github.com/docker/docker/blob/master/docs/extend/manifest.md) + + +The following example shows how to create a sample `plugin`. + +```bash + +$ ls -ls /home/pluginDir + +4 -rw-r--r-- 1 root root 431 Nov 7 01:40 manifest.json +0 drwxr-xr-x 19 root root 420 Nov 7 01:40 rootfs + +$ docker plugin create plugin /home/pluginDir +plugin + +NAME TAG DESCRIPTION ENABLED +plugin latest A sample plugin for Docker true +``` + +The plugin can subsequently be enabled for local use or pushed to the public registry. + +## 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) +* [plugin rm](plugin_rm.md) +* [plugin set](plugin_set.md) diff --git a/docs/reference/commandline/plugin_disable.md b/docs/reference/commandline/plugin_disable.md index 580d4b93d0..515fdec5cd 100644 --- a/docs/reference/commandline/plugin_disable.md +++ b/docs/reference/commandline/plugin_disable.md @@ -55,6 +55,7 @@ tiborvass/no-remove latest A test plugin for Docker false ## Related information * [plugin ls](plugin_ls.md) +* [plugin create](plugin_create.md) * [plugin enable](plugin_enable.md) * [plugin inspect](plugin_inspect.md) * [plugin install](plugin_install.md) diff --git a/docs/reference/commandline/plugin_enable.md b/docs/reference/commandline/plugin_enable.md index edb2e0bf1f..d23a6661c6 100644 --- a/docs/reference/commandline/plugin_enable.md +++ b/docs/reference/commandline/plugin_enable.md @@ -54,6 +54,7 @@ tiborvass/no-remove latest A test plugin for Docker true ## Related information +* [plugin create](plugin_create.md) * [plugin ls](plugin_ls.md) * [plugin disable](plugin_disable.md) * [plugin inspect](plugin_inspect.md) diff --git a/docs/reference/commandline/plugin_inspect.md b/docs/reference/commandline/plugin_inspect.md old mode 100755 new mode 100644 index 3fa55405a1..4c81a65142 --- a/docs/reference/commandline/plugin_inspect.md +++ b/docs/reference/commandline/plugin_inspect.md @@ -154,6 +154,7 @@ $ docker plugin inspect -f '{{.Id}}' tiborvass/no-remove:latest ## Related information +* [plugin create](plugin_create.md) * [plugin ls](plugin_ls.md) * [plugin enable](plugin_enable.md) * [plugin disable](plugin_disable.md) diff --git a/docs/reference/commandline/plugin_install.md b/docs/reference/commandline/plugin_install.md index 524b4d8d5c..3ac26fd74d 100644 --- a/docs/reference/commandline/plugin_install.md +++ b/docs/reference/commandline/plugin_install.md @@ -59,6 +59,7 @@ tiborvass/no-remove latest A test plugin for Docker true ## Related information +* [plugin create](plugin_create.md) * [plugin ls](plugin_ls.md) * [plugin enable](plugin_enable.md) * [plugin disable](plugin_disable.md) diff --git a/docs/reference/commandline/plugin_ls.md b/docs/reference/commandline/plugin_ls.md index 12de328142..3afae4f2de 100644 --- a/docs/reference/commandline/plugin_ls.md +++ b/docs/reference/commandline/plugin_ls.md @@ -43,6 +43,7 @@ tiborvass/no-remove latest A test plugin for Docker true ## Related information +* [plugin create](plugin_create.md) * [plugin enable](plugin_enable.md) * [plugin disable](plugin_disable.md) * [plugin inspect](plugin_inspect.md) diff --git a/docs/reference/commandline/plugin_rm.md b/docs/reference/commandline/plugin_rm.md index 4a86bc5c2b..ff56ab8d57 100644 --- a/docs/reference/commandline/plugin_rm.md +++ b/docs/reference/commandline/plugin_rm.md @@ -46,6 +46,7 @@ tiborvass/no-remove ## Related information +* [plugin create](plugin_create.md) * [plugin ls](plugin_ls.md) * [plugin enable](plugin_enable.md) * [plugin disable](plugin_disable.md) diff --git a/docs/reference/commandline/plugin_set.md b/docs/reference/commandline/plugin_set.md index 8c9eb74e1c..e211b72608 100644 --- a/docs/reference/commandline/plugin_set.md +++ b/docs/reference/commandline/plugin_set.md @@ -43,6 +43,7 @@ $ docker plugin inspect -f {{.Config.Env}} tiborvass/no-remove ## Related information +* [plugin create](plugin_create.md) * [plugin ls](plugin_ls.md) * [plugin enable](plugin_enable.md) * [plugin disable](plugin_disable.md) diff --git a/plugin/backend.go b/plugin/backend.go index edc93a858c..f67a6f6890 100644 --- a/plugin/backend.go +++ b/plugin/backend.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "os" @@ -12,9 +13,11 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/plugin/distribution" "github.com/docker/docker/plugin/v2" + "golang.org/x/net/context" ) // Disable deactivates a plugin, which implies that they cannot be used by containers. @@ -174,3 +177,37 @@ func (pm *Manager) Set(name string, args []string) error { } return p.Set(args) } + +// CreateFromContext creates a plugin from the given pluginDir which contains +// both the rootfs and the manifest.json and a repoName with optional tag. +func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.Reader, options *types.PluginCreateOptions) error { + pluginID := stringid.GenerateNonCryptoID() + + pluginDir := filepath.Join(pm.libRoot, pluginID) + if err := os.MkdirAll(pluginDir, 0755); err != nil { + return err + } + + if err := chrootarchive.Untar(tarCtx, pluginDir, nil); err != nil { + return err + } + + repoName := options.RepoName + ref, err := distribution.GetRef(repoName) + if err != nil { + return err + } + name := ref.Name() + tag := distribution.GetTag(ref) + + p := v2.NewPlugin(name, pluginID, pm.runRoot, pm.libRoot, tag) + if err := p.InitPlugin(); err != nil { + return err + } + + pm.pluginStore.Add(p) + + pm.pluginEventLogger(p.GetID(), repoName, "create") + + return nil +}