Implement content addressability for plugins

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>
This commit is contained in:
Tonis Tiigi 2016-12-12 15:05:53 -08:00
parent 4b933cc26d
commit 66f7194250
4 changed files with 59 additions and 32 deletions

View File

@ -111,8 +111,8 @@ type PluginAPIClient interface {
PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error
PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error
PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error
PluginPush(ctx context.Context, name string, registryAuth string) error
PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error)
PluginSet(ctx context.Context, name string, args []string) error
PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error)
PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error

View File

@ -2,73 +2,96 @@ package client
import (
"encoding/json"
"io"
"net/http"
"net/url"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
// PluginInstall installs a plugin
func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (err error) {
// FIXME(vdemeester) name is a ref, we might want to parse/validate it here.
func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
query := url.Values{}
query.Set("name", name)
if _, err := reference.ParseNamed(options.RemoteRef); err != nil {
return nil, errors.Wrap(err, "invalid remote reference")
}
query.Set("remote", options.RemoteRef)
resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
// todo: do inspect before to check existing name before checking privileges
newAuthHeader, privilegeErr := options.PrivilegeFunc()
if privilegeErr != nil {
ensureReaderClosed(resp)
return privilegeErr
return nil, privilegeErr
}
options.RegistryAuth = newAuthHeader
resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
}
if err != nil {
ensureReaderClosed(resp)
return err
return nil, err
}
var privileges types.PluginPrivileges
if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
ensureReaderClosed(resp)
return err
return nil, err
}
ensureReaderClosed(resp)
if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
accept, err := options.AcceptPermissionsFunc(privileges)
if err != nil {
return err
return nil, err
}
if !accept {
return pluginPermissionDenied{name}
return nil, pluginPermissionDenied{options.RemoteRef}
}
}
_, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
// set name for plugin pull, if empty should default to remote reference
query.Set("name", name)
resp, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
if err != nil {
return err
return nil, err
}
defer func() {
name = resp.header.Get("Docker-Plugin-Name")
pr, pw := io.Pipe()
go func() { // todo: the client should probably be designed more around the actual api
_, err := io.Copy(pw, resp.body)
if err != nil {
delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil)
ensureReaderClosed(delResp)
pw.CloseWithError(err)
return
}
defer func() {
if err != nil {
delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil)
ensureReaderClosed(delResp)
}
}()
if len(options.Args) > 0 {
if err := cli.PluginSet(ctx, name, options.Args); err != nil {
pw.CloseWithError(err)
return
}
}
if options.Disabled {
pw.Close()
return
}
err = cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0})
pw.CloseWithError(err)
}()
if len(options.Args) > 0 {
if err := cli.PluginSet(ctx, name, options.Args); err != nil {
return err
}
}
if options.Disabled {
return nil
}
return cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0})
return pr, nil
}
func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {

View File

@ -1,13 +1,17 @@
package client
import (
"io"
"golang.org/x/net/context"
)
// PluginPush pushes a plugin to a registry
func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) error {
func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) {
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, headers)
ensureReaderClosed(resp)
return err
if err != nil {
return nil, err
}
return resp.body, nil
}

View File

@ -16,7 +16,7 @@ func TestPluginPushError(t *testing.T) {
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.PluginPush(context.Background(), "plugin_name", "")
_, err := client.PluginPush(context.Background(), "plugin_name", "")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
@ -44,7 +44,7 @@ func TestPluginPush(t *testing.T) {
}),
}
err := client.PluginPush(context.Background(), "plugin_name", "authtoken")
_, err := client.PluginPush(context.Background(), "plugin_name", "authtoken")
if err != nil {
t.Fatal(err)
}