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 <vibhav.bobde@gmail.com>
pull/7755/head
Vibhav Bobade 5 years ago
parent 3779d95966
commit 132531812a

@ -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

@ -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})
}
}

@ -0,0 +1,13 @@
name: env
commands:
- name: list
flags:
- a
- all
- log
- name: remove
validArgs:
- all
- one
flags:
- global

@ -0,0 +1,4 @@
name: env
usage: "env stuff"
description: "show the env"
command: "echo $HELM_PLUGIN_NAME"

@ -0,0 +1,5 @@
commands:
- name: code
flags:
- a
- b

@ -0,0 +1,4 @@
name: exitwith
usage: "exitwith code"
description: "This exits with the specified exit code"
command: "$HELM_PLUGIN_DIR/exitwith.sh"

@ -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

@ -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

@ -0,0 +1,4 @@
name: fullenv
usage: "show env vars"
description: "show all env vars"
command: "$HELM_PLUGIN_DIR/fullenv.sh"

@ -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 :<directive>
if [ "$HELM_NAMESPACE" = "default" ]; then
echo ":4"
else
echo ":2"
fi

@ -0,0 +1,4 @@
name: args
usage: "echo args"
description: "This echos args"
command: "$HELM_PLUGIN_DIR/args.sh"

@ -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 :<directive>
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

@ -0,0 +1,4 @@
name: echo
usage: "echo stuff"
description: "This echos stuff"
command: "echo hello"

@ -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, "")

@ -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")),

@ -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 != "" {

@ -16,6 +16,7 @@ package helmpath
import (
"os"
"path/filepath"
"strings"
"helm.sh/helm/v3/pkg/helmpath/xdg"
)
@ -43,10 +44,13 @@ 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)
base := ""
if helmEnvVar != "" {
base = os.Getenv(helmEnvVar)
if base != "" {
return filepath.Join(base, filepath.Join(elem...))
}
}
base = os.Getenv(xdgEnvVar)
if base == "" {
base = defaultFn()
@ -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, ":"))
}

@ -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)}, ":")
}

@ -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"
)

Loading…
Cancel
Save