Move plugins to shared distribution stack with images.
Create immutable plugin config that matches schema2 requirements.
Ensure data being pushed is same as pulled/created.
Store distribution artifacts in a blobstore.
Run init layer setup for every plugin start.
Fix breakouts from unsafe file accesses.
Add support for `docker plugin install --alias`
Uses normalized references for default names to avoid collisions when using default hosts/tags.
Some refactoring of the plugin manager to support the change, like removing the singleton manager and adding manager config struct.
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Signed-off-by: Derek McGowan <derek@mcgstyle.net>
| ... | ... |
@@ -5,6 +5,7 @@ import ( |
| 5 | 5 |
"net/http" |
| 6 | 6 |
|
| 7 | 7 |
enginetypes "github.com/docker/docker/api/types" |
| 8 |
+ "github.com/docker/docker/reference" |
|
| 8 | 9 |
"golang.org/x/net/context" |
| 9 | 10 |
) |
| 10 | 11 |
|
| ... | ... |
@@ -13,11 +14,11 @@ type Backend interface {
|
| 13 | 13 |
Disable(name string, config *enginetypes.PluginDisableConfig) error |
| 14 | 14 |
Enable(name string, config *enginetypes.PluginEnableConfig) error |
| 15 | 15 |
List() ([]enginetypes.Plugin, error) |
| 16 |
- Inspect(name string) (enginetypes.Plugin, error) |
|
| 16 |
+ Inspect(name string) (*enginetypes.Plugin, error) |
|
| 17 | 17 |
Remove(name string, config *enginetypes.PluginRmConfig) error |
| 18 | 18 |
Set(name string, args []string) error |
| 19 |
- Privileges(name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error) |
|
| 20 |
- Pull(name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges) error |
|
| 21 |
- Push(name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) error |
|
| 22 |
- CreateFromContext(ctx context.Context, tarCtx io.Reader, options *enginetypes.PluginCreateOptions) error |
|
| 19 |
+ Privileges(ctx context.Context, ref reference.Named, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error) |
|
| 20 |
+ Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error |
|
| 21 |
+ Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error |
|
| 22 |
+ CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error |
|
| 23 | 23 |
} |
| ... | ... |
@@ -30,8 +30,8 @@ func (r *pluginRouter) initRoutes() {
|
| 30 | 30 |
router.NewDeleteRoute("/plugins/{name:.*}", r.removePlugin),
|
| 31 | 31 |
router.NewPostRoute("/plugins/{name:.*}/enable", r.enablePlugin), // PATCH?
|
| 32 | 32 |
router.NewPostRoute("/plugins/{name:.*}/disable", r.disablePlugin),
|
| 33 |
- router.NewPostRoute("/plugins/pull", r.pullPlugin),
|
|
| 34 |
- router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin),
|
|
| 33 |
+ router.Cancellable(router.NewPostRoute("/plugins/pull", r.pullPlugin)),
|
|
| 34 |
+ router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin)),
|
|
| 35 | 35 |
router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin),
|
| 36 | 36 |
router.NewPostRoute("/plugins/create", r.createPlugin),
|
| 37 | 37 |
} |
| ... | ... |
@@ -7,8 +7,13 @@ import ( |
| 7 | 7 |
"strconv" |
| 8 | 8 |
"strings" |
| 9 | 9 |
|
| 10 |
+ distreference "github.com/docker/distribution/reference" |
|
| 10 | 11 |
"github.com/docker/docker/api/server/httputils" |
| 11 | 12 |
"github.com/docker/docker/api/types" |
| 13 |
+ "github.com/docker/docker/pkg/ioutils" |
|
| 14 |
+ "github.com/docker/docker/pkg/streamformatter" |
|
| 15 |
+ "github.com/docker/docker/reference" |
|
| 16 |
+ "github.com/pkg/errors" |
|
| 12 | 17 |
"golang.org/x/net/context" |
| 13 | 18 |
) |
| 14 | 19 |
|
| ... | ... |
@@ -34,6 +39,48 @@ func parseHeaders(headers http.Header) (map[string][]string, *types.AuthConfig) |
| 34 | 34 |
return metaHeaders, authConfig |
| 35 | 35 |
} |
| 36 | 36 |
|
| 37 |
+// parseRemoteRef parses the remote reference into a reference.Named |
|
| 38 |
+// returning the tag associated with the reference. In the case the |
|
| 39 |
+// given reference string includes both digest and tag, the returned |
|
| 40 |
+// reference will have the digest without the tag, but the tag will |
|
| 41 |
+// be returned. |
|
| 42 |
+func parseRemoteRef(remote string) (reference.Named, string, error) {
|
|
| 43 |
+ // Parse remote reference, supporting remotes with name and tag |
|
| 44 |
+ // NOTE: Using distribution reference to handle references |
|
| 45 |
+ // containing both a name and digest |
|
| 46 |
+ remoteRef, err := distreference.ParseNamed(remote) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return nil, "", err |
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ var tag string |
|
| 52 |
+ if t, ok := remoteRef.(distreference.Tagged); ok {
|
|
| 53 |
+ tag = t.Tag() |
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 56 |
+ // Convert distribution reference to docker reference |
|
| 57 |
+ // TODO: remove when docker reference changes reconciled upstream |
|
| 58 |
+ ref, err := reference.WithName(remoteRef.Name()) |
|
| 59 |
+ if err != nil {
|
|
| 60 |
+ return nil, "", err |
|
| 61 |
+ } |
|
| 62 |
+ if d, ok := remoteRef.(distreference.Digested); ok {
|
|
| 63 |
+ ref, err = reference.WithDigest(ref, d.Digest()) |
|
| 64 |
+ if err != nil {
|
|
| 65 |
+ return nil, "", err |
|
| 66 |
+ } |
|
| 67 |
+ } else if tag != "" {
|
|
| 68 |
+ ref, err = reference.WithTag(ref, tag) |
|
| 69 |
+ if err != nil {
|
|
| 70 |
+ return nil, "", err |
|
| 71 |
+ } |
|
| 72 |
+ } else {
|
|
| 73 |
+ ref = reference.WithDefaultTag(ref) |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ return ref, tag, nil |
|
| 77 |
+} |
|
| 78 |
+ |
|
| 37 | 79 |
func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 38 | 80 |
if err := httputils.ParseForm(r); err != nil {
|
| 39 | 81 |
return err |
| ... | ... |
@@ -41,7 +88,12 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter |
| 41 | 41 |
|
| 42 | 42 |
metaHeaders, authConfig := parseHeaders(r.Header) |
| 43 | 43 |
|
| 44 |
- privileges, err := pr.backend.Privileges(r.FormValue("name"), metaHeaders, authConfig)
|
|
| 44 |
+ ref, _, err := parseRemoteRef(r.FormValue("remote"))
|
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ return err |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ privileges, err := pr.backend.Privileges(ctx, ref, metaHeaders, authConfig) |
|
| 45 | 50 |
if err != nil {
|
| 46 | 51 |
return err |
| 47 | 52 |
} |
| ... | ... |
@@ -50,20 +102,66 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter |
| 50 | 50 |
|
| 51 | 51 |
func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 52 | 52 |
if err := httputils.ParseForm(r); err != nil {
|
| 53 |
- return err |
|
| 53 |
+ return errors.Wrap(err, "failed to parse form") |
|
| 54 | 54 |
} |
| 55 | 55 |
|
| 56 | 56 |
var privileges types.PluginPrivileges |
| 57 |
- if err := json.NewDecoder(r.Body).Decode(&privileges); err != nil {
|
|
| 58 |
- return err |
|
| 57 |
+ dec := json.NewDecoder(r.Body) |
|
| 58 |
+ if err := dec.Decode(&privileges); err != nil {
|
|
| 59 |
+ return errors.Wrap(err, "failed to parse privileges") |
|
| 60 |
+ } |
|
| 61 |
+ if dec.More() {
|
|
| 62 |
+ return errors.New("invalid privileges")
|
|
| 59 | 63 |
} |
| 60 | 64 |
|
| 61 | 65 |
metaHeaders, authConfig := parseHeaders(r.Header) |
| 62 | 66 |
|
| 63 |
- if err := pr.backend.Pull(r.FormValue("name"), metaHeaders, authConfig, privileges); err != nil {
|
|
| 67 |
+ ref, tag, err := parseRemoteRef(r.FormValue("remote"))
|
|
| 68 |
+ if err != nil {
|
|
| 64 | 69 |
return err |
| 65 | 70 |
} |
| 66 |
- w.WriteHeader(http.StatusCreated) |
|
| 71 |
+ |
|
| 72 |
+ name := r.FormValue("name")
|
|
| 73 |
+ if name == "" {
|
|
| 74 |
+ if _, ok := ref.(reference.Canonical); ok {
|
|
| 75 |
+ trimmed := reference.TrimNamed(ref) |
|
| 76 |
+ if tag != "" {
|
|
| 77 |
+ nt, err := reference.WithTag(trimmed, tag) |
|
| 78 |
+ if err != nil {
|
|
| 79 |
+ return err |
|
| 80 |
+ } |
|
| 81 |
+ name = nt.String() |
|
| 82 |
+ } else {
|
|
| 83 |
+ name = reference.WithDefaultTag(trimmed).String() |
|
| 84 |
+ } |
|
| 85 |
+ } else {
|
|
| 86 |
+ name = ref.String() |
|
| 87 |
+ } |
|
| 88 |
+ } else {
|
|
| 89 |
+ localRef, err := reference.ParseNamed(name) |
|
| 90 |
+ if err != nil {
|
|
| 91 |
+ return err |
|
| 92 |
+ } |
|
| 93 |
+ if _, ok := localRef.(reference.Canonical); ok {
|
|
| 94 |
+ return errors.New("cannot use digest in plugin tag")
|
|
| 95 |
+ } |
|
| 96 |
+ if distreference.IsNameOnly(localRef) {
|
|
| 97 |
+ // TODO: log change in name to out stream |
|
| 98 |
+ name = reference.WithDefaultTag(localRef).String() |
|
| 99 |
+ } |
|
| 100 |
+ } |
|
| 101 |
+ w.Header().Set("Docker-Plugin-Name", name)
|
|
| 102 |
+ |
|
| 103 |
+ w.Header().Set("Content-Type", "application/json")
|
|
| 104 |
+ output := ioutils.NewWriteFlusher(w) |
|
| 105 |
+ |
|
| 106 |
+ if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
|
|
| 107 |
+ if !output.Flushed() {
|
|
| 108 |
+ return err |
|
| 109 |
+ } |
|
| 110 |
+ output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err)) |
|
| 111 |
+ } |
|
| 112 |
+ |
|
| 67 | 113 |
return nil |
| 68 | 114 |
} |
| 69 | 115 |
|
| ... | ... |
@@ -125,12 +223,21 @@ func (pr *pluginRouter) removePlugin(ctx context.Context, w http.ResponseWriter, |
| 125 | 125 |
|
| 126 | 126 |
func (pr *pluginRouter) pushPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 127 | 127 |
if err := httputils.ParseForm(r); err != nil {
|
| 128 |
- return err |
|
| 128 |
+ return errors.Wrap(err, "failed to parse form") |
|
| 129 | 129 |
} |
| 130 | 130 |
|
| 131 | 131 |
metaHeaders, authConfig := parseHeaders(r.Header) |
| 132 | 132 |
|
| 133 |
- return pr.backend.Push(vars["name"], metaHeaders, authConfig) |
|
| 133 |
+ w.Header().Set("Content-Type", "application/json")
|
|
| 134 |
+ output := ioutils.NewWriteFlusher(w) |
|
| 135 |
+ |
|
| 136 |
+ if err := pr.backend.Push(ctx, vars["name"], metaHeaders, authConfig, output); err != nil {
|
|
| 137 |
+ if !output.Flushed() {
|
|
| 138 |
+ return err |
|
| 139 |
+ } |
|
| 140 |
+ output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err)) |
|
| 141 |
+ } |
|
| 142 |
+ return nil |
|
| 134 | 143 |
} |
| 135 | 144 |
|
| 136 | 145 |
func (pr *pluginRouter) setPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| ... | ... |
@@ -1347,16 +1347,13 @@ definitions: |
| 1347 | 1347 |
Plugin: |
| 1348 | 1348 |
description: "A plugin for the Engine API" |
| 1349 | 1349 |
type: "object" |
| 1350 |
- required: [Settings, Enabled, Config, Name, Tag] |
|
| 1350 |
+ required: [Settings, Enabled, Config, Name] |
|
| 1351 | 1351 |
properties: |
| 1352 | 1352 |
Id: |
| 1353 | 1353 |
type: "string" |
| 1354 | 1354 |
Name: |
| 1355 | 1355 |
type: "string" |
| 1356 | 1356 |
x-nullable: false |
| 1357 |
- Tag: |
|
| 1358 |
- type: "string" |
|
| 1359 |
- x-nullable: false |
|
| 1360 | 1357 |
Enabled: |
| 1361 | 1358 |
description: "True when the plugin is running. False when the plugin is not running, only installed." |
| 1362 | 1359 |
type: "boolean" |
| ... | ... |
@@ -1392,7 +1389,7 @@ definitions: |
| 1392 | 1392 |
- Documentation |
| 1393 | 1393 |
- Interface |
| 1394 | 1394 |
- Entrypoint |
| 1395 |
- - Workdir |
|
| 1395 |
+ - WorkDir |
|
| 1396 | 1396 |
- Network |
| 1397 | 1397 |
- Linux |
| 1398 | 1398 |
- PropagatedMount |
| ... | ... |
@@ -1423,7 +1420,7 @@ definitions: |
| 1423 | 1423 |
type: "array" |
| 1424 | 1424 |
items: |
| 1425 | 1425 |
type: "string" |
| 1426 |
- Workdir: |
|
| 1426 |
+ WorkDir: |
|
| 1427 | 1427 |
type: "string" |
| 1428 | 1428 |
x-nullable: false |
| 1429 | 1429 |
User: |
| ... | ... |
@@ -1490,6 +1487,15 @@ definitions: |
| 1490 | 1490 |
type: "array" |
| 1491 | 1491 |
items: |
| 1492 | 1492 |
type: "string" |
| 1493 |
+ rootfs: |
|
| 1494 |
+ type: "object" |
|
| 1495 |
+ properties: |
|
| 1496 |
+ type: |
|
| 1497 |
+ type: "string" |
|
| 1498 |
+ diff_ids: |
|
| 1499 |
+ type: "array" |
|
| 1500 |
+ items: |
|
| 1501 |
+ type: "string" |
|
| 1493 | 1502 |
example: |
| 1494 | 1503 |
Id: "5724e2c8652da337ab2eedd19fc6fc0ec908e4bd907c7421bf6a8dfc70c4c078" |
| 1495 | 1504 |
Name: "tiborvass/no-remove" |
| ... | ... |
@@ -1528,7 +1534,7 @@ definitions: |
| 1528 | 1528 |
Entrypoint: |
| 1529 | 1529 |
- "plugin-no-remove" |
| 1530 | 1530 |
- "/data" |
| 1531 |
- Workdir: "" |
|
| 1531 |
+ WorkDir: "" |
|
| 1532 | 1532 |
User: {}
|
| 1533 | 1533 |
Network: |
| 1534 | 1534 |
Type: "host" |
| ... | ... |
@@ -6397,7 +6403,7 @@ paths: |
| 6397 | 6397 |
Entrypoint: |
| 6398 | 6398 |
- "plugin-no-remove" |
| 6399 | 6399 |
- "/data" |
| 6400 |
- Workdir: "" |
|
| 6400 |
+ WorkDir: "" |
|
| 6401 | 6401 |
User: {}
|
| 6402 | 6402 |
Network: |
| 6403 | 6403 |
Type: "host" |
| ... | ... |
@@ -6503,14 +6509,22 @@ paths: |
| 6503 | 6503 |
schema: |
| 6504 | 6504 |
$ref: "#/definitions/ErrorResponse" |
| 6505 | 6505 |
parameters: |
| 6506 |
- - name: "name" |
|
| 6506 |
+ - name: "remote" |
|
| 6507 | 6507 |
in: "query" |
| 6508 | 6508 |
description: | |
| 6509 |
- The plugin to install. |
|
| 6509 |
+ Remote reference for plugin to install. |
|
| 6510 | 6510 |
|
| 6511 | 6511 |
The `:latest` tag is optional, and is used as the default if omitted. |
| 6512 | 6512 |
required: true |
| 6513 | 6513 |
type: "string" |
| 6514 |
+ - name: "name" |
|
| 6515 |
+ in: "query" |
|
| 6516 |
+ description: | |
|
| 6517 |
+ Local name for the pulled plugin. |
|
| 6518 |
+ |
|
| 6519 |
+ The `:latest` tag is optional, and is used as the default if omitted. |
|
| 6520 |
+ required: false |
|
| 6521 |
+ type: "string" |
|
| 6514 | 6522 |
- name: "X-Registry-Auth" |
| 6515 | 6523 |
in: "header" |
| 6516 | 6524 |
description: "A base64-encoded auth configuration to use when pulling a plugin from a registry. [See the authentication section for details.](#section/Authentication)" |
| ... | ... |
@@ -350,6 +350,7 @@ type PluginInstallOptions struct {
|
| 350 | 350 |
Disabled bool |
| 351 | 351 |
AcceptAllPermissions bool |
| 352 | 352 |
RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry |
| 353 |
+ RemoteRef string // RemoteRef is the plugin name on the registry |
|
| 353 | 354 |
PrivilegeFunc RequestPrivilegeFunc |
| 354 | 355 |
AcceptPermissionsFunc func(PluginPrivileges) (bool, error) |
| 355 | 356 |
Args []string |
| ... | ... |
@@ -25,10 +25,6 @@ type Plugin struct {
|
| 25 | 25 |
// settings |
| 26 | 26 |
// Required: true |
| 27 | 27 |
Settings PluginSettings `json:"Settings"` |
| 28 |
- |
|
| 29 |
- // tag |
|
| 30 |
- // Required: true |
|
| 31 |
- Tag string `json:"Tag"` |
|
| 32 | 28 |
} |
| 33 | 29 |
|
| 34 | 30 |
// PluginConfig The config of a plugin. |
| ... | ... |
@@ -78,9 +74,12 @@ type PluginConfig struct {
|
| 78 | 78 |
// user |
| 79 | 79 |
User PluginConfigUser `json:"User,omitempty"` |
| 80 | 80 |
|
| 81 |
- // workdir |
|
| 81 |
+ // work dir |
|
| 82 | 82 |
// Required: true |
| 83 |
- Workdir string `json:"Workdir"` |
|
| 83 |
+ WorkDir string `json:"WorkDir"` |
|
| 84 |
+ |
|
| 85 |
+ // rootfs |
|
| 86 |
+ Rootfs *PluginConfigRootfs `json:"rootfs,omitempty"` |
|
| 84 | 87 |
} |
| 85 | 88 |
|
| 86 | 89 |
// PluginConfigArgs plugin config args |
| ... | ... |
@@ -143,6 +142,17 @@ type PluginConfigNetwork struct {
|
| 143 | 143 |
Type string `json:"Type"` |
| 144 | 144 |
} |
| 145 | 145 |
|
| 146 |
+// PluginConfigRootfs plugin config rootfs |
|
| 147 |
+// swagger:model PluginConfigRootfs |
|
| 148 |
+type PluginConfigRootfs struct {
|
|
| 149 |
+ |
|
| 150 |
+ // diff ids |
|
| 151 |
+ DiffIds []string `json:"diff_ids"` |
|
| 152 |
+ |
|
| 153 |
+ // type |
|
| 154 |
+ Type string `json:"type,omitempty"` |
|
| 155 |
+} |
|
| 156 |
+ |
|
| 146 | 157 |
// PluginConfigUser plugin config user |
| 147 | 158 |
// swagger:model PluginConfigUser |
| 148 | 159 |
type PluginConfigUser struct {
|
| ... | ... |
@@ -64,8 +64,8 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 64 | 64 |
options := pluginCreateOptions{}
|
| 65 | 65 |
|
| 66 | 66 |
cmd := &cobra.Command{
|
| 67 |
- Use: "create [OPTIONS] PLUGIN[:tag] PATH-TO-ROOTFS(rootfs + config.json)", |
|
| 68 |
- Short: "Create a plugin from a rootfs and config", |
|
| 67 |
+ Use: "create [OPTIONS] PLUGIN PLUGIN-DATA-DIR", |
|
| 68 |
+ Short: "Create a plugin from a rootfs and configuration. Plugin data directory must contain config.json and rootfs directory.", |
|
| 69 | 69 |
Args: cli.RequiresMinArgs(2), |
| 70 | 70 |
RunE: func(cmd *cobra.Command, args []string) error {
|
| 71 | 71 |
options.repoName = args[0] |
| ... | ... |
@@ -6,7 +6,6 @@ import ( |
| 6 | 6 |
"github.com/docker/docker/api/types" |
| 7 | 7 |
"github.com/docker/docker/cli" |
| 8 | 8 |
"github.com/docker/docker/cli/command" |
| 9 |
- "github.com/docker/docker/reference" |
|
| 10 | 9 |
"github.com/spf13/cobra" |
| 11 | 10 |
"golang.org/x/net/context" |
| 12 | 11 |
) |
| ... | ... |
@@ -29,18 +28,7 @@ func newDisableCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 29 | 29 |
} |
| 30 | 30 |
|
| 31 | 31 |
func runDisable(dockerCli *command.DockerCli, name string, force bool) error {
|
| 32 |
- named, err := reference.ParseNamed(name) // FIXME: validate |
|
| 33 |
- if err != nil {
|
|
| 34 |
- return err |
|
| 35 |
- } |
|
| 36 |
- if reference.IsNameOnly(named) {
|
|
| 37 |
- named = reference.WithDefaultTag(named) |
|
| 38 |
- } |
|
| 39 |
- ref, ok := named.(reference.NamedTagged) |
|
| 40 |
- if !ok {
|
|
| 41 |
- return fmt.Errorf("invalid name: %s", named.String())
|
|
| 42 |
- } |
|
| 43 |
- if err := dockerCli.Client().PluginDisable(context.Background(), ref.String(), types.PluginDisableOptions{Force: force}); err != nil {
|
|
| 32 |
+ if err := dockerCli.Client().PluginDisable(context.Background(), name, types.PluginDisableOptions{Force: force}); err != nil {
|
|
| 44 | 33 |
return err |
| 45 | 34 |
} |
| 46 | 35 |
fmt.Fprintln(dockerCli.Out(), name) |
| ... | ... |
@@ -6,7 +6,6 @@ import ( |
| 6 | 6 |
"github.com/docker/docker/api/types" |
| 7 | 7 |
"github.com/docker/docker/cli" |
| 8 | 8 |
"github.com/docker/docker/cli/command" |
| 9 |
- "github.com/docker/docker/reference" |
|
| 10 | 9 |
"github.com/spf13/cobra" |
| 11 | 10 |
"golang.org/x/net/context" |
| 12 | 11 |
) |
| ... | ... |
@@ -36,23 +35,11 @@ func newEnableCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 36 | 36 |
|
| 37 | 37 |
func runEnable(dockerCli *command.DockerCli, opts *enableOpts) error {
|
| 38 | 38 |
name := opts.name |
| 39 |
- |
|
| 40 |
- named, err := reference.ParseNamed(name) // FIXME: validate |
|
| 41 |
- if err != nil {
|
|
| 42 |
- return err |
|
| 43 |
- } |
|
| 44 |
- if reference.IsNameOnly(named) {
|
|
| 45 |
- named = reference.WithDefaultTag(named) |
|
| 46 |
- } |
|
| 47 |
- ref, ok := named.(reference.NamedTagged) |
|
| 48 |
- if !ok {
|
|
| 49 |
- return fmt.Errorf("invalid name: %s", named.String())
|
|
| 50 |
- } |
|
| 51 | 39 |
if opts.timeout < 0 {
|
| 52 | 40 |
return fmt.Errorf("negative timeout %d is invalid", opts.timeout)
|
| 53 | 41 |
} |
| 54 | 42 |
|
| 55 |
- if err := dockerCli.Client().PluginEnable(context.Background(), ref.String(), types.PluginEnableOptions{Timeout: opts.timeout}); err != nil {
|
|
| 43 |
+ if err := dockerCli.Client().PluginEnable(context.Background(), name, types.PluginEnableOptions{Timeout: opts.timeout}); err != nil {
|
|
| 56 | 44 |
return err |
| 57 | 45 |
} |
| 58 | 46 |
fmt.Fprintln(dockerCli.Out(), name) |
| ... | ... |
@@ -2,12 +2,16 @@ package plugin |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"bufio" |
| 5 |
+ "errors" |
|
| 5 | 6 |
"fmt" |
| 6 | 7 |
"strings" |
| 7 | 8 |
|
| 9 |
+ distreference "github.com/docker/distribution/reference" |
|
| 8 | 10 |
"github.com/docker/docker/api/types" |
| 11 |
+ registrytypes "github.com/docker/docker/api/types/registry" |
|
| 9 | 12 |
"github.com/docker/docker/cli" |
| 10 | 13 |
"github.com/docker/docker/cli/command" |
| 14 |
+ "github.com/docker/docker/pkg/jsonmessage" |
|
| 11 | 15 |
"github.com/docker/docker/reference" |
| 12 | 16 |
"github.com/docker/docker/registry" |
| 13 | 17 |
"github.com/spf13/cobra" |
| ... | ... |
@@ -16,6 +20,7 @@ import ( |
| 16 | 16 |
|
| 17 | 17 |
type pluginOptions struct {
|
| 18 | 18 |
name string |
| 19 |
+ alias string |
|
| 19 | 20 |
grantPerms bool |
| 20 | 21 |
disable bool |
| 21 | 22 |
args []string |
| ... | ... |
@@ -39,41 +44,67 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 39 | 39 |
flags := cmd.Flags() |
| 40 | 40 |
flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") |
| 41 | 41 |
flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") |
| 42 |
+ flags.StringVar(&options.alias, "alias", "", "Local name for plugin") |
|
| 42 | 43 |
|
| 43 | 44 |
return cmd |
| 44 | 45 |
} |
| 45 | 46 |
|
| 46 |
-func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
|
|
| 47 |
- named, err := reference.ParseNamed(opts.name) // FIXME: validate |
|
| 47 |
+func getRepoIndexFromUnnormalizedRef(ref distreference.Named) (*registrytypes.IndexInfo, error) {
|
|
| 48 |
+ named, err := reference.ParseNamed(ref.Name()) |
|
| 48 | 49 |
if err != nil {
|
| 49 |
- return err |
|
| 50 |
+ return nil, err |
|
| 50 | 51 |
} |
| 51 |
- if reference.IsNameOnly(named) {
|
|
| 52 |
- named = reference.WithDefaultTag(named) |
|
| 52 |
+ |
|
| 53 |
+ repoInfo, err := registry.ParseRepositoryInfo(named) |
|
| 54 |
+ if err != nil {
|
|
| 55 |
+ return nil, err |
|
| 53 | 56 |
} |
| 54 |
- ref, ok := named.(reference.NamedTagged) |
|
| 55 |
- if !ok {
|
|
| 56 |
- return fmt.Errorf("invalid name: %s", named.String())
|
|
| 57 |
+ |
|
| 58 |
+ return repoInfo.Index, nil |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
|
|
| 62 |
+ // Parse name using distribution reference package to support name |
|
| 63 |
+ // containing both tag and digest. Names with both tag and digest |
|
| 64 |
+ // will be treated by the daemon as a pull by digest with |
|
| 65 |
+ // an alias for the tag (if no alias is provided). |
|
| 66 |
+ ref, err := distreference.ParseNamed(opts.name) |
|
| 67 |
+ if err != nil {
|
|
| 68 |
+ return err |
|
| 57 | 69 |
} |
| 58 | 70 |
|
| 59 |
- ctx := context.Background() |
|
| 71 |
+ alias := "" |
|
| 72 |
+ if opts.alias != "" {
|
|
| 73 |
+ aref, err := reference.ParseNamed(opts.alias) |
|
| 74 |
+ if err != nil {
|
|
| 75 |
+ return err |
|
| 76 |
+ } |
|
| 77 |
+ aref = reference.WithDefaultTag(aref) |
|
| 78 |
+ if _, ok := aref.(reference.NamedTagged); !ok {
|
|
| 79 |
+ return fmt.Errorf("invalid name: %s", opts.alias)
|
|
| 80 |
+ } |
|
| 81 |
+ alias = aref.String() |
|
| 82 |
+ } |
|
| 60 | 83 |
|
| 61 |
- repoInfo, err := registry.ParseRepositoryInfo(named) |
|
| 84 |
+ index, err := getRepoIndexFromUnnormalizedRef(ref) |
|
| 62 | 85 |
if err != nil {
|
| 63 | 86 |
return err |
| 64 | 87 |
} |
| 65 | 88 |
|
| 66 |
- authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) |
|
| 89 |
+ ctx := context.Background() |
|
| 90 |
+ |
|
| 91 |
+ authConfig := command.ResolveAuthConfig(ctx, dockerCli, index) |
|
| 67 | 92 |
|
| 68 | 93 |
encodedAuth, err := command.EncodeAuthToBase64(authConfig) |
| 69 | 94 |
if err != nil {
|
| 70 | 95 |
return err |
| 71 | 96 |
} |
| 72 | 97 |
|
| 73 |
- registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "plugin install") |
|
| 98 |
+ registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, index, "plugin install") |
|
| 74 | 99 |
|
| 75 | 100 |
options := types.PluginInstallOptions{
|
| 76 | 101 |
RegistryAuth: encodedAuth, |
| 102 |
+ RemoteRef: ref.String(), |
|
| 77 | 103 |
Disabled: opts.disable, |
| 78 | 104 |
AcceptAllPermissions: opts.grantPerms, |
| 79 | 105 |
AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name), |
| ... | ... |
@@ -81,10 +112,19 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
|
| 81 | 81 |
PrivilegeFunc: registryAuthFunc, |
| 82 | 82 |
Args: opts.args, |
| 83 | 83 |
} |
| 84 |
- if err := dockerCli.Client().PluginInstall(ctx, ref.String(), options); err != nil {
|
|
| 84 |
+ |
|
| 85 |
+ responseBody, err := dockerCli.Client().PluginInstall(ctx, alias, options) |
|
| 86 |
+ if err != nil {
|
|
| 87 |
+ if strings.Contains(err.Error(), "target is image") {
|
|
| 88 |
+ return errors.New(err.Error() + " - Use `docker image pull`") |
|
| 89 |
+ } |
|
| 90 |
+ return err |
|
| 91 |
+ } |
|
| 92 |
+ defer responseBody.Close() |
|
| 93 |
+ if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
|
|
| 85 | 94 |
return err |
| 86 | 95 |
} |
| 87 |
- fmt.Fprintln(dockerCli.Out(), opts.name) |
|
| 96 |
+ fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.name) // todo: return proper values from the API for this result |
|
| 88 | 97 |
return nil |
| 89 | 98 |
} |
| 90 | 99 |
|
| ... | ... |
@@ -44,7 +44,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
| 44 | 44 |
} |
| 45 | 45 |
|
| 46 | 46 |
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) |
| 47 |
- fmt.Fprintf(w, "ID \tNAME \tTAG \tDESCRIPTION\tENABLED") |
|
| 47 |
+ fmt.Fprintf(w, "ID \tNAME \tDESCRIPTION\tENABLED") |
|
| 48 | 48 |
fmt.Fprintf(w, "\n") |
| 49 | 49 |
|
| 50 | 50 |
for _, p := range plugins {
|
| ... | ... |
@@ -56,7 +56,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
| 56 | 56 |
desc = stringutils.Ellipsis(desc, 45) |
| 57 | 57 |
} |
| 58 | 58 |
|
| 59 |
- fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%v\n", id, p.Name, p.Tag, desc, p.Enabled) |
|
| 59 |
+ fmt.Fprintf(w, "%s\t%s\t%s\t%v\n", id, p.Name, desc, p.Enabled) |
|
| 60 | 60 |
} |
| 61 | 61 |
w.Flush() |
| 62 | 62 |
return nil |
| ... | ... |
@@ -7,6 +7,7 @@ import ( |
| 7 | 7 |
|
| 8 | 8 |
"github.com/docker/docker/cli" |
| 9 | 9 |
"github.com/docker/docker/cli/command" |
| 10 |
+ "github.com/docker/docker/pkg/jsonmessage" |
|
| 10 | 11 |
"github.com/docker/docker/reference" |
| 11 | 12 |
"github.com/docker/docker/registry" |
| 12 | 13 |
"github.com/spf13/cobra" |
| ... | ... |
@@ -49,5 +50,10 @@ func runPush(dockerCli *command.DockerCli, name string) error {
|
| 49 | 49 |
if err != nil {
|
| 50 | 50 |
return err |
| 51 | 51 |
} |
| 52 |
- return dockerCli.Client().PluginPush(ctx, ref.String(), encodedAuth) |
|
| 52 |
+ responseBody, err := dockerCli.Client().PluginPush(ctx, ref.String(), encodedAuth) |
|
| 53 |
+ if err != nil {
|
|
| 54 |
+ return err |
|
| 55 |
+ } |
|
| 56 |
+ defer responseBody.Close() |
|
| 57 |
+ return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) |
|
| 53 | 58 |
} |
| ... | ... |
@@ -6,7 +6,6 @@ import ( |
| 6 | 6 |
"github.com/docker/docker/api/types" |
| 7 | 7 |
"github.com/docker/docker/cli" |
| 8 | 8 |
"github.com/docker/docker/cli/command" |
| 9 |
- "github.com/docker/docker/reference" |
|
| 10 | 9 |
"github.com/spf13/cobra" |
| 11 | 10 |
"golang.org/x/net/context" |
| 12 | 11 |
) |
| ... | ... |
@@ -41,21 +40,8 @@ func runRemove(dockerCli *command.DockerCli, opts *rmOptions) error {
|
| 41 | 41 |
|
| 42 | 42 |
var errs cli.Errors |
| 43 | 43 |
for _, name := range opts.plugins {
|
| 44 |
- named, err := reference.ParseNamed(name) // FIXME: validate |
|
| 45 |
- if err != nil {
|
|
| 46 |
- errs = append(errs, err) |
|
| 47 |
- continue |
|
| 48 |
- } |
|
| 49 |
- if reference.IsNameOnly(named) {
|
|
| 50 |
- named = reference.WithDefaultTag(named) |
|
| 51 |
- } |
|
| 52 |
- ref, ok := named.(reference.NamedTagged) |
|
| 53 |
- if !ok {
|
|
| 54 |
- errs = append(errs, fmt.Errorf("invalid name: %s", named.String()))
|
|
| 55 |
- continue |
|
| 56 |
- } |
|
| 57 | 44 |
// TODO: pass names to api instead of making multiple api calls |
| 58 |
- if err := dockerCli.Client().PluginRemove(ctx, ref.String(), types.PluginRemoveOptions{Force: opts.force}); err != nil {
|
|
| 45 |
+ if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil {
|
|
| 59 | 46 |
errs = append(errs, err) |
| 60 | 47 |
continue |
| 61 | 48 |
} |
| ... | ... |
@@ -1,13 +1,10 @@ |
| 1 | 1 |
package plugin |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "fmt" |
|
| 5 |
- |
|
| 6 | 4 |
"golang.org/x/net/context" |
| 7 | 5 |
|
| 8 | 6 |
"github.com/docker/docker/cli" |
| 9 | 7 |
"github.com/docker/docker/cli/command" |
| 10 |
- "github.com/docker/docker/reference" |
|
| 11 | 8 |
"github.com/spf13/cobra" |
| 12 | 9 |
) |
| 13 | 10 |
|
| ... | ... |
@@ -17,24 +14,9 @@ func newSetCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 17 | 17 |
Short: "Change settings for a plugin", |
| 18 | 18 |
Args: cli.RequiresMinArgs(2), |
| 19 | 19 |
RunE: func(cmd *cobra.Command, args []string) error {
|
| 20 |
- return runSet(dockerCli, args[0], args[1:]) |
|
| 20 |
+ return dockerCli.Client().PluginSet(context.Background(), args[0], args[1:]) |
|
| 21 | 21 |
}, |
| 22 | 22 |
} |
| 23 | 23 |
|
| 24 | 24 |
return cmd |
| 25 | 25 |
} |
| 26 |
- |
|
| 27 |
-func runSet(dockerCli *command.DockerCli, name string, args []string) error {
|
|
| 28 |
- named, err := reference.ParseNamed(name) // FIXME: validate |
|
| 29 |
- if err != nil {
|
|
| 30 |
- return err |
|
| 31 |
- } |
|
| 32 |
- if reference.IsNameOnly(named) {
|
|
| 33 |
- named = reference.WithDefaultTag(named) |
|
| 34 |
- } |
|
| 35 |
- ref, ok := named.(reference.NamedTagged) |
|
| 36 |
- if !ok {
|
|
| 37 |
- return fmt.Errorf("invalid name: %s", named.String())
|
|
| 38 |
- } |
|
| 39 |
- return dockerCli.Client().PluginSet(context.Background(), ref.String(), args) |
|
| 40 |
-} |
| ... | ... |
@@ -111,8 +111,8 @@ type PluginAPIClient interface {
|
| 111 | 111 |
PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error |
| 112 | 112 |
PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error |
| 113 | 113 |
PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error |
| 114 |
- PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error |
|
| 115 |
- PluginPush(ctx context.Context, name string, registryAuth string) error |
|
| 114 |
+ PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) |
|
| 115 |
+ PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) |
|
| 116 | 116 |
PluginSet(ctx context.Context, name string, args []string) error |
| 117 | 117 |
PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) |
| 118 | 118 |
PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error |
| ... | ... |
@@ -2,73 +2,96 @@ package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"encoding/json" |
| 5 |
+ "io" |
|
| 5 | 6 |
"net/http" |
| 6 | 7 |
"net/url" |
| 7 | 8 |
|
| 9 |
+ "github.com/docker/distribution/reference" |
|
| 8 | 10 |
"github.com/docker/docker/api/types" |
| 11 |
+ "github.com/pkg/errors" |
|
| 9 | 12 |
"golang.org/x/net/context" |
| 10 | 13 |
) |
| 11 | 14 |
|
| 12 | 15 |
// PluginInstall installs a plugin |
| 13 |
-func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (err error) {
|
|
| 14 |
- // FIXME(vdemeester) name is a ref, we might want to parse/validate it here. |
|
| 16 |
+func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
|
|
| 15 | 17 |
query := url.Values{}
|
| 16 |
- query.Set("name", name)
|
|
| 18 |
+ if _, err := reference.ParseNamed(options.RemoteRef); err != nil {
|
|
| 19 |
+ return nil, errors.Wrap(err, "invalid remote reference") |
|
| 20 |
+ } |
|
| 21 |
+ query.Set("remote", options.RemoteRef)
|
|
| 22 |
+ |
|
| 17 | 23 |
resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) |
| 18 | 24 |
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
|
| 25 |
+ // todo: do inspect before to check existing name before checking privileges |
|
| 19 | 26 |
newAuthHeader, privilegeErr := options.PrivilegeFunc() |
| 20 | 27 |
if privilegeErr != nil {
|
| 21 | 28 |
ensureReaderClosed(resp) |
| 22 |
- return privilegeErr |
|
| 29 |
+ return nil, privilegeErr |
|
| 23 | 30 |
} |
| 24 | 31 |
options.RegistryAuth = newAuthHeader |
| 25 | 32 |
resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) |
| 26 | 33 |
} |
| 27 | 34 |
if err != nil {
|
| 28 | 35 |
ensureReaderClosed(resp) |
| 29 |
- return err |
|
| 36 |
+ return nil, err |
|
| 30 | 37 |
} |
| 31 | 38 |
|
| 32 | 39 |
var privileges types.PluginPrivileges |
| 33 | 40 |
if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
|
| 34 | 41 |
ensureReaderClosed(resp) |
| 35 |
- return err |
|
| 42 |
+ return nil, err |
|
| 36 | 43 |
} |
| 37 | 44 |
ensureReaderClosed(resp) |
| 38 | 45 |
|
| 39 | 46 |
if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
|
| 40 | 47 |
accept, err := options.AcceptPermissionsFunc(privileges) |
| 41 | 48 |
if err != nil {
|
| 42 |
- return err |
|
| 49 |
+ return nil, err |
|
| 43 | 50 |
} |
| 44 | 51 |
if !accept {
|
| 45 |
- return pluginPermissionDenied{name}
|
|
| 52 |
+ return nil, pluginPermissionDenied{options.RemoteRef}
|
|
| 46 | 53 |
} |
| 47 | 54 |
} |
| 48 | 55 |
|
| 49 |
- _, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) |
|
| 56 |
+ // set name for plugin pull, if empty should default to remote reference |
|
| 57 |
+ query.Set("name", name)
|
|
| 58 |
+ |
|
| 59 |
+ resp, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) |
|
| 50 | 60 |
if err != nil {
|
| 51 |
- return err |
|
| 61 |
+ return nil, err |
|
| 52 | 62 |
} |
| 53 | 63 |
|
| 54 |
- defer func() {
|
|
| 64 |
+ name = resp.header.Get("Docker-Plugin-Name")
|
|
| 65 |
+ |
|
| 66 |
+ pr, pw := io.Pipe() |
|
| 67 |
+ go func() { // todo: the client should probably be designed more around the actual api
|
|
| 68 |
+ _, err := io.Copy(pw, resp.body) |
|
| 55 | 69 |
if err != nil {
|
| 56 |
- delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) |
|
| 57 |
- ensureReaderClosed(delResp) |
|
| 70 |
+ pw.CloseWithError(err) |
|
| 71 |
+ return |
|
| 58 | 72 |
} |
| 59 |
- }() |
|
| 60 |
- |
|
| 61 |
- if len(options.Args) > 0 {
|
|
| 62 |
- if err := cli.PluginSet(ctx, name, options.Args); err != nil {
|
|
| 63 |
- return err |
|
| 73 |
+ defer func() {
|
|
| 74 |
+ if err != nil {
|
|
| 75 |
+ delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) |
|
| 76 |
+ ensureReaderClosed(delResp) |
|
| 77 |
+ } |
|
| 78 |
+ }() |
|
| 79 |
+ if len(options.Args) > 0 {
|
|
| 80 |
+ if err := cli.PluginSet(ctx, name, options.Args); err != nil {
|
|
| 81 |
+ pw.CloseWithError(err) |
|
| 82 |
+ return |
|
| 83 |
+ } |
|
| 64 | 84 |
} |
| 65 |
- } |
|
| 66 | 85 |
|
| 67 |
- if options.Disabled {
|
|
| 68 |
- return nil |
|
| 69 |
- } |
|
| 86 |
+ if options.Disabled {
|
|
| 87 |
+ pw.Close() |
|
| 88 |
+ return |
|
| 89 |
+ } |
|
| 70 | 90 |
|
| 71 |
- return cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0})
|
|
| 91 |
+ err = cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0})
|
|
| 92 |
+ pw.CloseWithError(err) |
|
| 93 |
+ }() |
|
| 94 |
+ return pr, nil |
|
| 72 | 95 |
} |
| 73 | 96 |
|
| 74 | 97 |
func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {
|
| ... | ... |
@@ -1,13 +1,17 @@ |
| 1 | 1 |
package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "io" |
|
| 5 |
+ |
|
| 4 | 6 |
"golang.org/x/net/context" |
| 5 | 7 |
) |
| 6 | 8 |
|
| 7 | 9 |
// PluginPush pushes a plugin to a registry |
| 8 |
-func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) error {
|
|
| 10 |
+func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) {
|
|
| 9 | 11 |
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
|
| 10 | 12 |
resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, headers) |
| 11 |
- ensureReaderClosed(resp) |
|
| 12 |
- return err |
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ return nil, err |
|
| 15 |
+ } |
|
| 16 |
+ return resp.body, nil |
|
| 13 | 17 |
} |
| ... | ... |
@@ -16,7 +16,7 @@ func TestPluginPushError(t *testing.T) {
|
| 16 | 16 |
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
| 17 | 17 |
} |
| 18 | 18 |
|
| 19 |
- err := client.PluginPush(context.Background(), "plugin_name", "") |
|
| 19 |
+ _, err := client.PluginPush(context.Background(), "plugin_name", "") |
|
| 20 | 20 |
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
| 21 | 21 |
t.Fatalf("expected a Server Error, got %v", err)
|
| 22 | 22 |
} |
| ... | ... |
@@ -44,7 +44,7 @@ func TestPluginPush(t *testing.T) {
|
| 44 | 44 |
}), |
| 45 | 45 |
} |
| 46 | 46 |
|
| 47 |
- err := client.PluginPush(context.Background(), "plugin_name", "authtoken") |
|
| 47 |
+ _, err := client.PluginPush(context.Background(), "plugin_name", "authtoken") |
|
| 48 | 48 |
if err != nil {
|
| 49 | 49 |
t.Fatal(err) |
| 50 | 50 |
} |
| ... | ... |
@@ -42,7 +42,6 @@ import ( |
| 42 | 42 |
"github.com/docker/docker/pkg/plugingetter" |
| 43 | 43 |
"github.com/docker/docker/pkg/signal" |
| 44 | 44 |
"github.com/docker/docker/pkg/system" |
| 45 |
- "github.com/docker/docker/plugin" |
|
| 46 | 45 |
"github.com/docker/docker/registry" |
| 47 | 46 |
"github.com/docker/docker/runconfig" |
| 48 | 47 |
"github.com/docker/go-connections/tlsconfig" |
| ... | ... |
@@ -471,7 +470,7 @@ func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) {
|
| 471 | 471 |
volume.NewRouter(d), |
| 472 | 472 |
build.NewRouter(dockerfile.NewBuildManager(d)), |
| 473 | 473 |
swarmrouter.NewRouter(c), |
| 474 |
- pluginrouter.NewRouter(plugin.GetManager()), |
|
| 474 |
+ pluginrouter.NewRouter(d.PluginManager()), |
|
| 475 | 475 |
} |
| 476 | 476 |
|
| 477 | 477 |
if d.NetworkControllerEnabled() {
|
| ... | ... |
@@ -13,6 +13,7 @@ import ( |
| 13 | 13 |
"github.com/docker/docker/api/types/network" |
| 14 | 14 |
swarmtypes "github.com/docker/docker/api/types/swarm" |
| 15 | 15 |
clustertypes "github.com/docker/docker/daemon/cluster/provider" |
| 16 |
+ "github.com/docker/docker/plugin" |
|
| 16 | 17 |
"github.com/docker/docker/reference" |
| 17 | 18 |
"github.com/docker/libnetwork" |
| 18 | 19 |
"github.com/docker/libnetwork/cluster" |
| ... | ... |
@@ -54,4 +55,5 @@ type Backend interface {
|
| 54 | 54 |
WaitForDetachment(context.Context, string, string, string, string) error |
| 55 | 55 |
GetRepository(context.Context, reference.NamedTagged, *types.AuthConfig) (distribution.Repository, bool, error) |
| 56 | 56 |
LookupImage(name string) (*types.ImageInspect, error) |
| 57 |
+ PluginManager() *plugin.Manager |
|
| 57 | 58 |
} |
| ... | ... |
@@ -8,7 +8,6 @@ import ( |
| 8 | 8 |
"github.com/docker/docker/api/types/network" |
| 9 | 9 |
executorpkg "github.com/docker/docker/daemon/cluster/executor" |
| 10 | 10 |
clustertypes "github.com/docker/docker/daemon/cluster/provider" |
| 11 |
- "github.com/docker/docker/plugin" |
|
| 12 | 11 |
networktypes "github.com/docker/libnetwork/types" |
| 13 | 12 |
"github.com/docker/swarmkit/agent/exec" |
| 14 | 13 |
"github.com/docker/swarmkit/agent/secrets" |
| ... | ... |
@@ -54,7 +53,7 @@ func (e *executor) Describe(ctx context.Context) (*api.NodeDescription, error) {
|
| 54 | 54 |
addPlugins("Authorization", info.Plugins.Authorization)
|
| 55 | 55 |
|
| 56 | 56 |
// add v2 plugins |
| 57 |
- v2Plugins, err := plugin.GetManager().List() |
|
| 57 |
+ v2Plugins, err := e.backend.PluginManager().List() |
|
| 58 | 58 |
if err == nil {
|
| 59 | 59 |
for _, plgn := range v2Plugins {
|
| 60 | 60 |
for _, typ := range plgn.Config.Interface.Types {
|
| ... | ... |
@@ -67,13 +66,9 @@ func (e *executor) Describe(ctx context.Context) (*api.NodeDescription, error) {
|
| 67 | 67 |
} else if typ.Capability == "networkdriver" {
|
| 68 | 68 |
plgnTyp = "Network" |
| 69 | 69 |
} |
| 70 |
- plgnName := plgn.Name |
|
| 71 |
- if plgn.Tag != "" {
|
|
| 72 |
- plgnName += ":" + plgn.Tag |
|
| 73 |
- } |
|
| 74 | 70 |
plugins[api.PluginDescription{
|
| 75 | 71 |
Type: plgnTyp, |
| 76 |
- Name: plgnName, |
|
| 72 |
+ Name: plgn.Name, |
|
| 77 | 73 |
}] = struct{}{}
|
| 78 | 74 |
} |
| 79 | 75 |
} |
| ... | ... |
@@ -8,7 +8,6 @@ package daemon |
| 8 | 8 |
import ( |
| 9 | 9 |
"encoding/json" |
| 10 | 10 |
"fmt" |
| 11 |
- "io" |
|
| 12 | 11 |
"io/ioutil" |
| 13 | 12 |
"net" |
| 14 | 13 |
"os" |
| ... | ... |
@@ -17,7 +16,6 @@ import ( |
| 17 | 17 |
"runtime" |
| 18 | 18 |
"strings" |
| 19 | 19 |
"sync" |
| 20 |
- "syscall" |
|
| 21 | 20 |
"time" |
| 22 | 21 |
|
| 23 | 22 |
"github.com/Sirupsen/logrus" |
| ... | ... |
@@ -28,6 +26,7 @@ import ( |
| 28 | 28 |
"github.com/docker/docker/container" |
| 29 | 29 |
"github.com/docker/docker/daemon/events" |
| 30 | 30 |
"github.com/docker/docker/daemon/exec" |
| 31 |
+ "github.com/docker/docker/daemon/initlayer" |
|
| 31 | 32 |
"github.com/docker/docker/dockerversion" |
| 32 | 33 |
"github.com/docker/docker/plugin" |
| 33 | 34 |
"github.com/docker/libnetwork/cluster" |
| ... | ... |
@@ -42,14 +41,11 @@ import ( |
| 42 | 42 |
"github.com/docker/docker/pkg/fileutils" |
| 43 | 43 |
"github.com/docker/docker/pkg/idtools" |
| 44 | 44 |
"github.com/docker/docker/pkg/plugingetter" |
| 45 |
- "github.com/docker/docker/pkg/progress" |
|
| 46 | 45 |
"github.com/docker/docker/pkg/registrar" |
| 47 | 46 |
"github.com/docker/docker/pkg/signal" |
| 48 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 49 | 47 |
"github.com/docker/docker/pkg/sysinfo" |
| 50 | 48 |
"github.com/docker/docker/pkg/system" |
| 51 | 49 |
"github.com/docker/docker/pkg/truncindex" |
| 52 |
- pluginstore "github.com/docker/docker/plugin/store" |
|
| 53 | 50 |
"github.com/docker/docker/reference" |
| 54 | 51 |
"github.com/docker/docker/registry" |
| 55 | 52 |
"github.com/docker/docker/runconfig" |
| ... | ... |
@@ -59,6 +55,7 @@ import ( |
| 59 | 59 |
"github.com/docker/libnetwork" |
| 60 | 60 |
nwconfig "github.com/docker/libnetwork/config" |
| 61 | 61 |
"github.com/docker/libtrust" |
| 62 |
+ "github.com/pkg/errors" |
|
| 62 | 63 |
) |
| 63 | 64 |
|
| 64 | 65 |
var ( |
| ... | ... |
@@ -99,7 +96,8 @@ type Daemon struct {
|
| 99 | 99 |
gidMaps []idtools.IDMap |
| 100 | 100 |
layerStore layer.Store |
| 101 | 101 |
imageStore image.Store |
| 102 |
- PluginStore *pluginstore.Store |
|
| 102 |
+ PluginStore *plugin.Store // todo: remove |
|
| 103 |
+ pluginManager *plugin.Manager |
|
| 103 | 104 |
nameIndex *registrar.Registrar |
| 104 | 105 |
linkIndex *linkIndex |
| 105 | 106 |
containerd libcontainerd.Client |
| ... | ... |
@@ -554,10 +552,19 @@ func NewDaemon(config *Config, registryService registry.Service, containerdRemot |
| 554 | 554 |
} |
| 555 | 555 |
|
| 556 | 556 |
d.RegistryService = registryService |
| 557 |
- d.PluginStore = pluginstore.NewStore(config.Root) |
|
| 557 |
+ d.PluginStore = plugin.NewStore(config.Root) // todo: remove |
|
| 558 | 558 |
// Plugin system initialization should happen before restore. Do not change order. |
| 559 |
- if err := d.pluginInit(config, containerdRemote); err != nil {
|
|
| 560 |
- return nil, err |
|
| 559 |
+ d.pluginManager, err = plugin.NewManager(plugin.ManagerConfig{
|
|
| 560 |
+ Root: filepath.Join(config.Root, "plugins"), |
|
| 561 |
+ ExecRoot: "/run/docker/plugins", // possibly needs fixing |
|
| 562 |
+ Store: d.PluginStore, |
|
| 563 |
+ Executor: containerdRemote, |
|
| 564 |
+ RegistryService: registryService, |
|
| 565 |
+ LiveRestoreEnabled: config.LiveRestoreEnabled, |
|
| 566 |
+ LogPluginEvent: d.LogPluginEvent, // todo: make private |
|
| 567 |
+ }) |
|
| 568 |
+ if err != nil {
|
|
| 569 |
+ return nil, errors.Wrap(err, "couldn't create plugin manager") |
|
| 561 | 570 |
} |
| 562 | 571 |
|
| 563 | 572 |
d.layerStore, err = layer.NewStoreFromOptions(layer.StoreOptions{
|
| ... | ... |
@@ -895,36 +902,6 @@ func (daemon *Daemon) V6Subnets() []net.IPNet {
|
| 895 | 895 |
return subnets |
| 896 | 896 |
} |
| 897 | 897 |
|
| 898 |
-func writeDistributionProgress(cancelFunc func(), outStream io.Writer, progressChan <-chan progress.Progress) {
|
|
| 899 |
- progressOutput := streamformatter.NewJSONStreamFormatter().NewProgressOutput(outStream, false) |
|
| 900 |
- operationCancelled := false |
|
| 901 |
- |
|
| 902 |
- for prog := range progressChan {
|
|
| 903 |
- if err := progressOutput.WriteProgress(prog); err != nil && !operationCancelled {
|
|
| 904 |
- // don't log broken pipe errors as this is the normal case when a client aborts |
|
| 905 |
- if isBrokenPipe(err) {
|
|
| 906 |
- logrus.Info("Pull session cancelled")
|
|
| 907 |
- } else {
|
|
| 908 |
- logrus.Errorf("error writing progress to client: %v", err)
|
|
| 909 |
- } |
|
| 910 |
- cancelFunc() |
|
| 911 |
- operationCancelled = true |
|
| 912 |
- // Don't return, because we need to continue draining |
|
| 913 |
- // progressChan until it's closed to avoid a deadlock. |
|
| 914 |
- } |
|
| 915 |
- } |
|
| 916 |
-} |
|
| 917 |
- |
|
| 918 |
-func isBrokenPipe(e error) bool {
|
|
| 919 |
- if netErr, ok := e.(*net.OpError); ok {
|
|
| 920 |
- e = netErr.Err |
|
| 921 |
- if sysErr, ok := netErr.Err.(*os.SyscallError); ok {
|
|
| 922 |
- e = sysErr.Err |
|
| 923 |
- } |
|
| 924 |
- } |
|
| 925 |
- return e == syscall.EPIPE |
|
| 926 |
-} |
|
| 927 |
- |
|
| 928 | 898 |
// GraphDriverName returns the name of the graph driver used by the layer.Store |
| 929 | 899 |
func (daemon *Daemon) GraphDriverName() string {
|
| 930 | 900 |
return daemon.layerStore.DriverName() |
| ... | ... |
@@ -956,7 +933,7 @@ func tempDir(rootDir string, rootUID, rootGID int) (string, error) {
|
| 956 | 956 |
|
| 957 | 957 |
func (daemon *Daemon) setupInitLayer(initPath string) error {
|
| 958 | 958 |
rootUID, rootGID := daemon.GetRemappedUIDGID() |
| 959 |
- return setupInitLayer(initPath, rootUID, rootGID) |
|
| 959 |
+ return initlayer.Setup(initPath, rootUID, rootGID) |
|
| 960 | 960 |
} |
| 961 | 961 |
|
| 962 | 962 |
func setDefaultMtu(config *Config) {
|
| ... | ... |
@@ -1270,12 +1247,8 @@ func (daemon *Daemon) SetCluster(cluster Cluster) {
|
| 1270 | 1270 |
daemon.cluster = cluster |
| 1271 | 1271 |
} |
| 1272 | 1272 |
|
| 1273 |
-func (daemon *Daemon) pluginInit(cfg *Config, remote libcontainerd.Remote) error {
|
|
| 1274 |
- return plugin.Init(cfg.Root, daemon.PluginStore, remote, daemon.RegistryService, cfg.LiveRestoreEnabled, daemon.LogPluginEvent) |
|
| 1275 |
-} |
|
| 1276 |
- |
|
| 1277 | 1273 |
func (daemon *Daemon) pluginShutdown() {
|
| 1278 |
- manager := plugin.GetManager() |
|
| 1274 |
+ manager := daemon.pluginManager |
|
| 1279 | 1275 |
// Check for a valid manager object. In error conditions, daemon init can fail |
| 1280 | 1276 |
// and shutdown called, before plugin manager is initialized. |
| 1281 | 1277 |
if manager != nil {
|
| ... | ... |
@@ -1283,6 +1256,11 @@ func (daemon *Daemon) pluginShutdown() {
|
| 1283 | 1283 |
} |
| 1284 | 1284 |
} |
| 1285 | 1285 |
|
| 1286 |
+// PluginManager returns current pluginManager associated with the daemon |
|
| 1287 |
+func (daemon *Daemon) PluginManager() *plugin.Manager { // set up before daemon to avoid this method
|
|
| 1288 |
+ return daemon.pluginManager |
|
| 1289 |
+} |
|
| 1290 |
+ |
|
| 1286 | 1291 |
// CreateDaemonRoot creates the root for the daemon |
| 1287 | 1292 |
func CreateDaemonRoot(config *Config) error {
|
| 1288 | 1293 |
// get the canonical path to the Docker root directory |
| ... | ... |
@@ -96,16 +96,6 @@ func (daemon *Daemon) getLayerInit() func(string) error {
|
| 96 | 96 |
return nil |
| 97 | 97 |
} |
| 98 | 98 |
|
| 99 |
-// setupInitLayer populates a directory with mountpoints suitable |
|
| 100 |
-// for bind-mounting dockerinit into the container. The mountpoint is simply an |
|
| 101 |
-// empty file at /.dockerinit |
|
| 102 |
-// |
|
| 103 |
-// This extra layer is used by all containers as the top-most ro layer. It protects |
|
| 104 |
-// the container from unwanted side-effects on the rw layer. |
|
| 105 |
-func setupInitLayer(initLayer string, rootUID, rootGID int) error {
|
|
| 106 |
- return nil |
|
| 107 |
-} |
|
| 108 |
- |
|
| 109 | 99 |
func checkKernel() error {
|
| 110 | 100 |
// solaris can rely upon checkSystem() below, we don't skew kernel versions |
| 111 | 101 |
return nil |
| ... | ... |
@@ -858,63 +858,6 @@ func (daemon *Daemon) getLayerInit() func(string) error {
|
| 858 | 858 |
return daemon.setupInitLayer |
| 859 | 859 |
} |
| 860 | 860 |
|
| 861 |
-// setupInitLayer populates a directory with mountpoints suitable |
|
| 862 |
-// for bind-mounting things into the container. |
|
| 863 |
-// |
|
| 864 |
-// This extra layer is used by all containers as the top-most ro layer. It protects |
|
| 865 |
-// the container from unwanted side-effects on the rw layer. |
|
| 866 |
-func setupInitLayer(initLayer string, rootUID, rootGID int) error {
|
|
| 867 |
- for pth, typ := range map[string]string{
|
|
| 868 |
- "/dev/pts": "dir", |
|
| 869 |
- "/dev/shm": "dir", |
|
| 870 |
- "/proc": "dir", |
|
| 871 |
- "/sys": "dir", |
|
| 872 |
- "/.dockerenv": "file", |
|
| 873 |
- "/etc/resolv.conf": "file", |
|
| 874 |
- "/etc/hosts": "file", |
|
| 875 |
- "/etc/hostname": "file", |
|
| 876 |
- "/dev/console": "file", |
|
| 877 |
- "/etc/mtab": "/proc/mounts", |
|
| 878 |
- } {
|
|
| 879 |
- parts := strings.Split(pth, "/") |
|
| 880 |
- prev := "/" |
|
| 881 |
- for _, p := range parts[1:] {
|
|
| 882 |
- prev = filepath.Join(prev, p) |
|
| 883 |
- syscall.Unlink(filepath.Join(initLayer, prev)) |
|
| 884 |
- } |
|
| 885 |
- |
|
| 886 |
- if _, err := os.Stat(filepath.Join(initLayer, pth)); err != nil {
|
|
| 887 |
- if os.IsNotExist(err) {
|
|
| 888 |
- if err := idtools.MkdirAllNewAs(filepath.Join(initLayer, filepath.Dir(pth)), 0755, rootUID, rootGID); err != nil {
|
|
| 889 |
- return err |
|
| 890 |
- } |
|
| 891 |
- switch typ {
|
|
| 892 |
- case "dir": |
|
| 893 |
- if err := idtools.MkdirAllNewAs(filepath.Join(initLayer, pth), 0755, rootUID, rootGID); err != nil {
|
|
| 894 |
- return err |
|
| 895 |
- } |
|
| 896 |
- case "file": |
|
| 897 |
- f, err := os.OpenFile(filepath.Join(initLayer, pth), os.O_CREATE, 0755) |
|
| 898 |
- if err != nil {
|
|
| 899 |
- return err |
|
| 900 |
- } |
|
| 901 |
- f.Chown(rootUID, rootGID) |
|
| 902 |
- f.Close() |
|
| 903 |
- default: |
|
| 904 |
- if err := os.Symlink(typ, filepath.Join(initLayer, pth)); err != nil {
|
|
| 905 |
- return err |
|
| 906 |
- } |
|
| 907 |
- } |
|
| 908 |
- } else {
|
|
| 909 |
- return err |
|
| 910 |
- } |
|
| 911 |
- } |
|
| 912 |
- } |
|
| 913 |
- |
|
| 914 |
- // Layer is ready to use, if it wasn't before. |
|
| 915 |
- return nil |
|
| 916 |
-} |
|
| 917 |
- |
|
| 918 | 861 |
// Parse the remapped root (user namespace) option, which can be one of: |
| 919 | 862 |
// username - valid username from /etc/passwd |
| 920 | 863 |
// username:groupname - valid username; valid groupname from /etc/group |
| ... | ... |
@@ -61,10 +61,6 @@ func getBlkioWriteBpsDevices(config *containertypes.HostConfig) ([]blkiodev.Thro |
| 61 | 61 |
return nil, nil |
| 62 | 62 |
} |
| 63 | 63 |
|
| 64 |
-func setupInitLayer(initLayer string, rootUID, rootGID int) error {
|
|
| 65 |
- return nil |
|
| 66 |
-} |
|
| 67 |
- |
|
| 68 | 64 |
func (daemon *Daemon) getLayerInit() func(string) error {
|
| 69 | 65 |
return nil |
| 70 | 66 |
} |
| ... | ... |
@@ -9,6 +9,7 @@ import ( |
| 9 | 9 |
"github.com/docker/docker/api/types" |
| 10 | 10 |
"github.com/docker/docker/builder" |
| 11 | 11 |
"github.com/docker/docker/distribution" |
| 12 |
+ progressutils "github.com/docker/docker/distribution/utils" |
|
| 12 | 13 |
"github.com/docker/docker/pkg/progress" |
| 13 | 14 |
"github.com/docker/docker/reference" |
| 14 | 15 |
"github.com/docker/docker/registry" |
| ... | ... |
@@ -84,7 +85,7 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference. |
| 84 | 84 |
ctx, cancelFunc := context.WithCancel(ctx) |
| 85 | 85 |
|
| 86 | 86 |
go func() {
|
| 87 |
- writeDistributionProgress(cancelFunc, outStream, progressChan) |
|
| 87 |
+ progressutils.WriteDistributionProgress(cancelFunc, outStream, progressChan) |
|
| 88 | 88 |
close(writesDone) |
| 89 | 89 |
}() |
| 90 | 90 |
|
| ... | ... |
@@ -6,6 +6,7 @@ import ( |
| 6 | 6 |
"github.com/docker/distribution/manifest/schema2" |
| 7 | 7 |
"github.com/docker/docker/api/types" |
| 8 | 8 |
"github.com/docker/docker/distribution" |
| 9 |
+ progressutils "github.com/docker/docker/distribution/utils" |
|
| 9 | 10 |
"github.com/docker/docker/pkg/progress" |
| 10 | 11 |
"github.com/docker/docker/reference" |
| 11 | 12 |
"golang.org/x/net/context" |
| ... | ... |
@@ -34,7 +35,7 @@ func (daemon *Daemon) PushImage(ctx context.Context, image, tag string, metaHead |
| 34 | 34 |
ctx, cancelFunc := context.WithCancel(ctx) |
| 35 | 35 |
|
| 36 | 36 |
go func() {
|
| 37 |
- writeDistributionProgress(cancelFunc, outStream, progressChan) |
|
| 37 |
+ progressutils.WriteDistributionProgress(cancelFunc, outStream, progressChan) |
|
| 38 | 38 |
close(writesDone) |
| 39 | 39 |
}() |
| 40 | 40 |
|
| 41 | 41 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,13 @@ |
| 0 |
+// +build solaris,cgo |
|
| 1 |
+ |
|
| 2 |
+package initlayer |
|
| 3 |
+ |
|
| 4 |
+// Setup populates a directory with mountpoints suitable |
|
| 5 |
+// for bind-mounting dockerinit into the container. The mountpoint is simply an |
|
| 6 |
+// empty file at /.dockerinit |
|
| 7 |
+// |
|
| 8 |
+// This extra layer is used by all containers as the top-most ro layer. It protects |
|
| 9 |
+// the container from unwanted side-effects on the rw layer. |
|
| 10 |
+func Setup(initLayer string, rootUID, rootGID int) error {
|
|
| 11 |
+ return nil |
|
| 12 |
+} |
| 0 | 13 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,69 @@ |
| 0 |
+// +build linux freebsd |
|
| 1 |
+ |
|
| 2 |
+package initlayer |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "os" |
|
| 6 |
+ "path/filepath" |
|
| 7 |
+ "strings" |
|
| 8 |
+ "syscall" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/pkg/idtools" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// Setup populates a directory with mountpoints suitable |
|
| 14 |
+// for bind-mounting things into the container. |
|
| 15 |
+// |
|
| 16 |
+// This extra layer is used by all containers as the top-most ro layer. It protects |
|
| 17 |
+// the container from unwanted side-effects on the rw layer. |
|
| 18 |
+func Setup(initLayer string, rootUID, rootGID int) error {
|
|
| 19 |
+ for pth, typ := range map[string]string{
|
|
| 20 |
+ "/dev/pts": "dir", |
|
| 21 |
+ "/dev/shm": "dir", |
|
| 22 |
+ "/proc": "dir", |
|
| 23 |
+ "/sys": "dir", |
|
| 24 |
+ "/.dockerenv": "file", |
|
| 25 |
+ "/etc/resolv.conf": "file", |
|
| 26 |
+ "/etc/hosts": "file", |
|
| 27 |
+ "/etc/hostname": "file", |
|
| 28 |
+ "/dev/console": "file", |
|
| 29 |
+ "/etc/mtab": "/proc/mounts", |
|
| 30 |
+ } {
|
|
| 31 |
+ parts := strings.Split(pth, "/") |
|
| 32 |
+ prev := "/" |
|
| 33 |
+ for _, p := range parts[1:] {
|
|
| 34 |
+ prev = filepath.Join(prev, p) |
|
| 35 |
+ syscall.Unlink(filepath.Join(initLayer, prev)) |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ if _, err := os.Stat(filepath.Join(initLayer, pth)); err != nil {
|
|
| 39 |
+ if os.IsNotExist(err) {
|
|
| 40 |
+ if err := idtools.MkdirAllNewAs(filepath.Join(initLayer, filepath.Dir(pth)), 0755, rootUID, rootGID); err != nil {
|
|
| 41 |
+ return err |
|
| 42 |
+ } |
|
| 43 |
+ switch typ {
|
|
| 44 |
+ case "dir": |
|
| 45 |
+ if err := idtools.MkdirAllNewAs(filepath.Join(initLayer, pth), 0755, rootUID, rootGID); err != nil {
|
|
| 46 |
+ return err |
|
| 47 |
+ } |
|
| 48 |
+ case "file": |
|
| 49 |
+ f, err := os.OpenFile(filepath.Join(initLayer, pth), os.O_CREATE, 0755) |
|
| 50 |
+ if err != nil {
|
|
| 51 |
+ return err |
|
| 52 |
+ } |
|
| 53 |
+ f.Chown(rootUID, rootGID) |
|
| 54 |
+ f.Close() |
|
| 55 |
+ default: |
|
| 56 |
+ if err := os.Symlink(typ, filepath.Join(initLayer, pth)); err != nil {
|
|
| 57 |
+ return err |
|
| 58 |
+ } |
|
| 59 |
+ } |
|
| 60 |
+ } else {
|
|
| 61 |
+ return err |
|
| 62 |
+ } |
|
| 63 |
+ } |
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ // Layer is ready to use, if it wasn't before. |
|
| 67 |
+ return nil |
|
| 68 |
+} |
| 0 | 69 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,13 @@ |
| 0 |
+// +build windows |
|
| 1 |
+ |
|
| 2 |
+package initlayer |
|
| 3 |
+ |
|
| 4 |
+// Setup populates a directory with mountpoints suitable |
|
| 5 |
+// for bind-mounting dockerinit into the container. The mountpoint is simply an |
|
| 6 |
+// empty file at /.dockerinit |
|
| 7 |
+// |
|
| 8 |
+// This extra layer is used by all containers as the top-most ro layer. It protects |
|
| 9 |
+// the container from unwanted side-effects on the rw layer. |
|
| 10 |
+func Setup(initLayer string, rootUID, rootGID int) error {
|
|
| 11 |
+ return nil |
|
| 12 |
+} |
| ... | ... |
@@ -3,6 +3,7 @@ package metadata |
| 3 | 3 |
import ( |
| 4 | 4 |
"github.com/docker/docker/image/v1" |
| 5 | 5 |
"github.com/docker/docker/layer" |
| 6 |
+ "github.com/pkg/errors" |
|
| 6 | 7 |
) |
| 7 | 8 |
|
| 8 | 9 |
// V1IDService maps v1 IDs to layers on disk. |
| ... | ... |
@@ -24,6 +25,9 @@ func (idserv *V1IDService) namespace() string {
|
| 24 | 24 |
|
| 25 | 25 |
// Get finds a layer by its V1 ID. |
| 26 | 26 |
func (idserv *V1IDService) Get(v1ID, registry string) (layer.DiffID, error) {
|
| 27 |
+ if idserv.store == nil {
|
|
| 28 |
+ return "", errors.New("no v1IDService storage")
|
|
| 29 |
+ } |
|
| 27 | 30 |
if err := v1.ValidateID(v1ID); err != nil {
|
| 28 | 31 |
return layer.DiffID(""), err
|
| 29 | 32 |
} |
| ... | ... |
@@ -37,6 +41,9 @@ func (idserv *V1IDService) Get(v1ID, registry string) (layer.DiffID, error) {
|
| 37 | 37 |
|
| 38 | 38 |
// Set associates an image with a V1 ID. |
| 39 | 39 |
func (idserv *V1IDService) Set(v1ID, registry string, id layer.DiffID) error {
|
| 40 |
+ if idserv.store == nil {
|
|
| 41 |
+ return nil |
|
| 42 |
+ } |
|
| 40 | 43 |
if err := v1.ValidateID(v1ID); err != nil {
|
| 41 | 44 |
return err |
| 42 | 45 |
} |
| ... | ... |
@@ -5,6 +5,7 @@ import ( |
| 5 | 5 |
"crypto/sha256" |
| 6 | 6 |
"encoding/hex" |
| 7 | 7 |
"encoding/json" |
| 8 |
+ "errors" |
|
| 8 | 9 |
|
| 9 | 10 |
"github.com/docker/distribution/digest" |
| 10 | 11 |
"github.com/docker/docker/api/types" |
| ... | ... |
@@ -125,6 +126,9 @@ func (serv *v2MetadataService) digestKey(dgst digest.Digest) string {
|
| 125 | 125 |
|
| 126 | 126 |
// GetMetadata finds the metadata associated with a layer DiffID. |
| 127 | 127 |
func (serv *v2MetadataService) GetMetadata(diffID layer.DiffID) ([]V2Metadata, error) {
|
| 128 |
+ if serv.store == nil {
|
|
| 129 |
+ return nil, errors.New("no metadata storage")
|
|
| 130 |
+ } |
|
| 128 | 131 |
jsonBytes, err := serv.store.Get(serv.diffIDNamespace(), serv.diffIDKey(diffID)) |
| 129 | 132 |
if err != nil {
|
| 130 | 133 |
return nil, err |
| ... | ... |
@@ -140,6 +144,9 @@ func (serv *v2MetadataService) GetMetadata(diffID layer.DiffID) ([]V2Metadata, e |
| 140 | 140 |
|
| 141 | 141 |
// GetDiffID finds a layer DiffID from a digest. |
| 142 | 142 |
func (serv *v2MetadataService) GetDiffID(dgst digest.Digest) (layer.DiffID, error) {
|
| 143 |
+ if serv.store == nil {
|
|
| 144 |
+ return layer.DiffID(""), errors.New("no metadata storage")
|
|
| 145 |
+ } |
|
| 143 | 146 |
diffIDBytes, err := serv.store.Get(serv.digestNamespace(), serv.digestKey(dgst)) |
| 144 | 147 |
if err != nil {
|
| 145 | 148 |
return layer.DiffID(""), err
|
| ... | ... |
@@ -151,6 +158,12 @@ func (serv *v2MetadataService) GetDiffID(dgst digest.Digest) (layer.DiffID, erro |
| 151 | 151 |
// Add associates metadata with a layer DiffID. If too many metadata entries are |
| 152 | 152 |
// present, the oldest one is dropped. |
| 153 | 153 |
func (serv *v2MetadataService) Add(diffID layer.DiffID, metadata V2Metadata) error {
|
| 154 |
+ if serv.store == nil {
|
|
| 155 |
+ // Support a service which has no backend storage, in this case |
|
| 156 |
+ // an add becomes a no-op. |
|
| 157 |
+ // TODO: implement in memory storage |
|
| 158 |
+ return nil |
|
| 159 |
+ } |
|
| 154 | 160 |
oldMetadata, err := serv.GetMetadata(diffID) |
| 155 | 161 |
if err != nil {
|
| 156 | 162 |
oldMetadata = nil |
| ... | ... |
@@ -192,6 +205,12 @@ func (serv *v2MetadataService) TagAndAdd(diffID layer.DiffID, hmacKey []byte, me |
| 192 | 192 |
|
| 193 | 193 |
// Remove unassociates a metadata entry from a layer DiffID. |
| 194 | 194 |
func (serv *v2MetadataService) Remove(metadata V2Metadata) error {
|
| 195 |
+ if serv.store == nil {
|
|
| 196 |
+ // Support a service which has no backend storage, in this case |
|
| 197 |
+ // an remove becomes a no-op. |
|
| 198 |
+ // TODO: implement in memory storage |
|
| 199 |
+ return nil |
|
| 200 |
+ } |
|
| 195 | 201 |
diffID, err := serv.GetDiffID(metadata.Digest) |
| 196 | 202 |
if err != nil {
|
| 197 | 203 |
return err |
| ... | ... |
@@ -102,11 +102,7 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end |
| 102 | 102 |
scope := auth.RepositoryScope{
|
| 103 | 103 |
Repository: repoName, |
| 104 | 104 |
Actions: actions, |
| 105 |
- } |
|
| 106 |
- |
|
| 107 |
- // Keep image repositories blank for scope compatibility |
|
| 108 |
- if repoInfo.Class != "image" {
|
|
| 109 |
- scope.Class = repoInfo.Class |
|
| 105 |
+ Class: repoInfo.Class, |
|
| 110 | 106 |
} |
| 111 | 107 |
|
| 112 | 108 |
creds := registry.NewStaticCredentialStore(authConfig) |
| 113 | 109 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,44 @@ |
| 0 |
+package utils |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io" |
|
| 4 |
+ "net" |
|
| 5 |
+ "os" |
|
| 6 |
+ "syscall" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/Sirupsen/logrus" |
|
| 9 |
+ "github.com/docker/docker/pkg/progress" |
|
| 10 |
+ "github.com/docker/docker/pkg/streamformatter" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// WriteDistributionProgress is a helper for writing progress from chan to JSON |
|
| 14 |
+// stream with an optional cancel function. |
|
| 15 |
+func WriteDistributionProgress(cancelFunc func(), outStream io.Writer, progressChan <-chan progress.Progress) {
|
|
| 16 |
+ progressOutput := streamformatter.NewJSONStreamFormatter().NewProgressOutput(outStream, false) |
|
| 17 |
+ operationCancelled := false |
|
| 18 |
+ |
|
| 19 |
+ for prog := range progressChan {
|
|
| 20 |
+ if err := progressOutput.WriteProgress(prog); err != nil && !operationCancelled {
|
|
| 21 |
+ // don't log broken pipe errors as this is the normal case when a client aborts |
|
| 22 |
+ if isBrokenPipe(err) {
|
|
| 23 |
+ logrus.Info("Pull session cancelled")
|
|
| 24 |
+ } else {
|
|
| 25 |
+ logrus.Errorf("error writing progress to client: %v", err)
|
|
| 26 |
+ } |
|
| 27 |
+ cancelFunc() |
|
| 28 |
+ operationCancelled = true |
|
| 29 |
+ // Don't return, because we need to continue draining |
|
| 30 |
+ // progressChan until it's closed to avoid a deadlock. |
|
| 31 |
+ } |
|
| 32 |
+ } |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+func isBrokenPipe(e error) bool {
|
|
| 36 |
+ if netErr, ok := e.(*net.OpError); ok {
|
|
| 37 |
+ e = netErr.Err |
|
| 38 |
+ if sysErr, ok := netErr.Err.(*os.SyscallError); ok {
|
|
| 39 |
+ e = sysErr.Err |
|
| 40 |
+ } |
|
| 41 |
+ } |
|
| 42 |
+ return e == syscall.EPIPE |
|
| 43 |
+} |
| ... | ... |
@@ -109,93 +109,6 @@ commands and options, see the |
| 109 | 109 |
|
| 110 | 110 |
## Developing a plugin |
| 111 | 111 |
|
| 112 |
-Currently, there are no CLI commands available to help you develop a plugin. |
|
| 113 |
-This is expected to change in a future release. The manual process for creating |
|
| 114 |
-plugins is described in this section. |
|
| 115 |
- |
|
| 116 |
-### Plugin location and files |
|
| 117 |
- |
|
| 118 |
-Plugins are stored in `/var/lib/docker/plugins`. The `plugins.json` file lists |
|
| 119 |
-each plugin's configuration, and each plugin is stored in a directory with a |
|
| 120 |
-unique identifier. |
|
| 121 |
- |
|
| 122 |
-```bash |
|
| 123 |
-# ls -la /var/lib/docker/plugins |
|
| 124 |
-total 20 |
|
| 125 |
-drwx------ 4 root root 4096 Aug 8 18:03 . |
|
| 126 |
-drwx--x--x 12 root root 4096 Aug 8 17:53 .. |
|
| 127 |
-drwxr-xr-x 3 root root 4096 Aug 8 17:56 cd851ce43a403 |
|
| 128 |
--rw------- 1 root root 2107 Aug 8 18:03 plugins.json |
|
| 129 |
-``` |
|
| 130 |
- |
|
| 131 |
-### Format of plugins.json |
|
| 132 |
- |
|
| 133 |
-The `plugins.json` is an inventory of all installed plugins. This example shows |
|
| 134 |
-a `plugins.json` with a single plugin installed. |
|
| 135 |
- |
|
| 136 |
-```json |
|
| 137 |
-# cat plugins.json |
|
| 138 |
-{
|
|
| 139 |
- "cd851ce43a403": {
|
|
| 140 |
- "plugin": {
|
|
| 141 |
- "Config": {
|
|
| 142 |
- "Args": {
|
|
| 143 |
- "Value": null, |
|
| 144 |
- "Settable": null, |
|
| 145 |
- "Description": "", |
|
| 146 |
- "Name": "" |
|
| 147 |
- }, |
|
| 148 |
- "Env": null, |
|
| 149 |
- "Devices": null, |
|
| 150 |
- "Mounts": null, |
|
| 151 |
- "Capabilities": [ |
|
| 152 |
- "CAP_SYS_ADMIN" |
|
| 153 |
- ], |
|
| 154 |
- "Description": "sshFS plugin for Docker", |
|
| 155 |
- "Documentation": "https://docs.docker.com/engine/extend/plugins/", |
|
| 156 |
- "Interface": {
|
|
| 157 |
- "Socket": "sshfs.sock", |
|
| 158 |
- "Types": [ |
|
| 159 |
- "docker.volumedriver/1.0" |
|
| 160 |
- ] |
|
| 161 |
- }, |
|
| 162 |
- "Entrypoint": [ |
|
| 163 |
- "/go/bin/docker-volume-sshfs" |
|
| 164 |
- ], |
|
| 165 |
- "Workdir": "", |
|
| 166 |
- "User": {},
|
|
| 167 |
- "Network": {
|
|
| 168 |
- "Type": "host" |
|
| 169 |
- } |
|
| 170 |
- }, |
|
| 171 |
- "Config": {
|
|
| 172 |
- "Devices": null, |
|
| 173 |
- "Args": null, |
|
| 174 |
- "Env": [], |
|
| 175 |
- "Mounts": [] |
|
| 176 |
- }, |
|
| 177 |
- "Active": true, |
|
| 178 |
- "Tag": "latest", |
|
| 179 |
- "Name": "vieux/sshfs", |
|
| 180 |
- "Id": "cd851ce43a403" |
|
| 181 |
- } |
|
| 182 |
- } |
|
| 183 |
-} |
|
| 184 |
-``` |
|
| 185 |
- |
|
| 186 |
-### Contents of a plugin directory |
|
| 187 |
- |
|
| 188 |
-Each directory within `/var/lib/docker/plugins/` contains a `rootfs` directory |
|
| 189 |
-and two JSON files. |
|
| 190 |
- |
|
| 191 |
-```bash |
|
| 192 |
-# ls -la /var/lib/docker/plugins/cd851ce43a403 |
|
| 193 |
-total 12 |
|
| 194 |
-drwx------ 19 root root 4096 Aug 8 17:56 rootfs |
|
| 195 |
--rw-r--r-- 1 root root 50 Aug 8 17:56 plugin-settings.json |
|
| 196 |
--rw------- 1 root root 347 Aug 8 17:56 config.json |
|
| 197 |
-``` |
|
| 198 |
- |
|
| 199 | 112 |
#### The rootfs directory |
| 200 | 113 |
The `rootfs` directory represents the root filesystem of the plugin. In this |
| 201 | 114 |
example, it was created from a Dockerfile: |
| ... | ... |
@@ -206,20 +119,17 @@ plugin's filesystem for docker to communicate with the plugin. |
| 206 | 206 |
```bash |
| 207 | 207 |
$ git clone https://github.com/vieux/docker-volume-sshfs |
| 208 | 208 |
$ cd docker-volume-sshfs |
| 209 |
-$ docker build -t rootfs . |
|
| 210 |
-$ id=$(docker create rootfs true) # id was cd851ce43a403 when the image was created |
|
| 211 |
-$ sudo mkdir -p /var/lib/docker/plugins/$id/rootfs |
|
| 212 |
-$ sudo docker export "$id" | sudo tar -x -C /var/lib/docker/plugins/$id/rootfs |
|
| 213 |
-$ sudo chgrp -R docker /var/lib/docker/plugins/ |
|
| 209 |
+$ docker build -t rootfsimage . |
|
| 210 |
+$ id=$(docker create rootfsimage true) # id was cd851ce43a403 when the image was created |
|
| 211 |
+$ sudo mkdir -p myplugin/rootfs |
|
| 212 |
+$ sudo docker export "$id" | sudo tar -x -C myplugin/rootfs |
|
| 214 | 213 |
$ docker rm -vf "$id" |
| 215 |
-$ docker rmi rootfs |
|
| 214 |
+$ docker rmi rootfsimage |
|
| 216 | 215 |
``` |
| 217 | 216 |
|
| 218 |
-#### The config.json and plugin-settings.json files |
|
| 217 |
+#### The config.json file |
|
| 219 | 218 |
|
| 220 |
-The `config.json` file describes the plugin. The `plugin-settings.json` file |
|
| 221 |
-contains runtime parameters and is only required if your plugin has runtime |
|
| 222 |
-parameters. [See the Plugins Config reference](config.md). |
|
| 219 |
+The `config.json` file describes the plugin. See the [plugins config reference](config.md). |
|
| 223 | 220 |
|
| 224 | 221 |
Consider the following `config.json` file. |
| 225 | 222 |
|
| ... | ... |
@@ -242,56 +152,15 @@ Consider the following `config.json` file. |
| 242 | 242 |
This plugin is a volume driver. It requires a `host` network and the |
| 243 | 243 |
`CAP_SYS_ADMIN` capability. It depends upon the `/go/bin/docker-volume-sshfs` |
| 244 | 244 |
entrypoint and uses the `/run/docker/plugins/sshfs.sock` socket to communicate |
| 245 |
-with Docker Engine. |
|
| 246 |
- |
|
| 247 |
- |
|
| 248 |
-Consider the following `plugin-settings.json` file. |
|
| 249 |
- |
|
| 250 |
-```json |
|
| 251 |
-{
|
|
| 252 |
- "Devices": null, |
|
| 253 |
- "Args": null, |
|
| 254 |
- "Env": [], |
|
| 255 |
- "Mounts": [] |
|
| 256 |
-} |
|
| 257 |
-``` |
|
| 258 |
- |
|
| 259 |
-This plugin has no runtime parameters. |
|
| 260 |
- |
|
| 261 |
-Each of these JSON files is included as part of `plugins.json`, as you can see |
|
| 262 |
-by looking back at the example above. After a plugin is installed, `config.json` |
|
| 263 |
-is read-only, but `plugin-settings.json` is read-write, and includes all runtime |
|
| 264 |
-configuration options for the plugin. |
|
| 245 |
+with Docker Engine. This plugin has no runtime parameters. |
|
| 265 | 246 |
|
| 266 | 247 |
### Creating the plugin |
| 267 | 248 |
|
| 268 |
-Follow these steps to create a plugin: |
|
| 269 |
- |
|
| 270 |
-1. Choose a name for the plugin. Plugin name uses the same format as images, |
|
| 271 |
- for example: `<repo_name>/<name>`. |
|
| 272 |
- |
|
| 273 |
-2. Create a `rootfs` and export it to `/var/lib/docker/plugins/$id/rootfs` |
|
| 274 |
- using `docker export`. See [The rootfs directory](#the-rootfs-directory) for |
|
| 275 |
- an example of creating a `rootfs`. |
|
| 276 |
- |
|
| 277 |
-3. Create a `config.json` file in `/var/lib/docker/plugins/$id/`. |
|
| 278 |
- |
|
| 279 |
-4. Create a `plugin-settings.json` file if needed. |
|
| 280 |
- |
|
| 281 |
-5. Create or add a section to `/var/lib/docker/plugins/plugins.json`. Use |
|
| 282 |
- `<user>/<name>` as “Name” and `$id` as “Id”. |
|
| 283 |
- |
|
| 284 |
-6. Restart the Docker Engine service. |
|
| 285 |
- |
|
| 286 |
-7. Run `docker plugin ls`. |
|
| 287 |
- * If your plugin is enabled, you can push it to the |
|
| 288 |
- registry. |
|
| 289 |
- * If the plugin is not listed or is disabled, something went wrong. |
|
| 290 |
- Check the daemon logs for errors. |
|
| 291 |
- |
|
| 292 |
-8. If you are not already logged in, use `docker login` to authenticate against |
|
| 293 |
- the registry so that you can push to it. |
|
| 294 |
- |
|
| 295 |
-9. Run `docker plugin push <repo_name>/<name>` to push the plugin. |
|
| 249 |
+A new plugin can be created by running |
|
| 250 |
+`docker plugin create <plugin-name> ./path/to/plugin/data` where the plugin |
|
| 251 |
+data contains a plugin configuration file `config.json` and a root filesystem |
|
| 252 |
+in subdirectory `rootfs`. |
|
| 296 | 253 |
|
| 297 |
-The plugin can now be used by any user with access to your registry. |
|
| 254 |
+After that the plugin `<plugin-name>` will show up in `docker plugin ls`. |
|
| 255 |
+Plugins can be pushed to remote registries with |
|
| 256 |
+`docker plugin push <plugin-name>`. |
|
| 298 | 257 |
\ No newline at end of file |
| ... | ... |
@@ -16,9 +16,9 @@ keywords: "plugin, create" |
| 16 | 16 |
# plugin create |
| 17 | 17 |
|
| 18 | 18 |
```markdown |
| 19 |
-Usage: docker plugin create [OPTIONS] PLUGIN[:tag] PATH-TO-ROOTFS(rootfs + config.json) |
|
| 19 |
+Usage: docker plugin create [OPTIONS] PLUGIN PLUGIN-DATA-DIR |
|
| 20 | 20 |
|
| 21 |
-Create a plugin from a rootfs and configuration |
|
| 21 |
+Create a plugin from a rootfs and configuration. Plugin data directory must contain config.json and rootfs directory. |
|
| 22 | 22 |
|
| 23 | 23 |
Options: |
| 24 | 24 |
--compress Compress the context using gzip |
| ... | ... |
@@ -36,8 +36,7 @@ $ docker plugin inspect tiborvass/no-remove:latest |
| 36 | 36 |
```JSON |
| 37 | 37 |
{
|
| 38 | 38 |
"Id": "8c74c978c434745c3ade82f1bc0acf38d04990eaf494fa507c16d9f1daa99c21", |
| 39 |
- "Name": "tiborvass/no-remove", |
|
| 40 |
- "Tag": "latest", |
|
| 39 |
+ "Name": "tiborvass/no-remove:latest", |
|
| 41 | 40 |
"Enabled": true, |
| 42 | 41 |
"Config": {
|
| 43 | 42 |
"Mounts": [ |
| ... | ... |
@@ -21,6 +21,7 @@ Usage: docker plugin install [OPTIONS] PLUGIN [KEY=VALUE...] |
| 21 | 21 |
Install a plugin |
| 22 | 22 |
|
| 23 | 23 |
Options: |
| 24 |
+ --alias string Local name for plugin |
|
| 24 | 25 |
--disable Do not enable the plugin on install |
| 25 | 26 |
--grant-all-permissions Grant all permissions necessary to run the plugin |
| 26 | 27 |
--help Print usage |
| ... | ... |
@@ -12,10 +12,10 @@ import ( |
| 12 | 12 |
) |
| 13 | 13 |
|
| 14 | 14 |
var ( |
| 15 |
- authzPluginName = "riyaz/authz-no-volume-plugin" |
|
| 15 |
+ authzPluginName = "tonistiigi/authz-no-volume-plugin" |
|
| 16 | 16 |
authzPluginTag = "latest" |
| 17 | 17 |
authzPluginNameWithTag = authzPluginName + ":" + authzPluginTag |
| 18 |
- authzPluginBadManifestName = "riyaz/authz-plugin-bad-manifest" |
|
| 18 |
+ authzPluginBadManifestName = "tonistiigi/authz-plugin-bad-manifest" |
|
| 19 | 19 |
nonexistentAuthzPluginName = "riyaz/nonexistent-authz-plugin" |
| 20 | 20 |
) |
| 21 | 21 |
|
| ... | ... |
@@ -425,20 +425,20 @@ func (s *DockerSuite) TestInspectPlugin(c *check.C) {
|
| 425 | 425 |
|
| 426 | 426 |
out, _, err := dockerCmdWithError("inspect", "--type", "plugin", "--format", "{{.Name}}", pNameWithTag)
|
| 427 | 427 |
c.Assert(err, checker.IsNil) |
| 428 |
- c.Assert(strings.TrimSpace(out), checker.Equals, pName) |
|
| 428 |
+ c.Assert(strings.TrimSpace(out), checker.Equals, pNameWithTag) |
|
| 429 | 429 |
|
| 430 | 430 |
out, _, err = dockerCmdWithError("inspect", "--format", "{{.Name}}", pNameWithTag)
|
| 431 | 431 |
c.Assert(err, checker.IsNil) |
| 432 |
- c.Assert(strings.TrimSpace(out), checker.Equals, pName) |
|
| 432 |
+ c.Assert(strings.TrimSpace(out), checker.Equals, pNameWithTag) |
|
| 433 | 433 |
|
| 434 | 434 |
// Even without tag the inspect still work |
| 435 |
- out, _, err = dockerCmdWithError("inspect", "--type", "plugin", "--format", "{{.Name}}", pName)
|
|
| 435 |
+ out, _, err = dockerCmdWithError("inspect", "--type", "plugin", "--format", "{{.Name}}", pNameWithTag)
|
|
| 436 | 436 |
c.Assert(err, checker.IsNil) |
| 437 |
- c.Assert(strings.TrimSpace(out), checker.Equals, pName) |
|
| 437 |
+ c.Assert(strings.TrimSpace(out), checker.Equals, pNameWithTag) |
|
| 438 | 438 |
|
| 439 |
- out, _, err = dockerCmdWithError("inspect", "--format", "{{.Name}}", pName)
|
|
| 439 |
+ out, _, err = dockerCmdWithError("inspect", "--format", "{{.Name}}", pNameWithTag)
|
|
| 440 | 440 |
c.Assert(err, checker.IsNil) |
| 441 |
- c.Assert(strings.TrimSpace(out), checker.Equals, pName) |
|
| 441 |
+ c.Assert(strings.TrimSpace(out), checker.Equals, pNameWithTag) |
|
| 442 | 442 |
|
| 443 | 443 |
_, _, err = dockerCmdWithError("plugin", "disable", pNameWithTag)
|
| 444 | 444 |
c.Assert(err, checker.IsNil) |
| ... | ... |
@@ -777,7 +777,7 @@ func (s *DockerNetworkSuite) TestDockerPluginV2NetworkDriver(c *check.C) {
|
| 777 | 777 |
testRequires(c, DaemonIsLinux, IsAmd64, Network) |
| 778 | 778 |
|
| 779 | 779 |
var ( |
| 780 |
- npName = "tiborvass/test-docker-netplugin" |
|
| 780 |
+ npName = "tonistiigi/test-docker-netplugin" |
|
| 781 | 781 |
npTag = "latest" |
| 782 | 782 |
npNameWithTag = npName + ":" + npTag |
| 783 | 783 |
) |
| ... | ... |
@@ -1,6 +1,8 @@ |
| 1 | 1 |
package main |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "fmt" |
|
| 5 |
+ |
|
| 4 | 6 |
"github.com/docker/docker/pkg/integration/checker" |
| 5 | 7 |
"github.com/go-check/check" |
| 6 | 8 |
|
| ... | ... |
@@ -12,7 +14,7 @@ import ( |
| 12 | 12 |
|
| 13 | 13 |
var ( |
| 14 | 14 |
pluginProcessName = "sample-volume-plugin" |
| 15 |
- pName = "tiborvass/sample-volume-plugin" |
|
| 15 |
+ pName = "tonistiigi/sample-volume-plugin" |
|
| 16 | 16 |
pTag = "latest" |
| 17 | 17 |
pNameWithTag = pName + ":" + pTag |
| 18 | 18 |
) |
| ... | ... |
@@ -139,11 +141,18 @@ func (s *DockerSuite) TestPluginInstallArgs(c *check.C) {
|
| 139 | 139 |
c.Assert(strings.TrimSpace(env), checker.Equals, "[DEBUG=1]") |
| 140 | 140 |
} |
| 141 | 141 |
|
| 142 |
-func (s *DockerSuite) TestPluginInstallImage(c *check.C) {
|
|
| 143 |
- testRequires(c, DaemonIsLinux, IsAmd64, Network) |
|
| 144 |
- out, _, err := dockerCmdWithError("plugin", "install", "redis")
|
|
| 142 |
+func (s *DockerRegistrySuite) TestPluginInstallImage(c *check.C) {
|
|
| 143 |
+ testRequires(c, DaemonIsLinux, IsAmd64) |
|
| 144 |
+ |
|
| 145 |
+ repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
|
| 146 |
+ // tag the image to upload it to the private registry |
|
| 147 |
+ dockerCmd(c, "tag", "busybox", repoName) |
|
| 148 |
+ // push the image to the registry |
|
| 149 |
+ dockerCmd(c, "push", repoName) |
|
| 150 |
+ |
|
| 151 |
+ out, _, err := dockerCmdWithError("plugin", "install", repoName)
|
|
| 145 | 152 |
c.Assert(err, checker.NotNil) |
| 146 |
- c.Assert(out, checker.Contains, "content is not a plugin") |
|
| 153 |
+ c.Assert(out, checker.Contains, "target is image") |
|
| 147 | 154 |
} |
| 148 | 155 |
|
| 149 | 156 |
func (s *DockerSuite) TestPluginEnableDisableNegative(c *check.C) {
|
| ... | ... |
@@ -179,6 +188,9 @@ func (s *DockerSuite) TestPluginCreate(c *check.C) {
|
| 179 | 179 |
err = ioutil.WriteFile(filepath.Join(temp, "config.json"), []byte(data), 0644) |
| 180 | 180 |
c.Assert(err, checker.IsNil) |
| 181 | 181 |
|
| 182 |
+ err = os.MkdirAll(filepath.Join(temp, "rootfs"), 0700) |
|
| 183 |
+ c.Assert(err, checker.IsNil) |
|
| 184 |
+ |
|
| 182 | 185 |
out, _, err := dockerCmdWithError("plugin", "create", name, temp)
|
| 183 | 186 |
c.Assert(err, checker.IsNil) |
| 184 | 187 |
c.Assert(out, checker.Contains, name) |
| ... | ... |
@@ -31,7 +31,7 @@ import ( |
| 31 | 31 |
icmd "github.com/docker/docker/pkg/integration/cmd" |
| 32 | 32 |
"github.com/docker/docker/pkg/ioutils" |
| 33 | 33 |
"github.com/docker/docker/pkg/stringutils" |
| 34 |
- "github.com/docker/go-units" |
|
| 34 |
+ units "github.com/docker/go-units" |
|
| 35 | 35 |
"github.com/go-check/check" |
| 36 | 36 |
) |
| 37 | 37 |
|
| ... | ... |
@@ -250,11 +250,7 @@ func deleteAllPlugins(c *check.C) {
|
| 250 | 250 |
var errs []string |
| 251 | 251 |
for _, p := range plugins {
|
| 252 | 252 |
pluginName := p.Name |
| 253 |
- tag := p.Tag |
|
| 254 |
- if tag == "" {
|
|
| 255 |
- tag = "latest" |
|
| 256 |
- } |
|
| 257 |
- status, b, err := sockRequest("DELETE", "/plugins/"+pluginName+":"+tag+"?force=1", nil)
|
|
| 253 |
+ status, b, err := sockRequest("DELETE", "/plugins/"+pluginName+"?force=1", nil)
|
|
| 258 | 254 |
if err != nil {
|
| 259 | 255 |
errs = append(errs, err.Error()) |
| 260 | 256 |
continue |
| ... | ... |
@@ -44,6 +44,17 @@ func ChanOutput(progressChan chan<- Progress) Output {
|
| 44 | 44 |
return chanOutput(progressChan) |
| 45 | 45 |
} |
| 46 | 46 |
|
| 47 |
+type discardOutput struct{}
|
|
| 48 |
+ |
|
| 49 |
+func (discardOutput) WriteProgress(Progress) error {
|
|
| 50 |
+ return nil |
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+// DiscardOutput returns an Output that discards progress |
|
| 54 |
+func DiscardOutput() Output {
|
|
| 55 |
+ return discardOutput{}
|
|
| 56 |
+} |
|
| 57 |
+ |
|
| 47 | 58 |
// Update is a convenience function to write a progress update to the channel. |
| 48 | 59 |
func Update(out Output, id, action string) {
|
| 49 | 60 |
out.WriteProgress(Progress{ID: id, Action: action})
|
| ... | ... |
@@ -3,37 +3,39 @@ |
| 3 | 3 |
package plugin |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 |
- "bytes" |
|
| 6 |
+ "archive/tar" |
|
| 7 |
+ "compress/gzip" |
|
| 7 | 8 |
"encoding/json" |
| 8 | 9 |
"fmt" |
| 9 | 10 |
"io" |
| 10 | 11 |
"io/ioutil" |
| 11 | 12 |
"net/http" |
| 12 | 13 |
"os" |
| 14 |
+ "path" |
|
| 13 | 15 |
"path/filepath" |
| 14 |
- "reflect" |
|
| 15 |
- "regexp" |
|
| 16 |
+ "strings" |
|
| 16 | 17 |
|
| 17 | 18 |
"github.com/Sirupsen/logrus" |
| 19 |
+ "github.com/docker/distribution/digest" |
|
| 20 |
+ "github.com/docker/distribution/manifest/schema2" |
|
| 18 | 21 |
"github.com/docker/docker/api/types" |
| 19 |
- "github.com/docker/docker/pkg/archive" |
|
| 22 |
+ "github.com/docker/docker/distribution" |
|
| 23 |
+ progressutils "github.com/docker/docker/distribution/utils" |
|
| 24 |
+ "github.com/docker/docker/distribution/xfer" |
|
| 25 |
+ "github.com/docker/docker/image" |
|
| 26 |
+ "github.com/docker/docker/layer" |
|
| 20 | 27 |
"github.com/docker/docker/pkg/chrootarchive" |
| 21 |
- "github.com/docker/docker/pkg/stringid" |
|
| 22 |
- "github.com/docker/docker/plugin/distribution" |
|
| 28 |
+ "github.com/docker/docker/pkg/pools" |
|
| 29 |
+ "github.com/docker/docker/pkg/progress" |
|
| 23 | 30 |
"github.com/docker/docker/plugin/v2" |
| 24 | 31 |
"github.com/docker/docker/reference" |
| 25 | 32 |
"github.com/pkg/errors" |
| 26 | 33 |
"golang.org/x/net/context" |
| 27 | 34 |
) |
| 28 | 35 |
|
| 29 |
-var ( |
|
| 30 |
- validFullID = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
|
| 31 |
- validPartialID = regexp.MustCompile(`^([a-f0-9]{1,64})$`)
|
|
| 32 |
-) |
|
| 33 |
- |
|
| 34 | 36 |
// Disable deactivates a plugin. This means resources (volumes, networks) cant use them. |
| 35 |
-func (pm *Manager) Disable(name string, config *types.PluginDisableConfig) error {
|
|
| 36 |
- p, err := pm.pluginStore.GetByName(name) |
|
| 37 |
+func (pm *Manager) Disable(refOrID string, config *types.PluginDisableConfig) error {
|
|
| 38 |
+ p, err := pm.config.Store.GetV2Plugin(refOrID) |
|
| 37 | 39 |
if err != nil {
|
| 38 | 40 |
return err |
| 39 | 41 |
} |
| ... | ... |
@@ -48,13 +50,13 @@ func (pm *Manager) Disable(name string, config *types.PluginDisableConfig) error |
| 48 | 48 |
if err := pm.disable(p, c); err != nil {
|
| 49 | 49 |
return err |
| 50 | 50 |
} |
| 51 |
- pm.pluginEventLogger(p.GetID(), name, "disable") |
|
| 51 |
+ pm.config.LogPluginEvent(p.GetID(), refOrID, "disable") |
|
| 52 | 52 |
return nil |
| 53 | 53 |
} |
| 54 | 54 |
|
| 55 | 55 |
// Enable activates a plugin, which implies that they are ready to be used by containers. |
| 56 |
-func (pm *Manager) Enable(name string, config *types.PluginEnableConfig) error {
|
|
| 57 |
- p, err := pm.pluginStore.GetByName(name) |
|
| 56 |
+func (pm *Manager) Enable(refOrID string, config *types.PluginEnableConfig) error {
|
|
| 57 |
+ p, err := pm.config.Store.GetV2Plugin(refOrID) |
|
| 58 | 58 |
if err != nil {
|
| 59 | 59 |
return err |
| 60 | 60 |
} |
| ... | ... |
@@ -63,71 +65,74 @@ func (pm *Manager) Enable(name string, config *types.PluginEnableConfig) error {
|
| 63 | 63 |
if err := pm.enable(p, c, false); err != nil {
|
| 64 | 64 |
return err |
| 65 | 65 |
} |
| 66 |
- pm.pluginEventLogger(p.GetID(), name, "enable") |
|
| 66 |
+ pm.config.LogPluginEvent(p.GetID(), refOrID, "enable") |
|
| 67 | 67 |
return nil |
| 68 | 68 |
} |
| 69 | 69 |
|
| 70 | 70 |
// Inspect examines a plugin config |
| 71 |
-func (pm *Manager) Inspect(refOrID string) (tp types.Plugin, err error) {
|
|
| 72 |
- // Match on full ID |
|
| 73 |
- if validFullID.MatchString(refOrID) {
|
|
| 74 |
- p, err := pm.pluginStore.GetByID(refOrID) |
|
| 75 |
- if err == nil {
|
|
| 76 |
- return p.PluginObj, nil |
|
| 77 |
- } |
|
| 71 |
+func (pm *Manager) Inspect(refOrID string) (tp *types.Plugin, err error) {
|
|
| 72 |
+ p, err := pm.config.Store.GetV2Plugin(refOrID) |
|
| 73 |
+ if err != nil {
|
|
| 74 |
+ return nil, err |
|
| 78 | 75 |
} |
| 79 | 76 |
|
| 80 |
- // Match on full name |
|
| 81 |
- if pluginName, err := getPluginName(refOrID); err == nil {
|
|
| 82 |
- if p, err := pm.pluginStore.GetByName(pluginName); err == nil {
|
|
| 83 |
- return p.PluginObj, nil |
|
| 84 |
- } |
|
| 85 |
- } |
|
| 77 |
+ return &p.PluginObj, nil |
|
| 78 |
+} |
|
| 86 | 79 |
|
| 87 |
- // Match on partial ID |
|
| 88 |
- if validPartialID.MatchString(refOrID) {
|
|
| 89 |
- p, err := pm.pluginStore.Search(refOrID) |
|
| 90 |
- if err == nil {
|
|
| 91 |
- return p.PluginObj, nil |
|
| 92 |
- } |
|
| 93 |
- return tp, err |
|
| 80 |
+func (pm *Manager) pull(ctx context.Context, ref reference.Named, config *distribution.ImagePullConfig, outStream io.Writer) error {
|
|
| 81 |
+ if outStream != nil {
|
|
| 82 |
+ // Include a buffer so that slow client connections don't affect |
|
| 83 |
+ // transfer performance. |
|
| 84 |
+ progressChan := make(chan progress.Progress, 100) |
|
| 85 |
+ |
|
| 86 |
+ writesDone := make(chan struct{})
|
|
| 87 |
+ |
|
| 88 |
+ defer func() {
|
|
| 89 |
+ close(progressChan) |
|
| 90 |
+ <-writesDone |
|
| 91 |
+ }() |
|
| 92 |
+ |
|
| 93 |
+ var cancelFunc context.CancelFunc |
|
| 94 |
+ ctx, cancelFunc = context.WithCancel(ctx) |
|
| 95 |
+ |
|
| 96 |
+ go func() {
|
|
| 97 |
+ progressutils.WriteDistributionProgress(cancelFunc, outStream, progressChan) |
|
| 98 |
+ close(writesDone) |
|
| 99 |
+ }() |
|
| 100 |
+ |
|
| 101 |
+ config.ProgressOutput = progress.ChanOutput(progressChan) |
|
| 102 |
+ } else {
|
|
| 103 |
+ config.ProgressOutput = progress.DiscardOutput() |
|
| 94 | 104 |
} |
| 105 |
+ return distribution.Pull(ctx, ref, config) |
|
| 106 |
+} |
|
| 95 | 107 |
|
| 96 |
- return tp, fmt.Errorf("no such plugin name or ID associated with %q", refOrID)
|
|
| 108 |
+type tempConfigStore struct {
|
|
| 109 |
+ config []byte |
|
| 110 |
+ configDigest digest.Digest |
|
| 97 | 111 |
} |
| 98 | 112 |
|
| 99 |
-func (pm *Manager) pull(name string, metaHeader http.Header, authConfig *types.AuthConfig) (reference.Named, distribution.PullData, error) {
|
|
| 100 |
- ref, err := distribution.GetRef(name) |
|
| 101 |
- if err != nil {
|
|
| 102 |
- logrus.Debugf("error in distribution.GetRef: %v", err)
|
|
| 103 |
- return nil, nil, err |
|
| 104 |
- } |
|
| 105 |
- name = ref.String() |
|
| 113 |
+func (s *tempConfigStore) Put(c []byte) (digest.Digest, error) {
|
|
| 114 |
+ dgst := digest.FromBytes(c) |
|
| 106 | 115 |
|
| 107 |
- if p, _ := pm.pluginStore.GetByName(name); p != nil {
|
|
| 108 |
- logrus.Debug("plugin already exists")
|
|
| 109 |
- return nil, nil, fmt.Errorf("%s exists", name)
|
|
| 110 |
- } |
|
| 116 |
+ s.config = c |
|
| 117 |
+ s.configDigest = dgst |
|
| 111 | 118 |
|
| 112 |
- pd, err := distribution.Pull(ref, pm.registryService, metaHeader, authConfig) |
|
| 113 |
- if err != nil {
|
|
| 114 |
- logrus.Debugf("error in distribution.Pull(): %v", err)
|
|
| 115 |
- return nil, nil, err |
|
| 116 |
- } |
|
| 117 |
- return ref, pd, nil |
|
| 119 |
+ return dgst, nil |
|
| 118 | 120 |
} |
| 119 | 121 |
|
| 120 |
-func computePrivileges(pd distribution.PullData) (types.PluginPrivileges, error) {
|
|
| 121 |
- config, err := pd.Config() |
|
| 122 |
- if err != nil {
|
|
| 123 |
- return nil, err |
|
| 122 |
+func (s *tempConfigStore) Get(d digest.Digest) ([]byte, error) {
|
|
| 123 |
+ if d != s.configDigest {
|
|
| 124 |
+ return nil, digest.ErrDigestNotFound |
|
| 124 | 125 |
} |
| 126 |
+ return s.config, nil |
|
| 127 |
+} |
|
| 125 | 128 |
|
| 126 |
- var c types.PluginConfig |
|
| 127 |
- if err := json.Unmarshal(config, &c); err != nil {
|
|
| 128 |
- return nil, err |
|
| 129 |
- } |
|
| 129 |
+func (s *tempConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
|
|
| 130 |
+ return configToRootFS(c) |
|
| 131 |
+} |
|
| 130 | 132 |
|
| 133 |
+func computePrivileges(c types.PluginConfig) (types.PluginPrivileges, error) {
|
|
| 131 | 134 |
var privileges types.PluginPrivileges |
| 132 | 135 |
if c.Network.Type != "null" && c.Network.Type != "bridge" && c.Network.Type != "" {
|
| 133 | 136 |
privileges = append(privileges, types.PluginPrivilege{
|
| ... | ... |
@@ -173,67 +178,89 @@ func computePrivileges(pd distribution.PullData) (types.PluginPrivileges, error) |
| 173 | 173 |
} |
| 174 | 174 |
|
| 175 | 175 |
// Privileges pulls a plugin config and computes the privileges required to install it. |
| 176 |
-func (pm *Manager) Privileges(name string, metaHeader http.Header, authConfig *types.AuthConfig) (types.PluginPrivileges, error) {
|
|
| 177 |
- _, pd, err := pm.pull(name, metaHeader, authConfig) |
|
| 178 |
- if err != nil {
|
|
| 176 |
+func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHeader http.Header, authConfig *types.AuthConfig) (types.PluginPrivileges, error) {
|
|
| 177 |
+ // create image store instance |
|
| 178 |
+ cs := &tempConfigStore{}
|
|
| 179 |
+ |
|
| 180 |
+ // DownloadManager not defined because only pulling configuration. |
|
| 181 |
+ pluginPullConfig := &distribution.ImagePullConfig{
|
|
| 182 |
+ Config: distribution.Config{
|
|
| 183 |
+ MetaHeaders: metaHeader, |
|
| 184 |
+ AuthConfig: authConfig, |
|
| 185 |
+ RegistryService: pm.config.RegistryService, |
|
| 186 |
+ ImageEventLogger: func(string, string, string) {},
|
|
| 187 |
+ ImageStore: cs, |
|
| 188 |
+ }, |
|
| 189 |
+ Schema2Types: distribution.PluginTypes, |
|
| 190 |
+ } |
|
| 191 |
+ |
|
| 192 |
+ if err := pm.pull(ctx, ref, pluginPullConfig, nil); err != nil {
|
|
| 179 | 193 |
return nil, err |
| 180 | 194 |
} |
| 181 |
- return computePrivileges(pd) |
|
| 195 |
+ |
|
| 196 |
+ if cs.config == nil {
|
|
| 197 |
+ return nil, errors.New("no configuration pulled")
|
|
| 198 |
+ } |
|
| 199 |
+ var config types.PluginConfig |
|
| 200 |
+ if err := json.Unmarshal(cs.config, &config); err != nil {
|
|
| 201 |
+ return nil, err |
|
| 202 |
+ } |
|
| 203 |
+ |
|
| 204 |
+ return computePrivileges(config) |
|
| 182 | 205 |
} |
| 183 | 206 |
|
| 184 | 207 |
// Pull pulls a plugin, check if the correct privileges are provided and install the plugin. |
| 185 |
-func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges) (err error) {
|
|
| 186 |
- ref, pd, err := pm.pull(name, metaHeader, authConfig) |
|
| 208 |
+func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
|
|
| 209 |
+ pm.muGC.RLock() |
|
| 210 |
+ defer pm.muGC.RUnlock() |
|
| 211 |
+ |
|
| 212 |
+ // revalidate because Pull is public |
|
| 213 |
+ nameref, err := reference.ParseNamed(name) |
|
| 187 | 214 |
if err != nil {
|
| 188 |
- return err |
|
| 215 |
+ return errors.Wrapf(err, "failed to parse %q", name) |
|
| 189 | 216 |
} |
| 217 |
+ name = reference.WithDefaultTag(nameref).String() |
|
| 190 | 218 |
|
| 191 |
- requiredPrivileges, err := computePrivileges(pd) |
|
| 192 |
- if err != nil {
|
|
| 219 |
+ if err := pm.config.Store.validateName(name); err != nil {
|
|
| 193 | 220 |
return err |
| 194 | 221 |
} |
| 195 | 222 |
|
| 196 |
- if !reflect.DeepEqual(privileges, requiredPrivileges) {
|
|
| 197 |
- return errors.New("incorrect privileges")
|
|
| 198 |
- } |
|
| 223 |
+ tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs") |
|
| 224 |
+ defer os.RemoveAll(tmpRootFSDir) |
|
| 199 | 225 |
|
| 200 |
- pluginID := stringid.GenerateNonCryptoID() |
|
| 201 |
- pluginDir := filepath.Join(pm.libRoot, pluginID) |
|
| 202 |
- if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
|
| 203 |
- logrus.Debugf("error in MkdirAll: %v", err)
|
|
| 204 |
- return err |
|
| 226 |
+ dm := &downloadManager{
|
|
| 227 |
+ tmpDir: tmpRootFSDir, |
|
| 228 |
+ blobStore: pm.blobStore, |
|
| 205 | 229 |
} |
| 206 | 230 |
|
| 207 |
- defer func() {
|
|
| 208 |
- if err != nil {
|
|
| 209 |
- if delErr := os.RemoveAll(pluginDir); delErr != nil {
|
|
| 210 |
- logrus.Warnf("unable to remove %q from failed plugin pull: %v", pluginDir, delErr)
|
|
| 211 |
- } |
|
| 212 |
- } |
|
| 213 |
- }() |
|
| 231 |
+ pluginPullConfig := &distribution.ImagePullConfig{
|
|
| 232 |
+ Config: distribution.Config{
|
|
| 233 |
+ MetaHeaders: metaHeader, |
|
| 234 |
+ AuthConfig: authConfig, |
|
| 235 |
+ RegistryService: pm.config.RegistryService, |
|
| 236 |
+ ImageEventLogger: pm.config.LogPluginEvent, |
|
| 237 |
+ ImageStore: dm, |
|
| 238 |
+ }, |
|
| 239 |
+ DownloadManager: dm, // todo: reevaluate if possible to substitute distribution/xfer dependencies instead |
|
| 240 |
+ Schema2Types: distribution.PluginTypes, |
|
| 241 |
+ } |
|
| 214 | 242 |
|
| 215 |
- err = distribution.WritePullData(pd, filepath.Join(pm.libRoot, pluginID), true) |
|
| 243 |
+ err = pm.pull(ctx, ref, pluginPullConfig, outStream) |
|
| 216 | 244 |
if err != nil {
|
| 217 |
- logrus.Debugf("error in distribution.WritePullData(): %v", err)
|
|
| 245 |
+ go pm.GC() |
|
| 218 | 246 |
return err |
| 219 | 247 |
} |
| 220 | 248 |
|
| 221 |
- tag := distribution.GetTag(ref) |
|
| 222 |
- p := v2.NewPlugin(ref.Name(), pluginID, pm.runRoot, pm.libRoot, tag) |
|
| 223 |
- err = p.InitPlugin() |
|
| 224 |
- if err != nil {
|
|
| 249 |
+ if _, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
|
|
| 225 | 250 |
return err |
| 226 | 251 |
} |
| 227 |
- pm.pluginStore.Add(p) |
|
| 228 |
- |
|
| 229 |
- pm.pluginEventLogger(pluginID, ref.String(), "pull") |
|
| 230 | 252 |
|
| 231 | 253 |
return nil |
| 232 | 254 |
} |
| 233 | 255 |
|
| 234 | 256 |
// List displays the list of plugins and associated metadata. |
| 235 | 257 |
func (pm *Manager) List() ([]types.Plugin, error) {
|
| 236 |
- plugins := pm.pluginStore.GetAll() |
|
| 258 |
+ plugins := pm.config.Store.GetAll() |
|
| 237 | 259 |
out := make([]types.Plugin, 0, len(plugins)) |
| 238 | 260 |
for _, p := range plugins {
|
| 239 | 261 |
out = append(out, p.PluginObj) |
| ... | ... |
@@ -242,38 +269,211 @@ func (pm *Manager) List() ([]types.Plugin, error) {
|
| 242 | 242 |
} |
| 243 | 243 |
|
| 244 | 244 |
// Push pushes a plugin to the store. |
| 245 |
-func (pm *Manager) Push(name string, metaHeader http.Header, authConfig *types.AuthConfig) error {
|
|
| 246 |
- p, err := pm.pluginStore.GetByName(name) |
|
| 245 |
+func (pm *Manager) Push(ctx context.Context, name string, metaHeader http.Header, authConfig *types.AuthConfig, outStream io.Writer) error {
|
|
| 246 |
+ p, err := pm.config.Store.GetV2Plugin(name) |
|
| 247 | 247 |
if err != nil {
|
| 248 | 248 |
return err |
| 249 | 249 |
} |
| 250 |
- dest := filepath.Join(pm.libRoot, p.GetID()) |
|
| 251 |
- config, err := ioutil.ReadFile(filepath.Join(dest, "config.json")) |
|
| 250 |
+ |
|
| 251 |
+ ref, err := reference.ParseNamed(p.Name()) |
|
| 252 | 252 |
if err != nil {
|
| 253 |
- return err |
|
| 253 |
+ return errors.Wrapf(err, "plugin has invalid name %v for push", p.Name()) |
|
| 254 | 254 |
} |
| 255 | 255 |
|
| 256 |
- var dummy types.Plugin |
|
| 257 |
- err = json.Unmarshal(config, &dummy) |
|
| 258 |
- if err != nil {
|
|
| 259 |
- return err |
|
| 256 |
+ var po progress.Output |
|
| 257 |
+ if outStream != nil {
|
|
| 258 |
+ // Include a buffer so that slow client connections don't affect |
|
| 259 |
+ // transfer performance. |
|
| 260 |
+ progressChan := make(chan progress.Progress, 100) |
|
| 261 |
+ |
|
| 262 |
+ writesDone := make(chan struct{})
|
|
| 263 |
+ |
|
| 264 |
+ defer func() {
|
|
| 265 |
+ close(progressChan) |
|
| 266 |
+ <-writesDone |
|
| 267 |
+ }() |
|
| 268 |
+ |
|
| 269 |
+ var cancelFunc context.CancelFunc |
|
| 270 |
+ ctx, cancelFunc = context.WithCancel(ctx) |
|
| 271 |
+ |
|
| 272 |
+ go func() {
|
|
| 273 |
+ progressutils.WriteDistributionProgress(cancelFunc, outStream, progressChan) |
|
| 274 |
+ close(writesDone) |
|
| 275 |
+ }() |
|
| 276 |
+ |
|
| 277 |
+ po = progress.ChanOutput(progressChan) |
|
| 278 |
+ } else {
|
|
| 279 |
+ po = progress.DiscardOutput() |
|
| 280 |
+ } |
|
| 281 |
+ |
|
| 282 |
+ // TODO: replace these with manager |
|
| 283 |
+ is := &pluginConfigStore{
|
|
| 284 |
+ pm: pm, |
|
| 285 |
+ plugin: p, |
|
| 286 |
+ } |
|
| 287 |
+ ls := &pluginLayerProvider{
|
|
| 288 |
+ pm: pm, |
|
| 289 |
+ plugin: p, |
|
| 290 |
+ } |
|
| 291 |
+ rs := &pluginReference{
|
|
| 292 |
+ name: ref, |
|
| 293 |
+ pluginID: p.Config, |
|
| 260 | 294 |
} |
| 261 | 295 |
|
| 262 |
- rootfs, err := archive.Tar(p.Rootfs, archive.Gzip) |
|
| 296 |
+ uploadManager := xfer.NewLayerUploadManager(3) |
|
| 297 |
+ |
|
| 298 |
+ imagePushConfig := &distribution.ImagePushConfig{
|
|
| 299 |
+ Config: distribution.Config{
|
|
| 300 |
+ MetaHeaders: metaHeader, |
|
| 301 |
+ AuthConfig: authConfig, |
|
| 302 |
+ ProgressOutput: po, |
|
| 303 |
+ RegistryService: pm.config.RegistryService, |
|
| 304 |
+ ReferenceStore: rs, |
|
| 305 |
+ ImageEventLogger: pm.config.LogPluginEvent, |
|
| 306 |
+ ImageStore: is, |
|
| 307 |
+ RequireSchema2: true, |
|
| 308 |
+ }, |
|
| 309 |
+ ConfigMediaType: schema2.MediaTypePluginConfig, |
|
| 310 |
+ LayerStore: ls, |
|
| 311 |
+ UploadManager: uploadManager, |
|
| 312 |
+ } |
|
| 313 |
+ |
|
| 314 |
+ return distribution.Push(ctx, ref, imagePushConfig) |
|
| 315 |
+} |
|
| 316 |
+ |
|
| 317 |
+type pluginReference struct {
|
|
| 318 |
+ name reference.Named |
|
| 319 |
+ pluginID digest.Digest |
|
| 320 |
+} |
|
| 321 |
+ |
|
| 322 |
+func (r *pluginReference) References(id digest.Digest) []reference.Named {
|
|
| 323 |
+ if r.pluginID != id {
|
|
| 324 |
+ return nil |
|
| 325 |
+ } |
|
| 326 |
+ return []reference.Named{r.name}
|
|
| 327 |
+} |
|
| 328 |
+ |
|
| 329 |
+func (r *pluginReference) ReferencesByName(ref reference.Named) []reference.Association {
|
|
| 330 |
+ return []reference.Association{
|
|
| 331 |
+ {
|
|
| 332 |
+ Ref: r.name, |
|
| 333 |
+ ID: r.pluginID, |
|
| 334 |
+ }, |
|
| 335 |
+ } |
|
| 336 |
+} |
|
| 337 |
+ |
|
| 338 |
+func (r *pluginReference) Get(ref reference.Named) (digest.Digest, error) {
|
|
| 339 |
+ if r.name.String() != ref.String() {
|
|
| 340 |
+ return digest.Digest(""), reference.ErrDoesNotExist
|
|
| 341 |
+ } |
|
| 342 |
+ return r.pluginID, nil |
|
| 343 |
+} |
|
| 344 |
+ |
|
| 345 |
+func (r *pluginReference) AddTag(ref reference.Named, id digest.Digest, force bool) error {
|
|
| 346 |
+ // Read only, ignore |
|
| 347 |
+ return nil |
|
| 348 |
+} |
|
| 349 |
+func (r *pluginReference) AddDigest(ref reference.Canonical, id digest.Digest, force bool) error {
|
|
| 350 |
+ // Read only, ignore |
|
| 351 |
+ return nil |
|
| 352 |
+} |
|
| 353 |
+func (r *pluginReference) Delete(ref reference.Named) (bool, error) {
|
|
| 354 |
+ // Read only, ignore |
|
| 355 |
+ return false, nil |
|
| 356 |
+} |
|
| 357 |
+ |
|
| 358 |
+type pluginConfigStore struct {
|
|
| 359 |
+ pm *Manager |
|
| 360 |
+ plugin *v2.Plugin |
|
| 361 |
+} |
|
| 362 |
+ |
|
| 363 |
+func (s *pluginConfigStore) Put([]byte) (digest.Digest, error) {
|
|
| 364 |
+ return digest.Digest(""), errors.New("cannot store config on push")
|
|
| 365 |
+} |
|
| 366 |
+ |
|
| 367 |
+func (s *pluginConfigStore) Get(d digest.Digest) ([]byte, error) {
|
|
| 368 |
+ if s.plugin.Config != d {
|
|
| 369 |
+ return nil, errors.New("plugin not found")
|
|
| 370 |
+ } |
|
| 371 |
+ rwc, err := s.pm.blobStore.Get(d) |
|
| 263 | 372 |
if err != nil {
|
| 264 |
- return err |
|
| 373 |
+ return nil, err |
|
| 374 |
+ } |
|
| 375 |
+ defer rwc.Close() |
|
| 376 |
+ return ioutil.ReadAll(rwc) |
|
| 377 |
+} |
|
| 378 |
+ |
|
| 379 |
+func (s *pluginConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
|
|
| 380 |
+ return configToRootFS(c) |
|
| 381 |
+} |
|
| 382 |
+ |
|
| 383 |
+type pluginLayerProvider struct {
|
|
| 384 |
+ pm *Manager |
|
| 385 |
+ plugin *v2.Plugin |
|
| 386 |
+} |
|
| 387 |
+ |
|
| 388 |
+func (p *pluginLayerProvider) Get(id layer.ChainID) (distribution.PushLayer, error) {
|
|
| 389 |
+ rootFS := rootFSFromPlugin(p.plugin.PluginObj.Config.Rootfs) |
|
| 390 |
+ var i int |
|
| 391 |
+ for i = 1; i <= len(rootFS.DiffIDs); i++ {
|
|
| 392 |
+ if layer.CreateChainID(rootFS.DiffIDs[:i]) == id {
|
|
| 393 |
+ break |
|
| 394 |
+ } |
|
| 395 |
+ } |
|
| 396 |
+ if i > len(rootFS.DiffIDs) {
|
|
| 397 |
+ return nil, errors.New("layer not found")
|
|
| 398 |
+ } |
|
| 399 |
+ return &pluginLayer{
|
|
| 400 |
+ pm: p.pm, |
|
| 401 |
+ diffIDs: rootFS.DiffIDs[:i], |
|
| 402 |
+ blobs: p.plugin.Blobsums[:i], |
|
| 403 |
+ }, nil |
|
| 404 |
+} |
|
| 405 |
+ |
|
| 406 |
+type pluginLayer struct {
|
|
| 407 |
+ pm *Manager |
|
| 408 |
+ diffIDs []layer.DiffID |
|
| 409 |
+ blobs []digest.Digest |
|
| 410 |
+} |
|
| 411 |
+ |
|
| 412 |
+func (l *pluginLayer) ChainID() layer.ChainID {
|
|
| 413 |
+ return layer.CreateChainID(l.diffIDs) |
|
| 414 |
+} |
|
| 415 |
+ |
|
| 416 |
+func (l *pluginLayer) DiffID() layer.DiffID {
|
|
| 417 |
+ return l.diffIDs[len(l.diffIDs)-1] |
|
| 418 |
+} |
|
| 419 |
+ |
|
| 420 |
+func (l *pluginLayer) Parent() distribution.PushLayer {
|
|
| 421 |
+ if len(l.diffIDs) == 1 {
|
|
| 422 |
+ return nil |
|
| 423 |
+ } |
|
| 424 |
+ return &pluginLayer{
|
|
| 425 |
+ pm: l.pm, |
|
| 426 |
+ diffIDs: l.diffIDs[:len(l.diffIDs)-1], |
|
| 427 |
+ blobs: l.blobs[:len(l.diffIDs)-1], |
|
| 265 | 428 |
} |
| 266 |
- defer rootfs.Close() |
|
| 429 |
+} |
|
| 267 | 430 |
|
| 268 |
- _, err = distribution.Push(name, pm.registryService, metaHeader, authConfig, ioutil.NopCloser(bytes.NewReader(config)), rootfs) |
|
| 269 |
- // XXX: Ignore returning digest for now. |
|
| 270 |
- // Since digest needs to be written to the ProgressWriter. |
|
| 271 |
- return err |
|
| 431 |
+func (l *pluginLayer) Open() (io.ReadCloser, error) {
|
|
| 432 |
+ return l.pm.blobStore.Get(l.blobs[len(l.diffIDs)-1]) |
|
| 433 |
+} |
|
| 434 |
+ |
|
| 435 |
+func (l *pluginLayer) Size() (int64, error) {
|
|
| 436 |
+ return l.pm.blobStore.Size(l.blobs[len(l.diffIDs)-1]) |
|
| 437 |
+} |
|
| 438 |
+ |
|
| 439 |
+func (l *pluginLayer) MediaType() string {
|
|
| 440 |
+ return schema2.MediaTypeLayer |
|
| 441 |
+} |
|
| 442 |
+ |
|
| 443 |
+func (l *pluginLayer) Release() {
|
|
| 444 |
+ // Nothing needs to be release, no references held |
|
| 272 | 445 |
} |
| 273 | 446 |
|
| 274 | 447 |
// Remove deletes plugin's root directory. |
| 275 |
-func (pm *Manager) Remove(name string, config *types.PluginRmConfig) (err error) {
|
|
| 276 |
- p, err := pm.pluginStore.GetByName(name) |
|
| 448 |
+func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error {
|
|
| 449 |
+ p, err := pm.config.Store.GetV2Plugin(name) |
|
| 277 | 450 |
pm.mu.RLock() |
| 278 | 451 |
c := pm.cMap[p] |
| 279 | 452 |
pm.mu.RUnlock() |
| ... | ... |
@@ -297,95 +497,194 @@ func (pm *Manager) Remove(name string, config *types.PluginRmConfig) (err error) |
| 297 | 297 |
} |
| 298 | 298 |
} |
| 299 | 299 |
|
| 300 |
- id := p.GetID() |
|
| 301 |
- pluginDir := filepath.Join(pm.libRoot, id) |
|
| 302 |
- |
|
| 303 | 300 |
defer func() {
|
| 304 |
- if err == nil || config.ForceRemove {
|
|
| 305 |
- pm.pluginStore.Remove(p) |
|
| 306 |
- pm.pluginEventLogger(id, name, "remove") |
|
| 307 |
- } |
|
| 301 |
+ go pm.GC() |
|
| 308 | 302 |
}() |
| 309 | 303 |
|
| 310 |
- if err = os.RemoveAll(pluginDir); err != nil {
|
|
| 311 |
- return errors.Wrap(err, "failed to remove plugin directory") |
|
| 304 |
+ id := p.GetID() |
|
| 305 |
+ pm.config.Store.Remove(p) |
|
| 306 |
+ pluginDir := filepath.Join(pm.config.Root, id) |
|
| 307 |
+ if err := os.RemoveAll(pluginDir); err != nil {
|
|
| 308 |
+ logrus.Warnf("unable to remove %q from plugin remove: %v", pluginDir, err)
|
|
| 312 | 309 |
} |
| 310 |
+ pm.config.LogPluginEvent(id, name, "remove") |
|
| 313 | 311 |
return nil |
| 314 | 312 |
} |
| 315 | 313 |
|
| 316 | 314 |
// Set sets plugin args |
| 317 | 315 |
func (pm *Manager) Set(name string, args []string) error {
|
| 318 |
- p, err := pm.pluginStore.GetByName(name) |
|
| 316 |
+ p, err := pm.config.Store.GetV2Plugin(name) |
|
| 319 | 317 |
if err != nil {
|
| 320 | 318 |
return err |
| 321 | 319 |
} |
| 322 |
- return p.Set(args) |
|
| 320 |
+ if err := p.Set(args); err != nil {
|
|
| 321 |
+ return err |
|
| 322 |
+ } |
|
| 323 |
+ return pm.save(p) |
|
| 323 | 324 |
} |
| 324 | 325 |
|
| 325 | 326 |
// CreateFromContext creates a plugin from the given pluginDir which contains |
| 326 | 327 |
// both the rootfs and the config.json and a repoName with optional tag. |
| 327 |
-func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.Reader, options *types.PluginCreateOptions) error {
|
|
| 328 |
- repoName := options.RepoName |
|
| 329 |
- ref, err := distribution.GetRef(repoName) |
|
| 328 |
+func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *types.PluginCreateOptions) (err error) {
|
|
| 329 |
+ pm.muGC.RLock() |
|
| 330 |
+ defer pm.muGC.RUnlock() |
|
| 331 |
+ |
|
| 332 |
+ ref, err := reference.ParseNamed(options.RepoName) |
|
| 330 | 333 |
if err != nil {
|
| 331 |
- return err |
|
| 334 |
+ return errors.Wrapf(err, "failed to parse reference %v", options.RepoName) |
|
| 335 |
+ } |
|
| 336 |
+ if _, ok := ref.(reference.Canonical); ok {
|
|
| 337 |
+ return errors.Errorf("canonical references are not permitted")
|
|
| 332 | 338 |
} |
| 339 |
+ name := reference.WithDefaultTag(ref).String() |
|
| 333 | 340 |
|
| 334 |
- name := ref.Name() |
|
| 335 |
- tag := distribution.GetTag(ref) |
|
| 336 |
- pluginID := stringid.GenerateNonCryptoID() |
|
| 341 |
+ if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
|
|
| 342 |
+ return err |
|
| 343 |
+ } |
|
| 337 | 344 |
|
| 338 |
- p := v2.NewPlugin(name, pluginID, pm.runRoot, pm.libRoot, tag) |
|
| 345 |
+ tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs") |
|
| 346 |
+ defer os.RemoveAll(tmpRootFSDir) |
|
| 347 |
+ if err != nil {
|
|
| 348 |
+ return errors.Wrap(err, "failed to create temp directory") |
|
| 349 |
+ } |
|
| 350 |
+ var configJSON []byte |
|
| 351 |
+ rootFS := splitConfigRootFSFromTar(tarCtx, &configJSON) |
|
| 339 | 352 |
|
| 340 |
- if v, _ := pm.pluginStore.GetByName(p.Name()); v != nil {
|
|
| 341 |
- return fmt.Errorf("plugin %q already exists", p.Name())
|
|
| 353 |
+ rootFSBlob, err := pm.blobStore.New() |
|
| 354 |
+ if err != nil {
|
|
| 355 |
+ return err |
|
| 342 | 356 |
} |
| 357 |
+ defer rootFSBlob.Close() |
|
| 358 |
+ gzw := gzip.NewWriter(rootFSBlob) |
|
| 359 |
+ layerDigester := digest.Canonical.New() |
|
| 360 |
+ rootFSReader := io.TeeReader(rootFS, io.MultiWriter(gzw, layerDigester.Hash())) |
|
| 343 | 361 |
|
| 344 |
- pluginDir := filepath.Join(pm.libRoot, pluginID) |
|
| 345 |
- if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
|
| 362 |
+ if err := chrootarchive.Untar(rootFSReader, tmpRootFSDir, nil); err != nil {
|
|
| 363 |
+ return err |
|
| 364 |
+ } |
|
| 365 |
+ if err := rootFS.Close(); err != nil {
|
|
| 346 | 366 |
return err |
| 347 | 367 |
} |
| 348 | 368 |
|
| 349 |
- // In case an error happens, remove the created directory. |
|
| 350 |
- if err := pm.createFromContext(ctx, tarCtx, pluginDir, repoName, p); err != nil {
|
|
| 351 |
- if err := os.RemoveAll(pluginDir); err != nil {
|
|
| 352 |
- logrus.Warnf("unable to remove %q from failed plugin creation: %v", pluginDir, err)
|
|
| 353 |
- } |
|
| 369 |
+ if configJSON == nil {
|
|
| 370 |
+ return errors.New("config not found")
|
|
| 371 |
+ } |
|
| 372 |
+ |
|
| 373 |
+ if err := gzw.Close(); err != nil {
|
|
| 374 |
+ return errors.Wrap(err, "error closing gzip writer") |
|
| 375 |
+ } |
|
| 376 |
+ |
|
| 377 |
+ var config types.PluginConfig |
|
| 378 |
+ if err := json.Unmarshal(configJSON, &config); err != nil {
|
|
| 379 |
+ return errors.Wrap(err, "failed to parse config") |
|
| 380 |
+ } |
|
| 381 |
+ |
|
| 382 |
+ if err := pm.validateConfig(config); err != nil {
|
|
| 354 | 383 |
return err |
| 355 | 384 |
} |
| 356 | 385 |
|
| 357 |
- return nil |
|
| 358 |
-} |
|
| 386 |
+ pm.mu.Lock() |
|
| 387 |
+ defer pm.mu.Unlock() |
|
| 359 | 388 |
|
| 360 |
-func (pm *Manager) createFromContext(ctx context.Context, tarCtx io.Reader, pluginDir, repoName string, p *v2.Plugin) error {
|
|
| 361 |
- if err := chrootarchive.Untar(tarCtx, pluginDir, nil); err != nil {
|
|
| 389 |
+ rootFSBlobsum, err := rootFSBlob.Commit() |
|
| 390 |
+ if err != nil {
|
|
| 362 | 391 |
return err |
| 363 | 392 |
} |
| 393 |
+ defer func() {
|
|
| 394 |
+ if err != nil {
|
|
| 395 |
+ go pm.GC() |
|
| 396 |
+ } |
|
| 397 |
+ }() |
|
| 398 |
+ |
|
| 399 |
+ config.Rootfs = &types.PluginConfigRootfs{
|
|
| 400 |
+ Type: "layers", |
|
| 401 |
+ DiffIds: []string{layerDigester.Digest().String()},
|
|
| 402 |
+ } |
|
| 364 | 403 |
|
| 365 |
- if err := p.InitPlugin(); err != nil {
|
|
| 404 |
+ configBlob, err := pm.blobStore.New() |
|
| 405 |
+ if err != nil {
|
|
| 406 |
+ return err |
|
| 407 |
+ } |
|
| 408 |
+ defer configBlob.Close() |
|
| 409 |
+ if err := json.NewEncoder(configBlob).Encode(config); err != nil {
|
|
| 410 |
+ return errors.Wrap(err, "error encoding json config") |
|
| 411 |
+ } |
|
| 412 |
+ configBlobsum, err := configBlob.Commit() |
|
| 413 |
+ if err != nil {
|
|
| 366 | 414 |
return err |
| 367 | 415 |
} |
| 368 | 416 |
|
| 369 |
- if err := pm.pluginStore.Add(p); err != nil {
|
|
| 417 |
+ p, err := pm.createPlugin(name, configBlobsum, []digest.Digest{rootFSBlobsum}, tmpRootFSDir, nil)
|
|
| 418 |
+ if err != nil {
|
|
| 370 | 419 |
return err |
| 371 | 420 |
} |
| 372 | 421 |
|
| 373 |
- pm.pluginEventLogger(p.GetID(), repoName, "create") |
|
| 422 |
+ pm.config.LogPluginEvent(p.PluginObj.ID, name, "create") |
|
| 374 | 423 |
|
| 375 | 424 |
return nil |
| 376 | 425 |
} |
| 377 | 426 |
|
| 378 |
-func getPluginName(name string) (string, error) {
|
|
| 379 |
- named, err := reference.ParseNamed(name) // FIXME: validate |
|
| 380 |
- if err != nil {
|
|
| 381 |
- return "", err |
|
| 382 |
- } |
|
| 383 |
- if reference.IsNameOnly(named) {
|
|
| 384 |
- named = reference.WithDefaultTag(named) |
|
| 385 |
- } |
|
| 386 |
- ref, ok := named.(reference.NamedTagged) |
|
| 387 |
- if !ok {
|
|
| 388 |
- return "", fmt.Errorf("invalid name: %s", named.String())
|
|
| 389 |
- } |
|
| 390 |
- return ref.String(), nil |
|
| 427 |
+func (pm *Manager) validateConfig(config types.PluginConfig) error {
|
|
| 428 |
+ return nil // TODO: |
|
| 429 |
+} |
|
| 430 |
+ |
|
| 431 |
+func splitConfigRootFSFromTar(in io.ReadCloser, config *[]byte) io.ReadCloser {
|
|
| 432 |
+ pr, pw := io.Pipe() |
|
| 433 |
+ go func() {
|
|
| 434 |
+ tarReader := tar.NewReader(in) |
|
| 435 |
+ tarWriter := tar.NewWriter(pw) |
|
| 436 |
+ defer in.Close() |
|
| 437 |
+ |
|
| 438 |
+ hasRootFS := false |
|
| 439 |
+ |
|
| 440 |
+ for {
|
|
| 441 |
+ hdr, err := tarReader.Next() |
|
| 442 |
+ if err == io.EOF {
|
|
| 443 |
+ if !hasRootFS {
|
|
| 444 |
+ pw.CloseWithError(errors.Wrap(err, "no rootfs found")) |
|
| 445 |
+ return |
|
| 446 |
+ } |
|
| 447 |
+ // Signals end of archive. |
|
| 448 |
+ tarWriter.Close() |
|
| 449 |
+ pw.Close() |
|
| 450 |
+ return |
|
| 451 |
+ } |
|
| 452 |
+ if err != nil {
|
|
| 453 |
+ pw.CloseWithError(errors.Wrap(err, "failed to read from tar")) |
|
| 454 |
+ return |
|
| 455 |
+ } |
|
| 456 |
+ |
|
| 457 |
+ content := io.Reader(tarReader) |
|
| 458 |
+ name := path.Clean(hdr.Name) |
|
| 459 |
+ if path.IsAbs(name) {
|
|
| 460 |
+ name = name[1:] |
|
| 461 |
+ } |
|
| 462 |
+ if name == configFileName {
|
|
| 463 |
+ dt, err := ioutil.ReadAll(content) |
|
| 464 |
+ if err != nil {
|
|
| 465 |
+ pw.CloseWithError(errors.Wrapf(err, "failed to read %s", configFileName)) |
|
| 466 |
+ return |
|
| 467 |
+ } |
|
| 468 |
+ *config = dt |
|
| 469 |
+ } |
|
| 470 |
+ if parts := strings.Split(name, "/"); len(parts) != 0 && parts[0] == rootFSFileName {
|
|
| 471 |
+ hdr.Name = path.Clean(path.Join(parts[1:]...)) |
|
| 472 |
+ if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(strings.ToLower(hdr.Linkname), rootFSFileName+"/") {
|
|
| 473 |
+ hdr.Linkname = hdr.Linkname[len(rootFSFileName)+1:] |
|
| 474 |
+ } |
|
| 475 |
+ if err := tarWriter.WriteHeader(hdr); err != nil {
|
|
| 476 |
+ pw.CloseWithError(errors.Wrap(err, "error writing tar header")) |
|
| 477 |
+ return |
|
| 478 |
+ } |
|
| 479 |
+ if _, err := pools.Copy(tarWriter, content); err != nil {
|
|
| 480 |
+ pw.CloseWithError(errors.Wrap(err, "error copying tar data")) |
|
| 481 |
+ return |
|
| 482 |
+ } |
|
| 483 |
+ hasRootFS = true |
|
| 484 |
+ } else {
|
|
| 485 |
+ io.Copy(ioutil.Discard, content) |
|
| 486 |
+ } |
|
| 487 |
+ } |
|
| 488 |
+ }() |
|
| 489 |
+ return pr |
|
| 391 | 490 |
} |
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
"net/http" |
| 9 | 9 |
|
| 10 | 10 |
"github.com/docker/docker/api/types" |
| 11 |
+ "github.com/docker/docker/reference" |
|
| 11 | 12 |
"golang.org/x/net/context" |
| 12 | 13 |
) |
| 13 | 14 |
|
| ... | ... |
@@ -24,17 +25,17 @@ func (pm *Manager) Enable(name string, config *types.PluginEnableConfig) error {
|
| 24 | 24 |
} |
| 25 | 25 |
|
| 26 | 26 |
// Inspect examines a plugin config |
| 27 |
-func (pm *Manager) Inspect(refOrID string) (tp types.Plugin, err error) {
|
|
| 28 |
- return tp, errNotSupported |
|
| 27 |
+func (pm *Manager) Inspect(refOrID string) (tp *types.Plugin, err error) {
|
|
| 28 |
+ return nil, errNotSupported |
|
| 29 | 29 |
} |
| 30 | 30 |
|
| 31 | 31 |
// Privileges pulls a plugin config and computes the privileges required to install it. |
| 32 |
-func (pm *Manager) Privileges(name string, metaHeaders http.Header, authConfig *types.AuthConfig) (types.PluginPrivileges, error) {
|
|
| 32 |
+func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHeader http.Header, authConfig *types.AuthConfig) (types.PluginPrivileges, error) {
|
|
| 33 | 33 |
return nil, errNotSupported |
| 34 | 34 |
} |
| 35 | 35 |
|
| 36 | 36 |
// Pull pulls a plugin, check if the correct privileges are provided and install the plugin. |
| 37 |
-func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges) error {
|
|
| 37 |
+func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, out io.Writer) error {
|
|
| 38 | 38 |
return errNotSupported |
| 39 | 39 |
} |
| 40 | 40 |
|
| ... | ... |
@@ -44,7 +45,7 @@ func (pm *Manager) List() ([]types.Plugin, error) {
|
| 44 | 44 |
} |
| 45 | 45 |
|
| 46 | 46 |
// Push pushes a plugin to the store. |
| 47 |
-func (pm *Manager) Push(name string, metaHeader http.Header, authConfig *types.AuthConfig) error {
|
|
| 47 |
+func (pm *Manager) Push(ctx context.Context, name string, metaHeader http.Header, authConfig *types.AuthConfig, out io.Writer) error {
|
|
| 48 | 48 |
return errNotSupported |
| 49 | 49 |
} |
| 50 | 50 |
|
| ... | ... |
@@ -60,6 +61,6 @@ func (pm *Manager) Set(name string, args []string) error {
|
| 60 | 60 |
|
| 61 | 61 |
// CreateFromContext creates a plugin from the given pluginDir which contains |
| 62 | 62 |
// both the rootfs and the config.json and a repoName with optional tag. |
| 63 |
-func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.Reader, options *types.PluginCreateOptions) error {
|
|
| 63 |
+func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *types.PluginCreateOptions) error {
|
|
| 64 | 64 |
return errNotSupported |
| 65 | 65 |
} |
| 66 | 66 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,181 @@ |
| 0 |
+package plugin |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io" |
|
| 4 |
+ "io/ioutil" |
|
| 5 |
+ "os" |
|
| 6 |
+ "path/filepath" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/Sirupsen/logrus" |
|
| 9 |
+ "github.com/docker/distribution/digest" |
|
| 10 |
+ "github.com/docker/docker/distribution/xfer" |
|
| 11 |
+ "github.com/docker/docker/image" |
|
| 12 |
+ "github.com/docker/docker/layer" |
|
| 13 |
+ "github.com/docker/docker/pkg/archive" |
|
| 14 |
+ "github.com/docker/docker/pkg/progress" |
|
| 15 |
+ "github.com/pkg/errors" |
|
| 16 |
+ "golang.org/x/net/context" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+type blobstore interface {
|
|
| 20 |
+ New() (WriteCommitCloser, error) |
|
| 21 |
+ Get(dgst digest.Digest) (io.ReadCloser, error) |
|
| 22 |
+ Size(dgst digest.Digest) (int64, error) |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+type basicBlobStore struct {
|
|
| 26 |
+ path string |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+func newBasicBlobStore(p string) (*basicBlobStore, error) {
|
|
| 30 |
+ tmpdir := filepath.Join(p, "tmp") |
|
| 31 |
+ if err := os.MkdirAll(tmpdir, 0700); err != nil {
|
|
| 32 |
+ return nil, errors.Wrapf(err, "failed to mkdir %v", p) |
|
| 33 |
+ } |
|
| 34 |
+ return &basicBlobStore{path: p}, nil
|
|
| 35 |
+} |
|
| 36 |
+ |
|
| 37 |
+func (b *basicBlobStore) New() (WriteCommitCloser, error) {
|
|
| 38 |
+ f, err := ioutil.TempFile(filepath.Join(b.path, "tmp"), ".insertion") |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return nil, errors.Wrap(err, "failed to create temp file") |
|
| 41 |
+ } |
|
| 42 |
+ return newInsertion(f), nil |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+func (b *basicBlobStore) Get(dgst digest.Digest) (io.ReadCloser, error) {
|
|
| 46 |
+ return os.Open(filepath.Join(b.path, string(dgst.Algorithm()), dgst.Hex())) |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+func (b *basicBlobStore) Size(dgst digest.Digest) (int64, error) {
|
|
| 50 |
+ stat, err := os.Stat(filepath.Join(b.path, string(dgst.Algorithm()), dgst.Hex())) |
|
| 51 |
+ if err != nil {
|
|
| 52 |
+ return 0, err |
|
| 53 |
+ } |
|
| 54 |
+ return stat.Size(), nil |
|
| 55 |
+} |
|
| 56 |
+ |
|
| 57 |
+func (b *basicBlobStore) gc(whitelist map[digest.Digest]struct{}) {
|
|
| 58 |
+ for _, alg := range []string{string(digest.Canonical)} {
|
|
| 59 |
+ items, err := ioutil.ReadDir(filepath.Join(b.path, alg)) |
|
| 60 |
+ if err != nil {
|
|
| 61 |
+ continue |
|
| 62 |
+ } |
|
| 63 |
+ for _, fi := range items {
|
|
| 64 |
+ if _, exists := whitelist[digest.Digest(alg+":"+fi.Name())]; !exists {
|
|
| 65 |
+ p := filepath.Join(b.path, alg, fi.Name()) |
|
| 66 |
+ err := os.RemoveAll(p) |
|
| 67 |
+ logrus.Debugf("cleaned up blob %v: %v", p, err)
|
|
| 68 |
+ } |
|
| 69 |
+ } |
|
| 70 |
+ } |
|
| 71 |
+ |
|
| 72 |
+} |
|
| 73 |
+ |
|
| 74 |
+// WriteCommitCloser defines object that can be committed to blobstore. |
|
| 75 |
+type WriteCommitCloser interface {
|
|
| 76 |
+ io.WriteCloser |
|
| 77 |
+ Commit() (digest.Digest, error) |
|
| 78 |
+} |
|
| 79 |
+ |
|
| 80 |
+type insertion struct {
|
|
| 81 |
+ io.Writer |
|
| 82 |
+ f *os.File |
|
| 83 |
+ digester digest.Digester |
|
| 84 |
+ closed bool |
|
| 85 |
+} |
|
| 86 |
+ |
|
| 87 |
+func newInsertion(tempFile *os.File) *insertion {
|
|
| 88 |
+ digester := digest.Canonical.New() |
|
| 89 |
+ return &insertion{f: tempFile, digester: digester, Writer: io.MultiWriter(tempFile, digester.Hash())}
|
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+func (i *insertion) Commit() (digest.Digest, error) {
|
|
| 93 |
+ p := i.f.Name() |
|
| 94 |
+ d := filepath.Join(filepath.Join(p, "../../")) |
|
| 95 |
+ i.f.Sync() |
|
| 96 |
+ defer os.RemoveAll(p) |
|
| 97 |
+ if err := i.f.Close(); err != nil {
|
|
| 98 |
+ return "", err |
|
| 99 |
+ } |
|
| 100 |
+ i.closed = true |
|
| 101 |
+ dgst := i.digester.Digest() |
|
| 102 |
+ if err := os.MkdirAll(filepath.Join(d, string(dgst.Algorithm())), 0700); err != nil {
|
|
| 103 |
+ return "", errors.Wrapf(err, "failed to mkdir %v", d) |
|
| 104 |
+ } |
|
| 105 |
+ if err := os.Rename(p, filepath.Join(d, string(dgst.Algorithm()), dgst.Hex())); err != nil {
|
|
| 106 |
+ return "", errors.Wrapf(err, "failed to rename %v", p) |
|
| 107 |
+ } |
|
| 108 |
+ return dgst, nil |
|
| 109 |
+} |
|
| 110 |
+ |
|
| 111 |
+func (i *insertion) Close() error {
|
|
| 112 |
+ if i.closed {
|
|
| 113 |
+ return nil |
|
| 114 |
+ } |
|
| 115 |
+ defer os.RemoveAll(i.f.Name()) |
|
| 116 |
+ return i.f.Close() |
|
| 117 |
+} |
|
| 118 |
+ |
|
| 119 |
+type downloadManager struct {
|
|
| 120 |
+ blobStore blobstore |
|
| 121 |
+ tmpDir string |
|
| 122 |
+ blobs []digest.Digest |
|
| 123 |
+ configDigest digest.Digest |
|
| 124 |
+} |
|
| 125 |
+ |
|
| 126 |
+func (dm *downloadManager) Download(ctx context.Context, initialRootFS image.RootFS, layers []xfer.DownloadDescriptor, progressOutput progress.Output) (image.RootFS, func(), error) {
|
|
| 127 |
+ for _, l := range layers {
|
|
| 128 |
+ b, err := dm.blobStore.New() |
|
| 129 |
+ if err != nil {
|
|
| 130 |
+ return initialRootFS, nil, err |
|
| 131 |
+ } |
|
| 132 |
+ defer b.Close() |
|
| 133 |
+ rc, _, err := l.Download(ctx, progressOutput) |
|
| 134 |
+ if err != nil {
|
|
| 135 |
+ return initialRootFS, nil, errors.Wrap(err, "failed to download") |
|
| 136 |
+ } |
|
| 137 |
+ defer rc.Close() |
|
| 138 |
+ r := io.TeeReader(rc, b) |
|
| 139 |
+ inflatedLayerData, err := archive.DecompressStream(r) |
|
| 140 |
+ if err != nil {
|
|
| 141 |
+ return initialRootFS, nil, err |
|
| 142 |
+ } |
|
| 143 |
+ digester := digest.Canonical.New() |
|
| 144 |
+ if _, err := archive.ApplyLayer(dm.tmpDir, io.TeeReader(inflatedLayerData, digester.Hash())); err != nil {
|
|
| 145 |
+ return initialRootFS, nil, err |
|
| 146 |
+ } |
|
| 147 |
+ initialRootFS.Append(layer.DiffID(digester.Digest())) |
|
| 148 |
+ d, err := b.Commit() |
|
| 149 |
+ if err != nil {
|
|
| 150 |
+ return initialRootFS, nil, err |
|
| 151 |
+ } |
|
| 152 |
+ dm.blobs = append(dm.blobs, d) |
|
| 153 |
+ } |
|
| 154 |
+ return initialRootFS, nil, nil |
|
| 155 |
+} |
|
| 156 |
+ |
|
| 157 |
+func (dm *downloadManager) Put(dt []byte) (digest.Digest, error) {
|
|
| 158 |
+ b, err := dm.blobStore.New() |
|
| 159 |
+ if err != nil {
|
|
| 160 |
+ return "", err |
|
| 161 |
+ } |
|
| 162 |
+ defer b.Close() |
|
| 163 |
+ n, err := b.Write(dt) |
|
| 164 |
+ if err != nil {
|
|
| 165 |
+ return "", err |
|
| 166 |
+ } |
|
| 167 |
+ if n != len(dt) {
|
|
| 168 |
+ return "", io.ErrShortWrite |
|
| 169 |
+ } |
|
| 170 |
+ d, err := b.Commit() |
|
| 171 |
+ dm.configDigest = d |
|
| 172 |
+ return d, err |
|
| 173 |
+} |
|
| 174 |
+ |
|
| 175 |
+func (dm *downloadManager) Get(d digest.Digest) ([]byte, error) {
|
|
| 176 |
+ return nil, digest.ErrDigestNotFound |
|
| 177 |
+} |
|
| 178 |
+func (dm *downloadManager) RootFSFromConfig(c []byte) (*image.RootFS, error) {
|
|
| 179 |
+ return configToRootFS(c) |
|
| 180 |
+} |
| 0 | 181 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,26 @@ |
| 0 |
+package plugin |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "sync" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/pkg/plugins" |
|
| 6 |
+ "github.com/docker/docker/plugin/v2" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// Store manages the plugin inventory in memory and on-disk |
|
| 10 |
+type Store struct {
|
|
| 11 |
+ sync.RWMutex |
|
| 12 |
+ plugins map[string]*v2.Plugin |
|
| 13 |
+ /* handlers are necessary for transition path of legacy plugins |
|
| 14 |
+ * to the new model. Legacy plugins use Handle() for registering an |
|
| 15 |
+ * activation callback.*/ |
|
| 16 |
+ handlers map[string][]func(string, *plugins.Client) |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+// NewStore creates a Store. |
|
| 20 |
+func NewStore(libRoot string) *Store {
|
|
| 21 |
+ return &Store{
|
|
| 22 |
+ plugins: make(map[string]*v2.Plugin), |
|
| 23 |
+ handlers: make(map[string][]func(string, *plugins.Client)), |
|
| 24 |
+ } |
|
| 25 |
+} |
| 0 | 26 |
deleted file mode 100644 |
| ... | ... |
@@ -1,222 +0,0 @@ |
| 1 |
-package distribution |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "encoding/json" |
|
| 5 |
- "fmt" |
|
| 6 |
- "io" |
|
| 7 |
- "io/ioutil" |
|
| 8 |
- "net/http" |
|
| 9 |
- "os" |
|
| 10 |
- "path/filepath" |
|
| 11 |
- |
|
| 12 |
- "github.com/Sirupsen/logrus" |
|
| 13 |
- "github.com/docker/distribution" |
|
| 14 |
- "github.com/docker/distribution/manifest/schema2" |
|
| 15 |
- "github.com/docker/docker/api/types" |
|
| 16 |
- dockerdist "github.com/docker/docker/distribution" |
|
| 17 |
- archive "github.com/docker/docker/pkg/chrootarchive" |
|
| 18 |
- "github.com/docker/docker/reference" |
|
| 19 |
- "github.com/docker/docker/registry" |
|
| 20 |
- "golang.org/x/net/context" |
|
| 21 |
-) |
|
| 22 |
- |
|
| 23 |
-// PullData is the plugin config and the rootfs |
|
| 24 |
-type PullData interface {
|
|
| 25 |
- Config() ([]byte, error) |
|
| 26 |
- Layer() (io.ReadCloser, error) |
|
| 27 |
-} |
|
| 28 |
- |
|
| 29 |
-type pullData struct {
|
|
| 30 |
- repository distribution.Repository |
|
| 31 |
- manifest schema2.Manifest |
|
| 32 |
- index int |
|
| 33 |
-} |
|
| 34 |
- |
|
| 35 |
-func (pd *pullData) Config() ([]byte, error) {
|
|
| 36 |
- blobs := pd.repository.Blobs(context.Background()) |
|
| 37 |
- config, err := blobs.Get(context.Background(), pd.manifest.Config.Digest) |
|
| 38 |
- if err != nil {
|
|
| 39 |
- return nil, err |
|
| 40 |
- } |
|
| 41 |
- // validate |
|
| 42 |
- var p types.Plugin |
|
| 43 |
- if err := json.Unmarshal(config, &p); err != nil {
|
|
| 44 |
- return nil, err |
|
| 45 |
- } |
|
| 46 |
- return config, nil |
|
| 47 |
-} |
|
| 48 |
- |
|
| 49 |
-func (pd *pullData) Layer() (io.ReadCloser, error) {
|
|
| 50 |
- if pd.index >= len(pd.manifest.Layers) {
|
|
| 51 |
- return nil, io.EOF |
|
| 52 |
- } |
|
| 53 |
- |
|
| 54 |
- blobs := pd.repository.Blobs(context.Background()) |
|
| 55 |
- rsc, err := blobs.Open(context.Background(), pd.manifest.Layers[pd.index].Digest) |
|
| 56 |
- if err != nil {
|
|
| 57 |
- return nil, err |
|
| 58 |
- } |
|
| 59 |
- pd.index++ |
|
| 60 |
- return rsc, nil |
|
| 61 |
-} |
|
| 62 |
- |
|
| 63 |
-// GetRef returns the distribution reference for a given name. |
|
| 64 |
-func GetRef(name string) (reference.Named, error) {
|
|
| 65 |
- ref, err := reference.ParseNamed(name) |
|
| 66 |
- if err != nil {
|
|
| 67 |
- return nil, err |
|
| 68 |
- } |
|
| 69 |
- return ref, nil |
|
| 70 |
-} |
|
| 71 |
- |
|
| 72 |
-// GetTag returns the tag associated with the given reference name. |
|
| 73 |
-func GetTag(ref reference.Named) string {
|
|
| 74 |
- tag := DefaultTag |
|
| 75 |
- if ref, ok := ref.(reference.NamedTagged); ok {
|
|
| 76 |
- tag = ref.Tag() |
|
| 77 |
- } |
|
| 78 |
- return tag |
|
| 79 |
-} |
|
| 80 |
- |
|
| 81 |
-// Pull downloads the plugin from Store |
|
| 82 |
-func Pull(ref reference.Named, rs registry.Service, metaheader http.Header, authConfig *types.AuthConfig) (PullData, error) {
|
|
| 83 |
- repoInfo, err := rs.ResolveRepository(ref) |
|
| 84 |
- if err != nil {
|
|
| 85 |
- logrus.Debugf("pull.go: error in ResolveRepository: %v", err)
|
|
| 86 |
- return nil, err |
|
| 87 |
- } |
|
| 88 |
- repoInfo.Class = "plugin" |
|
| 89 |
- |
|
| 90 |
- if err := dockerdist.ValidateRepoName(repoInfo.Name()); err != nil {
|
|
| 91 |
- logrus.Debugf("pull.go: error in ValidateRepoName: %v", err)
|
|
| 92 |
- return nil, err |
|
| 93 |
- } |
|
| 94 |
- |
|
| 95 |
- endpoints, err := rs.LookupPullEndpoints(repoInfo.Hostname()) |
|
| 96 |
- if err != nil {
|
|
| 97 |
- logrus.Debugf("pull.go: error in LookupPullEndpoints: %v", err)
|
|
| 98 |
- return nil, err |
|
| 99 |
- } |
|
| 100 |
- |
|
| 101 |
- var confirmedV2 bool |
|
| 102 |
- var repository distribution.Repository |
|
| 103 |
- |
|
| 104 |
- for _, endpoint := range endpoints {
|
|
| 105 |
- if confirmedV2 && endpoint.Version == registry.APIVersion1 {
|
|
| 106 |
- logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
|
|
| 107 |
- continue |
|
| 108 |
- } |
|
| 109 |
- |
|
| 110 |
- // TODO: reuse contexts |
|
| 111 |
- repository, confirmedV2, err = dockerdist.NewV2Repository(context.Background(), repoInfo, endpoint, metaheader, authConfig, "pull") |
|
| 112 |
- if err != nil {
|
|
| 113 |
- logrus.Debugf("pull.go: error in NewV2Repository: %v", err)
|
|
| 114 |
- return nil, err |
|
| 115 |
- } |
|
| 116 |
- if !confirmedV2 {
|
|
| 117 |
- logrus.Debug("pull.go: !confirmedV2")
|
|
| 118 |
- return nil, ErrUnsupportedRegistry |
|
| 119 |
- } |
|
| 120 |
- logrus.Debugf("Trying to pull %s from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
|
|
| 121 |
- break |
|
| 122 |
- } |
|
| 123 |
- |
|
| 124 |
- tag := DefaultTag |
|
| 125 |
- if ref, ok := ref.(reference.NamedTagged); ok {
|
|
| 126 |
- tag = ref.Tag() |
|
| 127 |
- } |
|
| 128 |
- |
|
| 129 |
- // tags := repository.Tags(context.Background()) |
|
| 130 |
- // desc, err := tags.Get(context.Background(), tag) |
|
| 131 |
- // if err != nil {
|
|
| 132 |
- // return nil, err |
|
| 133 |
- // } |
|
| 134 |
- // |
|
| 135 |
- msv, err := repository.Manifests(context.Background()) |
|
| 136 |
- if err != nil {
|
|
| 137 |
- logrus.Debugf("pull.go: error in repository.Manifests: %v", err)
|
|
| 138 |
- return nil, err |
|
| 139 |
- } |
|
| 140 |
- manifest, err := msv.Get(context.Background(), "", distribution.WithTag(tag)) |
|
| 141 |
- if err != nil {
|
|
| 142 |
- logrus.Debugf("pull.go: error in msv.Get(): %v", err)
|
|
| 143 |
- return nil, dockerdist.TranslatePullError(err, repoInfo) |
|
| 144 |
- } |
|
| 145 |
- |
|
| 146 |
- _, pl, err := manifest.Payload() |
|
| 147 |
- if err != nil {
|
|
| 148 |
- logrus.Debugf("pull.go: error in manifest.Payload(): %v", err)
|
|
| 149 |
- return nil, err |
|
| 150 |
- } |
|
| 151 |
- var m schema2.Manifest |
|
| 152 |
- if err := json.Unmarshal(pl, &m); err != nil {
|
|
| 153 |
- logrus.Debugf("pull.go: error in json.Unmarshal(): %v", err)
|
|
| 154 |
- return nil, err |
|
| 155 |
- } |
|
| 156 |
- if m.Config.MediaType != schema2.MediaTypePluginConfig {
|
|
| 157 |
- return nil, ErrUnsupportedMediaType |
|
| 158 |
- } |
|
| 159 |
- |
|
| 160 |
- pd := &pullData{
|
|
| 161 |
- repository: repository, |
|
| 162 |
- manifest: m, |
|
| 163 |
- } |
|
| 164 |
- |
|
| 165 |
- logrus.Debugf("manifest: %s", pl)
|
|
| 166 |
- return pd, nil |
|
| 167 |
-} |
|
| 168 |
- |
|
| 169 |
-// WritePullData extracts manifest and rootfs to the disk. |
|
| 170 |
-func WritePullData(pd PullData, dest string, extract bool) error {
|
|
| 171 |
- config, err := pd.Config() |
|
| 172 |
- if err != nil {
|
|
| 173 |
- return err |
|
| 174 |
- } |
|
| 175 |
- var p types.Plugin |
|
| 176 |
- if err := json.Unmarshal(config, &p); err != nil {
|
|
| 177 |
- return err |
|
| 178 |
- } |
|
| 179 |
- logrus.Debugf("plugin: %#v", p)
|
|
| 180 |
- |
|
| 181 |
- if err := os.MkdirAll(dest, 0700); err != nil {
|
|
| 182 |
- return err |
|
| 183 |
- } |
|
| 184 |
- |
|
| 185 |
- if extract {
|
|
| 186 |
- if err := ioutil.WriteFile(filepath.Join(dest, "config.json"), config, 0600); err != nil {
|
|
| 187 |
- return err |
|
| 188 |
- } |
|
| 189 |
- |
|
| 190 |
- if err := os.MkdirAll(filepath.Join(dest, "rootfs"), 0700); err != nil {
|
|
| 191 |
- return err |
|
| 192 |
- } |
|
| 193 |
- } |
|
| 194 |
- |
|
| 195 |
- for i := 0; ; i++ {
|
|
| 196 |
- l, err := pd.Layer() |
|
| 197 |
- if err == io.EOF {
|
|
| 198 |
- break |
|
| 199 |
- } |
|
| 200 |
- if err != nil {
|
|
| 201 |
- return err |
|
| 202 |
- } |
|
| 203 |
- |
|
| 204 |
- if !extract {
|
|
| 205 |
- f, err := os.Create(filepath.Join(dest, fmt.Sprintf("layer%d.tar", i)))
|
|
| 206 |
- if err != nil {
|
|
| 207 |
- l.Close() |
|
| 208 |
- return err |
|
| 209 |
- } |
|
| 210 |
- io.Copy(f, l) |
|
| 211 |
- l.Close() |
|
| 212 |
- f.Close() |
|
| 213 |
- continue |
|
| 214 |
- } |
|
| 215 |
- |
|
| 216 |
- if _, err := archive.ApplyLayer(filepath.Join(dest, "rootfs"), l); err != nil {
|
|
| 217 |
- return err |
|
| 218 |
- } |
|
| 219 |
- |
|
| 220 |
- } |
|
| 221 |
- return nil |
|
| 222 |
-} |
| 223 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,134 +0,0 @@ |
| 1 |
-package distribution |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "crypto/sha256" |
|
| 5 |
- "io" |
|
| 6 |
- "net/http" |
|
| 7 |
- |
|
| 8 |
- "github.com/Sirupsen/logrus" |
|
| 9 |
- "github.com/docker/distribution" |
|
| 10 |
- "github.com/docker/distribution/digest" |
|
| 11 |
- "github.com/docker/distribution/manifest/schema2" |
|
| 12 |
- "github.com/docker/docker/api/types" |
|
| 13 |
- dockerdist "github.com/docker/docker/distribution" |
|
| 14 |
- "github.com/docker/docker/reference" |
|
| 15 |
- "github.com/docker/docker/registry" |
|
| 16 |
- "golang.org/x/net/context" |
|
| 17 |
-) |
|
| 18 |
- |
|
| 19 |
-// Push pushes a plugin to a registry. |
|
| 20 |
-func Push(name string, rs registry.Service, metaHeader http.Header, authConfig *types.AuthConfig, config io.ReadCloser, layers io.ReadCloser) (digest.Digest, error) {
|
|
| 21 |
- ref, err := reference.ParseNamed(name) |
|
| 22 |
- if err != nil {
|
|
| 23 |
- return "", err |
|
| 24 |
- } |
|
| 25 |
- |
|
| 26 |
- repoInfo, err := rs.ResolveRepository(ref) |
|
| 27 |
- if err != nil {
|
|
| 28 |
- return "", err |
|
| 29 |
- } |
|
| 30 |
- repoInfo.Class = "plugin" |
|
| 31 |
- |
|
| 32 |
- if err := dockerdist.ValidateRepoName(repoInfo.Name()); err != nil {
|
|
| 33 |
- return "", err |
|
| 34 |
- } |
|
| 35 |
- |
|
| 36 |
- endpoints, err := rs.LookupPushEndpoints(repoInfo.Hostname()) |
|
| 37 |
- if err != nil {
|
|
| 38 |
- return "", err |
|
| 39 |
- } |
|
| 40 |
- |
|
| 41 |
- var confirmedV2 bool |
|
| 42 |
- var repository distribution.Repository |
|
| 43 |
- for _, endpoint := range endpoints {
|
|
| 44 |
- if confirmedV2 && endpoint.Version == registry.APIVersion1 {
|
|
| 45 |
- logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
|
|
| 46 |
- continue |
|
| 47 |
- } |
|
| 48 |
- repository, confirmedV2, err = dockerdist.NewV2Repository(context.Background(), repoInfo, endpoint, metaHeader, authConfig, "push", "pull") |
|
| 49 |
- if err != nil {
|
|
| 50 |
- return "", err |
|
| 51 |
- } |
|
| 52 |
- if !confirmedV2 {
|
|
| 53 |
- return "", ErrUnsupportedRegistry |
|
| 54 |
- } |
|
| 55 |
- logrus.Debugf("Trying to push %s to %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
|
|
| 56 |
- // This means that we found an endpoint. and we are ready to push |
|
| 57 |
- break |
|
| 58 |
- } |
|
| 59 |
- |
|
| 60 |
- // Returns a reference to the repository's blob service. |
|
| 61 |
- blobs := repository.Blobs(context.Background()) |
|
| 62 |
- |
|
| 63 |
- // Descriptor = {mediaType, size, digest}
|
|
| 64 |
- var descs []distribution.Descriptor |
|
| 65 |
- |
|
| 66 |
- for i, f := range []io.ReadCloser{config, layers} {
|
|
| 67 |
- bw, err := blobs.Create(context.Background()) |
|
| 68 |
- if err != nil {
|
|
| 69 |
- logrus.Debugf("Error in blobs.Create: %v", err)
|
|
| 70 |
- return "", err |
|
| 71 |
- } |
|
| 72 |
- h := sha256.New() |
|
| 73 |
- r := io.TeeReader(f, h) |
|
| 74 |
- _, err = io.Copy(bw, r) |
|
| 75 |
- if err != nil {
|
|
| 76 |
- f.Close() |
|
| 77 |
- logrus.Debugf("Error in io.Copy: %v", err)
|
|
| 78 |
- return "", err |
|
| 79 |
- } |
|
| 80 |
- f.Close() |
|
| 81 |
- mt := schema2.MediaTypeLayer |
|
| 82 |
- if i == 0 {
|
|
| 83 |
- mt = schema2.MediaTypePluginConfig |
|
| 84 |
- } |
|
| 85 |
- // Commit completes the write process to the BlobService. |
|
| 86 |
- // The descriptor arg to Commit is called the "provisional" descriptor and |
|
| 87 |
- // used for validation. |
|
| 88 |
- // The returned descriptor should be the one used. Its called the "Canonical" |
|
| 89 |
- // descriptor. |
|
| 90 |
- desc, err := bw.Commit(context.Background(), distribution.Descriptor{
|
|
| 91 |
- MediaType: mt, |
|
| 92 |
- // XXX: What about the Size? |
|
| 93 |
- Digest: digest.NewDigest("sha256", h),
|
|
| 94 |
- }) |
|
| 95 |
- if err != nil {
|
|
| 96 |
- logrus.Debugf("Error in bw.Commit: %v", err)
|
|
| 97 |
- return "", err |
|
| 98 |
- } |
|
| 99 |
- // The canonical descriptor is set the mediatype again, just in case. |
|
| 100 |
- // Don't touch the digest or the size here. |
|
| 101 |
- desc.MediaType = mt |
|
| 102 |
- logrus.Debugf("pushed blob: %s %s", desc.MediaType, desc.Digest)
|
|
| 103 |
- descs = append(descs, desc) |
|
| 104 |
- } |
|
| 105 |
- |
|
| 106 |
- // XXX: schema2.Versioned needs a MediaType as well. |
|
| 107 |
- // "application/vnd.docker.distribution.manifest.v2+json" |
|
| 108 |
- m, err := schema2.FromStruct(schema2.Manifest{Versioned: schema2.SchemaVersion, Config: descs[0], Layers: descs[1:]})
|
|
| 109 |
- if err != nil {
|
|
| 110 |
- logrus.Debugf("error in schema2.FromStruct: %v", err)
|
|
| 111 |
- return "", err |
|
| 112 |
- } |
|
| 113 |
- |
|
| 114 |
- msv, err := repository.Manifests(context.Background()) |
|
| 115 |
- if err != nil {
|
|
| 116 |
- logrus.Debugf("error in repository.Manifests: %v", err)
|
|
| 117 |
- return "", err |
|
| 118 |
- } |
|
| 119 |
- |
|
| 120 |
- _, pl, err := m.Payload() |
|
| 121 |
- if err != nil {
|
|
| 122 |
- logrus.Debugf("error in m.Payload: %v", err)
|
|
| 123 |
- return "", err |
|
| 124 |
- } |
|
| 125 |
- |
|
| 126 |
- logrus.Debugf("Pushed manifest: %s", pl)
|
|
| 127 |
- |
|
| 128 |
- tag := DefaultTag |
|
| 129 |
- if tagged, ok := ref.(reference.NamedTagged); ok {
|
|
| 130 |
- tag = tagged.Tag() |
|
| 131 |
- } |
|
| 132 |
- |
|
| 133 |
- return msv.Put(context.Background(), m, distribution.WithTag(tag)) |
|
| 134 |
-} |
| 135 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,12 +0,0 @@ |
| 1 |
-package distribution |
|
| 2 |
- |
|
| 3 |
-import "errors" |
|
| 4 |
- |
|
| 5 |
-// ErrUnsupportedRegistry indicates that the registry does not support v2 protocol |
|
| 6 |
-var ErrUnsupportedRegistry = errors.New("only V2 repositories are supported for plugin distribution")
|
|
| 7 |
- |
|
| 8 |
-// ErrUnsupportedMediaType indicates we are pulling content that's not a plugin |
|
| 9 |
-var ErrUnsupportedMediaType = errors.New("content is not a plugin")
|
|
| 10 |
- |
|
| 11 |
-// DefaultTag is the default tag for plugins |
|
| 12 |
-const DefaultTag = "latest" |
| ... | ... |
@@ -3,25 +3,34 @@ package plugin |
| 3 | 3 |
import ( |
| 4 | 4 |
"encoding/json" |
| 5 | 5 |
"io" |
| 6 |
+ "io/ioutil" |
|
| 6 | 7 |
"os" |
| 7 | 8 |
"path/filepath" |
| 9 |
+ "reflect" |
|
| 10 |
+ "regexp" |
|
| 8 | 11 |
"strings" |
| 9 | 12 |
"sync" |
| 10 | 13 |
|
| 11 | 14 |
"github.com/Sirupsen/logrus" |
| 15 |
+ "github.com/docker/distribution/digest" |
|
| 16 |
+ "github.com/docker/docker/api/types" |
|
| 17 |
+ "github.com/docker/docker/image" |
|
| 18 |
+ "github.com/docker/docker/layer" |
|
| 12 | 19 |
"github.com/docker/docker/libcontainerd" |
| 20 |
+ "github.com/docker/docker/pkg/ioutils" |
|
| 13 | 21 |
"github.com/docker/docker/pkg/mount" |
| 14 |
- "github.com/docker/docker/plugin/store" |
|
| 15 | 22 |
"github.com/docker/docker/plugin/v2" |
| 23 |
+ "github.com/docker/docker/reference" |
|
| 16 | 24 |
"github.com/docker/docker/registry" |
| 25 |
+ "github.com/pkg/errors" |
|
| 17 | 26 |
) |
| 18 | 27 |
|
| 19 |
-var ( |
|
| 20 |
- manager *Manager |
|
| 21 |
-) |
|
| 28 |
+const configFileName = "config.json" |
|
| 29 |
+const rootFSFileName = "rootfs" |
|
| 30 |
+ |
|
| 31 |
+var validFullID = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
|
| 22 | 32 |
|
| 23 | 33 |
func (pm *Manager) restorePlugin(p *v2.Plugin) error {
|
| 24 |
- p.Restore(pm.runRoot) |
|
| 25 | 34 |
if p.IsEnabled() {
|
| 26 | 35 |
return pm.restore(p) |
| 27 | 36 |
} |
| ... | ... |
@@ -30,17 +39,25 @@ func (pm *Manager) restorePlugin(p *v2.Plugin) error {
|
| 30 | 30 |
|
| 31 | 31 |
type eventLogger func(id, name, action string) |
| 32 | 32 |
|
| 33 |
+// ManagerConfig defines configuration needed to start new manager. |
|
| 34 |
+type ManagerConfig struct {
|
|
| 35 |
+ Store *Store // remove |
|
| 36 |
+ Executor libcontainerd.Remote |
|
| 37 |
+ RegistryService registry.Service |
|
| 38 |
+ LiveRestoreEnabled bool // TODO: remove |
|
| 39 |
+ LogPluginEvent eventLogger |
|
| 40 |
+ Root string |
|
| 41 |
+ ExecRoot string |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 33 | 44 |
// Manager controls the plugin subsystem. |
| 34 | 45 |
type Manager struct {
|
| 35 |
- libRoot string |
|
| 36 |
- runRoot string |
|
| 37 |
- pluginStore *store.Store |
|
| 38 |
- containerdClient libcontainerd.Client |
|
| 39 |
- registryService registry.Service |
|
| 40 |
- liveRestore bool |
|
| 41 |
- pluginEventLogger eventLogger |
|
| 42 |
- mu sync.RWMutex // protects cMap |
|
| 43 |
- cMap map[*v2.Plugin]*controller |
|
| 46 |
+ config ManagerConfig |
|
| 47 |
+ mu sync.RWMutex // protects cMap |
|
| 48 |
+ muGC sync.RWMutex // protects blobstore deletions |
|
| 49 |
+ cMap map[*v2.Plugin]*controller |
|
| 50 |
+ containerdClient libcontainerd.Client |
|
| 51 |
+ blobStore *basicBlobStore |
|
| 44 | 52 |
} |
| 45 | 53 |
|
| 46 | 54 |
// controller represents the manager's control on a plugin. |
| ... | ... |
@@ -50,36 +67,56 @@ type controller struct {
|
| 50 | 50 |
timeoutInSecs int |
| 51 | 51 |
} |
| 52 | 52 |
|
| 53 |
-// GetManager returns the singleton plugin Manager |
|
| 54 |
-func GetManager() *Manager {
|
|
| 55 |
- return manager |
|
| 53 |
+// pluginRegistryService ensures that all resolved repositories |
|
| 54 |
+// are of the plugin class. |
|
| 55 |
+type pluginRegistryService struct {
|
|
| 56 |
+ registry.Service |
|
| 56 | 57 |
} |
| 57 | 58 |
|
| 58 |
-// Init (was NewManager) instantiates the singleton Manager. |
|
| 59 |
-// TODO: revert this to NewManager once we get rid of all the singletons. |
|
| 60 |
-func Init(root string, ps *store.Store, remote libcontainerd.Remote, rs registry.Service, liveRestore bool, evL eventLogger) (err error) {
|
|
| 61 |
- if manager != nil {
|
|
| 62 |
- return nil |
|
| 59 |
+func (s pluginRegistryService) ResolveRepository(name reference.Named) (repoInfo *registry.RepositoryInfo, err error) {
|
|
| 60 |
+ repoInfo, err = s.Service.ResolveRepository(name) |
|
| 61 |
+ if repoInfo != nil {
|
|
| 62 |
+ repoInfo.Class = "plugin" |
|
| 63 | 63 |
} |
| 64 |
+ return |
|
| 65 |
+} |
|
| 64 | 66 |
|
| 65 |
- root = filepath.Join(root, "plugins") |
|
| 66 |
- manager = &Manager{
|
|
| 67 |
- libRoot: root, |
|
| 68 |
- runRoot: "/run/docker/plugins", |
|
| 69 |
- pluginStore: ps, |
|
| 70 |
- registryService: rs, |
|
| 71 |
- liveRestore: liveRestore, |
|
| 72 |
- pluginEventLogger: evL, |
|
| 67 |
+// NewManager returns a new plugin manager. |
|
| 68 |
+func NewManager(config ManagerConfig) (*Manager, error) {
|
|
| 69 |
+ if config.RegistryService != nil {
|
|
| 70 |
+ config.RegistryService = pluginRegistryService{config.RegistryService}
|
|
| 73 | 71 |
} |
| 74 |
- if err := os.MkdirAll(manager.runRoot, 0700); err != nil {
|
|
| 75 |
- return err |
|
| 72 |
+ manager := &Manager{
|
|
| 73 |
+ config: config, |
|
| 76 | 74 |
} |
| 77 |
- manager.containerdClient, err = remote.Client(manager) |
|
| 75 |
+ if err := os.MkdirAll(manager.config.Root, 0700); err != nil {
|
|
| 76 |
+ return nil, errors.Wrapf(err, "failed to mkdir %v", manager.config.Root) |
|
| 77 |
+ } |
|
| 78 |
+ if err := os.MkdirAll(manager.config.ExecRoot, 0700); err != nil {
|
|
| 79 |
+ return nil, errors.Wrapf(err, "failed to mkdir %v", manager.config.ExecRoot) |
|
| 80 |
+ } |
|
| 81 |
+ if err := os.MkdirAll(manager.tmpDir(), 0700); err != nil {
|
|
| 82 |
+ return nil, errors.Wrapf(err, "failed to mkdir %v", manager.tmpDir()) |
|
| 83 |
+ } |
|
| 84 |
+ var err error |
|
| 85 |
+ manager.containerdClient, err = config.Executor.Client(manager) // todo: move to another struct |
|
| 78 | 86 |
if err != nil {
|
| 79 |
- return err |
|
| 87 |
+ return nil, errors.Wrap(err, "failed to create containerd client") |
|
| 80 | 88 |
} |
| 89 |
+ manager.blobStore, err = newBasicBlobStore(filepath.Join(manager.config.Root, "storage/blobs")) |
|
| 90 |
+ if err != nil {
|
|
| 91 |
+ return nil, err |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 81 | 94 |
manager.cMap = make(map[*v2.Plugin]*controller) |
| 82 |
- return manager.reload() |
|
| 95 |
+ if err := manager.reload(); err != nil {
|
|
| 96 |
+ return nil, errors.Wrap(err, "failed to restore plugins") |
|
| 97 |
+ } |
|
| 98 |
+ return manager, nil |
|
| 99 |
+} |
|
| 100 |
+ |
|
| 101 |
+func (pm *Manager) tmpDir() string {
|
|
| 102 |
+ return filepath.Join(pm.config.Root, "tmp") |
|
| 83 | 103 |
} |
| 84 | 104 |
|
| 85 | 105 |
// StateChanged updates plugin internals using libcontainerd events. |
| ... | ... |
@@ -88,7 +125,7 @@ func (pm *Manager) StateChanged(id string, e libcontainerd.StateInfo) error {
|
| 88 | 88 |
|
| 89 | 89 |
switch e.State {
|
| 90 | 90 |
case libcontainerd.StateExit: |
| 91 |
- p, err := pm.pluginStore.GetByID(id) |
|
| 91 |
+ p, err := pm.config.Store.GetV2Plugin(id) |
|
| 92 | 92 |
if err != nil {
|
| 93 | 93 |
return err |
| 94 | 94 |
} |
| ... | ... |
@@ -102,7 +139,7 @@ func (pm *Manager) StateChanged(id string, e libcontainerd.StateInfo) error {
|
| 102 | 102 |
restart := c.restart |
| 103 | 103 |
pm.mu.RUnlock() |
| 104 | 104 |
|
| 105 |
- p.RemoveFromDisk() |
|
| 105 |
+ os.RemoveAll(filepath.Join(pm.config.ExecRoot, id)) |
|
| 106 | 106 |
|
| 107 | 107 |
if p.PropagatedMount != "" {
|
| 108 | 108 |
if err := mount.Unmount(p.PropagatedMount); err != nil {
|
| ... | ... |
@@ -118,37 +155,38 @@ func (pm *Manager) StateChanged(id string, e libcontainerd.StateInfo) error {
|
| 118 | 118 |
return nil |
| 119 | 119 |
} |
| 120 | 120 |
|
| 121 |
-// reload is used on daemon restarts to load the manager's state |
|
| 122 |
-func (pm *Manager) reload() error {
|
|
| 123 |
- dt, err := os.Open(filepath.Join(pm.libRoot, "plugins.json")) |
|
| 121 |
+func (pm *Manager) reload() error { // todo: restore
|
|
| 122 |
+ dir, err := ioutil.ReadDir(pm.config.Root) |
|
| 124 | 123 |
if err != nil {
|
| 125 |
- if os.IsNotExist(err) {
|
|
| 126 |
- return nil |
|
| 127 |
- } |
|
| 128 |
- return err |
|
| 124 |
+ return errors.Wrapf(err, "failed to read %v", pm.config.Root) |
|
| 129 | 125 |
} |
| 130 |
- defer dt.Close() |
|
| 131 |
- |
|
| 132 | 126 |
plugins := make(map[string]*v2.Plugin) |
| 133 |
- if err := json.NewDecoder(dt).Decode(&plugins); err != nil {
|
|
| 134 |
- return err |
|
| 127 |
+ for _, v := range dir {
|
|
| 128 |
+ if validFullID.MatchString(v.Name()) {
|
|
| 129 |
+ p, err := pm.loadPlugin(v.Name()) |
|
| 130 |
+ if err != nil {
|
|
| 131 |
+ return err |
|
| 132 |
+ } |
|
| 133 |
+ plugins[p.GetID()] = p |
|
| 134 |
+ } |
|
| 135 | 135 |
} |
| 136 |
- pm.pluginStore.SetAll(plugins) |
|
| 137 | 136 |
|
| 138 |
- var group sync.WaitGroup |
|
| 139 |
- group.Add(len(plugins)) |
|
| 137 |
+ pm.config.Store.SetAll(plugins) |
|
| 138 |
+ |
|
| 139 |
+ var wg sync.WaitGroup |
|
| 140 |
+ wg.Add(len(plugins)) |
|
| 140 | 141 |
for _, p := range plugins {
|
| 141 |
- c := &controller{}
|
|
| 142 |
+ c := &controller{} // todo: remove this
|
|
| 142 | 143 |
pm.cMap[p] = c |
| 143 | 144 |
go func(p *v2.Plugin) {
|
| 144 |
- defer group.Done() |
|
| 145 |
+ defer wg.Done() |
|
| 145 | 146 |
if err := pm.restorePlugin(p); err != nil {
|
| 146 | 147 |
logrus.Errorf("failed to restore plugin '%s': %s", p.Name(), err)
|
| 147 | 148 |
return |
| 148 | 149 |
} |
| 149 | 150 |
|
| 150 | 151 |
if p.Rootfs != "" {
|
| 151 |
- p.Rootfs = filepath.Join(pm.libRoot, p.PluginObj.ID, "rootfs") |
|
| 152 |
+ p.Rootfs = filepath.Join(pm.config.Root, p.PluginObj.ID, "rootfs") |
|
| 152 | 153 |
} |
| 153 | 154 |
|
| 154 | 155 |
// We should only enable rootfs propagation for certain plugin types that need it. |
| ... | ... |
@@ -165,8 +203,8 @@ func (pm *Manager) reload() error {
|
| 165 | 165 |
} |
| 166 | 166 |
} |
| 167 | 167 |
|
| 168 |
- pm.pluginStore.Update(p) |
|
| 169 |
- requiresManualRestore := !pm.liveRestore && p.IsEnabled() |
|
| 168 |
+ pm.save(p) |
|
| 169 |
+ requiresManualRestore := !pm.config.LiveRestoreEnabled && p.IsEnabled() |
|
| 170 | 170 |
|
| 171 | 171 |
if requiresManualRestore {
|
| 172 | 172 |
// if liveRestore is not enabled, the plugin will be stopped now so we should enable it |
| ... | ... |
@@ -176,10 +214,50 @@ func (pm *Manager) reload() error {
|
| 176 | 176 |
} |
| 177 | 177 |
}(p) |
| 178 | 178 |
} |
| 179 |
- group.Wait() |
|
| 179 |
+ wg.Wait() |
|
| 180 | 180 |
return nil |
| 181 | 181 |
} |
| 182 | 182 |
|
| 183 |
+func (pm *Manager) loadPlugin(id string) (*v2.Plugin, error) {
|
|
| 184 |
+ p := filepath.Join(pm.config.Root, id, configFileName) |
|
| 185 |
+ dt, err := ioutil.ReadFile(p) |
|
| 186 |
+ if err != nil {
|
|
| 187 |
+ return nil, errors.Wrapf(err, "error reading %v", p) |
|
| 188 |
+ } |
|
| 189 |
+ var plugin v2.Plugin |
|
| 190 |
+ if err := json.Unmarshal(dt, &plugin); err != nil {
|
|
| 191 |
+ return nil, errors.Wrapf(err, "error decoding %v", p) |
|
| 192 |
+ } |
|
| 193 |
+ return &plugin, nil |
|
| 194 |
+} |
|
| 195 |
+ |
|
| 196 |
+func (pm *Manager) save(p *v2.Plugin) error {
|
|
| 197 |
+ pluginJSON, err := json.Marshal(p) |
|
| 198 |
+ if err != nil {
|
|
| 199 |
+ return errors.Wrap(err, "failed to marshal plugin json") |
|
| 200 |
+ } |
|
| 201 |
+ if err := ioutils.AtomicWriteFile(filepath.Join(pm.config.Root, p.GetID(), configFileName), pluginJSON, 0600); err != nil {
|
|
| 202 |
+ return err |
|
| 203 |
+ } |
|
| 204 |
+ return nil |
|
| 205 |
+} |
|
| 206 |
+ |
|
| 207 |
+// GC cleans up unrefrenced blobs. This is recommended to run in a goroutine |
|
| 208 |
+func (pm *Manager) GC() {
|
|
| 209 |
+ pm.muGC.Lock() |
|
| 210 |
+ defer pm.muGC.Unlock() |
|
| 211 |
+ |
|
| 212 |
+ whitelist := make(map[digest.Digest]struct{})
|
|
| 213 |
+ for _, p := range pm.config.Store.GetAll() {
|
|
| 214 |
+ whitelist[p.Config] = struct{}{}
|
|
| 215 |
+ for _, b := range p.Blobsums {
|
|
| 216 |
+ whitelist[b] = struct{}{}
|
|
| 217 |
+ } |
|
| 218 |
+ } |
|
| 219 |
+ |
|
| 220 |
+ pm.blobStore.gc(whitelist) |
|
| 221 |
+} |
|
| 222 |
+ |
|
| 183 | 223 |
type logHook struct{ id string }
|
| 184 | 224 |
|
| 185 | 225 |
func (logHook) Levels() []logrus.Level {
|
| ... | ... |
@@ -209,3 +287,32 @@ func attachToLog(id string) func(libcontainerd.IOPipe) error {
|
| 209 | 209 |
return nil |
| 210 | 210 |
} |
| 211 | 211 |
} |
| 212 |
+ |
|
| 213 |
+func validatePrivileges(requiredPrivileges, privileges types.PluginPrivileges) error {
|
|
| 214 |
+ // todo: make a better function that doesn't check order |
|
| 215 |
+ if !reflect.DeepEqual(privileges, requiredPrivileges) {
|
|
| 216 |
+ return errors.New("incorrect privileges")
|
|
| 217 |
+ } |
|
| 218 |
+ return nil |
|
| 219 |
+} |
|
| 220 |
+ |
|
| 221 |
+func configToRootFS(c []byte) (*image.RootFS, error) {
|
|
| 222 |
+ var pluginConfig types.PluginConfig |
|
| 223 |
+ if err := json.Unmarshal(c, &pluginConfig); err != nil {
|
|
| 224 |
+ return nil, err |
|
| 225 |
+ } |
|
| 226 |
+ |
|
| 227 |
+ return rootFSFromPlugin(pluginConfig.Rootfs), nil |
|
| 228 |
+} |
|
| 229 |
+ |
|
| 230 |
+func rootFSFromPlugin(pluginfs *types.PluginConfigRootfs) *image.RootFS {
|
|
| 231 |
+ rootFS := image.RootFS{
|
|
| 232 |
+ Type: pluginfs.Type, |
|
| 233 |
+ DiffIDs: make([]layer.DiffID, len(pluginfs.DiffIds)), |
|
| 234 |
+ } |
|
| 235 |
+ for i := range pluginfs.DiffIds {
|
|
| 236 |
+ rootFS.DiffIDs[i] = layer.DiffID(pluginfs.DiffIds[i]) |
|
| 237 |
+ } |
|
| 238 |
+ |
|
| 239 |
+ return &rootFS |
|
| 240 |
+} |
| ... | ... |
@@ -3,26 +3,32 @@ |
| 3 | 3 |
package plugin |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 |
+ "encoding/json" |
|
| 6 | 7 |
"fmt" |
| 8 |
+ "os" |
|
| 7 | 9 |
"path/filepath" |
| 8 | 10 |
"syscall" |
| 9 | 11 |
"time" |
| 10 | 12 |
|
| 11 | 13 |
"github.com/Sirupsen/logrus" |
| 14 |
+ "github.com/docker/distribution/digest" |
|
| 15 |
+ "github.com/docker/docker/api/types" |
|
| 16 |
+ "github.com/docker/docker/daemon/initlayer" |
|
| 12 | 17 |
"github.com/docker/docker/libcontainerd" |
| 13 |
- "github.com/docker/docker/oci" |
|
| 14 | 18 |
"github.com/docker/docker/pkg/mount" |
| 15 | 19 |
"github.com/docker/docker/pkg/plugins" |
| 20 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 16 | 21 |
"github.com/docker/docker/plugin/v2" |
| 17 | 22 |
specs "github.com/opencontainers/runtime-spec/specs-go" |
| 23 |
+ "github.com/pkg/errors" |
|
| 18 | 24 |
) |
| 19 | 25 |
|
| 20 | 26 |
func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
|
| 21 |
- p.Rootfs = filepath.Join(pm.libRoot, p.PluginObj.ID, "rootfs") |
|
| 27 |
+ p.Rootfs = filepath.Join(pm.config.Root, p.PluginObj.ID, "rootfs") |
|
| 22 | 28 |
if p.IsEnabled() && !force {
|
| 23 | 29 |
return fmt.Errorf("plugin %s is already enabled", p.Name())
|
| 24 | 30 |
} |
| 25 |
- spec, err := p.InitSpec(oci.DefaultSpec()) |
|
| 31 |
+ spec, err := p.InitSpec(pm.config.ExecRoot) |
|
| 26 | 32 |
if err != nil {
|
| 27 | 33 |
return err |
| 28 | 34 |
} |
| ... | ... |
@@ -40,6 +46,10 @@ func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
|
| 40 | 40 |
} |
| 41 | 41 |
} |
| 42 | 42 |
|
| 43 |
+ if err := initlayer.Setup(filepath.Join(pm.config.Root, p.PluginObj.ID, rootFSFileName), 0, 0); err != nil {
|
|
| 44 |
+ return err |
|
| 45 |
+ } |
|
| 46 |
+ |
|
| 43 | 47 |
if err := pm.containerdClient.Create(p.GetID(), "", "", specs.Spec(*spec), attachToLog(p.GetID())); err != nil {
|
| 44 | 48 |
if p.PropagatedMount != "" {
|
| 45 | 49 |
if err := mount.Unmount(p.PropagatedMount); err != nil {
|
| ... | ... |
@@ -53,7 +63,7 @@ func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
|
| 53 | 53 |
} |
| 54 | 54 |
|
| 55 | 55 |
func (pm *Manager) pluginPostStart(p *v2.Plugin, c *controller) error {
|
| 56 |
- client, err := plugins.NewClientWithTimeout("unix://"+filepath.Join(p.GetRuntimeSourcePath(), p.GetSocket()), nil, c.timeoutInSecs)
|
|
| 56 |
+ client, err := plugins.NewClientWithTimeout("unix://"+filepath.Join(pm.config.ExecRoot, p.GetID(), p.GetSocket()), nil, c.timeoutInSecs)
|
|
| 57 | 57 |
if err != nil {
|
| 58 | 58 |
c.restart = false |
| 59 | 59 |
shutdownPlugin(p, c, pm.containerdClient) |
| ... | ... |
@@ -61,9 +71,10 @@ func (pm *Manager) pluginPostStart(p *v2.Plugin, c *controller) error {
|
| 61 | 61 |
} |
| 62 | 62 |
|
| 63 | 63 |
p.SetPClient(client) |
| 64 |
- pm.pluginStore.SetState(p, true) |
|
| 65 |
- pm.pluginStore.CallHandler(p) |
|
| 66 |
- return nil |
|
| 64 |
+ pm.config.Store.SetState(p, true) |
|
| 65 |
+ pm.config.Store.CallHandler(p) |
|
| 66 |
+ |
|
| 67 |
+ return pm.save(p) |
|
| 67 | 68 |
} |
| 68 | 69 |
|
| 69 | 70 |
func (pm *Manager) restore(p *v2.Plugin) error {
|
| ... | ... |
@@ -71,7 +82,7 @@ func (pm *Manager) restore(p *v2.Plugin) error {
|
| 71 | 71 |
return err |
| 72 | 72 |
} |
| 73 | 73 |
|
| 74 |
- if pm.liveRestore {
|
|
| 74 |
+ if pm.config.LiveRestoreEnabled {
|
|
| 75 | 75 |
c := &controller{}
|
| 76 | 76 |
if pids, _ := pm.containerdClient.GetPidsForContainer(p.GetID()); len(pids) == 0 {
|
| 77 | 77 |
// plugin is not running, so follow normal startup procedure |
| ... | ... |
@@ -115,19 +126,19 @@ func (pm *Manager) disable(p *v2.Plugin, c *controller) error {
|
| 115 | 115 |
|
| 116 | 116 |
c.restart = false |
| 117 | 117 |
shutdownPlugin(p, c, pm.containerdClient) |
| 118 |
- pm.pluginStore.SetState(p, false) |
|
| 119 |
- return nil |
|
| 118 |
+ pm.config.Store.SetState(p, false) |
|
| 119 |
+ return pm.save(p) |
|
| 120 | 120 |
} |
| 121 | 121 |
|
| 122 | 122 |
// Shutdown stops all plugins and called during daemon shutdown. |
| 123 | 123 |
func (pm *Manager) Shutdown() {
|
| 124 |
- plugins := pm.pluginStore.GetAll() |
|
| 124 |
+ plugins := pm.config.Store.GetAll() |
|
| 125 | 125 |
for _, p := range plugins {
|
| 126 | 126 |
pm.mu.RLock() |
| 127 | 127 |
c := pm.cMap[p] |
| 128 | 128 |
pm.mu.RUnlock() |
| 129 | 129 |
|
| 130 |
- if pm.liveRestore && p.IsEnabled() {
|
|
| 130 |
+ if pm.config.LiveRestoreEnabled && p.IsEnabled() {
|
|
| 131 | 131 |
logrus.Debug("Plugin active when liveRestore is set, skipping shutdown")
|
| 132 | 132 |
continue |
| 133 | 133 |
} |
| ... | ... |
@@ -137,3 +148,69 @@ func (pm *Manager) Shutdown() {
|
| 137 | 137 |
} |
| 138 | 138 |
} |
| 139 | 139 |
} |
| 140 |
+ |
|
| 141 |
+// createPlugin creates a new plugin. take lock before calling. |
|
| 142 |
+func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
|
|
| 143 |
+ if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
|
|
| 144 |
+ return nil, err |
|
| 145 |
+ } |
|
| 146 |
+ |
|
| 147 |
+ configRC, err := pm.blobStore.Get(configDigest) |
|
| 148 |
+ if err != nil {
|
|
| 149 |
+ return nil, err |
|
| 150 |
+ } |
|
| 151 |
+ defer configRC.Close() |
|
| 152 |
+ |
|
| 153 |
+ var config types.PluginConfig |
|
| 154 |
+ dec := json.NewDecoder(configRC) |
|
| 155 |
+ if err := dec.Decode(&config); err != nil {
|
|
| 156 |
+ return nil, errors.Wrapf(err, "failed to parse config") |
|
| 157 |
+ } |
|
| 158 |
+ if dec.More() {
|
|
| 159 |
+ return nil, errors.New("invalid config json")
|
|
| 160 |
+ } |
|
| 161 |
+ |
|
| 162 |
+ requiredPrivileges, err := computePrivileges(config) |
|
| 163 |
+ if err != nil {
|
|
| 164 |
+ return nil, err |
|
| 165 |
+ } |
|
| 166 |
+ if privileges != nil {
|
|
| 167 |
+ if err := validatePrivileges(requiredPrivileges, *privileges); err != nil {
|
|
| 168 |
+ return nil, err |
|
| 169 |
+ } |
|
| 170 |
+ } |
|
| 171 |
+ |
|
| 172 |
+ p = &v2.Plugin{
|
|
| 173 |
+ PluginObj: types.Plugin{
|
|
| 174 |
+ Name: name, |
|
| 175 |
+ ID: stringid.GenerateRandomID(), |
|
| 176 |
+ Config: config, |
|
| 177 |
+ }, |
|
| 178 |
+ Config: configDigest, |
|
| 179 |
+ Blobsums: blobsums, |
|
| 180 |
+ } |
|
| 181 |
+ p.InitEmptySettings() |
|
| 182 |
+ |
|
| 183 |
+ pdir := filepath.Join(pm.config.Root, p.PluginObj.ID) |
|
| 184 |
+ if err := os.MkdirAll(pdir, 0700); err != nil {
|
|
| 185 |
+ return nil, errors.Wrapf(err, "failed to mkdir %v", pdir) |
|
| 186 |
+ } |
|
| 187 |
+ |
|
| 188 |
+ defer func() {
|
|
| 189 |
+ if err != nil {
|
|
| 190 |
+ os.RemoveAll(pdir) |
|
| 191 |
+ } |
|
| 192 |
+ }() |
|
| 193 |
+ |
|
| 194 |
+ if err := os.Rename(rootFSDir, filepath.Join(pdir, rootFSFileName)); err != nil {
|
|
| 195 |
+ return nil, errors.Wrap(err, "failed to rename rootfs") |
|
| 196 |
+ } |
|
| 197 |
+ |
|
| 198 |
+ if err := pm.save(p); err != nil {
|
|
| 199 |
+ return nil, err |
|
| 200 |
+ } |
|
| 201 |
+ |
|
| 202 |
+ pm.config.Store.Add(p) // todo: remove |
|
| 203 |
+ |
|
| 204 |
+ return p, nil |
|
| 205 |
+} |
| 140 | 206 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,263 @@ |
| 0 |
+package plugin |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "strings" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/Sirupsen/logrus" |
|
| 7 |
+ "github.com/docker/docker/pkg/plugingetter" |
|
| 8 |
+ "github.com/docker/docker/pkg/plugins" |
|
| 9 |
+ "github.com/docker/docker/plugin/v2" |
|
| 10 |
+ "github.com/docker/docker/reference" |
|
| 11 |
+ "github.com/pkg/errors" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+/* allowV1PluginsFallback determines daemon's support for V1 plugins. |
|
| 15 |
+ * When the time comes to remove support for V1 plugins, flipping |
|
| 16 |
+ * this bool is all that will be needed. |
|
| 17 |
+ */ |
|
| 18 |
+const allowV1PluginsFallback bool = true |
|
| 19 |
+ |
|
| 20 |
+/* defaultAPIVersion is the version of the plugin API for volume, network, |
|
| 21 |
+ IPAM and authz. This is a very stable API. When we update this API, then |
|
| 22 |
+ pluginType should include a version. e.g. "networkdriver/2.0". |
|
| 23 |
+*/ |
|
| 24 |
+const defaultAPIVersion string = "1.0" |
|
| 25 |
+ |
|
| 26 |
+// ErrNotFound indicates that a plugin was not found locally. |
|
| 27 |
+type ErrNotFound string |
|
| 28 |
+ |
|
| 29 |
+func (name ErrNotFound) Error() string { return fmt.Sprintf("plugin %q not found", string(name)) }
|
|
| 30 |
+ |
|
| 31 |
+// ErrAmbiguous indicates that a plugin was not found locally. |
|
| 32 |
+type ErrAmbiguous string |
|
| 33 |
+ |
|
| 34 |
+func (name ErrAmbiguous) Error() string {
|
|
| 35 |
+ return fmt.Sprintf("multiple plugins found for %q", string(name))
|
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+// GetV2Plugin retreives a plugin by name, id or partial ID. |
|
| 39 |
+func (ps *Store) GetV2Plugin(refOrID string) (*v2.Plugin, error) {
|
|
| 40 |
+ ps.RLock() |
|
| 41 |
+ defer ps.RUnlock() |
|
| 42 |
+ |
|
| 43 |
+ id, err := ps.resolvePluginID(refOrID) |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ return nil, err |
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ p, idOk := ps.plugins[id] |
|
| 49 |
+ if !idOk {
|
|
| 50 |
+ return nil, errors.WithStack(ErrNotFound(id)) |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ return p, nil |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+// validateName returns error if name is already reserved. always call with lock and full name |
|
| 57 |
+func (ps *Store) validateName(name string) error {
|
|
| 58 |
+ for _, p := range ps.plugins {
|
|
| 59 |
+ if p.Name() == name {
|
|
| 60 |
+ return errors.Errorf("%v already exists", name)
|
|
| 61 |
+ } |
|
| 62 |
+ } |
|
| 63 |
+ return nil |
|
| 64 |
+} |
|
| 65 |
+ |
|
| 66 |
+// GetAll retreives all plugins. |
|
| 67 |
+func (ps *Store) GetAll() map[string]*v2.Plugin {
|
|
| 68 |
+ ps.RLock() |
|
| 69 |
+ defer ps.RUnlock() |
|
| 70 |
+ return ps.plugins |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+// SetAll initialized plugins during daemon restore. |
|
| 74 |
+func (ps *Store) SetAll(plugins map[string]*v2.Plugin) {
|
|
| 75 |
+ ps.Lock() |
|
| 76 |
+ defer ps.Unlock() |
|
| 77 |
+ ps.plugins = plugins |
|
| 78 |
+} |
|
| 79 |
+ |
|
| 80 |
+func (ps *Store) getAllByCap(capability string) []plugingetter.CompatPlugin {
|
|
| 81 |
+ ps.RLock() |
|
| 82 |
+ defer ps.RUnlock() |
|
| 83 |
+ |
|
| 84 |
+ result := make([]plugingetter.CompatPlugin, 0, 1) |
|
| 85 |
+ for _, p := range ps.plugins {
|
|
| 86 |
+ if p.IsEnabled() {
|
|
| 87 |
+ if _, err := p.FilterByCap(capability); err == nil {
|
|
| 88 |
+ result = append(result, p) |
|
| 89 |
+ } |
|
| 90 |
+ } |
|
| 91 |
+ } |
|
| 92 |
+ return result |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// SetState sets the active state of the plugin and updates plugindb. |
|
| 96 |
+func (ps *Store) SetState(p *v2.Plugin, state bool) {
|
|
| 97 |
+ ps.Lock() |
|
| 98 |
+ defer ps.Unlock() |
|
| 99 |
+ |
|
| 100 |
+ p.PluginObj.Enabled = state |
|
| 101 |
+} |
|
| 102 |
+ |
|
| 103 |
+// Add adds a plugin to memory and plugindb. |
|
| 104 |
+// An error will be returned if there is a collision. |
|
| 105 |
+func (ps *Store) Add(p *v2.Plugin) error {
|
|
| 106 |
+ ps.Lock() |
|
| 107 |
+ defer ps.Unlock() |
|
| 108 |
+ |
|
| 109 |
+ if v, exist := ps.plugins[p.GetID()]; exist {
|
|
| 110 |
+ return fmt.Errorf("plugin %q has the same ID %s as %q", p.Name(), p.GetID(), v.Name())
|
|
| 111 |
+ } |
|
| 112 |
+ ps.plugins[p.GetID()] = p |
|
| 113 |
+ return nil |
|
| 114 |
+} |
|
| 115 |
+ |
|
| 116 |
+// Remove removes a plugin from memory and plugindb. |
|
| 117 |
+func (ps *Store) Remove(p *v2.Plugin) {
|
|
| 118 |
+ ps.Lock() |
|
| 119 |
+ delete(ps.plugins, p.GetID()) |
|
| 120 |
+ ps.Unlock() |
|
| 121 |
+} |
|
| 122 |
+ |
|
| 123 |
+// Get returns an enabled plugin matching the given name and capability. |
|
| 124 |
+func (ps *Store) Get(name, capability string, mode int) (plugingetter.CompatPlugin, error) {
|
|
| 125 |
+ var ( |
|
| 126 |
+ p *v2.Plugin |
|
| 127 |
+ err error |
|
| 128 |
+ ) |
|
| 129 |
+ |
|
| 130 |
+ // Lookup using new model. |
|
| 131 |
+ if ps != nil {
|
|
| 132 |
+ p, err = ps.GetV2Plugin(name) |
|
| 133 |
+ if err == nil {
|
|
| 134 |
+ p.AddRefCount(mode) |
|
| 135 |
+ if p.IsEnabled() {
|
|
| 136 |
+ return p.FilterByCap(capability) |
|
| 137 |
+ } |
|
| 138 |
+ // Plugin was found but it is disabled, so we should not fall back to legacy plugins |
|
| 139 |
+ // but we should error out right away |
|
| 140 |
+ return nil, ErrNotFound(name) |
|
| 141 |
+ } |
|
| 142 |
+ if _, ok := errors.Cause(err).(ErrNotFound); !ok {
|
|
| 143 |
+ return nil, err |
|
| 144 |
+ } |
|
| 145 |
+ } |
|
| 146 |
+ |
|
| 147 |
+ // Lookup using legacy model. |
|
| 148 |
+ if allowV1PluginsFallback {
|
|
| 149 |
+ p, err := plugins.Get(name, capability) |
|
| 150 |
+ if err != nil {
|
|
| 151 |
+ return nil, fmt.Errorf("legacy plugin: %v", err)
|
|
| 152 |
+ } |
|
| 153 |
+ return p, nil |
|
| 154 |
+ } |
|
| 155 |
+ |
|
| 156 |
+ return nil, err |
|
| 157 |
+} |
|
| 158 |
+ |
|
| 159 |
+// GetAllManagedPluginsByCap returns a list of managed plugins matching the given capability. |
|
| 160 |
+func (ps *Store) GetAllManagedPluginsByCap(capability string) []plugingetter.CompatPlugin {
|
|
| 161 |
+ return ps.getAllByCap(capability) |
|
| 162 |
+} |
|
| 163 |
+ |
|
| 164 |
+// GetAllByCap returns a list of enabled plugins matching the given capability. |
|
| 165 |
+func (ps *Store) GetAllByCap(capability string) ([]plugingetter.CompatPlugin, error) {
|
|
| 166 |
+ result := make([]plugingetter.CompatPlugin, 0, 1) |
|
| 167 |
+ |
|
| 168 |
+ /* Daemon start always calls plugin.Init thereby initializing a store. |
|
| 169 |
+ * So store on experimental builds can never be nil, even while |
|
| 170 |
+ * handling legacy plugins. However, there are legacy plugin unit |
|
| 171 |
+ * tests where the volume subsystem directly talks with the plugin, |
|
| 172 |
+ * bypassing the daemon. For such tests, this check is necessary. |
|
| 173 |
+ */ |
|
| 174 |
+ if ps != nil {
|
|
| 175 |
+ ps.RLock() |
|
| 176 |
+ result = ps.getAllByCap(capability) |
|
| 177 |
+ ps.RUnlock() |
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ // Lookup with legacy model |
|
| 181 |
+ if allowV1PluginsFallback {
|
|
| 182 |
+ pl, err := plugins.GetAll(capability) |
|
| 183 |
+ if err != nil {
|
|
| 184 |
+ return nil, fmt.Errorf("legacy plugin: %v", err)
|
|
| 185 |
+ } |
|
| 186 |
+ for _, p := range pl {
|
|
| 187 |
+ result = append(result, p) |
|
| 188 |
+ } |
|
| 189 |
+ } |
|
| 190 |
+ return result, nil |
|
| 191 |
+} |
|
| 192 |
+ |
|
| 193 |
+// Handle sets a callback for a given capability. It is only used by network |
|
| 194 |
+// and ipam drivers during plugin registration. The callback registers the |
|
| 195 |
+// driver with the subsystem (network, ipam). |
|
| 196 |
+func (ps *Store) Handle(capability string, callback func(string, *plugins.Client)) {
|
|
| 197 |
+ pluginType := fmt.Sprintf("docker.%s/%s", strings.ToLower(capability), defaultAPIVersion)
|
|
| 198 |
+ |
|
| 199 |
+ // Register callback with new plugin model. |
|
| 200 |
+ ps.Lock() |
|
| 201 |
+ handlers, ok := ps.handlers[pluginType] |
|
| 202 |
+ if !ok {
|
|
| 203 |
+ handlers = []func(string, *plugins.Client){}
|
|
| 204 |
+ } |
|
| 205 |
+ handlers = append(handlers, callback) |
|
| 206 |
+ ps.handlers[pluginType] = handlers |
|
| 207 |
+ ps.Unlock() |
|
| 208 |
+ |
|
| 209 |
+ // Register callback with legacy plugin model. |
|
| 210 |
+ if allowV1PluginsFallback {
|
|
| 211 |
+ plugins.Handle(capability, callback) |
|
| 212 |
+ } |
|
| 213 |
+} |
|
| 214 |
+ |
|
| 215 |
+// CallHandler calls the registered callback. It is invoked during plugin enable. |
|
| 216 |
+func (ps *Store) CallHandler(p *v2.Plugin) {
|
|
| 217 |
+ for _, typ := range p.GetTypes() {
|
|
| 218 |
+ for _, handler := range ps.handlers[typ.String()] {
|
|
| 219 |
+ handler(p.Name(), p.Client()) |
|
| 220 |
+ } |
|
| 221 |
+ } |
|
| 222 |
+} |
|
| 223 |
+ |
|
| 224 |
+func (ps *Store) resolvePluginID(idOrName string) (string, error) {
|
|
| 225 |
+ ps.RLock() // todo: fix |
|
| 226 |
+ defer ps.RUnlock() |
|
| 227 |
+ |
|
| 228 |
+ if validFullID.MatchString(idOrName) {
|
|
| 229 |
+ return idOrName, nil |
|
| 230 |
+ } |
|
| 231 |
+ |
|
| 232 |
+ ref, err := reference.ParseNamed(idOrName) |
|
| 233 |
+ if err != nil {
|
|
| 234 |
+ return "", errors.Wrapf(err, "failed to parse %v", idOrName) |
|
| 235 |
+ } |
|
| 236 |
+ if _, ok := ref.(reference.Canonical); ok {
|
|
| 237 |
+ logrus.Warnf("canonical references cannot be resolved: %v", ref.String())
|
|
| 238 |
+ return "", errors.WithStack(ErrNotFound(idOrName)) |
|
| 239 |
+ } |
|
| 240 |
+ |
|
| 241 |
+ fullRef := reference.WithDefaultTag(ref) |
|
| 242 |
+ |
|
| 243 |
+ for _, p := range ps.plugins {
|
|
| 244 |
+ if p.PluginObj.Name == fullRef.String() {
|
|
| 245 |
+ return p.PluginObj.ID, nil |
|
| 246 |
+ } |
|
| 247 |
+ } |
|
| 248 |
+ |
|
| 249 |
+ var found *v2.Plugin |
|
| 250 |
+ for id, p := range ps.plugins { // this can be optimized
|
|
| 251 |
+ if strings.HasPrefix(id, idOrName) {
|
|
| 252 |
+ if found != nil {
|
|
| 253 |
+ return "", errors.WithStack(ErrAmbiguous(idOrName)) |
|
| 254 |
+ } |
|
| 255 |
+ found = p |
|
| 256 |
+ } |
|
| 257 |
+ } |
|
| 258 |
+ if found == nil {
|
|
| 259 |
+ return "", errors.WithStack(ErrNotFound(idOrName)) |
|
| 260 |
+ } |
|
| 261 |
+ return found.PluginObj.ID, nil |
|
| 262 |
+} |
| 0 | 263 |
deleted file mode 100644 |
| ... | ... |
@@ -1,31 +0,0 @@ |
| 1 |
-package store |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "path/filepath" |
|
| 5 |
- "sync" |
|
| 6 |
- |
|
| 7 |
- "github.com/docker/docker/pkg/plugins" |
|
| 8 |
- "github.com/docker/docker/plugin/v2" |
|
| 9 |
-) |
|
| 10 |
- |
|
| 11 |
-// Store manages the plugin inventory in memory and on-disk |
|
| 12 |
-type Store struct {
|
|
| 13 |
- sync.RWMutex |
|
| 14 |
- plugins map[string]*v2.Plugin |
|
| 15 |
- /* handlers are necessary for transition path of legacy plugins |
|
| 16 |
- * to the new model. Legacy plugins use Handle() for registering an |
|
| 17 |
- * activation callback.*/ |
|
| 18 |
- handlers map[string][]func(string, *plugins.Client) |
|
| 19 |
- nameToID map[string]string |
|
| 20 |
- plugindb string |
|
| 21 |
-} |
|
| 22 |
- |
|
| 23 |
-// NewStore creates a Store. |
|
| 24 |
-func NewStore(libRoot string) *Store {
|
|
| 25 |
- return &Store{
|
|
| 26 |
- plugins: make(map[string]*v2.Plugin), |
|
| 27 |
- handlers: make(map[string][]func(string, *plugins.Client)), |
|
| 28 |
- nameToID: make(map[string]string), |
|
| 29 |
- plugindb: filepath.Join(libRoot, "plugins", "plugins.json"), |
|
| 30 |
- } |
|
| 31 |
-} |
| 32 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,294 +0,0 @@ |
| 1 |
-package store |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "encoding/json" |
|
| 5 |
- "fmt" |
|
| 6 |
- "strings" |
|
| 7 |
- |
|
| 8 |
- "github.com/Sirupsen/logrus" |
|
| 9 |
- "github.com/docker/docker/pkg/ioutils" |
|
| 10 |
- "github.com/docker/docker/pkg/plugingetter" |
|
| 11 |
- "github.com/docker/docker/pkg/plugins" |
|
| 12 |
- "github.com/docker/docker/plugin/v2" |
|
| 13 |
- "github.com/docker/docker/reference" |
|
| 14 |
-) |
|
| 15 |
- |
|
| 16 |
-/* allowV1PluginsFallback determines daemon's support for V1 plugins. |
|
| 17 |
- * When the time comes to remove support for V1 plugins, flipping |
|
| 18 |
- * this bool is all that will be needed. |
|
| 19 |
- */ |
|
| 20 |
-const allowV1PluginsFallback bool = true |
|
| 21 |
- |
|
| 22 |
-/* defaultAPIVersion is the version of the plugin API for volume, network, |
|
| 23 |
- IPAM and authz. This is a very stable API. When we update this API, then |
|
| 24 |
- pluginType should include a version. e.g. "networkdriver/2.0". |
|
| 25 |
-*/ |
|
| 26 |
-const defaultAPIVersion string = "1.0" |
|
| 27 |
- |
|
| 28 |
-// ErrNotFound indicates that a plugin was not found locally. |
|
| 29 |
-type ErrNotFound string |
|
| 30 |
- |
|
| 31 |
-func (name ErrNotFound) Error() string { return fmt.Sprintf("plugin %q not found", string(name)) }
|
|
| 32 |
- |
|
| 33 |
-// ErrAmbiguous indicates that a plugin was not found locally. |
|
| 34 |
-type ErrAmbiguous string |
|
| 35 |
- |
|
| 36 |
-func (name ErrAmbiguous) Error() string {
|
|
| 37 |
- return fmt.Sprintf("multiple plugins found for %q", string(name))
|
|
| 38 |
-} |
|
| 39 |
- |
|
| 40 |
-// GetByName retreives a plugin by name. |
|
| 41 |
-func (ps *Store) GetByName(name string) (*v2.Plugin, error) {
|
|
| 42 |
- ps.RLock() |
|
| 43 |
- defer ps.RUnlock() |
|
| 44 |
- |
|
| 45 |
- id, nameOk := ps.nameToID[name] |
|
| 46 |
- if !nameOk {
|
|
| 47 |
- return nil, ErrNotFound(name) |
|
| 48 |
- } |
|
| 49 |
- |
|
| 50 |
- p, idOk := ps.plugins[id] |
|
| 51 |
- if !idOk {
|
|
| 52 |
- return nil, ErrNotFound(id) |
|
| 53 |
- } |
|
| 54 |
- return p, nil |
|
| 55 |
-} |
|
| 56 |
- |
|
| 57 |
-// GetByID retreives a plugin by ID. |
|
| 58 |
-func (ps *Store) GetByID(id string) (*v2.Plugin, error) {
|
|
| 59 |
- ps.RLock() |
|
| 60 |
- defer ps.RUnlock() |
|
| 61 |
- |
|
| 62 |
- p, idOk := ps.plugins[id] |
|
| 63 |
- if !idOk {
|
|
| 64 |
- return nil, ErrNotFound(id) |
|
| 65 |
- } |
|
| 66 |
- return p, nil |
|
| 67 |
-} |
|
| 68 |
- |
|
| 69 |
-// GetAll retreives all plugins. |
|
| 70 |
-func (ps *Store) GetAll() map[string]*v2.Plugin {
|
|
| 71 |
- ps.RLock() |
|
| 72 |
- defer ps.RUnlock() |
|
| 73 |
- return ps.plugins |
|
| 74 |
-} |
|
| 75 |
- |
|
| 76 |
-// SetAll initialized plugins during daemon restore. |
|
| 77 |
-func (ps *Store) SetAll(plugins map[string]*v2.Plugin) {
|
|
| 78 |
- ps.Lock() |
|
| 79 |
- defer ps.Unlock() |
|
| 80 |
- ps.plugins = plugins |
|
| 81 |
-} |
|
| 82 |
- |
|
| 83 |
-func (ps *Store) getAllByCap(capability string) []plugingetter.CompatPlugin {
|
|
| 84 |
- ps.RLock() |
|
| 85 |
- defer ps.RUnlock() |
|
| 86 |
- |
|
| 87 |
- result := make([]plugingetter.CompatPlugin, 0, 1) |
|
| 88 |
- for _, p := range ps.plugins {
|
|
| 89 |
- if p.IsEnabled() {
|
|
| 90 |
- if _, err := p.FilterByCap(capability); err == nil {
|
|
| 91 |
- result = append(result, p) |
|
| 92 |
- } |
|
| 93 |
- } |
|
| 94 |
- } |
|
| 95 |
- return result |
|
| 96 |
-} |
|
| 97 |
- |
|
| 98 |
-// SetState sets the active state of the plugin and updates plugindb. |
|
| 99 |
-func (ps *Store) SetState(p *v2.Plugin, state bool) {
|
|
| 100 |
- ps.Lock() |
|
| 101 |
- defer ps.Unlock() |
|
| 102 |
- |
|
| 103 |
- p.PluginObj.Enabled = state |
|
| 104 |
- ps.updatePluginDB() |
|
| 105 |
-} |
|
| 106 |
- |
|
| 107 |
-// Add adds a plugin to memory and plugindb. |
|
| 108 |
-// An error will be returned if there is a collision. |
|
| 109 |
-func (ps *Store) Add(p *v2.Plugin) error {
|
|
| 110 |
- ps.Lock() |
|
| 111 |
- defer ps.Unlock() |
|
| 112 |
- |
|
| 113 |
- if v, exist := ps.plugins[p.GetID()]; exist {
|
|
| 114 |
- return fmt.Errorf("plugin %q has the same ID %s as %q", p.Name(), p.GetID(), v.Name())
|
|
| 115 |
- } |
|
| 116 |
- // Since both Pull() and CreateFromContext() calls GetByName() before any plugin |
|
| 117 |
- // to search for collision (to fail fast), it is unlikely the following check |
|
| 118 |
- // will return an error. |
|
| 119 |
- // However, in case two CreateFromContext() are called at the same time, |
|
| 120 |
- // there is still a remote possibility that a collision might happen. |
|
| 121 |
- // For that reason we still perform the collision check below as it is protected |
|
| 122 |
- // by ps.Lock() and ps.Unlock() above. |
|
| 123 |
- if _, exist := ps.nameToID[p.Name()]; exist {
|
|
| 124 |
- return fmt.Errorf("plugin %q already exists", p.Name())
|
|
| 125 |
- } |
|
| 126 |
- ps.plugins[p.GetID()] = p |
|
| 127 |
- ps.nameToID[p.Name()] = p.GetID() |
|
| 128 |
- ps.updatePluginDB() |
|
| 129 |
- return nil |
|
| 130 |
-} |
|
| 131 |
- |
|
| 132 |
-// Update updates a plugin to memory and plugindb. |
|
| 133 |
-func (ps *Store) Update(p *v2.Plugin) {
|
|
| 134 |
- ps.Lock() |
|
| 135 |
- defer ps.Unlock() |
|
| 136 |
- |
|
| 137 |
- ps.plugins[p.GetID()] = p |
|
| 138 |
- ps.nameToID[p.Name()] = p.GetID() |
|
| 139 |
- ps.updatePluginDB() |
|
| 140 |
-} |
|
| 141 |
- |
|
| 142 |
-// Remove removes a plugin from memory and plugindb. |
|
| 143 |
-func (ps *Store) Remove(p *v2.Plugin) {
|
|
| 144 |
- ps.Lock() |
|
| 145 |
- delete(ps.plugins, p.GetID()) |
|
| 146 |
- delete(ps.nameToID, p.Name()) |
|
| 147 |
- ps.updatePluginDB() |
|
| 148 |
- ps.Unlock() |
|
| 149 |
-} |
|
| 150 |
- |
|
| 151 |
-// Callers are expected to hold the store lock. |
|
| 152 |
-func (ps *Store) updatePluginDB() error {
|
|
| 153 |
- jsonData, err := json.Marshal(ps.plugins) |
|
| 154 |
- if err != nil {
|
|
| 155 |
- logrus.Debugf("Error in json.Marshal: %v", err)
|
|
| 156 |
- return err |
|
| 157 |
- } |
|
| 158 |
- ioutils.AtomicWriteFile(ps.plugindb, jsonData, 0600) |
|
| 159 |
- return nil |
|
| 160 |
-} |
|
| 161 |
- |
|
| 162 |
-// Get returns an enabled plugin matching the given name and capability. |
|
| 163 |
-func (ps *Store) Get(name, capability string, mode int) (plugingetter.CompatPlugin, error) {
|
|
| 164 |
- var ( |
|
| 165 |
- p *v2.Plugin |
|
| 166 |
- err error |
|
| 167 |
- ) |
|
| 168 |
- |
|
| 169 |
- // Lookup using new model. |
|
| 170 |
- if ps != nil {
|
|
| 171 |
- fullName := name |
|
| 172 |
- if named, err := reference.ParseNamed(fullName); err == nil { // FIXME: validate
|
|
| 173 |
- if reference.IsNameOnly(named) {
|
|
| 174 |
- named = reference.WithDefaultTag(named) |
|
| 175 |
- } |
|
| 176 |
- ref, ok := named.(reference.NamedTagged) |
|
| 177 |
- if !ok {
|
|
| 178 |
- return nil, fmt.Errorf("invalid name: %s", named.String())
|
|
| 179 |
- } |
|
| 180 |
- fullName = ref.String() |
|
| 181 |
- } |
|
| 182 |
- p, err = ps.GetByName(fullName) |
|
| 183 |
- if err == nil {
|
|
| 184 |
- p.AddRefCount(mode) |
|
| 185 |
- if p.IsEnabled() {
|
|
| 186 |
- return p.FilterByCap(capability) |
|
| 187 |
- } |
|
| 188 |
- // Plugin was found but it is disabled, so we should not fall back to legacy plugins |
|
| 189 |
- // but we should error out right away |
|
| 190 |
- return nil, ErrNotFound(fullName) |
|
| 191 |
- } |
|
| 192 |
- if _, ok := err.(ErrNotFound); !ok {
|
|
| 193 |
- return nil, err |
|
| 194 |
- } |
|
| 195 |
- } |
|
| 196 |
- |
|
| 197 |
- // Lookup using legacy model. |
|
| 198 |
- if allowV1PluginsFallback {
|
|
| 199 |
- p, err := plugins.Get(name, capability) |
|
| 200 |
- if err != nil {
|
|
| 201 |
- return nil, fmt.Errorf("legacy plugin: %v", err)
|
|
| 202 |
- } |
|
| 203 |
- return p, nil |
|
| 204 |
- } |
|
| 205 |
- |
|
| 206 |
- return nil, err |
|
| 207 |
-} |
|
| 208 |
- |
|
| 209 |
-// GetAllManagedPluginsByCap returns a list of managed plugins matching the given capability. |
|
| 210 |
-func (ps *Store) GetAllManagedPluginsByCap(capability string) []plugingetter.CompatPlugin {
|
|
| 211 |
- return ps.getAllByCap(capability) |
|
| 212 |
-} |
|
| 213 |
- |
|
| 214 |
-// GetAllByCap returns a list of enabled plugins matching the given capability. |
|
| 215 |
-func (ps *Store) GetAllByCap(capability string) ([]plugingetter.CompatPlugin, error) {
|
|
| 216 |
- result := make([]plugingetter.CompatPlugin, 0, 1) |
|
| 217 |
- |
|
| 218 |
- /* Daemon start always calls plugin.Init thereby initializing a store. |
|
| 219 |
- * So store on experimental builds can never be nil, even while |
|
| 220 |
- * handling legacy plugins. However, there are legacy plugin unit |
|
| 221 |
- * tests where the volume subsystem directly talks with the plugin, |
|
| 222 |
- * bypassing the daemon. For such tests, this check is necessary. |
|
| 223 |
- */ |
|
| 224 |
- if ps != nil {
|
|
| 225 |
- ps.RLock() |
|
| 226 |
- result = ps.getAllByCap(capability) |
|
| 227 |
- ps.RUnlock() |
|
| 228 |
- } |
|
| 229 |
- |
|
| 230 |
- // Lookup with legacy model |
|
| 231 |
- if allowV1PluginsFallback {
|
|
| 232 |
- pl, err := plugins.GetAll(capability) |
|
| 233 |
- if err != nil {
|
|
| 234 |
- return nil, fmt.Errorf("legacy plugin: %v", err)
|
|
| 235 |
- } |
|
| 236 |
- for _, p := range pl {
|
|
| 237 |
- result = append(result, p) |
|
| 238 |
- } |
|
| 239 |
- } |
|
| 240 |
- return result, nil |
|
| 241 |
-} |
|
| 242 |
- |
|
| 243 |
-// Handle sets a callback for a given capability. It is only used by network |
|
| 244 |
-// and ipam drivers during plugin registration. The callback registers the |
|
| 245 |
-// driver with the subsystem (network, ipam). |
|
| 246 |
-func (ps *Store) Handle(capability string, callback func(string, *plugins.Client)) {
|
|
| 247 |
- pluginType := fmt.Sprintf("docker.%s/%s", strings.ToLower(capability), defaultAPIVersion)
|
|
| 248 |
- |
|
| 249 |
- // Register callback with new plugin model. |
|
| 250 |
- ps.Lock() |
|
| 251 |
- handlers, ok := ps.handlers[pluginType] |
|
| 252 |
- if !ok {
|
|
| 253 |
- handlers = []func(string, *plugins.Client){}
|
|
| 254 |
- } |
|
| 255 |
- handlers = append(handlers, callback) |
|
| 256 |
- ps.handlers[pluginType] = handlers |
|
| 257 |
- ps.Unlock() |
|
| 258 |
- |
|
| 259 |
- // Register callback with legacy plugin model. |
|
| 260 |
- if allowV1PluginsFallback {
|
|
| 261 |
- plugins.Handle(capability, callback) |
|
| 262 |
- } |
|
| 263 |
-} |
|
| 264 |
- |
|
| 265 |
-// CallHandler calls the registered callback. It is invoked during plugin enable. |
|
| 266 |
-func (ps *Store) CallHandler(p *v2.Plugin) {
|
|
| 267 |
- for _, typ := range p.GetTypes() {
|
|
| 268 |
- for _, handler := range ps.handlers[typ.String()] {
|
|
| 269 |
- handler(p.Name(), p.Client()) |
|
| 270 |
- } |
|
| 271 |
- } |
|
| 272 |
-} |
|
| 273 |
- |
|
| 274 |
-// Search retreives a plugin by ID Prefix |
|
| 275 |
-// If no plugin is found, then ErrNotFound is returned |
|
| 276 |
-// If multiple plugins are found, then ErrAmbiguous is returned |
|
| 277 |
-func (ps *Store) Search(partialID string) (*v2.Plugin, error) {
|
|
| 278 |
- ps.RLock() |
|
| 279 |
- defer ps.RUnlock() |
|
| 280 |
- |
|
| 281 |
- var found *v2.Plugin |
|
| 282 |
- for id, p := range ps.plugins {
|
|
| 283 |
- if strings.HasPrefix(id, partialID) {
|
|
| 284 |
- if found != nil {
|
|
| 285 |
- return nil, ErrAmbiguous(partialID) |
|
| 286 |
- } |
|
| 287 |
- found = p |
|
| 288 |
- } |
|
| 289 |
- } |
|
| 290 |
- if found == nil {
|
|
| 291 |
- return nil, ErrNotFound(partialID) |
|
| 292 |
- } |
|
| 293 |
- return found, nil |
|
| 294 |
-} |
| 295 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,34 +0,0 @@ |
| 1 |
-package store |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "testing" |
|
| 5 |
- |
|
| 6 |
- "github.com/docker/docker/api/types" |
|
| 7 |
- "github.com/docker/docker/plugin/v2" |
|
| 8 |
-) |
|
| 9 |
- |
|
| 10 |
-func TestFilterByCapNeg(t *testing.T) {
|
|
| 11 |
- p := v2.NewPlugin("test", "1234567890", "/run/docker", "/var/lib/docker/plugins", "latest")
|
|
| 12 |
- |
|
| 13 |
- iType := types.PluginInterfaceType{"volumedriver", "docker", "1.0"}
|
|
| 14 |
- i := types.PluginConfigInterface{"plugins.sock", []types.PluginInterfaceType{iType}}
|
|
| 15 |
- p.PluginObj.Config.Interface = i |
|
| 16 |
- |
|
| 17 |
- _, err := p.FilterByCap("foobar")
|
|
| 18 |
- if err == nil {
|
|
| 19 |
- t.Fatalf("expected inadequate error, got %v", err)
|
|
| 20 |
- } |
|
| 21 |
-} |
|
| 22 |
- |
|
| 23 |
-func TestFilterByCapPos(t *testing.T) {
|
|
| 24 |
- p := v2.NewPlugin("test", "1234567890", "/run/docker", "/var/lib/docker/plugins", "latest")
|
|
| 25 |
- |
|
| 26 |
- iType := types.PluginInterfaceType{"volumedriver", "docker", "1.0"}
|
|
| 27 |
- i := types.PluginConfigInterface{"plugins.sock", []types.PluginInterfaceType{iType}}
|
|
| 28 |
- p.PluginObj.Config.Interface = i |
|
| 29 |
- |
|
| 30 |
- _, err := p.FilterByCap("volumedriver")
|
|
| 31 |
- if err != nil {
|
|
| 32 |
- t.Fatalf("expected no error, got %v", err)
|
|
| 33 |
- } |
|
| 34 |
-} |
| 35 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,33 @@ |
| 0 |
+package plugin |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/types" |
|
| 6 |
+ "github.com/docker/docker/plugin/v2" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+func TestFilterByCapNeg(t *testing.T) {
|
|
| 10 |
+ p := v2.Plugin{PluginObj: types.Plugin{Name: "test:latest"}}
|
|
| 11 |
+ iType := types.PluginInterfaceType{"volumedriver", "docker", "1.0"}
|
|
| 12 |
+ i := types.PluginConfigInterface{"plugins.sock", []types.PluginInterfaceType{iType}}
|
|
| 13 |
+ p.PluginObj.Config.Interface = i |
|
| 14 |
+ |
|
| 15 |
+ _, err := p.FilterByCap("foobar")
|
|
| 16 |
+ if err == nil {
|
|
| 17 |
+ t.Fatalf("expected inadequate error, got %v", err)
|
|
| 18 |
+ } |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+func TestFilterByCapPos(t *testing.T) {
|
|
| 22 |
+ p := v2.Plugin{PluginObj: types.Plugin{Name: "test:latest"}}
|
|
| 23 |
+ |
|
| 24 |
+ iType := types.PluginInterfaceType{"volumedriver", "docker", "1.0"}
|
|
| 25 |
+ i := types.PluginConfigInterface{"plugins.sock", []types.PluginInterfaceType{iType}}
|
|
| 26 |
+ p.PluginObj.Config.Interface = i |
|
| 27 |
+ |
|
| 28 |
+ _, err := p.FilterByCap("volumedriver")
|
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ t.Fatalf("expected no error, got %v", err)
|
|
| 31 |
+ } |
|
| 32 |
+} |
| ... | ... |
@@ -1,32 +1,27 @@ |
| 1 | 1 |
package v2 |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "encoding/json" |
|
| 5 |
- "errors" |
|
| 6 | 4 |
"fmt" |
| 7 |
- "os" |
|
| 8 |
- "path/filepath" |
|
| 9 | 5 |
"strings" |
| 10 | 6 |
"sync" |
| 11 | 7 |
|
| 8 |
+ "github.com/docker/distribution/digest" |
|
| 12 | 9 |
"github.com/docker/docker/api/types" |
| 13 |
- "github.com/docker/docker/oci" |
|
| 14 | 10 |
"github.com/docker/docker/pkg/plugingetter" |
| 15 | 11 |
"github.com/docker/docker/pkg/plugins" |
| 16 |
- "github.com/docker/docker/pkg/system" |
|
| 17 |
- specs "github.com/opencontainers/runtime-spec/specs-go" |
|
| 18 | 12 |
) |
| 19 | 13 |
|
| 20 | 14 |
// Plugin represents an individual plugin. |
| 21 | 15 |
type Plugin struct {
|
| 22 |
- mu sync.RWMutex |
|
| 23 |
- PluginObj types.Plugin `json:"plugin"` |
|
| 24 |
- pClient *plugins.Client |
|
| 25 |
- runtimeSourcePath string |
|
| 26 |
- refCount int |
|
| 27 |
- LibRoot string // TODO: make private |
|
| 28 |
- PropagatedMount string // TODO: make private |
|
| 29 |
- Rootfs string // TODO: make private |
|
| 16 |
+ mu sync.RWMutex |
|
| 17 |
+ PluginObj types.Plugin `json:"plugin"` // todo: embed struct |
|
| 18 |
+ pClient *plugins.Client |
|
| 19 |
+ refCount int |
|
| 20 |
+ PropagatedMount string // TODO: make private |
|
| 21 |
+ Rootfs string // TODO: make private |
|
| 22 |
+ |
|
| 23 |
+ Config digest.Digest |
|
| 24 |
+ Blobsums []digest.Digest |
|
| 30 | 25 |
} |
| 31 | 26 |
|
| 32 | 27 |
const defaultPluginRuntimeDestination = "/run/docker/plugins" |
| ... | ... |
@@ -40,33 +35,6 @@ func (e ErrInadequateCapability) Error() string {
|
| 40 | 40 |
return fmt.Sprintf("plugin does not provide %q capability", e.cap)
|
| 41 | 41 |
} |
| 42 | 42 |
|
| 43 |
-func newPluginObj(name, id, tag string) types.Plugin {
|
|
| 44 |
- return types.Plugin{Name: name, ID: id, Tag: tag}
|
|
| 45 |
-} |
|
| 46 |
- |
|
| 47 |
-// NewPlugin creates a plugin. |
|
| 48 |
-func NewPlugin(name, id, runRoot, libRoot, tag string) *Plugin {
|
|
| 49 |
- return &Plugin{
|
|
| 50 |
- PluginObj: newPluginObj(name, id, tag), |
|
| 51 |
- runtimeSourcePath: filepath.Join(runRoot, id), |
|
| 52 |
- LibRoot: libRoot, |
|
| 53 |
- } |
|
| 54 |
-} |
|
| 55 |
- |
|
| 56 |
-// Restore restores the plugin |
|
| 57 |
-func (p *Plugin) Restore(runRoot string) {
|
|
| 58 |
- p.runtimeSourcePath = filepath.Join(runRoot, p.GetID()) |
|
| 59 |
-} |
|
| 60 |
- |
|
| 61 |
-// GetRuntimeSourcePath gets the Source (host) path of the plugin socket |
|
| 62 |
-// This path gets bind mounted into the plugin. |
|
| 63 |
-func (p *Plugin) GetRuntimeSourcePath() string {
|
|
| 64 |
- p.mu.RLock() |
|
| 65 |
- defer p.mu.RUnlock() |
|
| 66 |
- |
|
| 67 |
- return p.runtimeSourcePath |
|
| 68 |
-} |
|
| 69 |
- |
|
| 70 | 43 |
// BasePath returns the path to which all paths returned by the plugin are relative to. |
| 71 | 44 |
// For Plugin objects this returns the host path of the plugin container's rootfs. |
| 72 | 45 |
func (p *Plugin) BasePath() string {
|
| ... | ... |
@@ -96,12 +64,7 @@ func (p *Plugin) IsV1() bool {
|
| 96 | 96 |
|
| 97 | 97 |
// Name returns the plugin name. |
| 98 | 98 |
func (p *Plugin) Name() string {
|
| 99 |
- name := p.PluginObj.Name |
|
| 100 |
- if len(p.PluginObj.Tag) > 0 {
|
|
| 101 |
- // TODO: this feels hacky, maybe we should be storing the distribution reference rather than splitting these |
|
| 102 |
- name += ":" + p.PluginObj.Tag |
|
| 103 |
- } |
|
| 104 |
- return name |
|
| 99 |
+ return p.PluginObj.Name |
|
| 105 | 100 |
} |
| 106 | 101 |
|
| 107 | 102 |
// FilterByCap query the plugin for a given capability. |
| ... | ... |
@@ -115,23 +78,8 @@ func (p *Plugin) FilterByCap(capability string) (*Plugin, error) {
|
| 115 | 115 |
return nil, ErrInadequateCapability{capability}
|
| 116 | 116 |
} |
| 117 | 117 |
|
| 118 |
-// RemoveFromDisk deletes the plugin's runtime files from disk. |
|
| 119 |
-func (p *Plugin) RemoveFromDisk() error {
|
|
| 120 |
- return os.RemoveAll(p.runtimeSourcePath) |
|
| 121 |
-} |
|
| 122 |
- |
|
| 123 |
-// InitPlugin populates the plugin object from the plugin config file. |
|
| 124 |
-func (p *Plugin) InitPlugin() error {
|
|
| 125 |
- dt, err := os.Open(filepath.Join(p.LibRoot, p.PluginObj.ID, "config.json")) |
|
| 126 |
- if err != nil {
|
|
| 127 |
- return err |
|
| 128 |
- } |
|
| 129 |
- err = json.NewDecoder(dt).Decode(&p.PluginObj.Config) |
|
| 130 |
- dt.Close() |
|
| 131 |
- if err != nil {
|
|
| 132 |
- return err |
|
| 133 |
- } |
|
| 134 |
- |
|
| 118 |
+// InitEmptySettings initializes empty settings for a plugin. |
|
| 119 |
+func (p *Plugin) InitEmptySettings() {
|
|
| 135 | 120 |
p.PluginObj.Settings.Mounts = make([]types.PluginMount, len(p.PluginObj.Config.Mounts)) |
| 136 | 121 |
copy(p.PluginObj.Settings.Mounts, p.PluginObj.Config.Mounts) |
| 137 | 122 |
p.PluginObj.Settings.Devices = make([]types.PluginDevice, len(p.PluginObj.Config.Linux.Devices)) |
| ... | ... |
@@ -144,18 +92,6 @@ func (p *Plugin) InitPlugin() error {
|
| 144 | 144 |
} |
| 145 | 145 |
p.PluginObj.Settings.Args = make([]string, len(p.PluginObj.Config.Args.Value)) |
| 146 | 146 |
copy(p.PluginObj.Settings.Args, p.PluginObj.Config.Args.Value) |
| 147 |
- |
|
| 148 |
- return p.writeSettings() |
|
| 149 |
-} |
|
| 150 |
- |
|
| 151 |
-func (p *Plugin) writeSettings() error {
|
|
| 152 |
- f, err := os.Create(filepath.Join(p.LibRoot, p.PluginObj.ID, "plugin-settings.json")) |
|
| 153 |
- if err != nil {
|
|
| 154 |
- return err |
|
| 155 |
- } |
|
| 156 |
- err = json.NewEncoder(f).Encode(&p.PluginObj.Settings) |
|
| 157 |
- f.Close() |
|
| 158 |
- return err |
|
| 159 | 147 |
} |
| 160 | 148 |
|
| 161 | 149 |
// Set is used to pass arguments to the plugin. |
| ... | ... |
@@ -243,8 +179,7 @@ next: |
| 243 | 243 |
return fmt.Errorf("setting %q not found in the plugin configuration", s.name)
|
| 244 | 244 |
} |
| 245 | 245 |
|
| 246 |
- // update the settings on disk |
|
| 247 |
- return p.writeSettings() |
|
| 246 |
+ return nil |
|
| 248 | 247 |
} |
| 249 | 248 |
|
| 250 | 249 |
// IsEnabled returns the active state of the plugin. |
| ... | ... |
@@ -307,107 +242,3 @@ func (p *Plugin) Acquire() {
|
| 307 | 307 |
func (p *Plugin) Release() {
|
| 308 | 308 |
p.AddRefCount(plugingetter.RELEASE) |
| 309 | 309 |
} |
| 310 |
- |
|
| 311 |
-// InitSpec creates an OCI spec from the plugin's config. |
|
| 312 |
-func (p *Plugin) InitSpec(s specs.Spec) (*specs.Spec, error) {
|
|
| 313 |
- s.Root = specs.Root{
|
|
| 314 |
- Path: p.Rootfs, |
|
| 315 |
- Readonly: false, // TODO: all plugins should be readonly? settable in config? |
|
| 316 |
- } |
|
| 317 |
- |
|
| 318 |
- userMounts := make(map[string]struct{}, len(p.PluginObj.Settings.Mounts))
|
|
| 319 |
- for _, m := range p.PluginObj.Settings.Mounts {
|
|
| 320 |
- userMounts[m.Destination] = struct{}{}
|
|
| 321 |
- } |
|
| 322 |
- |
|
| 323 |
- if err := os.MkdirAll(p.runtimeSourcePath, 0755); err != nil {
|
|
| 324 |
- return nil, err |
|
| 325 |
- } |
|
| 326 |
- |
|
| 327 |
- mounts := append(p.PluginObj.Config.Mounts, types.PluginMount{
|
|
| 328 |
- Source: &p.runtimeSourcePath, |
|
| 329 |
- Destination: defaultPluginRuntimeDestination, |
|
| 330 |
- Type: "bind", |
|
| 331 |
- Options: []string{"rbind", "rshared"},
|
|
| 332 |
- }) |
|
| 333 |
- |
|
| 334 |
- if p.PluginObj.Config.Network.Type != "" {
|
|
| 335 |
- // TODO: if net == bridge, use libnetwork controller to create a new plugin-specific bridge, bind mount /etc/hosts and /etc/resolv.conf look at the docker code (allocateNetwork, initialize) |
|
| 336 |
- if p.PluginObj.Config.Network.Type == "host" {
|
|
| 337 |
- oci.RemoveNamespace(&s, specs.NamespaceType("network"))
|
|
| 338 |
- } |
|
| 339 |
- etcHosts := "/etc/hosts" |
|
| 340 |
- resolvConf := "/etc/resolv.conf" |
|
| 341 |
- mounts = append(mounts, |
|
| 342 |
- types.PluginMount{
|
|
| 343 |
- Source: &etcHosts, |
|
| 344 |
- Destination: etcHosts, |
|
| 345 |
- Type: "bind", |
|
| 346 |
- Options: []string{"rbind", "ro"},
|
|
| 347 |
- }, |
|
| 348 |
- types.PluginMount{
|
|
| 349 |
- Source: &resolvConf, |
|
| 350 |
- Destination: resolvConf, |
|
| 351 |
- Type: "bind", |
|
| 352 |
- Options: []string{"rbind", "ro"},
|
|
| 353 |
- }) |
|
| 354 |
- } |
|
| 355 |
- |
|
| 356 |
- for _, mnt := range mounts {
|
|
| 357 |
- m := specs.Mount{
|
|
| 358 |
- Destination: mnt.Destination, |
|
| 359 |
- Type: mnt.Type, |
|
| 360 |
- Options: mnt.Options, |
|
| 361 |
- } |
|
| 362 |
- if mnt.Source == nil {
|
|
| 363 |
- return nil, errors.New("mount source is not specified")
|
|
| 364 |
- } |
|
| 365 |
- m.Source = *mnt.Source |
|
| 366 |
- s.Mounts = append(s.Mounts, m) |
|
| 367 |
- } |
|
| 368 |
- |
|
| 369 |
- for i, m := range s.Mounts {
|
|
| 370 |
- if strings.HasPrefix(m.Destination, "/dev/") {
|
|
| 371 |
- if _, ok := userMounts[m.Destination]; ok {
|
|
| 372 |
- s.Mounts = append(s.Mounts[:i], s.Mounts[i+1:]...) |
|
| 373 |
- } |
|
| 374 |
- } |
|
| 375 |
- } |
|
| 376 |
- |
|
| 377 |
- if p.PluginObj.Config.PropagatedMount != "" {
|
|
| 378 |
- p.PropagatedMount = filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount) |
|
| 379 |
- s.Linux.RootfsPropagation = "rshared" |
|
| 380 |
- } |
|
| 381 |
- |
|
| 382 |
- if p.PluginObj.Config.Linux.DeviceCreation {
|
|
| 383 |
- rwm := "rwm" |
|
| 384 |
- s.Linux.Resources.Devices = []specs.DeviceCgroup{{Allow: true, Access: &rwm}}
|
|
| 385 |
- } |
|
| 386 |
- for _, dev := range p.PluginObj.Settings.Devices {
|
|
| 387 |
- path := *dev.Path |
|
| 388 |
- d, dPermissions, err := oci.DevicesFromPath(path, path, "rwm") |
|
| 389 |
- if err != nil {
|
|
| 390 |
- return nil, err |
|
| 391 |
- } |
|
| 392 |
- s.Linux.Devices = append(s.Linux.Devices, d...) |
|
| 393 |
- s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, dPermissions...) |
|
| 394 |
- } |
|
| 395 |
- |
|
| 396 |
- envs := make([]string, 1, len(p.PluginObj.Settings.Env)+1) |
|
| 397 |
- envs[0] = "PATH=" + system.DefaultPathEnv |
|
| 398 |
- envs = append(envs, p.PluginObj.Settings.Env...) |
|
| 399 |
- |
|
| 400 |
- args := append(p.PluginObj.Config.Entrypoint, p.PluginObj.Settings.Args...) |
|
| 401 |
- cwd := p.PluginObj.Config.Workdir |
|
| 402 |
- if len(cwd) == 0 {
|
|
| 403 |
- cwd = "/" |
|
| 404 |
- } |
|
| 405 |
- s.Process.Terminal = false |
|
| 406 |
- s.Process.Args = args |
|
| 407 |
- s.Process.Cwd = cwd |
|
| 408 |
- s.Process.Env = envs |
|
| 409 |
- |
|
| 410 |
- s.Process.Capabilities = append(s.Process.Capabilities, p.PluginObj.Config.Linux.Capabilities...) |
|
| 411 |
- |
|
| 412 |
- return &s, nil |
|
| 413 |
-} |
| 414 | 310 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,121 @@ |
| 0 |
+// +build linux |
|
| 1 |
+ |
|
| 2 |
+package v2 |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "errors" |
|
| 6 |
+ "os" |
|
| 7 |
+ "path/filepath" |
|
| 8 |
+ "strings" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/api/types" |
|
| 11 |
+ "github.com/docker/docker/oci" |
|
| 12 |
+ "github.com/docker/docker/pkg/system" |
|
| 13 |
+ specs "github.com/opencontainers/runtime-spec/specs-go" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+// InitSpec creates an OCI spec from the plugin's config. |
|
| 17 |
+func (p *Plugin) InitSpec(execRoot string) (*specs.Spec, error) {
|
|
| 18 |
+ s := oci.DefaultSpec() |
|
| 19 |
+ s.Root = specs.Root{
|
|
| 20 |
+ Path: p.Rootfs, |
|
| 21 |
+ Readonly: false, // TODO: all plugins should be readonly? settable in config? |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ userMounts := make(map[string]struct{}, len(p.PluginObj.Settings.Mounts))
|
|
| 25 |
+ for _, m := range p.PluginObj.Settings.Mounts {
|
|
| 26 |
+ userMounts[m.Destination] = struct{}{}
|
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ execRoot = filepath.Join(execRoot, p.PluginObj.ID) |
|
| 30 |
+ if err := os.MkdirAll(execRoot, 0700); err != nil {
|
|
| 31 |
+ return nil, err |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ mounts := append(p.PluginObj.Config.Mounts, types.PluginMount{
|
|
| 35 |
+ Source: &execRoot, |
|
| 36 |
+ Destination: defaultPluginRuntimeDestination, |
|
| 37 |
+ Type: "bind", |
|
| 38 |
+ Options: []string{"rbind", "rshared"},
|
|
| 39 |
+ }) |
|
| 40 |
+ |
|
| 41 |
+ if p.PluginObj.Config.Network.Type != "" {
|
|
| 42 |
+ // TODO: if net == bridge, use libnetwork controller to create a new plugin-specific bridge, bind mount /etc/hosts and /etc/resolv.conf look at the docker code (allocateNetwork, initialize) |
|
| 43 |
+ if p.PluginObj.Config.Network.Type == "host" {
|
|
| 44 |
+ oci.RemoveNamespace(&s, specs.NamespaceType("network"))
|
|
| 45 |
+ } |
|
| 46 |
+ etcHosts := "/etc/hosts" |
|
| 47 |
+ resolvConf := "/etc/resolv.conf" |
|
| 48 |
+ mounts = append(mounts, |
|
| 49 |
+ types.PluginMount{
|
|
| 50 |
+ Source: &etcHosts, |
|
| 51 |
+ Destination: etcHosts, |
|
| 52 |
+ Type: "bind", |
|
| 53 |
+ Options: []string{"rbind", "ro"},
|
|
| 54 |
+ }, |
|
| 55 |
+ types.PluginMount{
|
|
| 56 |
+ Source: &resolvConf, |
|
| 57 |
+ Destination: resolvConf, |
|
| 58 |
+ Type: "bind", |
|
| 59 |
+ Options: []string{"rbind", "ro"},
|
|
| 60 |
+ }) |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ for _, mnt := range mounts {
|
|
| 64 |
+ m := specs.Mount{
|
|
| 65 |
+ Destination: mnt.Destination, |
|
| 66 |
+ Type: mnt.Type, |
|
| 67 |
+ Options: mnt.Options, |
|
| 68 |
+ } |
|
| 69 |
+ if mnt.Source == nil {
|
|
| 70 |
+ return nil, errors.New("mount source is not specified")
|
|
| 71 |
+ } |
|
| 72 |
+ m.Source = *mnt.Source |
|
| 73 |
+ s.Mounts = append(s.Mounts, m) |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ for i, m := range s.Mounts {
|
|
| 77 |
+ if strings.HasPrefix(m.Destination, "/dev/") {
|
|
| 78 |
+ if _, ok := userMounts[m.Destination]; ok {
|
|
| 79 |
+ s.Mounts = append(s.Mounts[:i], s.Mounts[i+1:]...) |
|
| 80 |
+ } |
|
| 81 |
+ } |
|
| 82 |
+ } |
|
| 83 |
+ |
|
| 84 |
+ if p.PluginObj.Config.PropagatedMount != "" {
|
|
| 85 |
+ p.PropagatedMount = filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount) |
|
| 86 |
+ s.Linux.RootfsPropagation = "rshared" |
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ if p.PluginObj.Config.Linux.DeviceCreation {
|
|
| 90 |
+ rwm := "rwm" |
|
| 91 |
+ s.Linux.Resources.Devices = []specs.DeviceCgroup{{Allow: true, Access: &rwm}}
|
|
| 92 |
+ } |
|
| 93 |
+ for _, dev := range p.PluginObj.Settings.Devices {
|
|
| 94 |
+ path := *dev.Path |
|
| 95 |
+ d, dPermissions, err := oci.DevicesFromPath(path, path, "rwm") |
|
| 96 |
+ if err != nil {
|
|
| 97 |
+ return nil, err |
|
| 98 |
+ } |
|
| 99 |
+ s.Linux.Devices = append(s.Linux.Devices, d...) |
|
| 100 |
+ s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, dPermissions...) |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ envs := make([]string, 1, len(p.PluginObj.Settings.Env)+1) |
|
| 104 |
+ envs[0] = "PATH=" + system.DefaultPathEnv |
|
| 105 |
+ envs = append(envs, p.PluginObj.Settings.Env...) |
|
| 106 |
+ |
|
| 107 |
+ args := append(p.PluginObj.Config.Entrypoint, p.PluginObj.Settings.Args...) |
|
| 108 |
+ cwd := p.PluginObj.Config.WorkDir |
|
| 109 |
+ if len(cwd) == 0 {
|
|
| 110 |
+ cwd = "/" |
|
| 111 |
+ } |
|
| 112 |
+ s.Process.Terminal = false |
|
| 113 |
+ s.Process.Args = args |
|
| 114 |
+ s.Process.Cwd = cwd |
|
| 115 |
+ s.Process.Env = envs |
|
| 116 |
+ |
|
| 117 |
+ s.Process.Capabilities = append(s.Process.Capabilities, p.PluginObj.Config.Linux.Capabilities...) |
|
| 118 |
+ |
|
| 119 |
+ return &s, nil |
|
| 120 |
+} |
| 0 | 121 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,14 @@ |
| 0 |
+// +build !linux |
|
| 1 |
+ |
|
| 2 |
+package v2 |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "errors" |
|
| 6 |
+ |
|
| 7 |
+ specs "github.com/opencontainers/runtime-spec/specs-go" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// InitSpec creates an OCI spec from the plugin's config. |
|
| 11 |
+func (p *Plugin) InitSpec(execRoot string) (*specs.Spec, error) {
|
|
| 12 |
+ return nil, errors.New("not supported")
|
|
| 13 |
+} |
| ... | ... |
@@ -111,23 +111,25 @@ func lookup(name string, mode int) (volume.Driver, error) {
|
| 111 | 111 |
if ok {
|
| 112 | 112 |
return ext, nil |
| 113 | 113 |
} |
| 114 |
+ if drivers.plugingetter != nil {
|
|
| 115 |
+ p, err := drivers.plugingetter.Get(name, extName, mode) |
|
| 116 |
+ if err != nil {
|
|
| 117 |
+ return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err)
|
|
| 118 |
+ } |
|
| 114 | 119 |
|
| 115 |
- p, err := drivers.plugingetter.Get(name, extName, mode) |
|
| 116 |
- if err != nil {
|
|
| 117 |
- return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err)
|
|
| 118 |
- } |
|
| 119 |
- |
|
| 120 |
- d := NewVolumeDriver(p.Name(), p.BasePath(), p.Client()) |
|
| 121 |
- if err := validateDriver(d); err != nil {
|
|
| 122 |
- return nil, err |
|
| 123 |
- } |
|
| 120 |
+ d := NewVolumeDriver(p.Name(), p.BasePath(), p.Client()) |
|
| 121 |
+ if err := validateDriver(d); err != nil {
|
|
| 122 |
+ return nil, err |
|
| 123 |
+ } |
|
| 124 | 124 |
|
| 125 |
- if p.IsV1() {
|
|
| 126 |
- drivers.Lock() |
|
| 127 |
- drivers.extensions[name] = d |
|
| 128 |
- drivers.Unlock() |
|
| 125 |
+ if p.IsV1() {
|
|
| 126 |
+ drivers.Lock() |
|
| 127 |
+ drivers.extensions[name] = d |
|
| 128 |
+ drivers.Unlock() |
|
| 129 |
+ } |
|
| 130 |
+ return d, nil |
|
| 129 | 131 |
} |
| 130 |
- return d, nil |
|
| 132 |
+ return nil, fmt.Errorf("Error looking up volume plugin %s", name)
|
|
| 131 | 133 |
} |
| 132 | 134 |
|
| 133 | 135 |
func validateDriver(vd volume.Driver) error {
|
| ... | ... |
@@ -179,9 +181,13 @@ func GetDriverList() []string {
|
| 179 | 179 |
|
| 180 | 180 |
// GetAllDrivers lists all the registered drivers |
| 181 | 181 |
func GetAllDrivers() ([]volume.Driver, error) {
|
| 182 |
- plugins, err := drivers.plugingetter.GetAllByCap(extName) |
|
| 183 |
- if err != nil {
|
|
| 184 |
- return nil, fmt.Errorf("error listing plugins: %v", err)
|
|
| 182 |
+ var plugins []getter.CompatPlugin |
|
| 183 |
+ if drivers.plugingetter != nil {
|
|
| 184 |
+ var err error |
|
| 185 |
+ plugins, err = drivers.plugingetter.GetAllByCap(extName) |
|
| 186 |
+ if err != nil {
|
|
| 187 |
+ return nil, fmt.Errorf("error listing plugins: %v", err)
|
|
| 188 |
+ } |
|
| 185 | 189 |
} |
| 186 | 190 |
var ds []volume.Driver |
| 187 | 191 |
|
| ... | ... |
@@ -3,14 +3,10 @@ package volumedrivers |
| 3 | 3 |
import ( |
| 4 | 4 |
"testing" |
| 5 | 5 |
|
| 6 |
- pluginstore "github.com/docker/docker/plugin/store" |
|
| 7 | 6 |
volumetestutils "github.com/docker/docker/volume/testutils" |
| 8 | 7 |
) |
| 9 | 8 |
|
| 10 | 9 |
func TestGetDriver(t *testing.T) {
|
| 11 |
- pluginStore := pluginstore.NewStore("/var/lib/docker")
|
|
| 12 |
- RegisterPluginGetter(pluginStore) |
|
| 13 |
- |
|
| 14 | 10 |
_, err := GetDriver("missing")
|
| 15 | 11 |
if err == nil {
|
| 16 | 12 |
t.Fatal("Expected error, was nil")
|
| ... | ... |
@@ -7,15 +7,11 @@ import ( |
| 7 | 7 |
"strings" |
| 8 | 8 |
"testing" |
| 9 | 9 |
|
| 10 |
- pluginstore "github.com/docker/docker/plugin/store" |
|
| 11 | 10 |
"github.com/docker/docker/volume/drivers" |
| 12 | 11 |
volumetestutils "github.com/docker/docker/volume/testutils" |
| 13 | 12 |
) |
| 14 | 13 |
|
| 15 | 14 |
func TestCreate(t *testing.T) {
|
| 16 |
- pluginStore := pluginstore.NewStore("/var/lib/docker")
|
|
| 17 |
- volumedrivers.RegisterPluginGetter(pluginStore) |
|
| 18 |
- |
|
| 19 | 15 |
volumedrivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
|
| 20 | 16 |
defer volumedrivers.Unregister("fake")
|
| 21 | 17 |
dir, err := ioutil.TempDir("", "test-create")
|