From 01a2b72df5727a2ccbcefda895eb6ca854309d8c Mon Sep 17 00:00:00 2001 From: Rodion Date: Wed, 11 Nov 2026 17:58:18 +0300 Subject: [PATCH] cli-plugins: support extra directories via environment Signed-off-by: Rodion --- cli-plugins/manager/manager.go | 15 ++++++-- cli-plugins/manager/manager_test.go | 47 ++++++++++++++++++++++++++ cli-plugins/manager/manager_unix.go | 10 ++---- cli-plugins/manager/manager_windows.go | 10 ++---- docs/reference/commandline/docker.md | 28 +++++++++++++-- 5 files changed, 92 insertions(+), 18 deletions(-) diff --git a/cli-plugins/manager/manager.go b/cli-plugins/manager/manager.go index bf398533aa22..094cfc772f1b 100644 --- a/cli-plugins/manager/manager.go +++ b/cli-plugins/manager/manager.go @@ -37,14 +37,25 @@ func (e errPluginNotFound) Error() string { // // Plugin-discovery is performed in the following order of preference: // -// 1. The "cli-plugins" directory inside the CLIs [config.Path] (usually "~/.docker/cli-plugins"). +// 1. Additional plugin directories from the DOCKER_CLI_PLUGINS_EXTRA_DIRS environment variable. // 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs]. -// 3. Platform-specific defaultSystemPluginDirs. +// 3. The "cli-plugins" directory inside the CLIs [config.Path] (usually "~/.docker/cli-plugins"). +// 4. Platform-specific defaultSystemPluginDirs. +// +// The environment variable uses the platform's path-list separator. Empty entries +// are ignored so that appending to an unset variable preserves the search order. // // [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs func getPluginDirs(cfg *configfile.ConfigFile) []string { var pluginDirs []string + for _, dir := range filepath.SplitList(os.Getenv("DOCKER_CLI_PLUGINS_EXTRA_DIRS")) { + if dir == "" { + continue + } + pluginDirs = append(pluginDirs, dir) + } + if cfg != nil { pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...) } diff --git a/cli-plugins/manager/manager_test.go b/cli-plugins/manager/manager_test.go index 07df0f8ecccb..65d7382af091 100644 --- a/cli-plugins/manager/manager_test.go +++ b/cli-plugins/manager/manager_test.go @@ -1,6 +1,7 @@ package manager import ( + "os" "path/filepath" "strings" "testing" @@ -171,6 +172,8 @@ func TestErrPluginNotFound(t *testing.T) { func TestGetPluginDirs(t *testing.T) { cli := test.NewFakeCli(nil) + t.Setenv("DOCKER_CLI_PLUGINS_EXTRA_DIRS", "") + pluginDir := filepath.Join(config.Dir(), "cli-plugins") expected := append([]string{pluginDir}, defaultSystemPluginDirs...) @@ -186,3 +189,47 @@ func TestGetPluginDirs(t *testing.T) { pluginDirs = getPluginDirs(cli.ConfigFile()) assert.DeepEqual(t, expected, pluginDirs) } + +func TestGetPluginDirsFromEnv(t *testing.T) { + dir := t.TempDir() + envFirst := filepath.Join(dir, "env-first") + envSecond := filepath.Join(dir, "env-second") + configExtra := filepath.Join(dir, "config-extra") + + cfg := &configfile.ConfigFile{ + CLIPluginsExtraDirs: []string{configExtra}, + } + + testCases := []struct { + name string + paths []string + expected []string + }{ + { + name: "multiple paths", + paths: []string{envFirst, envSecond}, + expected: []string{envFirst, envSecond}, + }, + { + name: "empty entries", + paths: []string{"", envFirst, "", envSecond, ""}, + expected: []string{envFirst, envSecond}, + }, + { + name: "empty value", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("DOCKER_CLI_PLUGINS_EXTRA_DIRS", + strings.Join(tc.paths, string(os.PathListSeparator))) + + expected := append([]string{}, tc.expected...) + expected = append(expected, configExtra, filepath.Join(config.Dir(), "cli-plugins")) + expected = append(expected, defaultSystemPluginDirs...) + + assert.DeepEqual(t, getPluginDirs(cfg), expected) + }) + } +} diff --git a/cli-plugins/manager/manager_unix.go b/cli-plugins/manager/manager_unix.go index f546dc38496e..9ceec8c92018 100644 --- a/cli-plugins/manager/manager_unix.go +++ b/cli-plugins/manager/manager_unix.go @@ -5,13 +5,9 @@ package manager // defaultSystemPluginDirs are the platform-specific locations to search // for plugins in order of preference. // -// Plugin-discovery is performed in the following order of preference: -// -// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins"). -// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs]. -// 3. Platform-specific defaultSystemPluginDirs (as defined below). -// -// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs +// These directories are searched after the directories from the environment +// variable, the configuration file, and the CLI's config-directory. +// See [getPluginDirs] for the complete search order. var defaultSystemPluginDirs = []string{ "/usr/local/lib/docker/cli-plugins", "/usr/local/libexec/docker/cli-plugins", diff --git a/cli-plugins/manager/manager_windows.go b/cli-plugins/manager/manager_windows.go index b42b713e9988..0f0e8b51b3ff 100644 --- a/cli-plugins/manager/manager_windows.go +++ b/cli-plugins/manager/manager_windows.go @@ -8,13 +8,9 @@ import ( // defaultSystemPluginDirs are the platform-specific locations to search // for plugins in order of preference. // -// Plugin-discovery is performed in the following order of preference: -// -// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins"). -// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs]. -// 3. Platform-specific defaultSystemPluginDirs (as defined below). -// -// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs +// These directories are searched after the directories from the environment +// variable, the configuration file, and the CLI's config-directory. +// See [getPluginDirs] for the complete search order. var defaultSystemPluginDirs = []string{ filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"), } diff --git a/docs/reference/commandline/docker.md b/docs/reference/commandline/docker.md index 73ef948beae2..73520de9b65a 100644 --- a/docs/reference/commandline/docker.md +++ b/docs/reference/commandline/docker.md @@ -120,6 +120,7 @@ The following environment variables control the behavior of the `docker` command | :---------------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `DOCKER_API_VERSION` | Override the negotiated API version to use for debugging (e.g. `1.19`) | | `DOCKER_CERT_PATH` | Location of your authentication keys. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) | +| `DOCKER_CLI_PLUGINS_EXTRA_DIRS` | Additional directories to search for CLI plugins, separated by `:` on Linux and macOS or `;` on Windows. See [CLI plugin directories](#cli-plugin-directories). | | `DOCKER_CONFIG` | The location of your client configuration files. | | `DOCKER_CONTEXT` | Name of the `docker context` to use (overrides `DOCKER_HOST` env var and default context set with `docker context use`) | | `DOCKER_CUSTOM_HEADERS` | (Experimental) Configure [custom HTTP headers](#custom-http-headers) to be sent by the client. Headers must be provided as a comma-separated list of `name=value` pairs. This is the equivalent to the `HttpHeaders` field in the configuration file. | @@ -129,8 +130,7 @@ The following environment variables control the behavior of the `docker` command | `DOCKER_TLS` | Enable TLS for connections made by the `docker` CLI (equivalent of the `--tls` command-line option). Set to a non-empty value to enable TLS. Note that TLS is enabled automatically if any of the other TLS options are set. | | `DOCKER_TLS_VERIFY` | When set Docker uses TLS and verifies the remote. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) | | `BUILDKIT_PROGRESS` | Set type of progress output (`auto`, `plain`, `tty`, `rawjson`) when [building](https://docs.docker.com/reference/cli/docker/image/build/) with [BuildKit backend](https://docs.docker.com/build/buildkit/). Use plain to show container output (default `auto`). | -| `NO_COLOR` | Disable any ANSI escape codes in the output in accordance with https://no-color.org/ - | +| `NO_COLOR` | Disable any ANSI escape codes in the output in accordance with https://no-color.org/ | Because Docker is developed using Go, you can also use any environment variables used by the Go runtime. In particular, you may find these useful: @@ -144,6 +144,30 @@ variables used by the Go runtime. In particular, you may find these useful: See the [Go specification](https://pkg.go.dev/golang.org/x/net/http/httpproxy#Config) for details on these variables. +#### CLI plugin directories + +Use `DOCKER_CLI_PLUGINS_EXTRA_DIRS` to add CLI plugin directories without editing +`config.json`. Separate paths with `:` on Linux and macOS, or `;` on Windows. +Empty entries are ignored. If the variable is unset or empty, the search paths +remain unchanged. + +The CLI searches for plugins in the following order: + +1. Directories from `DOCKER_CLI_PLUGINS_EXTRA_DIRS`, in the order listed. +2. Directories from `cliPluginsExtraDirs` in `config.json`, in the order listed. +3. The `cli-plugins` directory inside the CLI configuration directory + (`~/.docker/cli-plugins` by default). +4. Platform-specific system plugin directories. + +If multiple directories contain a plugin with the same name, the first candidate +takes precedence. + +For example, append a directory to the environment variable on Linux or macOS: + +```console +$ export DOCKER_CLI_PLUGINS_EXTRA_DIRS="$DOCKER_CLI_PLUGINS_EXTRA_DIRS:/home/username/my/plugins" +``` + ### Option types Single character command line options can be combined, so rather than