Skip to content

Commit

Permalink
Add docker plugin upgrade
Browse files Browse the repository at this point in the history
This allows a plugin to be upgraded without requiring to
uninstall/reinstall a plugin.
Since plugin resources (e.g. volumes) are tied to a plugin ID, this is
important to ensure resources aren't lost.

The plugin must be disabled while upgrading (errors out if enabled).
This does not add any convenience flags for automatically
disabling/re-enabling the plugin during before/after upgrade.

Since an upgrade may change requested permissions, the user is required
to accept permissions just like `docker plugin install`.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
  • Loading branch information
cpuguy83 committed Feb 3, 2017
1 parent edd977d commit 03c6949
Show file tree
Hide file tree
Showing 26 changed files with 557 additions and 106 deletions.
1 change: 1 addition & 0 deletions api/server/router/plugin/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ type Backend interface {
Privileges(ctx context.Context, ref reference.Named, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error)
Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error
Upgrade(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error
}
1 change: 1 addition & 0 deletions api/server/router/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func (r *pluginRouter) initRoutes() {
router.NewPostRoute("/plugins/{name:.*}/disable", r.disablePlugin),
router.Cancellable(router.NewPostRoute("/plugins/pull", r.pullPlugin)),
router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/push", r.pushPlugin)),
router.Cancellable(router.NewPostRoute("/plugins/{name:.*}/upgrade", r.upgradePlugin)),
router.NewPostRoute("/plugins/{name:.*}/set", r.setPlugin),
router.NewPostRoute("/plugins/create", r.createPlugin),
}
Expand Down
81 changes: 63 additions & 18 deletions api/server/router/plugin/plugin_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter
return httputils.WriteJSON(w, http.StatusOK, privileges)
}

func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
func (pr *pluginRouter) upgradePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return errors.Wrap(err, "failed to parse form")
}
Expand All @@ -116,20 +116,77 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
}

metaHeaders, authConfig := parseHeaders(r.Header)
ref, tag, err := parseRemoteRef(r.FormValue("remote"))
if err != nil {
return err
}

name, err := getName(ref, tag, vars["name"])
if err != nil {
return err
}
w.Header().Set("Docker-Plugin-Name", name)

w.Header().Set("Content-Type", "application/json")
output := ioutils.NewWriteFlusher(w)

if err := pr.backend.Upgrade(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
if !output.Flushed() {
return err
}
output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
}

return nil
}

func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return errors.Wrap(err, "failed to parse form")
}

var privileges types.PluginPrivileges
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&privileges); err != nil {
return errors.Wrap(err, "failed to parse privileges")
}
if dec.More() {
return errors.New("invalid privileges")
}

metaHeaders, authConfig := parseHeaders(r.Header)
ref, tag, err := parseRemoteRef(r.FormValue("remote"))
if err != nil {
return err
}

name := r.FormValue("name")
name, err := getName(ref, tag, r.FormValue("name"))
if err != nil {
return err
}
w.Header().Set("Docker-Plugin-Name", name)

w.Header().Set("Content-Type", "application/json")
output := ioutils.NewWriteFlusher(w)

if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
if !output.Flushed() {
return err
}
output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
}

return nil
}

