Signed-off-by: Anusha Ragunathan <anusha@docker.com>
| ... | ... |
@@ -1,9 +1,11 @@ |
| 1 | 1 |
package plugin |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "io" |
|
| 4 | 5 |
"net/http" |
| 5 | 6 |
|
| 6 | 7 |
enginetypes "github.com/docker/docker/api/types" |
| 8 |
+ "golang.org/x/net/context" |
|
| 7 | 9 |
) |
| 8 | 10 |
|
| 9 | 11 |
// Backend for Plugin |
| ... | ... |
@@ -16,4 +18,5 @@ type Backend interface {
|
| 16 | 16 |
Set(name string, args []string) error |
| 17 | 17 |
Pull(name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error) |
| 18 | 18 |
Push(name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) error |
| 19 |
+ CreateFromContext(ctx context.Context, tarCtx io.Reader, options *enginetypes.PluginCreateOptions) error |
|
| 19 | 20 |
} |
| ... | ... |
@@ -32,5 +32,6 @@ func (r *pluginRouter) initRoutes() {
|
| 32 | 32 |
router.NewPostRoute("/plugins/pull", r.pullPlugin),
|
| 33 | 33 |
router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin),
|
| 34 | 34 |
router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin),
|
| 35 |
+ router.NewPostRoute("/plugins/create", r.createPlugin),
|
|
| 35 | 36 |
} |
| 36 | 37 |
} |
| ... | ... |
@@ -40,6 +40,21 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r |
| 40 | 40 |
return httputils.WriteJSON(w, http.StatusOK, privileges) |
| 41 | 41 |
} |
| 42 | 42 |
|
| 43 |
+func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 44 |
+ if err := httputils.ParseForm(r); err != nil {
|
|
| 45 |
+ return err |
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ options := &types.PluginCreateOptions{
|
|
| 49 |
+ RepoName: r.FormValue("name")}
|
|
| 50 |
+ |
|
| 51 |
+ if err := pr.backend.CreateFromContext(ctx, r.Body, options); err != nil {
|
|
| 52 |
+ return err |
|
| 53 |
+ } |
|
| 54 |
+ w.WriteHeader(http.StatusNoContent) |
|
| 55 |
+ return nil |
|
| 56 |
+} |
|
| 57 |
+ |
|
| 43 | 58 |
func (pr *pluginRouter) enablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 44 | 59 |
return pr.backend.Enable(vars["name"]) |
| 45 | 60 |
} |
| ... | ... |
@@ -6089,6 +6089,33 @@ paths: |
| 6089 | 6089 |
type: "string" |
| 6090 | 6090 |
tags: |
| 6091 | 6091 |
- "Plugins" |
| 6092 |
+ /plugins/create: |
|
| 6093 |
+ post: |
|
| 6094 |
+ summary: "Create a plugin" |
|
| 6095 |
+ operationId: "PostPluginsCreate" |
|
| 6096 |
+ consumes: |
|
| 6097 |
+ - "application/x-tar" |
|
| 6098 |
+ responses: |
|
| 6099 |
+ 204: |
|
| 6100 |
+ description: "no error" |
|
| 6101 |
+ 500: |
|
| 6102 |
+ description: "server error" |
|
| 6103 |
+ schema: |
|
| 6104 |
+ $ref: "#/definitions/ErrorResponse" |
|
| 6105 |
+ parameters: |
|
| 6106 |
+ - name: "name" |
|
| 6107 |
+ in: "query" |
|
| 6108 |
+ description: "The name of the plugin. The `:latest` tag is optional, and is the default if omitted." |
|
| 6109 |
+ required: true |
|
| 6110 |
+ type: "string" |
|
| 6111 |
+ - name: "tarContext" |
|
| 6112 |
+ in: "body" |
|
| 6113 |
+ description: "Path to tar containing plugin rootfs and manifest" |
|
| 6114 |
+ schema: |
|
| 6115 |
+ type: "string" |
|
| 6116 |
+ format: "binary" |
|
| 6117 |
+ tags: |
|
| 6118 |
+ - "Plugins" |
|
| 6092 | 6119 |
/nodes: |
| 6093 | 6120 |
get: |
| 6094 | 6121 |
summary: "List nodes" |
| ... | ... |
@@ -338,3 +338,8 @@ type PluginInstallOptions struct {
|
| 338 | 338 |
PrivilegeFunc RequestPrivilegeFunc |
| 339 | 339 |
AcceptPermissionsFunc func(PluginPrivileges) (bool, error) |
| 340 | 340 |
} |
| 341 |
+ |
|
| 342 |
+// PluginCreateOptions hold all options to plugin create. |
|
| 343 |
+type PluginCreateOptions struct {
|
|
| 344 |
+ RepoName string |
|
| 345 |
+} |
| 35 | 36 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,125 @@ |
| 0 |
+package plugin |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io" |
|
| 6 |
+ "os" |
|
| 7 |
+ "path/filepath" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/Sirupsen/logrus" |
|
| 10 |
+ "github.com/docker/docker/api/types" |
|
| 11 |
+ "github.com/docker/docker/cli" |
|
| 12 |
+ "github.com/docker/docker/cli/command" |
|
| 13 |
+ "github.com/docker/docker/pkg/archive" |
|
| 14 |
+ "github.com/docker/docker/reference" |
|
| 15 |
+ "github.com/spf13/cobra" |
|
| 16 |
+ "golang.org/x/net/context" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+// validateTag checks if the given repoName can be resolved. |
|
| 20 |
+func validateTag(rawRepo string) error {
|
|
| 21 |
+ _, err := reference.ParseNamed(rawRepo) |
|
| 22 |
+ |
|
| 23 |
+ return err |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+// validateManifest ensures that a valid manifest.json is available in the given path |
|
| 27 |
+func validateManifest(path string) error {
|
|
| 28 |
+ dt, err := os.Open(filepath.Join(path, "manifest.json")) |
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ return err |
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ m := types.PluginManifest{}
|
|
| 34 |
+ err = json.NewDecoder(dt).Decode(&m) |
|
| 35 |
+ dt.Close() |
|
| 36 |
+ |
|
| 37 |
+ return err |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// validateContextDir validates the given dir and returns abs path on success. |
|
| 41 |
+func validateContextDir(contextDir string) (string, error) {
|
|
| 42 |
+ absContextDir, err := filepath.Abs(contextDir) |
|
| 43 |
+ |
|
| 44 |
+ stat, err := os.Lstat(absContextDir) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ return "", err |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ if !stat.IsDir() {
|
|
| 50 |
+ return "", fmt.Errorf("context must be a directory")
|
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ return absContextDir, nil |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+type pluginCreateOptions struct {
|
|
| 57 |
+ repoName string |
|
| 58 |
+ context string |
|
| 59 |
+ compress bool |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|
| 63 |
+ options := pluginCreateOptions{}
|
|
| 64 |
+ |
|
| 65 |
+ cmd := &cobra.Command{
|
|
| 66 |
+ Use: "create [OPTIONS] reponame[:tag] PATH-TO-ROOTFS (rootfs + manifest.json)", |
|
| 67 |
+ Short: "Create a plugin from a rootfs and manifest", |
|
| 68 |
+ Args: cli.RequiresMinArgs(2), |
|
| 69 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 70 |
+ options.repoName = args[0] |
|
| 71 |
+ options.context = args[1] |
|
| 72 |
+ return runCreate(dockerCli, options) |
|
| 73 |
+ }, |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ flags := cmd.Flags() |
|
| 77 |
+ |
|
| 78 |
+ flags.BoolVar(&options.compress, "compress", false, "Compress the context using gzip") |
|
| 79 |
+ |
|
| 80 |
+ return cmd |
|
| 81 |
+} |
|
| 82 |
+ |
|
| 83 |
+func runCreate(dockerCli *command.DockerCli, options pluginCreateOptions) error {
|
|
| 84 |
+ var ( |
|
| 85 |
+ createCtx io.ReadCloser |
|
| 86 |
+ err error |
|
| 87 |
+ ) |
|
| 88 |
+ |
|
| 89 |
+ if err := validateTag(options.repoName); err != nil {
|
|
| 90 |
+ return err |
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ absContextDir, err := validateContextDir(options.context) |
|
| 94 |
+ if err != nil {
|
|
| 95 |
+ return err |
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ if err := validateManifest(options.context); err != nil {
|
|
| 99 |
+ return err |
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ compression := archive.Uncompressed |
|
| 103 |
+ if options.compress {
|
|
| 104 |
+ logrus.Debugf("compression enabled")
|
|
| 105 |
+ compression = archive.Gzip |
|
| 106 |
+ } |
|
| 107 |
+ |
|
| 108 |
+ createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{
|
|
| 109 |
+ Compression: compression, |
|
| 110 |
+ }) |
|
| 111 |
+ |
|
| 112 |
+ if err != nil {
|
|
| 113 |
+ return err |
|
| 114 |
+ } |
|
| 115 |
+ |
|
| 116 |
+ ctx := context.Background() |
|
| 117 |
+ |
|
| 118 |
+ createOptions := types.PluginCreateOptions{RepoName: options.repoName}
|
|
| 119 |
+ if err = dockerCli.Client().PluginCreate(ctx, createCtx, createOptions); err != nil {
|
|
| 120 |
+ return err |
|
| 121 |
+ } |
|
| 122 |
+ fmt.Fprintln(dockerCli.Out(), options.repoName) |
|
| 123 |
+ return nil |
|
| 124 |
+} |
| ... | ... |
@@ -1,6 +1,8 @@ |
| 1 | 1 |
package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "io" |
|
| 5 |
+ |
|
| 4 | 6 |
"github.com/docker/docker/api/types" |
| 5 | 7 |
"golang.org/x/net/context" |
| 6 | 8 |
) |
| ... | ... |
@@ -27,4 +29,5 @@ type PluginAPIClient interface {
|
| 27 | 27 |
PluginPush(ctx context.Context, name string, registryAuth string) error |
| 28 | 28 |
PluginSet(ctx context.Context, name string, args []string) error |
| 29 | 29 |
PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) |
| 30 |
+ PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error |
|
| 30 | 31 |
} |
| 31 | 32 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,26 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io" |
|
| 4 |
+ "net/http" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/types" |
|
| 8 |
+ "golang.org/x/net/context" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// PluginCreate creates a plugin |
|
| 12 |
+func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error {
|
|
| 13 |
+ headers := http.Header(make(map[string][]string)) |
|
| 14 |
+ headers.Set("Content-Type", "application/tar")
|
|
| 15 |
+ |
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ query.Set("name", createOptions.RepoName)
|
|
| 18 |
+ |
|
| 19 |
+ resp, err := cli.postRaw(ctx, "/plugins/create", query, createContext, headers) |
|
| 20 |
+ if err != nil {
|
|
| 21 |
+ return err |
|
| 22 |
+ } |
|
| 23 |
+ ensureReaderClosed(resp) |
|
| 24 |
+ return err |
|
| 25 |
+} |
| ... | ... |
@@ -4401,6 +4401,44 @@ Content-Type: text/plain; charset=utf-8 |
| 4401 | 4401 |
- **404** - plugin not installed |
| 4402 | 4402 |
- **500** - plugin is active |
| 4403 | 4403 |
|
| 4404 |
+### Create a plugin |
|
| 4405 |
+ |
|
| 4406 |
+`POST /v1.25/plugins/create?name=(plugin name)` |
|
| 4407 |
+ |
|
| 4408 |
+Create a plugin |
|
| 4409 |
+ |
|
| 4410 |
+**Example request**: |
|
| 4411 |
+ |
|
| 4412 |
+To create a plugin named `plugin` |
|
| 4413 |
+ |
|
| 4414 |
+``` |
|
| 4415 |
+POST /v1.25/plugins/create?name=plugin:latest HTTP/1.1 |
|
| 4416 |
+Content-Type: application/x-tar |
|
| 4417 |
+ |
|
| 4418 |
+{% raw %}
|
|
| 4419 |
+{{ TAR STREAM }}
|
|
| 4420 |
+{% endraw %}
|
|
| 4421 |
+``` |
|
| 4422 |
+ |
|
| 4423 |
+The `:latest` tag is optional, and is used as default if omitted. |
|
| 4424 |
+ |
|
| 4425 |
+**Example response**: |
|
| 4426 |
+ |
|
| 4427 |
+``` |
|
| 4428 |
+HTTP/1.1 204 No Content |
|
| 4429 |
+Content-Length: 0 |
|
| 4430 |
+Content-Type: text/plain; charset=utf-8 |
|
| 4431 |
+``` |
|
| 4432 |
+ |
|
| 4433 |
+**Query parameters**: |
|
| 4434 |
+ |
|
| 4435 |
+- **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. |
|
| 4436 |
+ |
|
| 4437 |
+**Status codes**: |
|
| 4438 |
+ |
|
| 4439 |
+- **204** - no error |
|
| 4440 |
+- **500** - server error |
|
| 4441 |
+ |
|
| 4404 | 4442 |
<!-- TODO Document "docker plugin push" endpoint once we have "plugin build" |
| 4405 | 4443 |
|
| 4406 | 4444 |
### Push a plugin |
| 4407 | 4445 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,57 @@ |
| 0 |
+--- |
|
| 1 |
+title: "plugin create (experimental)" |
|
| 2 |
+description: "the plugin create command description and usage" |
|
| 3 |
+keywords: "plugin, create" |
|
| 4 |
+advisory: "experimental" |
|
| 5 |
+--- |
|
| 6 |
+ |
|
| 7 |
+<!-- This file is maintained within the docker/docker Github |
|
| 8 |
+ repository at https://github.com/docker/docker/. Make all |
|
| 9 |
+ pull requests against that repo. If you see this file in |
|
| 10 |
+ another repository, consider it read-only there, as it will |
|
| 11 |
+ periodically be overwritten by the definitive file. Pull |
|
| 12 |
+ requests which include edits to this file in other repositories |
|
| 13 |
+ will be rejected. |
|
| 14 |
+--> |
|
| 15 |
+ |
|
| 16 |
+```markdown |
|
| 17 |
+Usage: docker plugin create [OPTIONS] reponame[:tag] PATH-TO-ROOTFS |
|
| 18 |
+ |
|
| 19 |
+create a plugin from the given PATH-TO-ROOTFS, which contains the plugin's root filesystem and the manifest file, manifest.json |
|
| 20 |
+ |
|
| 21 |
+Options: |
|
| 22 |
+ --compress Compress the context using gzip |
|
| 23 |
+ --help Print usage |
|
| 24 |
+``` |
|
| 25 |
+ |
|
| 26 |
+Creates a plugin. Before creating the plugin, prepare the plugin's root filesystem as well as |
|
| 27 |
+the manifest.json (https://github.com/docker/docker/blob/master/docs/extend/manifest.md) |
|
| 28 |
+ |
|
| 29 |
+ |
|
| 30 |
+The following example shows how to create a sample `plugin`. |
|
| 31 |
+ |
|
| 32 |
+```bash |
|
| 33 |
+ |
|
| 34 |
+$ ls -ls /home/pluginDir |
|
| 35 |
+ |
|
| 36 |
+4 -rw-r--r-- 1 root root 431 Nov 7 01:40 manifest.json |
|
| 37 |
+0 drwxr-xr-x 19 root root 420 Nov 7 01:40 rootfs |
|
| 38 |
+ |
|
| 39 |
+$ docker plugin create plugin /home/pluginDir |
|
| 40 |
+plugin |
|
| 41 |
+ |
|
| 42 |
+NAME TAG DESCRIPTION ENABLED |
|
| 43 |
+plugin latest A sample plugin for Docker true |
|
| 44 |
+``` |
|
| 45 |
+ |
|
| 46 |
+The plugin can subsequently be enabled for local use or pushed to the public registry. |
|
| 47 |
+ |
|
| 48 |
+## Related information |
|
| 49 |
+ |
|
| 50 |
+* [plugin ls](plugin_ls.md) |
|
| 51 |
+* [plugin enable](plugin_enable.md) |
|
| 52 |
+* [plugin disable](plugin_disable.md) |
|
| 53 |
+* [plugin inspect](plugin_inspect.md) |
|
| 54 |
+* [plugin install](plugin_install.md) |
|
| 55 |
+* [plugin rm](plugin_rm.md) |
|
| 56 |
+* [plugin set](plugin_set.md) |
| ... | ... |
@@ -55,6 +55,7 @@ tiborvass/no-remove latest A test plugin for Docker false |
| 55 | 55 |
## Related information |
| 56 | 56 |
|
| 57 | 57 |
* [plugin ls](plugin_ls.md) |
| 58 |
+* [plugin create](plugin_create.md) |
|
| 58 | 59 |
* [plugin enable](plugin_enable.md) |
| 59 | 60 |
* [plugin inspect](plugin_inspect.md) |
| 60 | 61 |
* [plugin install](plugin_install.md) |
| 60 | 61 |
old mode 100755 |
| 61 | 62 |
new mode 100644 |
| ... | ... |
@@ -154,6 +154,7 @@ $ docker plugin inspect -f '{{.Id}}' tiborvass/no-remove:latest
|
| 154 | 154 |
|
| 155 | 155 |
## Related information |
| 156 | 156 |
|
| 157 |
+* [plugin create](plugin_create.md) |
|
| 157 | 158 |
* [plugin ls](plugin_ls.md) |
| 158 | 159 |
* [plugin enable](plugin_enable.md) |
| 159 | 160 |
* [plugin disable](plugin_disable.md) |
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"bytes" |
| 5 | 5 |
"encoding/json" |
| 6 | 6 |
"fmt" |
| 7 |
+ "io" |
|
| 7 | 8 |
"io/ioutil" |
| 8 | 9 |
"net/http" |
| 9 | 10 |
"os" |
| ... | ... |
@@ -12,9 +13,11 @@ import ( |
| 12 | 12 |
"github.com/Sirupsen/logrus" |
| 13 | 13 |
"github.com/docker/docker/api/types" |
| 14 | 14 |
"github.com/docker/docker/pkg/archive" |
| 15 |
+ "github.com/docker/docker/pkg/chrootarchive" |
|
| 15 | 16 |
"github.com/docker/docker/pkg/stringid" |
| 16 | 17 |
"github.com/docker/docker/plugin/distribution" |
| 17 | 18 |
"github.com/docker/docker/plugin/v2" |
| 19 |
+ "golang.org/x/net/context" |
|
| 18 | 20 |
) |
| 19 | 21 |
|
| 20 | 22 |
// 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 {
|
| 174 | 174 |
} |
| 175 | 175 |
return p.Set(args) |
| 176 | 176 |
} |
| 177 |
+ |
|
| 178 |
+// CreateFromContext creates a plugin from the given pluginDir which contains |
|
| 179 |
+// both the rootfs and the manifest.json and a repoName with optional tag. |
|
| 180 |
+func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.Reader, options *types.PluginCreateOptions) error {
|
|
| 181 |
+ pluginID := stringid.GenerateNonCryptoID() |
|
| 182 |
+ |
|
| 183 |
+ pluginDir := filepath.Join(pm.libRoot, pluginID) |
|
| 184 |
+ if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
|
| 185 |
+ return err |
|
| 186 |
+ } |
|
| 187 |
+ |
|
| 188 |
+ if err := chrootarchive.Untar(tarCtx, pluginDir, nil); err != nil {
|
|
| 189 |
+ return err |
|
| 190 |
+ } |
|
| 191 |
+ |
|
| 192 |
+ repoName := options.RepoName |
|
| 193 |
+ ref, err := distribution.GetRef(repoName) |
|
| 194 |
+ if err != nil {
|
|
| 195 |
+ return err |
|
| 196 |
+ } |
|
| 197 |
+ name := ref.Name() |
|
| 198 |
+ tag := distribution.GetTag(ref) |
|
| 199 |
+ |
|
| 200 |
+ p := v2.NewPlugin(name, pluginID, pm.runRoot, pm.libRoot, tag) |
|
| 201 |
+ if err := p.InitPlugin(); err != nil {
|
|
| 202 |
+ return err |
|
| 203 |
+ } |
|
| 204 |
+ |
|
| 205 |
+ pm.pluginStore.Add(p) |
|
| 206 |
+ |
|
| 207 |
+ pm.pluginEventLogger(p.GetID(), repoName, "create") |
|
| 208 |
+ |
|
| 209 |
+ return nil |
|
| 210 |
+} |