Add plugin create functionality.

Signed-off-by: Anusha Ragunathan <anusha@docker.com>
This commit is contained in:
Anusha Ragunathan 2016-10-04 12:01:19 -07:00
parent a70b1d74a0
commit 5b6e1bc9e5
19 changed files with 345 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command {
newRemoveCommand(dockerCli),
newSetCommand(dockerCli),
newPushCommand(dockerCli),
newCreateCommand(dockerCli),
)
return cmd
}

View file

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

View file

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

26
client/plugin_create.go Normal file
View file

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

View file

@ -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
<!-- TODO Document "docker plugin push" endpoint once we have "plugin build"
### Push a plugin

View file

@ -0,0 +1,57 @@
---
title: "plugin create (experimental)"
description: "the plugin create command description and usage"
keywords: "plugin, create"
advisory: "experimental"
---
<!-- This file is maintained within the docker/docker Github
repository at https://github.com/docker/docker/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
```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)

View file

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

View file

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

1
docs/reference/commandline/plugin_inspect.md Executable file → Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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