mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
03c6949739
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>
323 lines
8.7 KiB
Go
323 lines
8.7 KiB
Go
package plugin
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
distreference "github.com/docker/distribution/reference"
|
|
"github.com/docker/docker/api/server/httputils"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
"github.com/docker/docker/pkg/streamformatter"
|
|
"github.com/docker/docker/reference"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
func parseHeaders(headers http.Header) (map[string][]string, *types.AuthConfig) {
|
|
|
|
metaHeaders := map[string][]string{}
|
|
for k, v := range headers {
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
metaHeaders[k] = v
|
|
}
|
|
}
|
|
|
|
// Get X-Registry-Auth
|
|
authEncoded := headers.Get("X-Registry-Auth")
|
|
authConfig := &types.AuthConfig{}
|
|
if authEncoded != "" {
|
|
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
|
|
if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
|
|
authConfig = &types.AuthConfig{}
|
|
}
|
|
}
|
|
|
|
return metaHeaders, authConfig
|
|
}
|
|
|
|
// parseRemoteRef parses the remote reference into a reference.Named
|
|
// returning the tag associated with the reference. In the case the
|
|
// given reference string includes both digest and tag, the returned
|
|
// reference will have the digest without the tag, but the tag will
|
|
// be returned.
|
|
func parseRemoteRef(remote string) (reference.Named, string, error) {
|
|
// Parse remote reference, supporting remotes with name and tag
|
|
// NOTE: Using distribution reference to handle references
|
|
// containing both a name and digest
|
|
remoteRef, err := distreference.ParseNamed(remote)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var tag string
|
|
if t, ok := remoteRef.(distreference.Tagged); ok {
|
|
tag = t.Tag()
|
|
}
|
|
|
|
// Convert distribution reference to docker reference
|
|
// TODO: remove when docker reference changes reconciled upstream
|
|
ref, err := reference.WithName(remoteRef.Name())
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if d, ok := remoteRef.(distreference.Digested); ok {
|
|
ref, err = reference.WithDigest(ref, d.Digest())
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
} else if tag != "" {
|
|
ref, err = reference.WithTag(ref, tag)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
} else {
|
|
ref = reference.WithDefaultTag(ref)
|
|
}
|
|
|
|
return ref, tag, nil
|
|
}
|
|
|
|
func (pr *pluginRouter) getPrivileges(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
metaHeaders, authConfig := parseHeaders(r.Header)
|
|
|
|
ref, _, err := parseRemoteRef(r.FormValue("remote"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
privileges, err := pr.backend.Privileges(ctx, ref, metaHeaders, authConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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")
|
|
}
|
|
|
|
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, 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
|
|
}
|
|
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
|
|
}
|
|
|
|
options := &types.PluginCreateOptions{
|
|
RepoName: r.FormValue("name")}
|
|
|
|
if err := pr.backend.CreateFromContext(ctx, r.Body, options); err != nil {
|
|
return err
|
|
}
|
|
//TODO: send progress bar
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return nil
|
|
}
|
|
|
|
func (pr *pluginRouter) enablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
name := vars["name"]
|
|
timeout, err := strconv.Atoi(r.Form.Get("timeout"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config := &types.PluginEnableConfig{Timeout: timeout}
|
|
|
|
return pr.backend.Enable(name, config)
|
|
}
|
|
|
|
func (pr *pluginRouter) disablePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
name := vars["name"]
|
|
config := &types.PluginDisableConfig{
|
|
ForceDisable: httputils.BoolValue(r, "force"),
|
|
}
|
|
|
|
return pr.backend.Disable(name, config)
|
|
}
|
|
|
|
func (pr *pluginRouter) removePlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
name := vars["name"]
|
|
config := &types.PluginRmConfig{
|
|
ForceRemove: httputils.BoolValue(r, "force"),
|
|
}
|
|
return pr.backend.Remove(name, config)
|
|
}
|
|
|
|
func (pr *pluginRouter) pushPlugin(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")
|
|
}
|
|
|
|
metaHeaders, authConfig := parseHeaders(r.Header)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
output := ioutils.NewWriteFlusher(w)
|
|
|
|
if err := pr.backend.Push(ctx, vars["name"], metaHeaders, authConfig, output); err != nil {
|
|
if !output.Flushed() {
|
|
return err
|
|
}
|
|
output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (pr *pluginRouter) setPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
var args []string
|
|
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
|
|
return err
|
|
}
|
|
if err := pr.backend.Set(vars["name"], args); err != nil {
|
|
return err
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return nil
|
|
}
|
|
|
|
func (pr *pluginRouter) listPlugins(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
pluginFilters, err := filters.FromParam(r.Form.Get("filters"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l, err := pr.backend.List(pluginFilters)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return httputils.WriteJSON(w, http.StatusOK, l)
|
|
}
|
|
|
|
func (pr *pluginRouter) inspectPlugin(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
result, err := pr.backend.Inspect(vars["name"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return httputils.WriteJSON(w, http.StatusOK, result)
|
|
}
|