Skip to content

Commit

Permalink
Merge pull request #1652 from ijc/plugins-config
Browse files Browse the repository at this point in the history
Add a field to the config file for plugin use.
  • Loading branch information
silvin-lubecki authored Feb 25, 2019
2 parents 11985c6 + 90f0742 commit cdba45b
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 46 deletions.
13 changes: 11 additions & 2 deletions cli-plugins/examples/helloworld/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,21 @@ func main() {
// the path where a plugin overrides this
// hook.
PersistentPreRunE: plugin.PersistentPreRunE,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if who == "" {
who, _ = dockerCli.ConfigFile().PluginConfig("helloworld", "who")
}
if who == "" {
who = "World"
}

fmt.Fprintf(dockerCli.Out(), "Hello %s!\n", who)
dockerCli.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
return dockerCli.ConfigFile().Save()
},
}
flags := cmd.Flags()
flags.StringVar(&who, "who", "World", "Who are we addressing?")
flags.StringVar(&who, "who", "", "Who are we addressing?")

cmd.AddCommand(goodbye, apiversion)
return cmd
Expand Down
88 changes: 63 additions & 25 deletions cli/config/configfile/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,32 @@ const (

// ConfigFile ~/.docker/config.json file info
type ConfigFile struct {
AuthConfigs map[string]types.AuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
PsFormat string `json:"psFormat,omitempty"`
ImagesFormat string `json:"imagesFormat,omitempty"`
NetworksFormat string `json:"networksFormat,omitempty"`
PluginsFormat string `json:"pluginsFormat,omitempty"`
VolumesFormat string `json:"volumesFormat,omitempty"`
StatsFormat string `json:"statsFormat,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"`
CredentialsStore string `json:"credsStore,omitempty"`
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
Filename string `json:"-"` // Note: for internal use only
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
ServicesFormat string `json:"servicesFormat,omitempty"`
TasksFormat string `json:"tasksFormat,omitempty"`
SecretFormat string `json:"secretFormat,omitempty"`
ConfigFormat string `json:"configFormat,omitempty"`
NodesFormat string `json:"nodesFormat,omitempty"`
PruneFilters []string `json:"pruneFilters,omitempty"`
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
Experimental string `json:"experimental,omitempty"`
StackOrchestrator string `json:"stackOrchestrator,omitempty"`
Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"`
CurrentContext string `json:"currentContext,omitempty"`
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
AuthConfigs map[string]types.AuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
PsFormat string `json:"psFormat,omitempty"`
ImagesFormat string `json:"imagesFormat,omitempty"`
NetworksFormat string `json:"networksFormat,omitempty"`
PluginsFormat string `json:"pluginsFormat,omitempty"`
VolumesFormat string `json:"volumesFormat,omitempty"`
StatsFormat string `json:"statsFormat,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"`
CredentialsStore string `json:"credsStore,omitempty"`
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
Filename string `json:"-"` // Note: for internal use only
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
ServicesFormat string `json:"servicesFormat,omitempty"`
TasksFormat string `json:"tasksFormat,omitempty"`
SecretFormat string `json:"secretFormat,omitempty"`
ConfigFormat string `json:"configFormat,omitempty"`
NodesFormat string `json:"nodesFormat,omitempty"`
PruneFilters []string `json:"pruneFilters,omitempty"`
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
Experimental string `json:"experimental,omitempty"`
StackOrchestrator string `json:"stackOrchestrator,omitempty"`
Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"`
CurrentContext string `json:"currentContext,omitempty"`
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
Plugins map[string]map[string]string `json:"plugins,omitempty"`
}

// ProxyConfig contains proxy configuration settings
Expand All @@ -70,6 +71,7 @@ func New(fn string) *ConfigFile {
AuthConfigs: make(map[string]types.AuthConfig),
HTTPHeaders: make(map[string]string),
Filename: fn,
Plugins: make(map[string]map[string]string),
}
}

