Signed-off-by: Victor Vieux <vieux@docker.com>
| ... | ... |
@@ -89,7 +89,11 @@ func (pr *pluginRouter) setPlugin(ctx context.Context, w http.ResponseWriter, r |
| 89 | 89 |
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
|
| 90 | 90 |
return err |
| 91 | 91 |
} |
| 92 |
- return pr.backend.Set(vars["name"], args) |
|
| 92 |
+ if err := pr.backend.Set(vars["name"], args); err != nil {
|
|
| 93 |
+ return err |
|
| 94 |
+ } |
|
| 95 |
+ w.WriteHeader(http.StatusNoContent) |
|
| 96 |
+ return nil |
|
| 93 | 97 |
} |
| 94 | 98 |
|
| 95 | 99 |
func (pr *pluginRouter) listPlugins(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| ... | ... |
@@ -4290,16 +4290,26 @@ Content-Type: application/json |
| 4290 | 4290 |
- **200** - no error |
| 4291 | 4291 |
- **404** - plugin not installed |
| 4292 | 4292 |
|
| 4293 |
-<!-- TODO Document "docker plugin set" endpoint once implemented |
|
| 4294 | 4293 |
### Configure a plugin |
| 4295 | 4294 |
|
| 4296 |
-`POST /plugins/(plugin name)/set` |
|
| 4295 |
+POST /plugins/(plugin name)/set` |
|
| 4297 | 4296 |
|
| 4298 |
-**Status codes**: |
|
| 4297 |
+**Example request**: |
|
| 4299 | 4298 |
|
| 4300 |
-- **500** - not implemented |
|
| 4301 | 4299 |
|
| 4300 |
+ POST /plugins/tiborvass/no-remove/set |
|
| 4301 |
+ Content-Type: application/json |
|
| 4302 |
+ |
|
| 4303 |
+ ["DEBUG=1"] |
|
| 4304 |
+ |
|
| 4305 |
+**Example response**: |
|
| 4306 |
+ |
|
| 4307 |
+ HTTP/1.1 204 No Content |
|
| 4308 |
+ |
|
| 4309 |
+**Status codes**: |
|
| 4310 |
+ |
|
| 4311 |
+- **204** - no error |
|
| 4312 |
+- **404** - plugin not installed |
|
| 4302 | 4313 |
|
| 4303 | 4314 |
### Enable a plugin |
| 4304 | 4315 |
|
| 54 | 55 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,51 @@ |
| 0 |
+--- |
|
| 1 |
+title: "plugin set" |
|
| 2 |
+description: "the plugin set command description and usage" |
|
| 3 |
+keywords: "plugin, set" |
|
| 4 |
+advisory: "experimental" |
|
| 5 |
+--- |
|
| 6 |
+ |
|
| 7 |
+<!-- This file is maintained within the docker/docker Github |
|
| 8 |
+ repository at https://github.com/docker/docker/. Make all |
|
| 9 |
+ pull requests against that repo. If you see this file in |
|
| 10 |
+ another repository, consider it read-only there, as it will |
|
| 11 |
+ periodically be overwritten by the definitive file. Pull |
|
| 12 |
+ requests which include edits to this file in other repositories |
|
| 13 |
+ will be rejected. |
|
| 14 |
+--> |
|
| 15 |
+ |
|
| 16 |
+# plugin set (experimental) |
|
| 17 |
+ |
|
| 18 |
+```markdown |
|
| 19 |
+Usage: docker plugin set PLUGIN key1=value1 [key2=value2...] |
|
| 20 |
+ |
|
| 21 |
+Change settings for a plugin |
|
| 22 |
+ |
|
| 23 |
+Options: |
|
| 24 |
+ --help Print usage |
|
| 25 |
+``` |
|
| 26 |
+ |
|
| 27 |
+Change settings for a plugin. The plugin must be disabled. |
|
| 28 |
+ |
|
| 29 |
+ |
|
| 30 |
+The following example installs change the env variable `DEBUG` of the |
|
| 31 |
+`no-remove` plugin. |
|
| 32 |
+ |
|
| 33 |
+```bash |
|
| 34 |
+$ docker plugin inspect -f {{.Config.Env}} tiborvass/no-remove
|
|
| 35 |
+[DEBUG=0] |
|
| 36 |
+ |
|
| 37 |
+$ docker plugin set DEBUG=1 tiborvass/no-remove |
|
| 38 |
+ |
|
| 39 |
+$ docker plugin inspect -f {{.Config.Env}} tiborvass/no-remove
|
|
| 40 |
+[DEBUG=1] |
|
| 41 |
+``` |
|
| 42 |
+ |
|
| 43 |
+## Related information |
|
| 44 |
+ |
|
| 45 |
+* [plugin ls](plugin_ls.md) |
|
| 46 |
+* [plugin enable](plugin_enable.md) |
|
| 47 |
+* [plugin disable](plugin_disable.md) |
|
| 48 |
+* [plugin inspect](plugin_inspect.md) |
|
| 49 |
+* [plugin install](plugin_install.md) |
|
| 50 |
+* [plugin rm](plugin_rm.md) |
| ... | ... |
@@ -117,6 +117,20 @@ func (s *DockerSuite) TestPluginInstallDisableVolumeLs(c *check.C) {
|
| 117 | 117 |
dockerCmd(c, "volume", "ls") |
| 118 | 118 |
} |
| 119 | 119 |
|
| 120 |
+func (s *DockerSuite) TestPluginSet(c *check.C) {
|
|
| 121 |
+ testRequires(c, DaemonIsLinux, ExperimentalDaemon, Network) |
|
| 122 |
+ out, _ := dockerCmd(c, "plugin", "install", "--grant-all-permissions", "--disable", pName) |
|
| 123 |
+ c.Assert(strings.TrimSpace(out), checker.Contains, pName) |
|
| 124 |
+ |
|
| 125 |
+ env, _ := dockerCmd(c, "plugin", "inspect", "-f", "{{.Config.Env}}", pName)
|
|
| 126 |
+ c.Assert(strings.TrimSpace(env), checker.Equals, "[DEBUG=0]") |
|
| 127 |
+ |
|
| 128 |
+ dockerCmd(c, "plugin", "set", pName, "DEBUG=1") |
|
| 129 |
+ |
|
| 130 |
+ env, _ = dockerCmd(c, "plugin", "inspect", "-f", "{{.Config.Env}}", pName)
|
|
| 131 |
+ c.Assert(strings.TrimSpace(env), checker.Equals, "[DEBUG=1]") |
|
| 132 |
+} |
|
| 133 |
+ |
|
| 120 | 134 |
func (s *DockerSuite) TestPluginInstallImage(c *check.C) {
|
| 121 | 135 |
testRequires(c, DaemonIsLinux, ExperimentalDaemon) |
| 122 | 136 |
out, _, err := dockerCmdWithError("plugin", "install", "redis")
|
| ... | ... |
@@ -85,8 +85,8 @@ func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.A |
| 85 | 85 |
} |
| 86 | 86 |
|
| 87 | 87 |
tag := distribution.GetTag(ref) |
| 88 |
- p := v2.NewPlugin(ref.Name(), pluginID, pm.runRoot, tag) |
|
| 89 |
- if err := p.InitPlugin(pm.libRoot); err != nil {
|
|
| 88 |
+ p := v2.NewPlugin(ref.Name(), pluginID, pm.runRoot, pm.libRoot, tag) |
|
| 89 |
+ if err := p.InitPlugin(); err != nil {
|
|
| 90 | 90 |
return nil, err |
| 91 | 91 |
} |
| 92 | 92 |
pm.pluginStore.Add(p) |
| ... | ... |
@@ -8,7 +8,7 @@ import ( |
| 8 | 8 |
) |
| 9 | 9 |
|
| 10 | 10 |
func TestFilterByCapNeg(t *testing.T) {
|
| 11 |
- p := v2.NewPlugin("test", "1234567890", "/run/docker", "latest")
|
|
| 11 |
+ p := v2.NewPlugin("test", "1234567890", "/run/docker", "/var/lib/docker/plugins", "latest")
|
|
| 12 | 12 |
|
| 13 | 13 |
iType := types.PluginInterfaceType{"volumedriver", "docker", "1.0"}
|
| 14 | 14 |
i := types.PluginManifestInterface{"plugins.sock", []types.PluginInterfaceType{iType}}
|
| ... | ... |
@@ -21,7 +21,7 @@ func TestFilterByCapNeg(t *testing.T) {
|
| 21 | 21 |
} |
| 22 | 22 |
|
| 23 | 23 |
func TestFilterByCapPos(t *testing.T) {
|
| 24 |
- p := v2.NewPlugin("test", "1234567890", "/run/docker", "latest")
|
|
| 24 |
+ p := v2.NewPlugin("test", "1234567890", "/run/docker", "/var/lib/docker/plugins", "latest")
|
|
| 25 | 25 |
|
| 26 | 26 |
iType := types.PluginInterfaceType{"volumedriver", "docker", "1.0"}
|
| 27 | 27 |
i := types.PluginManifestInterface{"plugins.sock", []types.PluginInterfaceType{iType}}
|
| ... | ... |
@@ -2,7 +2,6 @@ package v2 |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"encoding/json" |
| 5 |
- "errors" |
|
| 6 | 5 |
"fmt" |
| 7 | 6 |
"os" |
| 8 | 7 |
"path/filepath" |
| ... | ... |
@@ -24,6 +23,7 @@ type Plugin struct {
|
| 24 | 24 |
RefCount int `json:"-"` |
| 25 | 25 |
Restart bool `json:"-"` |
| 26 | 26 |
ExitChan chan bool `json:"-"` |
| 27 |
+ LibRoot string `json:"-"` |
|
| 27 | 28 |
} |
| 28 | 29 |
|
| 29 | 30 |
const defaultPluginRuntimeDestination = "/run/docker/plugins" |
| ... | ... |
@@ -42,10 +42,11 @@ func newPluginObj(name, id, tag string) types.Plugin {
|
| 42 | 42 |
} |
| 43 | 43 |
|
| 44 | 44 |
// NewPlugin creates a plugin. |
| 45 |
-func NewPlugin(name, id, runRoot, tag string) *Plugin {
|
|
| 45 |
+func NewPlugin(name, id, runRoot, libRoot, tag string) *Plugin {
|
|
| 46 | 46 |
return &Plugin{
|
| 47 | 47 |
PluginObj: newPluginObj(name, id, tag), |
| 48 | 48 |
RuntimeSourcePath: filepath.Join(runRoot, id), |
| 49 |
+ LibRoot: libRoot, |
|
| 49 | 50 |
} |
| 50 | 51 |
} |
| 51 | 52 |
|
| ... | ... |
@@ -86,8 +87,8 @@ func (p *Plugin) RemoveFromDisk() error {
|
| 86 | 86 |
} |
| 87 | 87 |
|
| 88 | 88 |
// InitPlugin populates the plugin object from the plugin manifest file. |
| 89 |
-func (p *Plugin) InitPlugin(libRoot string) error {
|
|
| 90 |
- dt, err := os.Open(filepath.Join(libRoot, p.PluginObj.ID, "manifest.json")) |
|
| 89 |
+func (p *Plugin) InitPlugin() error {
|
|
| 90 |
+ dt, err := os.Open(filepath.Join(p.LibRoot, p.PluginObj.ID, "manifest.json")) |
|
| 91 | 91 |
if err != nil {
|
| 92 | 92 |
return err |
| 93 | 93 |
} |
| ... | ... |
@@ -109,7 +110,11 @@ func (p *Plugin) InitPlugin(libRoot string) error {
|
| 109 | 109 |
} |
| 110 | 110 |
copy(p.PluginObj.Config.Args, p.PluginObj.Manifest.Args.Value) |
| 111 | 111 |
|
| 112 |
- f, err := os.Create(filepath.Join(libRoot, p.PluginObj.ID, "plugin-config.json")) |
|
| 112 |
+ return p.writeConfig() |
|
| 113 |
+} |
|
| 114 |
+ |
|
| 115 |
+func (p *Plugin) writeConfig() error {
|
|
| 116 |
+ f, err := os.Create(filepath.Join(p.LibRoot, p.PluginObj.ID, "plugin-config.json")) |
|
| 113 | 117 |
if err != nil {
|
| 114 | 118 |
return err |
| 115 | 119 |
} |
| ... | ... |
@@ -120,15 +125,43 @@ func (p *Plugin) InitPlugin(libRoot string) error {
|
| 120 | 120 |
|
| 121 | 121 |
// Set is used to pass arguments to the plugin. |
| 122 | 122 |
func (p *Plugin) Set(args []string) error {
|
| 123 |
- m := make(map[string]string, len(args)) |
|
| 124 |
- for _, arg := range args {
|
|
| 125 |
- i := strings.Index(arg, "=") |
|
| 126 |
- if i < 0 {
|
|
| 127 |
- return fmt.Errorf("No equal sign '=' found in %s", arg)
|
|
| 123 |
+ p.Lock() |
|
| 124 |
+ defer p.Unlock() |
|
| 125 |
+ |
|
| 126 |
+ if p.PluginObj.Enabled {
|
|
| 127 |
+ return fmt.Errorf("cannot set on an active plugin, disable plugin before setting")
|
|
| 128 |
+ } |
|
| 129 |
+ |
|
| 130 |
+ sets, err := newSettables(args) |
|
| 131 |
+ if err != nil {
|
|
| 132 |
+ return err |
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+next: |
|
| 136 |
+ for _, s := range sets {
|
|
| 137 |
+ // range over all the envs in the manifest |
|
| 138 |
+ for _, env := range p.PluginObj.Manifest.Env {
|
|
| 139 |
+ // found the env in the manifest |
|
| 140 |
+ if env.Name == s.name {
|
|
| 141 |
+ // is it settable ? |
|
| 142 |
+ if ok, err := s.isSettable(allowedSettableFieldsEnv, env.Settable); err != nil {
|
|
| 143 |
+ return err |
|
| 144 |
+ } else if !ok {
|
|
| 145 |
+ return fmt.Errorf("%q is not settable", s.prettyName())
|
|
| 146 |
+ } |
|
| 147 |
+ // is it, so lets update the config in memory |
|
| 148 |
+ updateConfigEnv(&p.PluginObj.Config.Env, &s) |
|
| 149 |
+ continue next |
|
| 150 |
+ } |
|
| 128 | 151 |
} |
| 129 |
- m[arg[:i]] = arg[i+1:] |
|
| 152 |
+ |
|
| 153 |
+ //TODO: check devices, mount and args |
|
| 154 |
+ |
|
| 155 |
+ return fmt.Errorf("setting %q not found in the plugin configuration", s.name)
|
|
| 130 | 156 |
} |
| 131 |
- return errors.New("not implemented")
|
|
| 157 |
+ |
|
| 158 |
+ // update the config on disk |
|
| 159 |
+ return p.writeConfig() |
|
| 132 | 160 |
} |
| 133 | 161 |
|
| 134 | 162 |
// ComputePrivileges takes the manifest file and computes the list of access necessary |
| 135 | 163 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,102 @@ |
| 0 |
+package v2 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "errors" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "strings" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+type settable struct {
|
|
| 9 |
+ name string |
|
| 10 |
+ field string |
|
| 11 |
+ value string |
|
| 12 |
+} |
|
| 13 |
+ |
|
| 14 |
+var ( |
|
| 15 |
+ allowedSettableFieldsEnv = []string{"value"}
|
|
| 16 |
+ allowedSettableFieldsArgs = []string{"value"}
|
|
| 17 |
+ allowedSettableFieldsDevices = []string{"path"}
|
|
| 18 |
+ allowedSettableFieldsMounts = []string{"source"}
|
|
| 19 |
+ |
|
| 20 |
+ errMultipleFields = errors.New("multiple fields are settable, one must be specified")
|
|
| 21 |
+ errInvalidFormat = errors.New("invalid format, must be <name>[.<field>][=<value>]")
|
|
| 22 |
+) |
|
| 23 |
+ |
|
| 24 |
+func newSettables(args []string) ([]settable, error) {
|
|
| 25 |
+ sets := make([]settable, 0, len(args)) |
|
| 26 |
+ for _, arg := range args {
|
|
| 27 |
+ set, err := newSettable(arg) |
|
| 28 |
+ if err != nil {
|
|
| 29 |
+ return nil, err |
|
| 30 |
+ } |
|
| 31 |
+ sets = append(sets, set) |
|
| 32 |
+ } |
|
| 33 |
+ return sets, nil |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+func newSettable(arg string) (settable, error) {
|
|
| 37 |
+ var set settable |
|
| 38 |
+ if i := strings.Index(arg, "="); i == 0 {
|
|
| 39 |
+ return set, errInvalidFormat |
|
| 40 |
+ } else if i < 0 {
|
|
| 41 |
+ set.name = arg |
|
| 42 |
+ } else {
|
|
| 43 |
+ set.name = arg[:i] |
|
| 44 |
+ set.value = arg[i+1:] |
|
| 45 |
+ } |
|
| 46 |
+ |
|
| 47 |
+ if i := strings.LastIndex(set.name, "."); i > 0 {
|
|
| 48 |
+ set.field = set.name[i+1:] |
|
| 49 |
+ set.name = arg[:i] |
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ return set, nil |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+// prettyName return name.field if there is a field, otherwise name. |
|
| 56 |
+func (set *settable) prettyName() string {
|
|
| 57 |
+ if set.field != "" {
|
|
| 58 |
+ return fmt.Sprintf("%s.%s", set.name, set.field)
|
|
| 59 |
+ } |
|
| 60 |
+ return set.name |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+func (set *settable) isSettable(allowedSettableFields []string, settable []string) (bool, error) {
|
|
| 64 |
+ if set.field == "" {
|
|
| 65 |
+ if len(settable) == 1 {
|
|
| 66 |
+ // if field is not specified and there only one settable, default to it. |
|
| 67 |
+ set.field = settable[0] |
|
| 68 |
+ } else if len(settable) > 1 {
|
|
| 69 |
+ return false, errMultipleFields |
|
| 70 |
+ } |
|
| 71 |
+ } |
|
| 72 |
+ |
|
| 73 |
+ isAllowed := false |
|
| 74 |
+ for _, allowedSettableField := range allowedSettableFields {
|
|
| 75 |
+ if set.field == allowedSettableField {
|
|
| 76 |
+ isAllowed = true |
|
| 77 |
+ break |
|
| 78 |
+ } |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ if isAllowed {
|
|
| 82 |
+ for _, settableField := range settable {
|
|
| 83 |
+ if set.field == settableField {
|
|
| 84 |
+ return true, nil |
|
| 85 |
+ } |
|
| 86 |
+ } |
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ return false, nil |
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+func updateConfigEnv(env *[]string, set *settable) {
|
|
| 93 |
+ for i, e := range *env {
|
|
| 94 |
+ if parts := strings.SplitN(e, "=", 2); parts[0] == set.name {
|
|
| 95 |
+ (*env)[i] = fmt.Sprintf("%s=%s", set.name, set.value)
|
|
| 96 |
+ return |
|
| 97 |
+ } |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ *env = append(*env, fmt.Sprintf("%s=%s", set.name, set.value))
|
|
| 101 |
+} |
| 0 | 102 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,91 @@ |
| 0 |
+package v2 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "reflect" |
|
| 4 |
+ "testing" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+func TestNewSettable(t *testing.T) {
|
|
| 8 |
+ contexts := []struct {
|
|
| 9 |
+ arg string |
|
| 10 |
+ name string |
|
| 11 |
+ field string |
|
| 12 |
+ value string |
|
| 13 |
+ err error |
|
| 14 |
+ }{
|
|
| 15 |
+ {"name=value", "name", "", "value", nil},
|
|
| 16 |
+ {"name", "name", "", "", nil},
|
|
| 17 |
+ {"name.field=value", "name", "field", "value", nil},
|
|
| 18 |
+ {"name.field", "name", "field", "", nil},
|
|
| 19 |
+ {"=value", "", "", "", errInvalidFormat},
|
|
| 20 |
+ {"=", "", "", "", errInvalidFormat},
|
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ for _, c := range contexts {
|
|
| 24 |
+ s, err := newSettable(c.arg) |
|
| 25 |
+ if err != c.err {
|
|
| 26 |
+ t.Fatalf("expected error to be %v, got %v", c.err, err)
|
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ if s.name != c.name {
|
|
| 30 |
+ t.Fatalf("expected name to be %q, got %q", c.name, s.name)
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ if s.field != c.field {
|
|
| 34 |
+ t.Fatalf("expected field to be %q, got %q", c.field, s.field)
|
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ if s.value != c.value {
|
|
| 38 |
+ t.Fatalf("expected value to be %q, got %q", c.value, s.value)
|
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ } |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+func TestIsSettable(t *testing.T) {
|
|
| 45 |
+ contexts := []struct {
|
|
| 46 |
+ allowedSettableFields []string |
|
| 47 |
+ set settable |
|
| 48 |
+ settable []string |
|
| 49 |
+ result bool |
|
| 50 |
+ err error |
|
| 51 |
+ }{
|
|
| 52 |
+ {allowedSettableFieldsEnv, settable{}, []string{}, false, nil},
|
|
| 53 |
+ {allowedSettableFieldsEnv, settable{field: "value"}, []string{}, false, nil},
|
|
| 54 |
+ {allowedSettableFieldsEnv, settable{}, []string{"value"}, true, nil},
|
|
| 55 |
+ {allowedSettableFieldsEnv, settable{field: "value"}, []string{"value"}, true, nil},
|
|
| 56 |
+ {allowedSettableFieldsEnv, settable{field: "foo"}, []string{"value"}, false, nil},
|
|
| 57 |
+ {allowedSettableFieldsEnv, settable{field: "foo"}, []string{"foo"}, false, nil},
|
|
| 58 |
+ {allowedSettableFieldsEnv, settable{}, []string{"value1", "value2"}, false, errMultipleFields},
|
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ for _, c := range contexts {
|
|
| 62 |
+ if res, err := c.set.isSettable(c.allowedSettableFields, c.settable); res != c.result {
|
|
| 63 |
+ t.Fatalf("expected result to be %t, got %t", c.result, res)
|
|
| 64 |
+ } else if err != c.err {
|
|
| 65 |
+ t.Fatalf("expected error to be %v, got %v", c.err, err)
|
|
| 66 |
+ } |
|
| 67 |
+ } |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+func TestUpdateConfigEnv(t *testing.T) {
|
|
| 71 |
+ contexts := []struct {
|
|
| 72 |
+ env []string |
|
| 73 |
+ set settable |
|
| 74 |
+ newEnv []string |
|
| 75 |
+ }{
|
|
| 76 |
+ {[]string{}, settable{name: "DEBUG", value: "1"}, []string{"DEBUG=1"}},
|
|
| 77 |
+ {[]string{"DEBUG=0"}, settable{name: "DEBUG", value: "1"}, []string{"DEBUG=1"}},
|
|
| 78 |
+ {[]string{"FOO=0"}, settable{name: "DEBUG", value: "1"}, []string{"FOO=0", "DEBUG=1"}},
|
|
| 79 |
+ {[]string{"FOO=0", "DEBUG=0"}, settable{name: "DEBUG", value: "1"}, []string{"FOO=0", "DEBUG=1"}},
|
|
| 80 |
+ {[]string{"FOO=0", "DEBUG=0", "BAR=1"}, settable{name: "DEBUG", value: "1"}, []string{"FOO=0", "DEBUG=1", "BAR=1"}},
|
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ for _, c := range contexts {
|
|
| 84 |
+ updateConfigEnv(&c.env, &c.set) |
|
| 85 |
+ |
|
| 86 |
+ if !reflect.DeepEqual(c.env, c.newEnv) {
|
|
| 87 |
+ t.Fatalf("expected env to be %q, got %q", c.newEnv, c.env)
|
|
| 88 |
+ } |
|
| 89 |
+ } |
|
| 90 |
+} |