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 PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error
PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error
PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
PluginPush(ctx context.Context, name string, registryAuth string) error PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error)
PluginSet(ctx context.Context, name string, args []string) error PluginSet(ctx context.Context, name string, args []string) error
PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error)
PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error

View File

@ -2,73 +2,96 @@ package client
import ( import (
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"net/url" "net/url"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pkg/errors"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
// PluginInstall installs a plugin // PluginInstall installs a plugin
func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (err error) { func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
// FIXME(vdemeester) name is a ref, we might want to parse/validate it here.
query := url.Values{} 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) resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
// todo: do inspect before to check existing name before checking privileges
newAuthHeader, privilegeErr := options.PrivilegeFunc() newAuthHeader, privilegeErr := options.PrivilegeFunc()
if privilegeErr != nil { if privilegeErr != nil {
ensureReaderClosed(resp) ensureReaderClosed(resp)
return privilegeErr return nil, privilegeErr
} }
options.RegistryAuth = newAuthHeader options.RegistryAuth = newAuthHeader
resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
} }
if err != nil { if err != nil {
ensureReaderClosed(resp) ensureReaderClosed(resp)
return err return nil, err
} }
var privileges types.PluginPrivileges var privileges types.PluginPrivileges
if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
ensureReaderClosed(resp) ensureReaderClosed(resp)
return err return nil, err
} }
ensureReaderClosed(resp) ensureReaderClosed(resp)
if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
accept, err := options.AcceptPermissionsFunc(privileges) accept, err := options.AcceptPermissionsFunc(privileges)
if err != nil { if err != nil {
return err return nil, err
} }
if !accept { 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 { 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 { if err != nil {
delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) pw.CloseWithError(err)
ensureReaderClosed(delResp) 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)
}() }()
return pr, nil
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})
} }
func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {

View File

@ -1,13 +1,17 @@
package client package client
import ( import (
"io"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
// PluginPush pushes a plugin to a registry // 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}} headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, headers) resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, headers)
ensureReaderClosed(resp) if err != nil {
return err 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")), 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" { if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }