From 132531812a28405b7d339e408d2e311364fbbcdf Mon Sep 17 00:00:00 2001 From: Vibhav Bobade Date: Mon, 18 May 2020 16:03:28 +0530 Subject: [PATCH] Add XDG_DATA_DIRS support for using plugins from multiple locs This allows the user to set their plugin directories in XDG_DATA_DIRS and based on the precedence, all the locations will be searched for the particular plugin and be used. Signed-off-by: Vibhav Bobade --- cmd/helm/load_plugins.go | 2 +- cmd/helm/plugin_test.go | 82 +++++++++++++++++++ .../helm/plugins_1/env/completion.yaml | 13 +++ .../helmhome/helm/plugins_1/env/plugin.yaml | 4 + .../helm/plugins_1/exitwith/completion.yaml | 5 ++ .../helm/plugins_1/exitwith/exitwith.sh | 2 + .../helm/plugins_1/exitwith/plugin.yaml | 4 + .../helm/plugins_1/fullenv/completion.yaml | 19 +++++ .../helm/plugins_1/fullenv/fullenv.sh | 7 ++ .../helm/plugins_1/fullenv/plugin.yaml | 4 + .../helmhome/helm/plugins_2/args/args.sh | 2 + .../helm/plugins_2/args/plugin.complete | 13 +++ .../helmhome/helm/plugins_2/args/plugin.yaml | 4 + .../helm/plugins_2/echo/completion.yaml | 0 .../helm/plugins_2/echo/plugin.complete | 14 ++++ .../helmhome/helm/plugins_2/echo/plugin.yaml | 4 + internal/test/ensure/ensure.go | 2 + pkg/cli/environment.go | 7 ++ pkg/helmpath/home.go | 3 + pkg/helmpath/lazypath.go | 17 +++- pkg/helmpath/lazypath_unix.go | 8 ++ pkg/helmpath/xdg/xdg.go | 5 ++ 22 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 cmd/helm/testdata/helmhome/helm/plugins_1/env/completion.yaml create mode 100644 cmd/helm/testdata/helmhome/helm/plugins_1/env/plugin.yaml create mode 100644 cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/completion.yaml create mode 100755 cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/exitwith.sh create mode 100644 cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/plugin.yaml create mode 100644 cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/completion.yaml create mode 100755 cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/fullenv.sh create mode 100644 cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/plugin.yaml create mode 100755 cmd/helm/testdata/helmhome/helm/plugins_2/args/args.sh create mode 100755 cmd/helm/testdata/helmhome/helm/plugins_2/args/plugin.complete create mode 100644 cmd/helm/testdata/helmhome/helm/plugins_2/args/plugin.yaml create mode 100644 cmd/helm/testdata/helmhome/helm/plugins_2/echo/completion.yaml create mode 100755 cmd/helm/testdata/helmhome/helm/plugins_2/echo/plugin.complete create mode 100644 cmd/helm/testdata/helmhome/helm/plugins_2/echo/plugin.yaml diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index 36de50135..18f79d256 100644 --- a/cmd/helm/load_plugins.go +++ b/cmd/helm/load_plugins.go @@ -58,7 +58,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) { return } - found, err := plugin.FindPlugins(settings.PluginsDirectory) + found, err := plugin.FindPlugins(settings.PluginsDirectories) if err != nil { fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err) return diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go index e43f277a5..7035d4543 100644 --- a/cmd/helm/plugin_test.go +++ b/cmd/helm/plugin_test.go @@ -78,6 +78,7 @@ func TestManuallyProcessArgs(t *testing.T) { func TestLoadPlugins(t *testing.T) { settings.PluginsDirectory = "testdata/helmhome/helm/plugins" + settings.PluginsDirectories = "testdata/helmhome/helm/plugins" settings.RepositoryConfig = "testdata/helmhome/helm/repositories.yaml" settings.RepositoryCache = "testdata/helmhome/helm/repository" @@ -155,6 +156,86 @@ func TestLoadPlugins(t *testing.T) { } } +func TestLoadPluginsFromDifferentLocations(t *testing.T) { + settings.PluginsDirectory = "testdata/helmhome/helm/plugins_1" + settings.PluginsDirectories = "testdata/helmhome/helm/plugins_1:testdata/helmhome/helm/plugins_2" + settings.RepositoryConfig = "testdata/helmhome/helm/repositories.yaml" + settings.RepositoryCache = "testdata/helmhome/helm/repository" + + var ( + out bytes.Buffer + cmd cobra.Command + ) + loadPlugins(&cmd, &out) + + envs := strings.Join([]string{ + "fullenv", + "testdata/helmhome/helm/plugins_1/fullenv", + "testdata/helmhome/helm/plugins_1", + "testdata/helmhome/helm/repositories.yaml", + "testdata/helmhome/helm/repository", + os.Args[0], + }, "\n") + + // Test that the YAML file was correctly converted to a command. + tests := []struct { + use string + short string + long string + expect string + args []string + code int + }{ + {"args", "echo args", "This echos args", "-a -b -c\n", []string{"-a", "-b", "-c"}, 0}, + {"echo", "echo stuff", "This echos stuff", "hello\n", []string{}, 0}, + {"env", "env stuff", "show the env", "env\n", []string{}, 0}, + {"exitwith", "exitwith code", "This exits with the specified exit code", "", []string{"2"}, 2}, + {"fullenv", "show env vars", "show all env vars", envs + "\n", []string{}, 0}, + } + + plugins := cmd.Commands() + + if len(plugins) != len(tests) { + t.Fatalf("Expected %d plugins, got %d", len(tests), len(plugins)) + } + + for i := 0; i < len(plugins); i++ { + out.Reset() + tt := tests[i] + pp := plugins[i] + if pp.Use != tt.use { + t.Errorf("%d: Expected Use=%q, got %q", i, tt.use, pp.Use) + } + if pp.Short != tt.short { + t.Errorf("%d: Expected Use=%q, got %q", i, tt.short, pp.Short) + } + if pp.Long != tt.long { + t.Errorf("%d: Expected Use=%q, got %q", i, tt.long, pp.Long) + } + + // Currently, plugins assume a Linux subsystem. Skip the execution + // tests until this is fixed + if runtime.GOOS != "windows" { + if err := pp.RunE(pp, tt.args); err != nil { + if tt.code > 0 { + perr, ok := err.(pluginError) + if !ok { + t.Errorf("Expected %s to return pluginError: got %v(%T)", tt.use, err, err) + } + if perr.code != tt.code { + t.Errorf("Expected %s to return %d: got %d", tt.use, tt.code, perr.code) + } + } else { + t.Errorf("Error running %s: %+v", tt.use, err) + } + } + if out.String() != tt.expect { + t.Errorf("Expected %s to output:\n%s\ngot\n%s", tt.use, tt.expect, out.String()) + } + } + } +} + type staticCompletionDetails struct { use string validArgs []string @@ -279,6 +360,7 @@ func TestPluginDynamicCompletion(t *testing.T) { }} for _, test := range tests { settings.PluginsDirectory = "testdata/helmhome/helm/plugins" + settings.PluginsDirectories = "testdata/helmhome/helm/plugins" runTestCmd(t, []cmdTestCase{test}) } } diff --git a/cmd/helm/testdata/helmhome/helm/plugins_1/env/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins_1/env/completion.yaml new file mode 100644 index 000000000..e479a0503 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_1/env/completion.yaml @@ -0,0 +1,13 @@ +name: env +commands: + - name: list + flags: + - a + - all + - log + - name: remove + validArgs: + - all + - one +flags: +- global diff --git a/cmd/helm/testdata/helmhome/helm/plugins_1/env/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins_1/env/plugin.yaml new file mode 100644 index 000000000..52cb7a848 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_1/env/plugin.yaml @@ -0,0 +1,4 @@ +name: env +usage: "env stuff" +description: "show the env" +command: "echo $HELM_PLUGIN_NAME" diff --git a/cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/completion.yaml new file mode 100644 index 000000000..e5bf440f6 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/completion.yaml @@ -0,0 +1,5 @@ +commands: + - name: code + flags: + - a + - b diff --git a/cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/exitwith.sh b/cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/exitwith.sh new file mode 100755 index 000000000..ec8469657 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/exitwith.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exit $* diff --git a/cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/plugin.yaml new file mode 100644 index 000000000..5691d1712 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_1/exitwith/plugin.yaml @@ -0,0 +1,4 @@ +name: exitwith +usage: "exitwith code" +description: "This exits with the specified exit code" +command: "$HELM_PLUGIN_DIR/exitwith.sh" diff --git a/cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/completion.yaml new file mode 100644 index 000000000..e0b161c69 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/completion.yaml @@ -0,0 +1,19 @@ +name: wrongname +commands: + - name: empty + - name: full + commands: + - name: more + validArgs: + - one + - two + flags: + - b + - ball + - name: less + flags: + - a + - all +flags: +- z +- q diff --git a/cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/fullenv.sh b/cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/fullenv.sh new file mode 100755 index 000000000..2efad9b3c --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/fullenv.sh @@ -0,0 +1,7 @@ +#!/bin/sh +echo $HELM_PLUGIN_NAME +echo $HELM_PLUGIN_DIR +echo $HELM_PLUGINS +echo $HELM_REPOSITORY_CONFIG +echo $HELM_REPOSITORY_CACHE +echo $HELM_BIN diff --git a/cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/plugin.yaml new file mode 100644 index 000000000..63f2f12db --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_1/fullenv/plugin.yaml @@ -0,0 +1,4 @@ +name: fullenv +usage: "show env vars" +description: "show all env vars" +command: "$HELM_PLUGIN_DIR/fullenv.sh" diff --git a/cmd/helm/testdata/helmhome/helm/plugins_2/args/args.sh b/cmd/helm/testdata/helmhome/helm/plugins_2/args/args.sh new file mode 100755 index 000000000..678b4eff5 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_2/args/args.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo $* diff --git a/cmd/helm/testdata/helmhome/helm/plugins_2/args/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins_2/args/plugin.complete new file mode 100755 index 000000000..2b00c2281 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_2/args/plugin.complete @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +echo "plugin.complete was called" +echo "Namespace: ${HELM_NAMESPACE:-NO_NS}" +echo "Num args received: ${#}" +echo "Args received: ${@}" + +# Final printout is the optional completion directive of the form : +if [ "$HELM_NAMESPACE" = "default" ]; then + echo ":4" +else + echo ":2" +fi diff --git a/cmd/helm/testdata/helmhome/helm/plugins_2/args/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins_2/args/plugin.yaml new file mode 100644 index 000000000..21e28a7c2 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_2/args/plugin.yaml @@ -0,0 +1,4 @@ +name: args +usage: "echo args" +description: "This echos args" +command: "$HELM_PLUGIN_DIR/args.sh" diff --git a/cmd/helm/testdata/helmhome/helm/plugins_2/echo/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins_2/echo/completion.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/helm/testdata/helmhome/helm/plugins_2/echo/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins_2/echo/plugin.complete new file mode 100755 index 000000000..6bc73d130 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_2/echo/plugin.complete @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +echo "echo plugin.complete was called" +echo "Namespace: ${HELM_NAMESPACE:-NO_NS}" +echo "Num args received: ${#}" +echo "Args received: ${@}" + +# Final printout is the optional completion directive of the form : +if [ "$HELM_NAMESPACE" = "default" ]; then + # Output an invalid directive, which should be ignored + echo ":2222" +# else + # Don't include the directive, to test it is really optional +fi diff --git a/cmd/helm/testdata/helmhome/helm/plugins_2/echo/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins_2/echo/plugin.yaml new file mode 100644 index 000000000..7b9362a08 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins_2/echo/plugin.yaml @@ -0,0 +1,4 @@ +name: echo +usage: "echo stuff" +description: "This echos stuff" +command: "echo hello" diff --git a/internal/test/ensure/ensure.go b/internal/test/ensure/ensure.go index 3c0e4575c..b168de914 100644 --- a/internal/test/ensure/ensure.go +++ b/internal/test/ensure/ensure.go @@ -20,6 +20,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" "helm.sh/helm/v3/pkg/helmpath" @@ -33,6 +34,7 @@ func HelmHome(t *testing.T) func() { os.Setenv(xdg.CacheHomeEnvVar, base) os.Setenv(xdg.ConfigHomeEnvVar, base) os.Setenv(xdg.DataHomeEnvVar, base) + os.Setenv(xdg.DataDirsEnvVar, strings.Join([]string{base, os.Getenv(xdg.DataDirsEnvVar)}, ":")) os.Setenv(helmpath.CacheHomeEnvVar, "") os.Setenv(helmpath.ConfigHomeEnvVar, "") os.Setenv(helmpath.DataHomeEnvVar, "") diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index d62f57a55..342635da0 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -26,6 +26,9 @@ import ( "fmt" "os" "strconv" + "strings" + + "helm.sh/helm/v3/pkg/helmpath/xdg" "github.com/spf13/pflag" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -56,6 +59,8 @@ type EnvSettings struct { RepositoryCache string // PluginsDirectory is the path to the plugins directory. PluginsDirectory string + // PluginsDirectories contains colon separated locations to different possible plugin directories + PluginsDirectories string } func New() *EnvSettings { @@ -65,6 +70,8 @@ func New() *EnvSettings { KubeToken: os.Getenv("HELM_KUBETOKEN"), KubeAPIServer: os.Getenv("HELM_KUBEAPISERVER"), PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")), + PluginsDirectories: strings.Join([]string{envOr("HELM_PLUGINS", helmpath.DataPath("plugins")), + os.Getenv(xdg.DataDirsEnvVar)}, ":"), RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry.json")), RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), diff --git a/pkg/helmpath/home.go b/pkg/helmpath/home.go index bd43e8890..70b41c150 100644 --- a/pkg/helmpath/home.go +++ b/pkg/helmpath/home.go @@ -26,6 +26,9 @@ func CachePath(elem ...string) string { return lp.cachePath(elem...) } // DataPath returns the path where Helm stores data. func DataPath(elem ...string) string { return lp.dataPath(elem...) } +// DataDirs returns the paths where Helm can store data. +func DataDirs(elem string) string { return lp.dataDirs(elem) } + // CacheIndexFile returns the path to an index for the given named repository. func CacheIndexFile(name string) string { if name != "" { diff --git a/pkg/helmpath/lazypath.go b/pkg/helmpath/lazypath.go index 22d7bf0a1..304117609 100644 --- a/pkg/helmpath/lazypath.go +++ b/pkg/helmpath/lazypath.go @@ -16,6 +16,7 @@ package helmpath import ( "os" "path/filepath" + "strings" "helm.sh/helm/v3/pkg/helmpath/xdg" ) @@ -43,9 +44,12 @@ func (l lazypath) path(helmEnvVar, xdgEnvVar string, defaultFn func() string, el // 1. See if a Helm specific environment variable has been set. // 2. Check if an XDG environment variable is set // 3. Fall back to a default - base := os.Getenv(helmEnvVar) - if base != "" { - return filepath.Join(base, filepath.Join(elem...)) + base := "" + if helmEnvVar != "" { + base = os.Getenv(helmEnvVar) + if base != "" { + return filepath.Join(base, filepath.Join(elem...)) + } } base = os.Getenv(xdgEnvVar) if base == "" { @@ -70,3 +74,10 @@ func (l lazypath) configPath(elem ...string) string { func (l lazypath) dataPath(elem ...string) string { return l.path(DataHomeEnvVar, xdg.DataHomeEnvVar, dataHome, filepath.Join(elem...)) } + +// dataDirs defines all the base directories in order of precedence separated by colon +// relative to which user specific data files can be stored. +// Not exposed on Helm side but used internally +func (l lazypath) dataDirs(elem ...string) string { + return l.path("", xdg.DataDirsEnvVar, dataDirs, strings.Join(elem, ":")) +} diff --git a/pkg/helmpath/lazypath_unix.go b/pkg/helmpath/lazypath_unix.go index b4eae9f66..7b9524022 100644 --- a/pkg/helmpath/lazypath_unix.go +++ b/pkg/helmpath/lazypath_unix.go @@ -16,7 +16,11 @@ package helmpath import ( + "os" "path/filepath" + "strings" + + "helm.sh/helm/v3/pkg/helmpath/xdg" "k8s.io/client-go/util/homedir" ) @@ -43,3 +47,7 @@ func configHome() string { func cacheHome() string { return filepath.Join(homedir.HomeDir(), ".cache") } + +func dataDirs() string { + return strings.Join([]string{dataHome(), os.Getenv(xdg.DataDirsEnvVar)}, ":") +} diff --git a/pkg/helmpath/xdg/xdg.go b/pkg/helmpath/xdg/xdg.go index eaa3e6864..4951595ba 100644 --- a/pkg/helmpath/xdg/xdg.go +++ b/pkg/helmpath/xdg/xdg.go @@ -31,4 +31,9 @@ const ( // DataHomeEnvVar is the environment variable used by the // XDG base directory specification for the data directory. DataHomeEnvVar = "XDG_DATA_HOME" + + // DataDirsEnvVar is the environment variable contains + // preference order set of base directories separated by a + // colon ":". + DataDirsEnvVar = "XDG_DATA_DIRS" )