Sfoglia il codice sorgente

support env for docker plugin set

Signed-off-by: Victor Vieux <vieux@docker.com>
Victor Vieux 8 anni fa
parent
commit
efbed4500e

+ 5 - 1
api/server/router/plugin/plugin_routes.go

@@ -89,7 +89,11 @@ func (pr *pluginRouter) setPlugin(ctx context.Context, w http.ResponseWriter, r
 	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
 		return err
 	}
-	return pr.backend.Set(vars["name"], args)
+	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 {

+ 15 - 5
docs/reference/api/docker_remote_api_v1.25.md

@@ -4290,16 +4290,26 @@ Content-Type: application/json
 -   **200** - no error
 -   **404** - plugin not installed
 
-<!-- TODO Document "docker plugin set" endpoint once implemented
 ### Configure a plugin
 
-`POST /plugins/(plugin name)/set`
+POST /plugins/(plugin name)/set`
 
-**Status codes**:
+**Example request**:
 
--   **500** - not implemented
 
--->
+    POST /plugins/tiborvass/no-remove/set
+    Content-Type: application/json
+
+    ["DEBUG=1"]
+
+**Example response**:
+
+    HTTP/1.1 204 No Content
+
+**Status codes**:
+
+-   **204** - no error
+-   **404** - plugin not installed
 
 ### Enable a plugin
 

+ 1 - 0
docs/reference/commandline/plugin_disable.md

@@ -59,3 +59,4 @@ tiborvass/no-remove   latest              A test plugin for Docker   false
 * [plugin inspect](plugin_inspect.md)
 * [plugin install](plugin_install.md)
 * [plugin rm](plugin_rm.md)
+* [plugin set](plugin_set.md)

+ 1 - 0
docs/reference/commandline/plugin_enable.md

@@ -59,3 +59,4 @@ tiborvass/no-remove   latest              A test plugin for Docker   true
 * [plugin inspect](plugin_inspect.md)
 * [plugin install](plugin_install.md)
 * [plugin rm](plugin_rm.md)
+* [plugin set](plugin_set.md)

+ 1 - 0
docs/reference/commandline/plugin_inspect.md

@@ -159,3 +159,4 @@ $ docker plugin inspect -f '{{.Id}}' tiborvass/no-remove:latest
 * [plugin disable](plugin_disable.md)
 * [plugin install](plugin_install.md)
 * [plugin rm](plugin_rm.md)
+* [plugin set](plugin_set.md)

+ 1 - 0
docs/reference/commandline/plugin_install.md

@@ -64,3 +64,4 @@ tiborvass/no-remove   latest              A test plugin for Docker   true
 * [plugin disable](plugin_disable.md)
 * [plugin inspect](plugin_inspect.md)
 * [plugin rm](plugin_rm.md)
+* [plugin set](plugin_set.md)

+ 1 - 0
docs/reference/commandline/plugin_ls.md

@@ -48,3 +48,4 @@ tiborvass/no-remove   latest              A test plugin for Docker   true
 * [plugin inspect](plugin_inspect.md)
 * [plugin install](plugin_install.md)
 * [plugin rm](plugin_rm.md)
+* [plugin set](plugin_set.md)

+ 1 - 0
docs/reference/commandline/plugin_rm.md

@@ -51,3 +51,4 @@ tiborvass/no-remove
 * [plugin disable](plugin_disable.md)
 * [plugin inspect](plugin_inspect.md)
 * [plugin install](plugin_install.md)
+* [plugin set](plugin_set.md)

+ 51 - 0
docs/reference/commandline/plugin_set.md

@@ -0,0 +1,51 @@
+---
+title: "plugin set"
+description: "the plugin set command description and usage"
+keywords: "plugin, set"
+advisory: "experimental"
+---
+
+<!-- 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 set (experimental)
+
+```markdown
+Usage:  docker plugin set PLUGIN key1=value1 [key2=value2...]
+
+Change settings for a plugin
+
+Options:
+      --help                    Print usage
+```
+
+Change settings for a plugin. The plugin must be disabled.
+
+
+The following example installs change the env variable `DEBUG` of the
+`no-remove` plugin.
+
+```bash
+$ docker plugin inspect -f {{.Config.Env}} tiborvass/no-remove
+[DEBUG=0]
+
+$ docker plugin set DEBUG=1 tiborvass/no-remove
+
+$ docker plugin inspect -f {{.Config.Env}} tiborvass/no-remove
+[DEBUG=1]
+```
+
+## Related information
+
+* [plugin ls](plugin_ls.md)
+* [plugin enable](plugin_enable.md)
+* [plugin disable](plugin_disable.md)
+* [plugin inspect](plugin_inspect.md)
+* [plugin install](plugin_install.md)
+* [plugin rm](plugin_rm.md)

+ 14 - 0
integration-cli/docker_cli_plugins_test.go

@@ -117,6 +117,20 @@ func (s *DockerSuite) TestPluginInstallDisableVolumeLs(c *check.C) {
 	dockerCmd(c, "volume", "ls")
 }
 
+func (s *DockerSuite) TestPluginSet(c *check.C) {
+	testRequires(c, DaemonIsLinux, ExperimentalDaemon, Network)
+	out, _ := dockerCmd(c, "plugin", "install", "--grant-all-permissions", "--disable", pName)
+	c.Assert(strings.TrimSpace(out), checker.Contains, pName)
+
+	env, _ := dockerCmd(c, "plugin", "inspect", "-f", "{{.Config.Env}}", pName)
+	c.Assert(strings.TrimSpace(env), checker.Equals, "[DEBUG=0]")
+
+	dockerCmd(c, "plugin", "set", pName, "DEBUG=1")
+
+	env, _ = dockerCmd(c, "plugin", "inspect", "-f", "{{.Config.Env}}", pName)
+	c.Assert(strings.TrimSpace(env), checker.Equals, "[DEBUG=1]")
+}
+
 func (s *DockerSuite) TestPluginInstallImage(c *check.C) {
 	testRequires(c, DaemonIsLinux, ExperimentalDaemon)
 	out, _, err := dockerCmdWithError("plugin", "install", "redis")

+ 2 - 2
plugin/backend.go

@@ -85,8 +85,8 @@ func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.A
 	}
 
 	tag := distribution.GetTag(ref)
-	p := v2.NewPlugin(ref.Name(), pluginID, pm.runRoot, tag)
-	if err := p.InitPlugin(pm.libRoot); err != nil {
+	p := v2.NewPlugin(ref.Name(), pluginID, pm.runRoot, pm.libRoot, tag)
+	if err := p.InitPlugin(); err != nil {
 		return nil, err
 	}
 	pm.pluginStore.Add(p)

+ 2 - 2
plugin/store/store_test.go

@@ -8,7 +8,7 @@ import (
 )
 
 func TestFilterByCapNeg(t *testing.T) {
-	p := v2.NewPlugin("test", "1234567890", "/run/docker", "latest")
+	p := v2.NewPlugin("test", "1234567890", "/run/docker", "/var/lib/docker/plugins", "latest")
 
 	iType := types.PluginInterfaceType{"volumedriver", "docker", "1.0"}
 	i := types.PluginManifestInterface{"plugins.sock", []types.PluginInterfaceType{iType}}
@@ -21,7 +21,7 @@ func TestFilterByCapNeg(t *testing.T) {
 }
 
 func TestFilterByCapPos(t *testing.T) {
-	p := v2.NewPlugin("test", "1234567890", "/run/docker", "latest")
+	p := v2.NewPlugin("test", "1234567890", "/run/docker", "/var/lib/docker/plugins", "latest")
 
 	iType := types.PluginInterfaceType{"volumedriver", "docker", "1.0"}
 	i := types.PluginManifestInterface{"plugins.sock", []types.PluginInterfaceType{iType}}

+ 45 - 12
plugin/v2/plugin.go

@@ -2,7 +2,6 @@ package v2
 
 import (
 	"encoding/json"
-	"errors"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -24,6 +23,7 @@ type Plugin struct {
 	RefCount          int             `json:"-"`
 	Restart           bool            `json:"-"`
 	ExitChan          chan bool       `json:"-"`
+	LibRoot           string          `json:"-"`
 }
 
 const defaultPluginRuntimeDestination = "/run/docker/plugins"
@@ -42,10 +42,11 @@ func newPluginObj(name, id, tag string) types.Plugin {
 }
 
 // NewPlugin creates a plugin.
-func NewPlugin(name, id, runRoot, tag string) *Plugin {
+func NewPlugin(name, id, runRoot, libRoot, tag string) *Plugin {
 	return &Plugin{
 		PluginObj:         newPluginObj(name, id, tag),
 		RuntimeSourcePath: filepath.Join(runRoot, id),
+		LibRoot:           libRoot,
 	}
 }
 
@@ -86,8 +87,8 @@ func (p *Plugin) RemoveFromDisk() error {
 }
 
 // InitPlugin populates the plugin object from the plugin manifest file.
-func (p *Plugin) InitPlugin(libRoot string) error {
-	dt, err := os.Open(filepath.Join(libRoot, p.PluginObj.ID, "manifest.json"))
+func (p *Plugin) InitPlugin() error {
+	dt, err := os.Open(filepath.Join(p.LibRoot, p.PluginObj.ID, "manifest.json"))
 	if err != nil {
 		return err
 	}
@@ -109,7 +110,11 @@ func (p *Plugin) InitPlugin(libRoot string) error {
 	}
 	copy(p.PluginObj.Config.Args, p.PluginObj.Manifest.Args.Value)
 
-	f, err := os.Create(filepath.Join(libRoot, p.PluginObj.ID, "plugin-config.json"))
+	return p.writeConfig()
+}
+
+func (p *Plugin) writeConfig() error {
+	f, err := os.Create(filepath.Join(p.LibRoot, p.PluginObj.ID, "plugin-config.json"))
 	if err != nil {
 		return err
 	}
@@ -120,15 +125,43 @@ func (p *Plugin) InitPlugin(libRoot string) error {
 
 // Set is used to pass arguments to the plugin.
 func (p *Plugin) Set(args []string) error {
-	m := make(map[string]string, len(args))
-	for _, arg := range args {
-		i := strings.Index(arg, "=")
-		if i < 0 {
-			return fmt.Errorf("No equal sign '=' found in %s", arg)
+	p.Lock()
+	defer p.Unlock()
+
+	if p.PluginObj.Enabled {
+		return fmt.Errorf("cannot set on an active plugin, disable plugin before setting")
+	}
+
+	sets, err := newSettables(args)
+	if err != nil {
+		return err
+	}
+
+next:
+	for _, s := range sets {
+		// range over all the envs in the manifest
+		for _, env := range p.PluginObj.Manifest.Env {
+			// found the env in the manifest
+			if env.Name == s.name {
+				// is it settable ?
+				if ok, err := s.isSettable(allowedSettableFieldsEnv, env.Settable); err != nil {
+					return err
+				} else if !ok {
+					return fmt.Errorf("%q is not settable", s.prettyName())
+				}
+				// is it, so lets update the config in memory
+				updateConfigEnv(&p.PluginObj.Config.Env, &s)
+				continue next
+			}
 		}
-		m[arg[:i]] = arg[i+1:]
+
+		//TODO: check devices, mount and args
+
+		return fmt.Errorf("setting %q not found in the plugin configuration", s.name)
 	}
-	return errors.New("not implemented")
+
+	// update the config on disk
+	return p.writeConfig()
 }
 
 // ComputePrivileges takes the manifest file and computes the list of access necessary

+ 102 - 0
plugin/v2/settable.go

@@ -0,0 +1,102 @@
+package v2
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+)
+
+type settable struct {
+	name  string
+	field string
+	value string
+}
+
+var (
+	allowedSettableFieldsEnv     = []string{"value"}
+	allowedSettableFieldsArgs    = []string{"value"}
+	allowedSettableFieldsDevices = []string{"path"}
+	allowedSettableFieldsMounts  = []string{"source"}
+
+	errMultipleFields = errors.New("multiple fields are settable, one must be specified")
+	errInvalidFormat  = errors.New("invalid format, must be <name>[.<field>][=<value>]")
+)
+
+func newSettables(args []string) ([]settable, error) {
+	sets := make([]settable, 0, len(args))
+	for _, arg := range args {
+		set, err := newSettable(arg)
+		if err != nil {
+			return nil, err
+		}
+		sets = append(sets, set)
+	}
+	return sets, nil
+}
+
+func newSettable(arg string) (settable, error) {
+	var set settable
+	if i := strings.Index(arg, "="); i == 0 {
+		return set, errInvalidFormat
+	} else if i < 0 {
+		set.name = arg
+	} else {
+		set.name = arg[:i]
+		set.value = arg[i+1:]
+	}
+
+	if i := strings.LastIndex(set.name, "."); i > 0 {
+		set.field = set.name[i+1:]
+		set.name = arg[:i]
+	}
+
+	return set, nil
+}
+
+// prettyName return name.field if there is a field, otherwise name.
+func (set *settable) prettyName() string {
+	if set.field != "" {
+		return fmt.Sprintf("%s.%s", set.name, set.field)
+	}
+	return set.name
+}
+
+func (set *settable) isSettable(allowedSettableFields []string, settable []string) (bool, error) {
+	if set.field == "" {
+		if len(settable) == 1 {
+			// if field is not specified and there only one settable, default to it.
+			set.field = settable[0]
+		} else if len(settable) > 1 {
+			return false, errMultipleFields
+		}
+	}
+
+	isAllowed := false
+	for _, allowedSettableField := range allowedSettableFields {
+		if set.field == allowedSettableField {
+			isAllowed = true
+			break
+		}
+	}
+
+	if isAllowed {
+		for _, settableField := range settable {
+			if set.field == settableField {
+				return true, nil
+			}
+		}
+	}
+
+	return false, nil
+}
+
+func updateConfigEnv(env *[]string, set *settable) {
+	for i, e := range *env {
+		if parts := strings.SplitN(e, "=", 2); parts[0] == set.name {
+			(*env)[i] = fmt.Sprintf("%s=%s", set.name, set.value)
+			return
+		}
+	}
+
+	*env = append(*env, fmt.Sprintf("%s=%s", set.name, set.value))
+}

+ 91 - 0
plugin/v2/settable_test.go

@@ -0,0 +1,91 @@
+package v2
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestNewSettable(t *testing.T) {
+	contexts := []struct {
+		arg   string
+		name  string
+		field string
+		value string
+		err   error
+	}{
+		{"name=value", "name", "", "value", nil},
+		{"name", "name", "", "", nil},
+		{"name.field=value", "name", "field", "value", nil},
+		{"name.field", "name", "field", "", nil},
+		{"=value", "", "", "", errInvalidFormat},
+		{"=", "", "", "", errInvalidFormat},
+	}
+
+	for _, c := range contexts {
+		s, err := newSettable(c.arg)
+		if err != c.err {
+			t.Fatalf("expected error to be %v, got %v", c.err, err)
+		}
+
+		if s.name != c.name {
+			t.Fatalf("expected name to be %q, got %q", c.name, s.name)
+		}
+
+		if s.field != c.field {
+			t.Fatalf("expected field to be %q, got %q", c.field, s.field)
+		}
+
+		if s.value != c.value {
+			t.Fatalf("expected value to be %q, got %q", c.value, s.value)
+		}
+
+	}
+}
+
+func TestIsSettable(t *testing.T) {
+	contexts := []struct {
+		allowedSettableFields []string
+		set                   settable
+		settable              []string
+		result                bool
+		err                   error
+	}{
+		{allowedSettableFieldsEnv, settable{}, []string{}, false, nil},
+		{allowedSettableFieldsEnv, settable{field: "value"}, []string{}, false, nil},
+		{allowedSettableFieldsEnv, settable{}, []string{"value"}, true, nil},
+		{allowedSettableFieldsEnv, settable{field: "value"}, []string{"value"}, true, nil},
+		{allowedSettableFieldsEnv, settable{field: "foo"}, []string{"value"}, false, nil},
+		{allowedSettableFieldsEnv, settable{field: "foo"}, []string{"foo"}, false, nil},
+		{allowedSettableFieldsEnv, settable{}, []string{"value1", "value2"}, false, errMultipleFields},
+	}
+
+	for _, c := range contexts {
+		if res, err := c.set.isSettable(c.allowedSettableFields, c.settable); res != c.result {
+			t.Fatalf("expected result to be %t, got %t", c.result, res)
+		} else if err != c.err {
+			t.Fatalf("expected error to be %v, got %v", c.err, err)
+		}
+	}
+}
+
+func TestUpdateConfigEnv(t *testing.T) {
+	contexts := []struct {
+		env    []string
+		set    settable
+		newEnv []string
+	}{
+		{[]string{}, settable{name: "DEBUG", value: "1"}, []string{"DEBUG=1"}},
+		{[]string{"DEBUG=0"}, settable{name: "DEBUG", value: "1"}, []string{"DEBUG=1"}},
+		{[]string{"FOO=0"}, settable{name: "DEBUG", value: "1"}, []string{"FOO=0", "DEBUG=1"}},
+		{[]string{"FOO=0", "DEBUG=0"}, settable{name: "DEBUG", value: "1"}, []string{"FOO=0", "DEBUG=1"}},
+		{[]string{"FOO=0", "DEBUG=0", "BAR=1"}, settable{name: "DEBUG", value: "1"}, []string{"FOO=0", "DEBUG=1", "BAR=1"}},
+	}
+
+	for _, c := range contexts {
+		updateConfigEnv(&c.env, &c.set)
+
+		if !reflect.DeepEqual(c.env, c.newEnv) {
+			t.Fatalf("expected env to be %q, got %q", c.newEnv, c.env)
+		}
+	}
+}