func getName(ref reference.Named, tag, name string) (string, error) {
if name == "" {
if _, ok := ref.(reference.Canonical); ok {
trimmed := reference.TrimNamed(ref)
if tag != "" {
nt, err := reference.WithTag(trimmed, tag)
if err != nil {
return err
return "", err
}
name = nt.String()
} else {
Expand All @@ -141,29 +198,17 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
} else {
localRef, err := reference.ParseNamed(name)
if err != nil {
return err
return "", err
}
if _, ok := localRef.(reference.Canonical); ok {
return errors.New("cannot use digest in plugin tag")
return "", errors.New("cannot use digest in plugin tag")
}
if distreference.IsNameOnly(localRef) {
// TODO: log change in name to out stream
name = reference.WithDefaultTag(localRef).String()
}
}
w.Header().Set("Docker-Plugin-Name", name)

w.Header().Set("Content-Type", "application/json")
output := ioutils.NewWriteFlusher(w)

if err := pr.backend.Pull(ctx, ref, name, metaHeaders, authConfig, privileges, output); err != nil {
if !output.Flushed() {
return err
}
output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
}

return nil
return name, nil
}

func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
Expand Down
4 changes: 4 additions & 0 deletions api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,10 @@ definitions:
type: "array"
items:
$ref: "#/definitions/PluginDevice"
PluginReference:
description: "plugin remote reference used to push/pull the plugin"
type: "string"
x-nullable: false
Config:
description: "The config of a plugin."
type: "object"
Expand Down
3 changes: 3 additions & 0 deletions api/types/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ type Plugin struct {
// Required: true
Name string `json:"Name"`

// plugin remote reference used to push/pull the plugin
PluginReference string `json:"PluginReference,omitempty"`

// settings
// Required: true
Settings PluginSettings `json:"Settings"`
Expand Down
5 changes: 5 additions & 0 deletions cli/command/formatter/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,8 @@ func (c *pluginContext) Enabled() bool {
c.AddHeader(enabledHeader)
return c.p.Enabled
}

func (c *pluginContext) PluginReference() string {
c.AddHeader(imageHeader)
return c.p.PluginReference
}
4 changes: 2 additions & 2 deletions cli/command/formatter/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ func TestPluginContextWriteJSON(t *testing.T) {
{ID: "pluginID2", Name: "foobar_bar"},
}
expectedJSONs := []map[string]interface{}{
{"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz"},
{"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar"},
{"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz", "PluginReference": ""},
{"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar", "PluginReference": ""},
}

out := bytes.NewBufferString("")
Expand Down
1 change: 1 addition & 0 deletions cli/command/plugin/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command {
newSetCommand(dockerCli),
newPushCommand(dockerCli),
newCreateCommand(dockerCli),
newUpgradeCommand(dockerCli),
)
return cmd
}
89 changes: 48 additions & 41 deletions cli/command/plugin/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@ import (
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/net/context"
)

type pluginOptions struct {
name string
alias string
grantPerms bool
disable bool
args []string
remote string
localName string
grantPerms bool
disable bool
args []string
skipRemoteCheck bool
}

func loadPullFlags(opts *pluginOptions, flags *pflag.FlagSet) {
flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
command.AddTrustVerificationFlags(flags)
}

func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
Expand All @@ -33,7 +40,7 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Install a plugin",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
options.name = args[0]
options.remote = args[0]
if len(args) > 1 {
options.args = args[1:]
}
Expand All @@ -42,12 +49,9 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
}

flags := cmd.Flags()
flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
loadPullFlags(&options, flags)
flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install")
flags.StringVar(&options.alias, "alias", "", "Local name for plugin")

command.AddTrustVerificationFlags(flags)

flags.StringVar(&options.localName, "alias", "", "Local name for plugin")
return cmd
}

Expand Down Expand Up @@ -83,49 +87,33 @@ func newRegistryService() registry.Service {
}
}

func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) {
// Names with both tag and digest will be treated by the daemon
// as a pull by digest with an alias for the tag
// (if no alias is provided).
ref, err := reference.ParseNormalizedNamed(opts.name)
// as a pull by digest with a local name for the tag
// (if no local name is provided).
ref, err := reference.ParseNormalizedNamed(opts.remote)
if err != nil {
return err
}

alias := ""
if opts.alias != "" {
aref, err := reference.ParseNormalizedNamed(opts.alias)
if err != nil {
return err
}
if _, ok := aref.(reference.Canonical); ok {
return fmt.Errorf("invalid name: %s", opts.alias)
}
alias = reference.FamiliarString(reference.EnsureTagged(aref))
return types.PluginInstallOptions{}, err
}
ctx := context.Background()

repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return err
return types.PluginInstallOptions{}, err
}

remote := ref.String()

_, isCanonical := ref.(reference.Canonical)
if command.IsTrusted() && !isCanonical {
if alias == "" {
alias = reference.FamiliarString(ref)
}

nt, ok := ref.(reference.NamedTagged)
if !ok {
nt = reference.EnsureTagged(ref)
}

ctx := context.Background()
trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService())
if err != nil {
return err
return types.PluginInstallOptions{}, err
}
remote = reference.FamiliarString(trusted)
}
Expand All @@ -134,23 +122,42 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {

encodedAuth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return err
return types.PluginInstallOptions{}, err
}

registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "plugin install")
registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, cmdName)

options := types.PluginInstallOptions{
RegistryAuth: encodedAuth,
RemoteRef: remote,
Disabled: opts.disable,
AcceptAllPermissions: opts.grantPerms,
AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name),
AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote),
// TODO: Rename PrivilegeFunc, it has nothing to do with privileges
PrivilegeFunc: registryAuthFunc,
Args: opts.args,
}
return options, nil
}

responseBody, err := dockerCli.Client().PluginInstall(ctx, alias, options)
func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
var localName string
if opts.localName != "" {
aref, err := reference.ParseNormalizedNamed(opts.localName)
if err != nil {
return err
}
if _, ok := aref.(reference.Canonical); ok {
return fmt.Errorf("invalid name: %s", opts.localName)
}
localName = reference.FamiliarString(reference.EnsureTagged(aref))
}

ctx := context.Background()
options, err := buildPullConfig(ctx, dockerCli, opts, "plugin install")
if err != nil {
return err
}
responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options)
if err != nil {
if strings.Contains(err.Error(), "target is image") {
return errors.New(err.Error() + " - Use `docker image pull`")
Expand All @@ -161,7 +168,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
return err
}
fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.name) // todo: return proper values from the API for this result
fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.remote) // todo: return proper values from the API for this result
return nil
}

Expand Down
Loading

0 comments on commit 03c6949

Please sign in to comment.