Add plugin create functionality.
Signed-off-by: Anusha Ragunathan <anusha@docker.com>
This commit is contained in:
parent
a70b1d74a0
commit
5b6e1bc9e5
19 changed files with 345 additions and 0 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"])
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|||
newRemoveCommand(dockerCli),
|
||||
newSetCommand(dockerCli),
|
||||
newPushCommand(dockerCli),
|
||||
newCreateCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
|
125
cli/command/plugin/create.go
Normal file
125
cli/command/plugin/create.go
Normal 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
|
||||
}
|
|
@ -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
26
client/plugin_create.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
57
docs/reference/commandline/plugin_create.md
Normal file
57
docs/reference/commandline/plugin_create.md
Normal 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)
|
|
@ -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)
|
||||
|
|
|
@ -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
1
docs/reference/commandline/plugin_inspect.md
Executable file → Normal 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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue