mirror of https://github.com/docker/cli.git
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:
parent
4b933cc26d
commit
66f7194250
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue