mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #29414 from cpuguy83/plugin_upgrade
Add docker plugin upgrade
This commit is contained in:
commit
fa49c076d4
28 changed files with 658 additions and 119 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -101,6 +101,45 @@ func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter
|
|||
return httputils.WriteJSON(w, http.StatusOK, privileges)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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, 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")
|
||||
|
@ -116,40 +155,14 @@ 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 := r.FormValue("name")
|
||||
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
|
||||
}
|
||||
name = nt.String()
|
||||
} else {
|
||||
name = reference.WithDefaultTag(trimmed).String()
|
||||
}
|
||||
} else {
|
||||
name = ref.String()
|
||||
}
|
||||
} else {
|
||||
localRef, err := reference.ParseNamed(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := localRef.(reference.Canonical); ok {
|
||||
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()
|
||||
}
|
||||
name, err := getName(ref, tag, r.FormValue("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Docker-Plugin-Name", name)
|
||||
|
||||
|
@ -166,6 +179,38 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
|
|||
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
|
||||
}
|
||||
name = nt.String()
|
||||
} else {
|
||||
name = reference.WithDefaultTag(trimmed).String()
|
||||
}
|
||||
} else {
|
||||
name = ref.String()
|
||||
}
|
||||
} else {
|
||||
localRef, err := reference.ParseNamed(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, ok := localRef.(reference.Canonical); ok {
|
||||
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()
|
||||
}
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (pr *pluginRouter) createPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := httputils.ParseForm(r); err != nil {
|
||||
return err
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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("")
|
||||
|
|
|
@ -25,6 +25,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|||
newSetCommand(dockerCli),
|
||||
newPushCommand(dockerCli),
|
||||
newCreateCommand(dockerCli),
|
||||
newUpgradeCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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:]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
return types.PluginInstallOptions{}, 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))
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
@ -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`")
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
|
100
cli/command/plugin/upgrade.go
Normal file
100
cli/command/plugin/upgrade.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newUpgradeCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var options pluginOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "upgrade [OPTIONS] PLUGIN [REMOTE]",
|
||||
Short: "Upgrade an existing plugin",
|
||||
Args: cli.RequiresRangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.localName = args[0]
|
||||
if len(args) == 2 {
|
||||
options.remote = args[1]
|
||||
}
|
||||
return runUpgrade(dockerCli, options)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
loadPullFlags(&options, flags)
|
||||
flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error {
|
||||
ctx := context.Background()
|
||||
p, _, err := dockerCli.Client().PluginInspectWithRaw(ctx, opts.localName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading plugin data: %v", err)
|
||||
}
|
||||
|
||||
if p.Enabled {
|
||||
return fmt.Errorf("the plugin must be disabled before upgrading")
|
||||
}
|
||||
|
||||
opts.localName = p.Name
|
||||
if opts.remote == "" {
|
||||
opts.remote = p.PluginReference
|
||||
}
|
||||
remote, err := reference.ParseNamed(opts.remote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing remote upgrade image reference")
|
||||
}
|
||||
remote = reference.WithDefaultTag(remote)
|
||||
|
||||
old, err := reference.ParseNamed(p.PluginReference)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing current image reference")
|
||||
}
|
||||
old = reference.WithDefaultTag(old)
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, old, remote)
|
||||
if !opts.skipRemoteCheck && remote.String() != old.String() {
|
||||
_, err := fmt.Fprint(dockerCli.Out(), "Plugin images do not match, are you sure? ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error writing to stdout")
|
||||
}
|
||||
|
||||
rdr := bufio.NewReader(dockerCli.In())
|
||||
line, _, err := rdr.ReadLine()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error reading from stdin")
|
||||
}
|
||||
if strings.ToLower(string(line)) != "y" {
|
||||
return errors.New("canceling upgrade request")
|
||||
}
|
||||
}
|
||||
|
||||
options, err := buildPullConfig(ctx, dockerCli, opts, "plugin upgrade")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().PluginUpgrade(ctx, opts.localName, options)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "target is image") {
|
||||
return errors.New(err.Error() + " - Use `docker image pull`")
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "Upgraded plugin %s to %s\n", opts.localName, opts.remote) // todo: return proper values from the API for this result
|
||||
return nil
|
||||
}
|
|
@ -113,6 +113,7 @@ type PluginAPIClient interface {
|
|||
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) (io.ReadCloser, error)
|
||||
PluginUpgrade(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)
|
||||
|
|
|
@ -20,43 +20,15 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types
|
|||
}
|
||||
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 nil, privilegeErr
|
||||
}
|
||||
options.RegistryAuth = newAuthHeader
|
||||
resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
|
||||
}
|
||||
privileges, err := cli.checkPluginPermissions(ctx, query, options)
|
||||
if err != nil {
|
||||
ensureReaderClosed(resp)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var privileges types.PluginPrivileges
|
||||
if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
|
||||
ensureReaderClosed(resp)
|
||||
return nil, err
|
||||
}
|
||||
ensureReaderClosed(resp)
|
||||
|
||||
if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
|
||||
accept, err := options.AcceptPermissionsFunc(privileges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !accept {
|
||||
return nil, pluginPermissionDenied{options.RemoteRef}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -103,3 +75,39 @@ func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileg
|
|||
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
|
||||
return cli.post(ctx, "/plugins/pull", query, privileges, headers)
|
||||
}
|
||||
|
||||
func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options types.PluginInstallOptions) (types.PluginPrivileges, error) {
|
||||
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 nil, privilegeErr
|
||||
}
|
||||
options.RegistryAuth = newAuthHeader
|
||||
resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth)
|
||||
}
|
||||
if err != nil {
|
||||
ensureReaderClosed(resp)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var privileges types.PluginPrivileges
|
||||
if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
|
||||
ensureReaderClosed(resp)
|
||||
return nil, err
|
||||
}
|
||||
ensureReaderClosed(resp)
|
||||
|
||||
if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
|
||||
accept, err := options.AcceptPermissionsFunc(privileges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !accept {
|
||||
return nil, pluginPermissionDenied{options.RemoteRef}
|
||||
}
|
||||
}
|
||||
return privileges, nil
|
||||
}
|
||||
|
|
37
client/plugin_upgrade.go
Normal file
37
client/plugin_upgrade.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// PluginUpgrade upgrades a plugin
|
||||
func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
|
||||
query := url.Values{}
|
||||
if _, err := reference.ParseNamed(options.RemoteRef); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid remote reference")
|
||||
}
|
||||
query.Set("remote", options.RemoteRef)
|
||||
|
||||
privileges, err := cli.checkPluginPermissions(ctx, query, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := cli.tryPluginUpgrade(ctx, query, privileges, name, options.RegistryAuth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.body, nil
|
||||
}
|
||||
|
||||
func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (serverResponse, error) {
|
||||
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
|
||||
return cli.post(ctx, fmt.Sprintf("/plugins/%s/upgrade", name), query, privileges, headers)
|
||||
}
|
|
@ -118,6 +118,8 @@ Config provides the base accessible fields for working with V0 plugin format
|
|||
- **`propagatedMount`** *string*
|
||||
|
||||
path to be mounted as rshared, so that mounts under that path are visible to docker. This is useful for volume plugins.
|
||||
This path will be bind-mounted outisde of the plugin rootfs so it's contents
|
||||
are preserved on upgrade.
|
||||
|
||||
- **`env`** *PluginEnv array*
|
||||
|
||||
|
|
|
@ -58,3 +58,4 @@ The plugin can subsequently be enabled for local use or pushed to the public reg
|
|||
* [plugin push](plugin_push.md)
|
||||
* [plugin rm](plugin_rm.md)
|
||||
* [plugin set](plugin_set.md)
|
||||
* [plugin upgrade](plugin_upgrade.md)
|
||||
|
|
|
@ -63,3 +63,4 @@ ID NAME TAG DESCRIP
|
|||
* [plugin push](plugin_push.md)
|
||||
* [plugin rm](plugin_rm.md)
|
||||
* [plugin set](plugin_set.md)
|
||||
* [plugin upgrade](plugin_upgrade.md)
|
||||
|
|
|
@ -62,3 +62,4 @@ ID NAME TAG DESCRIP
|
|||
* [plugin push](plugin_push.md)
|
||||
* [plugin rm](plugin_rm.md)
|
||||
* [plugin set](plugin_set.md)
|
||||
* [plugin upgrade](plugin_upgrade.md)
|
||||
|
|
|
@ -37,6 +37,7 @@ $ docker plugin inspect tiborvass/sample-volume-plugin:latest
|
|||
{
|
||||
"Id": "8c74c978c434745c3ade82f1bc0acf38d04990eaf494fa507c16d9f1daa99c21",
|
||||
"Name": "tiborvass/sample-volume-plugin:latest",
|
||||
"PluginReference": "tiborvas/sample-volume-plugin:latest",
|
||||
"Enabled": true,
|
||||
"Config": {
|
||||
"Mounts": [
|
||||
|
@ -160,3 +161,4 @@ $ docker plugin inspect -f '{{.Id}}' tiborvass/sample-volume-plugin:latest
|
|||
* [plugin push](plugin_push.md)
|
||||
* [plugin rm](plugin_rm.md)
|
||||
* [plugin set](plugin_set.md)
|
||||
* [plugin upgrade](plugin_upgrade.md)
|
||||
|
|
|
@ -69,3 +69,4 @@ ID NAME TAG DESCRIPTION
|
|||
* [plugin push](plugin_push.md)
|
||||
* [plugin rm](plugin_rm.md)
|
||||
* [plugin set](plugin_set.md)
|
||||
* [plugin upgrade](plugin_upgrade.md)
|
||||
|
|
|
@ -83,10 +83,11 @@ Valid placeholders for the Go template are listed below:
|
|||
|
||||
Placeholder | Description
|
||||
---------------|------------------------------------------------------------------------------------------
|
||||
`.ID` | Plugin ID
|
||||
`.Name` | Plugin name
|
||||
`.Description` | Plugin description
|
||||
`.Enabled` | Whether plugin is enabled or not
|
||||
`.ID` | Plugin ID
|
||||
`.Name` | Plugin name
|
||||
`.Description` | Plugin description
|
||||
`.Enabled` | Whether plugin is enabled or not
|
||||
`.PluginReference` | The reference used to push/pull from a registry
|
||||
|
||||
When using the `--format` option, the `plugin ls` command will either
|
||||
output the data exactly as the template declares or, when using the
|
||||
|
@ -111,3 +112,4 @@ $ docker plugin ls --format "{{.ID}}: {{.Name}}"
|
|||
* [plugin push](plugin_push.md)
|
||||
* [plugin rm](plugin_rm.md)
|
||||
* [plugin set](plugin_set.md)
|
||||
* [plugin upgrade](plugin_upgrade.md)
|
||||
|
|
|
@ -48,3 +48,4 @@ $ docker plugin push user/plugin
|
|||
* [plugin ls](plugin_ls.md)
|
||||
* [plugin rm](plugin_rm.md)
|
||||
* [plugin set](plugin_set.md)
|
||||
* [plugin upgrade](plugin_upgrade.md)
|
||||
|
|
|
@ -53,3 +53,4 @@ tiborvass/sample-volume-plugin
|
|||
* [plugin ls](plugin_ls.md)
|
||||
* [plugin push](plugin_push.md)
|
||||
* [plugin set](plugin_set.md)
|
||||
* [plugin upgrade](plugin_upgrade.md)
|
||||
|
|
84
docs/reference/commandline/plugin_upgrade.md
Normal file
84
docs/reference/commandline/plugin_upgrade.md
Normal file
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
title: "plugin upgrade"
|
||||
description: "the plugin upgrade command description and usage"
|
||||
keywords: "plugin, upgrade"
|
||||
---
|
||||
|
||||
<!-- This file is maintained within the docker/docker Github
|
||||
repository at https://github.com/docker/docker/. Make all
|
||||
pull requests against that repo. If you see this file in
|
||||
another repository, consider it read-only there, as it will
|
||||
periodically be overwritten by the definitive file. Pull
|
||||
requests which include edits to this file in other repositories
|
||||
will be rejected.
|
||||
-->
|
||||
|
||||
# plugin upgrade
|
||||
|
||||
```markdown
|
||||
Usage: docker plugin upgrade [OPTIONS] PLUGIN [REMOTE]
|
||||
|
||||
Upgrade a plugin
|
||||
|
||||
Options:
|
||||
--disable-content-trust Skip image verification (default true)
|
||||
--grant-all-permissions Grant all permissions necessary to run the plugin
|
||||
--help Print usage
|
||||
--skip-remote-check Do not check if specified remote plugin matches existing plugin image
|
||||
```
|
||||
|
||||
Upgrades an existing plugin to the specified remote plugin image. If no remote
|
||||
is specified, Docker will re-pull the current image and use the updated version.
|
||||
All existing references to the plugin will continue to work.
|
||||
The plugin must be disabled before running the upgrade.
|
||||
|
||||
The following example installs `vieus/sshfs` plugin, uses it to create and use
|
||||
a volume, then upgrades the plugin.
|
||||
|
||||
```bash
|
||||
$ docker plugin install vieux/sshfs DEBUG=1
|
||||
|
||||
Plugin "vieux/sshfs:next" is requesting the following privileges:
|
||||
- network: [host]
|
||||
- device: [/dev/fuse]
|
||||
- capabilities: [CAP_SYS_ADMIN]
|
||||
Do you grant the above permissions? [y/N] y
|
||||
vieux/sshfs:next
|
||||
|
||||
$ docker volume create -d vieux/sshfs:next -o sshcmd=root@1.2.3.4:/tmp/shared -o password=XXX sshvolume
|
||||
sshvolume
|
||||
$ docker run -it -v sshvolume:/data alpine sh -c "touch /data/hello"
|
||||
$ docker plugin disable -f vieux/sshfs:next
|
||||
viex/sshfs:next
|
||||
|
||||
# Here docker volume ls doesn't show 'sshfsvolume', since the plugin is disabled
|
||||
$ docker volume ls
|
||||
DRIVER VOLUME NAME
|
||||
|
||||
$ docker plugin upgrade vieux/sshfs:next vieux/sshfs:next
|
||||
Plugin "vieux/sshfs:next" is requesting the following privileges:
|
||||
- network: [host]
|
||||
- device: [/dev/fuse]
|
||||
- capabilities: [CAP_SYS_ADMIN]
|
||||
Do you grant the above permissions? [y/N] y
|
||||
Upgrade plugin vieux/sshfs:next to vieux/sshfs:next
|
||||
$ docker plugin enable vieux/sshfs:next
|
||||
viex/sshfs:next
|
||||
$ docker volume ls
|
||||
DRIVER VOLUME NAME
|
||||
viuex/sshfs:next sshvolume
|
||||
$ docker run -it -v sshvolume:/data alpine sh -c "ls /data"
|
||||
hello
|
||||
```
|
||||
|
||||
## Related information
|
||||
|
||||
* [plugin create](plugin_create.md)
|
||||
* [plugin disable](plugin_disable.md)
|
||||
* [plugin enable](plugin_enable.md)
|
||||
* [plugin inspect](plugin_inspect.md)
|
||||
* [plugin install](plugin_install.md)
|
||||
* [plugin ls](plugin_ls.md)
|
||||
* [plugin push](plugin_push.md)
|
||||
* [plugin rm](plugin_rm.md)
|
||||
* [plugin set](plugin_set.md)
|
|
@ -427,3 +427,35 @@ enabled: true`, id, pNameWithTag)
|
|||
out, _ = dockerCmd(c, "--config", config, "plugin", "ls", "--no-trunc")
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, expectedOutput)
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TestPluginUpgrade(c *check.C) {
|
||||
testRequires(c, DaemonIsLinux, Network, SameHostDaemon, IsAmd64)
|
||||
plugin := "cpuguy83/docker-volume-driver-plugin-local:latest"
|
||||
pluginV2 := "cpuguy83/docker-volume-driver-plugin-local:v2"
|
||||
|
||||
dockerCmd(c, "plugin", "install", "--grant-all-permissions", plugin)
|
||||
dockerCmd(c, "volume", "create", "--driver", plugin, "bananas")
|
||||
dockerCmd(c, "run", "--rm", "-v", "bananas:/apple", "busybox", "sh", "-c", "touch /apple/core")
|
||||
|
||||
out, _, err := dockerCmdWithError("plugin", "upgrade", "--grant-all-permissions", plugin, pluginV2)
|
||||
c.Assert(err, checker.NotNil, check.Commentf(out))
|
||||
c.Assert(out, checker.Contains, "disabled before upgrading")
|
||||
|
||||
out, _ = dockerCmd(c, "plugin", "inspect", "--format={{.ID}}", plugin)
|
||||
id := strings.TrimSpace(out)
|
||||
|
||||
// make sure "v2" does not exists
|
||||
_, err = os.Stat(filepath.Join(testEnv.DockerBasePath(), "plugins", id, "rootfs", "v2"))
|
||||
c.Assert(os.IsNotExist(err), checker.True, check.Commentf(out))
|
||||
|
||||
dockerCmd(c, "plugin", "disable", "-f", plugin)
|
||||
dockerCmd(c, "plugin", "upgrade", "--grant-all-permissions", "--skip-remote-check", plugin, pluginV2)
|
||||
|
||||
// make sure "v2" file exists
|
||||
_, err = os.Stat(filepath.Join(testEnv.DockerBasePath(), "plugins", id, "rootfs", "v2"))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
dockerCmd(c, "plugin", "enable", plugin)
|
||||
dockerCmd(c, "volume", "inspect", "bananas")
|
||||
dockerCmd(c, "run", "--rm", "-v", "bananas:/apple", "busybox", "sh", "-c", "ls -lh /apple/core")
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
|
@ -25,6 +26,7 @@ import (
|
|||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/chrootarchive"
|
||||
"github.com/docker/docker/pkg/mount"
|
||||
"github.com/docker/docker/pkg/pools"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/plugin/v2"
|
||||
|
@ -215,6 +217,60 @@ func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHead
|
|||
return computePrivileges(config)
|
||||
}
|
||||
|
||||
// Upgrade upgrades a plugin
|
||||
func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
|
||||
p, err := pm.config.Store.GetV2Plugin(name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "plugin must be installed before upgrading")
|
||||
}
|
||||
|
||||
if p.IsEnabled() {
|
||||
return fmt.Errorf("plugin must be disabled before upgrading")
|
||||
}
|
||||
|
||||
pm.muGC.RLock()
|
||||
defer pm.muGC.RUnlock()
|
||||
|
||||
// revalidate because Pull is public
|
||||
nameref, err := reference.ParseNamed(name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse %q", name)
|
||||
}
|
||||
name = reference.WithDefaultTag(nameref).String()
|
||||
|
||||
tmpRootFSDir, err := ioutil.TempDir(pm.tmpDir(), ".rootfs")
|
||||
defer os.RemoveAll(tmpRootFSDir)
|
||||
|
||||
dm := &downloadManager{
|
||||
tmpDir: tmpRootFSDir,
|
||||
blobStore: pm.blobStore,
|
||||
}
|
||||
|
||||
pluginPullConfig := &distribution.ImagePullConfig{
|
||||
Config: distribution.Config{
|
||||
MetaHeaders: metaHeader,
|
||||
AuthConfig: authConfig,
|
||||
RegistryService: pm.config.RegistryService,
|
||||
ImageEventLogger: pm.config.LogPluginEvent,
|
||||
ImageStore: dm,
|
||||
},
|
||||
DownloadManager: dm, // todo: reevaluate if possible to substitute distribution/xfer dependencies instead
|
||||
Schema2Types: distribution.PluginTypes,
|
||||
}
|
||||
|
||||
err = pm.pull(ctx, ref, pluginPullConfig, outStream)
|
||||
if err != nil {
|
||||
go pm.GC()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pm.upgradePlugin(p, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
|
||||
return err
|
||||
}
|
||||
p.PluginObj.PluginReference = ref.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
|
||||
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) {
|
||||
pm.muGC.RLock()
|
||||
|
@ -257,9 +313,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
|
|||
return err
|
||||
}
|
||||
|
||||
if _, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges); err != nil {
|
||||
p, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.PluginObj.PluginReference = ref.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -541,6 +599,9 @@ func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error {
|
|||
id := p.GetID()
|
||||
pm.config.Store.Remove(p)
|
||||
pluginDir := filepath.Join(pm.config.Root, id)
|
||||
if err := recursiveUnmount(pm.config.Root); err != nil {
|
||||
logrus.WithField("dir", pm.config.Root).WithField("id", id).Warn(err)
|
||||
}
|
||||
if err := os.RemoveAll(pluginDir); err != nil {
|
||||
logrus.Warnf("unable to remove %q from plugin remove: %v", pluginDir, err)
|
||||
}
|
||||
|
@ -548,6 +609,43 @@ func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getMounts(root string) ([]string, error) {
|
||||
infos, err := mount.GetMounts()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read mount table while performing recursive unmount")
|
||||
}
|
||||
|
||||
var mounts []string
|
||||
for _, m := range infos {
|
||||
if strings.HasPrefix(m.Mountpoint, root) {
|
||||
mounts = append(mounts, m.Mountpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
func recursiveUnmount(root string) error {
|
||||
mounts, err := getMounts(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sort in reverse-lexicographic order so the root mount will always be last
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(mounts)))
|
||||
|
||||
for i, m := range mounts {
|
||||
if err := mount.Unmount(m); err != nil {
|
||||
if i == len(mounts)-1 {
|
||||
return errors.Wrapf(err, "error performing recursive unmount on %s", root)
|
||||
}
|
||||
logrus.WithError(err).WithField("mountpoint", m).Warn("could not unmount")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set sets plugin args
|
||||
func (pm *Manager) Set(name string, args []string) error {
|
||||
p, err := pm.config.Store.GetV2Plugin(name)
|
||||
|
@ -573,7 +671,8 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
|
|||
if _, ok := ref.(reference.Canonical); ok {
|
||||
return errors.Errorf("canonical references are not permitted")
|
||||
}
|
||||
name := reference.WithDefaultTag(ref).String()
|
||||
taggedRef := reference.WithDefaultTag(ref)
|
||||
name := taggedRef.String()
|
||||
|
||||
if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin()
|
||||
return err
|
||||
|
@ -655,6 +754,7 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.PluginObj.PluginReference = taggedRef.String()
|
||||
|
||||
pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")
|
||||
|
||||
|
|
|
@ -40,6 +40,11 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
|
|||
return errNotSupported
|
||||
}
|
||||
|
||||
// Upgrade pulls a plugin, check if the correct privileges are provided and install the plugin.
|
||||
func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) error {
|
||||
return errNotSupported
|
||||
}
|
||||
|
||||
// List displays the list of plugins and associated metadata.
|
||||
func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) {
|
||||
return nil, errNotSupported
|
||||
|
|
|
@ -145,6 +145,10 @@ func (pm *Manager) StateChanged(id string, e libcontainerd.StateInfo) error {
|
|||
if err := mount.Unmount(p.PropagatedMount); err != nil {
|
||||
logrus.Warnf("Could not unmount %s: %v", p.PropagatedMount, err)
|
||||
}
|
||||
propRoot := filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
|
||||
if err := mount.Unmount(propRoot); err != nil {
|
||||
logrus.Warn("Could not unmount %s: %v", propRoot, err)
|
||||
}
|
||||
}
|
||||
|
||||
if restart {
|
||||
|
@ -193,6 +197,27 @@ func (pm *Manager) reload() error { // todo: restore
|
|||
for _, typ := range p.PluginObj.Config.Interface.Types {
|
||||
if (typ.Capability == "volumedriver" || typ.Capability == "graphdriver") && typ.Prefix == "docker" && strings.HasPrefix(typ.Version, "1.") {
|
||||
if p.PluginObj.Config.PropagatedMount != "" {
|
||||
propRoot := filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
|
||||
|
||||
// check if we need to migrate an older propagated mount from before
|
||||
// these mounts were stored outside the plugin rootfs
|
||||
if _, err := os.Stat(propRoot); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(p.PropagatedMount); err == nil {
|
||||
// make sure nothing is mounted here
|
||||
// don't care about errors
|
||||
mount.Unmount(p.PropagatedMount)
|
||||
if err := os.Rename(p.PropagatedMount, propRoot); err != nil {
|
||||
logrus.WithError(err).WithField("dir", propRoot).Error("error migrating propagated mount storage")
|
||||
}
|
||||
if err := os.MkdirAll(p.PropagatedMount, 0755); err != nil {
|
||||
logrus.WithError(err).WithField("dir", p.PropagatedMount).Error("error migrating propagated mount storage")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(propRoot, 0755); err != nil {
|
||||
logrus.Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
|
||||
}
|
||||
// TODO: sanitize PropagatedMount and prevent breakout
|
||||
p.PropagatedMount = filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
|
||||
if err := os.MkdirAll(p.PropagatedMount, 0755); err != nil {
|
||||
|
|
|
@ -40,9 +40,20 @@ func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
|
|||
pm.cMap[p] = c
|
||||
pm.mu.Unlock()
|
||||
|
||||
var propRoot string
|
||||
if p.PropagatedMount != "" {
|
||||
if err := mount.MakeRShared(p.PropagatedMount); err != nil {
|
||||
return errors.WithStack(err)
|
||||
propRoot = filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
|
||||
|
||||
if err := os.MkdirAll(propRoot, 0755); err != nil {
|
||||
logrus.Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
|
||||
}
|
||||
|
||||
if err := mount.MakeRShared(propRoot); err != nil {
|
||||
return errors.Wrap(err, "error setting up propagated mount dir")
|
||||
}
|
||||
|
||||
if err := mount.Mount(propRoot, p.PropagatedMount, "none", "rbind"); err != nil {
|
||||
return errors.Wrap(err, "error creating mount for propagated mount")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,6 +66,9 @@ func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
|
|||
if err := mount.Unmount(p.PropagatedMount); err != nil {
|
||||
logrus.Warnf("Could not unmount %s: %v", p.PropagatedMount, err)
|
||||
}
|
||||
if err := mount.Unmount(propRoot); err != nil {
|
||||
logrus.Warnf("Could not unmount %s: %v", propRoot, err)
|
||||
}
|
||||
}
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -149,37 +163,91 @@ func (pm *Manager) Shutdown() {
|
|||
}
|
||||
}
|
||||
|
||||
// createPlugin creates a new plugin. take lock before calling.
|
||||
func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
|
||||
if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
|
||||
return nil, err
|
||||
func (pm *Manager) upgradePlugin(p *v2.Plugin, configDigest digest.Digest, blobsums []digest.Digest, tmpRootFSDir string, privileges *types.PluginPrivileges) (err error) {
|
||||
config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
|
||||
orig := filepath.Join(pdir, "rootfs")
|
||||
backup := orig + "-old"
|
||||
if err := os.Rename(orig, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if rmErr := os.RemoveAll(orig); rmErr != nil && !os.IsNotExist(rmErr) {
|
||||
logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up after failed upgrade")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Rename(backup, orig); err != nil {
|
||||
err = errors.Wrap(err, "error restoring old plugin root on upgrade failure")
|
||||
}
|
||||
if rmErr := os.RemoveAll(tmpRootFSDir); rmErr != nil && !os.IsNotExist(rmErr) {
|
||||
logrus.WithError(rmErr).WithField("plugin", p.Name()).Errorf("error cleaning up plugin upgrade dir: %s", tmpRootFSDir)
|
||||
}
|
||||
} else {
|
||||
if rmErr := os.RemoveAll(backup); rmErr != nil && !os.IsNotExist(rmErr) {
|
||||
logrus.WithError(rmErr).WithField("dir", backup).Error("error cleaning up old plugin root after successful upgrade")
|
||||
}
|
||||
|
||||
p.Config = configDigest
|
||||
p.Blobsums = blobsums
|
||||
}
|
||||
}()
|
||||
|
||||
if err := os.Rename(tmpRootFSDir, orig); err != nil {
|
||||
return errors.Wrap(err, "error upgrading")
|
||||
}
|
||||
|
||||
p.PluginObj.Config = config
|
||||
err = pm.save(p)
|
||||
return errors.Wrap(err, "error saving upgraded plugin config")
|
||||
}
|
||||
|
||||
func (pm *Manager) setupNewPlugin(configDigest digest.Digest, blobsums []digest.Digest, privileges *types.PluginPrivileges) (types.PluginConfig, error) {
|
||||
configRC, err := pm.blobStore.Get(configDigest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return types.PluginConfig{}, err
|
||||
}
|
||||
defer configRC.Close()
|
||||
|
||||
var config types.PluginConfig
|
||||
dec := json.NewDecoder(configRC)
|
||||
if err := dec.Decode(&config); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse config")
|
||||
return types.PluginConfig{}, errors.Wrapf(err, "failed to parse config")
|
||||
}
|
||||
if dec.More() {
|
||||
return nil, errors.New("invalid config json")
|
||||
return types.PluginConfig{}, errors.New("invalid config json")
|
||||
}
|
||||
|
||||
requiredPrivileges, err := computePrivileges(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return types.PluginConfig{}, err
|
||||
}
|
||||
if privileges != nil {
|
||||
if err := validatePrivileges(requiredPrivileges, *privileges); err != nil {
|
||||
return nil, err
|
||||
return types.PluginConfig{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// createPlugin creates a new plugin. take lock before calling.
|
||||
func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
|
||||
if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := pm.setupNewPlugin(configDigest, blobsums, privileges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p = &v2.Plugin{
|
||||
PluginObj: types.Plugin{
|
||||
Name: name,
|
||||
|
|
Loading…
Add table
Reference in a new issue