Expand Down Expand Up @@ -330,6 +332,42 @@ func (configFile *ConfigFile) GetFilename() string {
return configFile.Filename
}

// PluginConfig retrieves the requested option for the given plugin.
func (configFile *ConfigFile) PluginConfig(pluginname, option string) (string, bool) {
if configFile.Plugins == nil {
return "", false
}
pluginConfig, ok := configFile.Plugins[pluginname]
if !ok {
return "", false
}
value, ok := pluginConfig[option]
return value, ok
}

// SetPluginConfig sets the option to the given value for the given
// plugin. Passing a value of "" will remove the option. If removing
// the final config item for a given plugin then also cleans up the
// overall plugin entry.
func (configFile *ConfigFile) SetPluginConfig(pluginname, option, value string) {
if configFile.Plugins == nil {
configFile.Plugins = make(map[string]map[string]string)
}
pluginConfig, ok := configFile.Plugins[pluginname]
if !ok {
pluginConfig = make(map[string]string)
configFile.Plugins[pluginname] = pluginConfig
}
if value != "" {
pluginConfig[option] = value
} else {
delete(pluginConfig, option)
}
if len(pluginConfig) == 0 {
delete(configFile.Plugins, pluginname)
}
}

func checkKubernetesConfiguration(kubeConfig *KubernetesConfig) error {
if kubeConfig == nil {
return nil
Expand Down
67 changes: 67 additions & 0 deletions cli/config/configfile/file_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package configfile

import (
"bytes"
"io/ioutil"
"os"
"testing"
Expand All @@ -9,6 +10,7 @@ import (
"github.com/docker/cli/cli/config/types"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/golden"
)

func TestEncodeAuth(t *testing.T) {
Expand Down Expand Up @@ -429,3 +431,68 @@ func TestSave(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.Equal(string(cfg), "{\n \"auths\": {}\n}"))
}

func TestPluginConfig(t *testing.T) {
configFile := New("test-plugin")
defer os.Remove("test-plugin")

// Populate some initial values
configFile.SetPluginConfig("plugin1", "data1", "some string")
configFile.SetPluginConfig("plugin1", "data2", "42")
configFile.SetPluginConfig("plugin2", "data3", "some other string")

// Save a config file with some plugin config
err := configFile.Save()
assert.NilError(t, err)

// Read it back and check it has the expected content
cfg, err := ioutil.ReadFile("test-plugin")
assert.NilError(t, err)
golden.Assert(t, string(cfg), "plugin-config.golden")

// Load it, resave and check again that the content is
// preserved through a load/save cycle.
configFile = New("test-plugin2")
defer os.Remove("test-plugin2")
assert.NilError(t, configFile.LoadFromReader(bytes.NewReader(cfg)))
err = configFile.Save()
assert.NilError(t, err)
cfg, err = ioutil.ReadFile("test-plugin2")
assert.NilError(t, err)
golden.Assert(t, string(cfg), "plugin-config.golden")

// Check that the contents was reloaded properly
v, ok := configFile.PluginConfig("plugin1", "data1")
assert.Assert(t, ok)
assert.Equal(t, v, "some string")
v, ok = configFile.PluginConfig("plugin1", "data2")
assert.Assert(t, ok)
assert.Equal(t, v, "42")
v, ok = configFile.PluginConfig("plugin1", "data3")
assert.Assert(t, !ok)
assert.Equal(t, v, "")
v, ok = configFile.PluginConfig("plugin2", "data3")
assert.Assert(t, ok)
assert.Equal(t, v, "some other string")
v, ok = configFile.PluginConfig("plugin2", "data4")
assert.Assert(t, !ok)
assert.Equal(t, v, "")
v, ok = configFile.PluginConfig("plugin3", "data5")
assert.Assert(t, !ok)
assert.Equal(t, v, "")

// Add, remove and modify
configFile.SetPluginConfig("plugin1", "data1", "some replacement string") // replacing a key
configFile.SetPluginConfig("plugin1", "data2", "") // deleting a key
configFile.SetPluginConfig("plugin1", "data3", "some additional string") // new key
configFile.SetPluginConfig("plugin2", "data3", "") // delete the whole plugin, since this was the only data
configFile.SetPluginConfig("plugin3", "data5", "a new plugin") // add a new plugin

err = configFile.Save()
assert.NilError(t, err)

// Read it back and check it has the expected content again
cfg, err = ioutil.ReadFile("test-plugin2")
assert.NilError(t, err)
golden.Assert(t, string(cfg), "plugin-config-2.golden")
}
12 changes: 12 additions & 0 deletions cli/config/configfile/testdata/plugin-config-2.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"auths": {},
"plugins": {
"plugin1": {
"data1": "some replacement string",
"data3": "some additional string"
},
"plugin3": {
"data5": "a new plugin"
}
}
}
12 changes: 12 additions & 0 deletions cli/config/configfile/testdata/plugin-config.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"auths": {},
"plugins": {
"plugin1": {
"data1": "some string",
"data2": "42"
},
"plugin2": {
"data3": "some other string"
}
}
}
12 changes: 12 additions & 0 deletions docs/extend/cli_plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ A plugin is required to support all of the global options of the
top-level CLI, i.e. those listed by `man docker 1` with the exception
of `-v`.

## Configuration

Plugins are expected to make use of existing global configuration
where it makes sense and likewise to consider extending the global
configuration (by patching `docker/cli` to add new fields) where that
is sensible.

Where plugins unavoidably require specific configuration the
`.plugins.«name»` key in the global `config.json` is reserved for
their use. However the preference should be for shared/global
configuration whenever that makes sense.

## Connecting to the docker engine

For consistency plugins should prefer to dial the engine by using the
Expand Down
15 changes: 14 additions & 1 deletion docs/reference/commandline/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ Users can override your custom or the default key sequence on a per-container
basis. To do this, the user specifies the `--detach-keys` flag with the `docker
attach`, `docker exec`, `docker run` or `docker start` command.

The property `plugins` contains settings specific to CLI plugins. The
key is the plugin name, while the value is a further map of options,
which are specific to that plugin.

Following is a sample `config.json` file:

```json
Expand All @@ -246,7 +250,16 @@ Following is a sample `config.json` file:
"awesomereg.example.org": "hip-star",
"unicorn.example.com": "vcbait"
},
"stackOrchestrator": "kubernetes"
"stackOrchestrator": "kubernetes",
"plugins": {
"plugin1": {
"option": "value"
},
"plugin2": {
"anotheroption": "anothervalue",
"athirdoption": "athirdvalue"
}
}
}
{% endraw %}
```
Expand Down
34 changes: 34 additions & 0 deletions e2e/cli-plugins/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cliplugins

import (
"path/filepath"
"testing"

"github.com/docker/cli/cli/config"
"gotest.tools/assert"
"gotest.tools/icmd"
)

func TestConfig(t *testing.T) {
run, cfg, cleanup := prepare(t)
defer cleanup()

cfg.SetPluginConfig("helloworld", "who", "Cambridge")
err := cfg.Save()
assert.NilError(t, err)

res := icmd.RunCmd(run("helloworld"))
res.Assert(t, icmd.Expected{
ExitCode: 0,
Out: "Hello Cambridge!",
})

cfg2, err := config.Load(filepath.Dir(cfg.GetFilename()))
assert.NilError(t, err)
assert.DeepEqual(t, cfg2.Plugins, map[string]map[string]string{
"helloworld": {
"who": "Cambridge",
"lastwho": "Cambridge",
},
})
}
2 changes: 1 addition & 1 deletion e2e/cli-plugins/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

// TestGlobalHelp ensures correct behaviour when running `docker help`
func TestGlobalHelp(t *testing.T) {
run, cleanup := prepare(t)
run, _, cleanup := prepare(t)
defer cleanup()

res := icmd.RunCmd(run("help"))
Expand Down
Loading

0 comments on commit cdba45b

Please sign in to comment.