diff --git a/api/server/router/plugin/plugin_routes.go b/api/server/router/plugin/plugin_routes.go index ec1a4ac758..2a62c2adc6 100644 --- a/api/server/router/plugin/plugin_routes.go +++ b/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 { diff --git a/docs/reference/api/docker_remote_api_v1.25.md b/docs/reference/api/docker_remote_api_v1.25.md index a820b49986..a7dbe0ee15 100644 --- a/docs/reference/api/docker_remote_api_v1.25.md +++ b/docs/reference/api/docker_remote_api_v1.25.md @@ -4290,16 +4290,26 @@ Content-Type: application/json - **200** - no error - **404** - plugin not installed - +- **204** - no error +- **404** - plugin not installed ### Enable a plugin diff --git a/docs/reference/commandline/plugin_disable.md b/docs/reference/commandline/plugin_disable.md index b08509363a..580d4b93d0 100644 --- a/docs/reference/commandline/plugin_disable.md +++ b/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) diff --git a/docs/reference/commandline/plugin_enable.md b/docs/reference/commandline/plugin_enable.md index f9635d9cec..edb2e0bf1f 100644 --- a/docs/reference/commandline/plugin_enable.md +++ b/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) diff --git a/docs/reference/commandline/plugin_inspect.md b/docs/reference/commandline/plugin_inspect.md index 5aea7cce40..3fa55405a1 100755 --- a/docs/reference/commandline/plugin_inspect.md +++ b/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) diff --git a/docs/reference/commandline/plugin_install.md b/docs/reference/commandline/plugin_install.md index ce0ac17dff..524b4d8d5c 100644 --- a/docs/reference/commandline/plugin_install.md +++ b/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) diff --git a/docs/reference/commandline/plugin_ls.md b/docs/reference/commandline/plugin_ls.md index baa0db7a53..12de328142 100644 --- a/docs/reference/commandline/plugin_ls.md +++ b/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) diff --git a/docs/reference/commandline/plugin_rm.md b/docs/reference/commandline/plugin_rm.md index 57dbfc9af1..4a86bc5c2b 100644 --- a/docs/reference/commandline/plugin_rm.md +++ b/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) diff --git a/docs/reference/commandline/plugin_set.md b/docs/reference/commandline/plugin_set.md new file mode 100644 index 0000000000..8c9eb74e1c --- /dev/null +++ b/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" +--- + + + +# 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) diff --git a/integration-cli/docker_cli_plugins_test.go b/integration-cli/docker_cli_plugins_test.go index 011729e29e..b05cecae68 100644 --- a/integration-cli/docker_cli_plugins_test.go +++ b/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") diff --git a/plugin/backend.go b/plugin/backend.go index dd1bc6679f..edc93a858c 100644 --- a/plugin/backend.go +++ b/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) diff --git a/plugin/store/store_test.go b/plugin/store/store_test.go index 89071b997d..8591c4ead6 100644 --- a/plugin/store/store_test.go +++ b/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}} diff --git a/plugin/v2/plugin.go b/plugin/v2/plugin.go index f5413c3a3a..2335f552e1 100644 --- a/plugin/v2/plugin.go +++ b/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) - } - m[arg[:i]] = arg[i+1:] + p.Lock() + defer p.Unlock() + + if p.PluginObj.Enabled { + return fmt.Errorf("cannot set on an active plugin, disable plugin before setting") } - return errors.New("not implemented") + + 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 + } + } + + //TODO: check devices, mount and args + + return fmt.Errorf("setting %q not found in the plugin configuration", s.name) + } + + // update the config on disk + return p.writeConfig() } // ComputePrivileges takes the manifest file and computes the list of access necessary diff --git a/plugin/v2/settable.go b/plugin/v2/settable.go new file mode 100644 index 0000000000..dc0f56a844 --- /dev/null +++ b/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 [.][=]") +) + +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)) +} diff --git a/plugin/v2/settable_test.go b/plugin/v2/settable_test.go new file mode 100644 index 0000000000..57260bf7a3 --- /dev/null +++ b/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) + } + } +}