From 72139043730bc30c3537ef9140440f2f4f0c806a Mon Sep 17 00:00:00 2001 From: Vicente Zepeda Mas Date: Wed, 17 Oct 2018 14:40:11 +0200 Subject: [PATCH] loading external plugins Signed-off-by: Vicente Zepeda Mas (cherry picked from commit 3f7304e52ec57a0701f307c4d048c4269455d04a) --- cmd/helm/helm_test.go | 1 - cmd/helm/load_plugins.go | 18 +++-- cmd/helm/plugin_list.go | 6 +- cmd/helm/plugin_remove.go | 3 +- cmd/helm/plugin_test.go | 4 +- cmd/helm/plugin_update.go | 3 +- docs/plugins.md | 99 ++++++++++++++++++++++-- pkg/getter/plugingetter.go | 3 +- pkg/helm/environment/environment.go | 4 +- pkg/helm/environment/environment_test.go | 6 +- pkg/plugin/plugin.go | 17 ++-- 11 files changed, 127 insertions(+), 37 deletions(-) diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 9bc68a8be..a4a86e6fd 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -25,7 +25,6 @@ import ( shellwords "github.com/mattn/go-shellwords" "github.com/spf13/cobra" - "k8s.io/helm/internal/test" "k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/helm" diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index 124ebb5c1..fd575e63a 100644 --- a/cmd/helm/load_plugins.go +++ b/cmd/helm/load_plugins.go @@ -25,7 +25,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/plugin" ) @@ -41,8 +40,9 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) { return } + pluginsDirs := []string{settings.PluginDirs(), settings.SystemPluginsDir} // debug("HELM_PLUGIN_DIRS=%s", settings.PluginDirs()) - found, err := findPlugins(settings.PluginDirs()) + found, err := findPlugins(pluginsDirs) if err != nil { fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err) return @@ -137,15 +137,17 @@ func manuallyProcessArgs(args []string) ([]string, []string) { } // findPlugins returns a list of YAML files that describe plugins. -func findPlugins(plugdirs string) ([]*plugin.Plugin, error) { +func findPlugins(plugdirs []string) ([]*plugin.Plugin, error) { found := []*plugin.Plugin{} // Let's get all UNIXy and allow path separators - for _, p := range filepath.SplitList(plugdirs) { - matches, err := plugin.LoadAll(p) - if err != nil { - return matches, err + for _, pd := range plugdirs { + for _, p := range filepath.SplitList(pd) { + matches, err := plugin.LoadAll(p) + if err != nil { + return matches, err + } + found = append(found, matches...) } - found = append(found, matches...) } return found, nil } diff --git a/cmd/helm/plugin_list.go b/cmd/helm/plugin_list.go index 31a8b57b0..e09e23232 100644 --- a/cmd/helm/plugin_list.go +++ b/cmd/helm/plugin_list.go @@ -19,10 +19,9 @@ import ( "fmt" "io" - "k8s.io/helm/pkg/helm/helmpath" - "github.com/gosuri/uitable" "github.com/spf13/cobra" + "k8s.io/helm/pkg/helm/helmpath" ) type pluginListOptions struct { @@ -44,7 +43,8 @@ func newPluginListCmd(out io.Writer) *cobra.Command { func (o *pluginListOptions) run(out io.Writer) error { debug("pluginDirs: %s", settings.PluginDirs()) - plugins, err := findPlugins(settings.PluginDirs()) + directories := []string{settings.PluginDirs(), settings.SystemPluginsDir} + plugins, err := findPlugins(directories) if err != nil { return err } diff --git a/cmd/helm/plugin_remove.go b/cmd/helm/plugin_remove.go index a0ff78ceb..d8a7d5a2a 100644 --- a/cmd/helm/plugin_remove.go +++ b/cmd/helm/plugin_remove.go @@ -23,7 +23,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/plugin" ) @@ -59,7 +58,7 @@ func (o *pluginRemoveOptions) complete(args []string) error { func (o *pluginRemoveOptions) run(out io.Writer) error { debug("loading installed plugins from %s", settings.PluginDirs()) - plugins, err := findPlugins(settings.PluginDirs()) + plugins, err := findPlugins([]string{settings.PluginDirs()}) if err != nil { return err } diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go index 537ca1ce1..9575426c3 100644 --- a/cmd/helm/plugin_test.go +++ b/cmd/helm/plugin_test.go @@ -23,10 +23,9 @@ import ( "strings" "testing" + "github.com/spf13/cobra" "k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/plugin" - - "github.com/spf13/cobra" ) func TestManuallyProcessArgs(t *testing.T) { @@ -65,6 +64,7 @@ func TestLoadPlugins(t *testing.T) { defer resetEnv()() settings.Home = "testdata/helmhome" + settings.SystemPluginsDir = "test" os.Setenv("HELM_HOME", settings.Home.String()) hh := settings.Home diff --git a/cmd/helm/plugin_update.go b/cmd/helm/plugin_update.go index a84312eb0..eab55ecc2 100644 --- a/cmd/helm/plugin_update.go +++ b/cmd/helm/plugin_update.go @@ -23,7 +23,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/plugin" "k8s.io/helm/pkg/plugin/installer" @@ -61,7 +60,7 @@ func (o *pluginUpdateOptions) complete(args []string) error { func (o *pluginUpdateOptions) run(out io.Writer) error { installer.Debug = settings.Debug debug("loading installed plugins from %s", settings.PluginDirs()) - plugins, err := findPlugins(settings.PluginDirs()) + plugins, err := findPlugins([]string{settings.PluginDirs()}) if err != nil { return err } diff --git a/docs/plugins.md b/docs/plugins.md index 185257bb8..b618e6a24 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -21,6 +21,10 @@ Helm plugins have the following features: - They can be written in any programming language. - They integrate with Helm, and will show up in `helm help` and other places. +## Plugins Backends + +### Native + Helm plugins live in `$(helm home)/plugins`. The Helm plugin model is partially modeled on Git's plugin model. To that end, @@ -29,9 +33,27 @@ plugins being the _plumbing_. This is a shorthand way of suggesting that Helm provides the user experience and top level processing logic, while the plugins do the "detail work" of performing a desired action. -## Installing a Plugin +### External + +Helm external plugins can live anywhere in your `PATH`. + +These plugins become necessary because of systems that are closed to manual +configuration and the only way to get software installed is by an administrator +managing the plugins at system level via a linux distribution package manager. + +These plugins are going to be available globally. All users may use the plugin, +not limited to a user like Native Plugins. + +The only condition for these plugins is that they provide the yaml description +in the folder `/usr/share`; and the full path should look like this +`/usr/share//plugin.yaml`. -Plugins are installed using the `$ helm plugin install ` command. You can pass in a path to a plugin on your local file system or a url of a remote VCS repo. The `helm plugin install` command clones or copies the plugin at the path/url given into `$ (helm home)/plugins` +## Installing a Native Plugin + +Native Plugins are installed using the `$ helm plugin install ` command. +You can pass in a path to a plugin on your local file system or a url of a remote +VCS repo. The `helm plugin install` command clones or copies the plugin at the +path/url given into `$ (helm home)/plugins` ```console $ helm plugin install https://github.com/technosophos/helm-template @@ -42,7 +64,12 @@ If you have a plugin tar distribution, simply untar the plugin into the You can also install tarball plugins directly from url by issuing `helm plugin install http://domain/path/to/plugin.tar.gz` -## Building Plugins +## Installing an External Plugin + +Each external plugin has its own installation process, you should check with the plugin's +site/help to get the correct information. + +## Building Native Plugins In many ways, a plugin is similar to a chart. Each plugin has a top-level directory, and then a `plugin.yaml` file. @@ -73,7 +100,7 @@ ignoreFlags: false command: "$HELM_PLUGIN_DIR/keybase.sh" ``` -The `name` is the name of the plugin. When Helm executes it plugin, this is the +The `name` is the name of the plugin. When Helm executes a plugin, this is the name it will use (e.g. `helm NAME` will invoke this plugin). _`name` should match the directory name._ In our example above, that means the @@ -111,8 +138,8 @@ There are some strategies for working with plugin commands: Helm will use `usage` and `description` for `helm help` and `helm help myplugin`, but will not handle `helm myplugin --help`. -## Downloader Plugins -By default, Helm is able to pull Charts using HTTP/S. As of Helm 2.4.0, plugins +## Downloader Native Plugins +By default, Helm is able to fetch Charts using HTTP/S. As of Helm 2.4.0, plugins can have a special capability to download Charts from arbitrary sources. Plugins shall declare this special capability in the `plugin.yaml` file (top level): @@ -137,6 +164,66 @@ The defined command will be invoked with the following scheme: repo definition, stored in `$HELM_HOME/repository/repositories.yaml`. Downloader plugin is expected to dump the raw content to stdout and report errors on stderr. +## External Plugins Breakout + +External plugins have to comply with two conditions: + +- Be sure it is accessible by being in the `PATH`. + +- Have the `plugin.yaml` installed in the `/usr/share/` folder. + +### Example + +```console +/usr/ + |- bin/ + |- helm-mirror + |- share/ + |- helm-mirror + |- plugin.yaml +``` + +### plugin.yaml for External Plugins + +``` +name: "mirror" +version: "0.1.0" +usage: "Mirrors Helm charts to a local folder" +description: "Mirrors Helm charts to a local folder" +ignoreFlags: false +useTunnel: false +command: "/usr/bin/helm-mirror" +``` + +The `name` is the name of the plugin. When Helm executes a plugin, this is the +name it will use (e.g. `helm NAME` will invoke this plugin). + +Restrictions on `name`: + +- `name` cannot duplicate one of the existing `helm` top-level commands. +- `name` must be restricted to the characters ASCII a-z, A-Z, 0-9, `_` and `-`. + +`version` is the SemVer 2 version of the plugin. + +`usage` and `description` are both used to generate the help text of a command. + +The `ignoreFlags` switch tells Helm to _not_ pass flags to the plugin. So if a +plugin is called with `helm myplugin --foo` and `ignoreFlags: true`, then `--foo` +is silently discarded. + +The `useTunnel` switch indicates that the plugin needs a tunnel to Tiller. This +should be set to `true` _anytime a plugin talks to Tiller_. It will cause Helm +to open a tunnel, and then set `$TILLER_HOST` to the right local address for that +tunnel. But don't worry: if Helm detects that a tunnel is not necessary because +Tiller is running locally, it will not create the tunnel. + +Finally, and most importantly, `command` is the command that this plugin will +execute when it is called. Environment variables are interpolated before the plugin +is executed. The pattern above illustrates the preferred way to indicate where +the plugin program lives. + +> NOTE: For External Plugins the `plugin.yaml` definition **MUST NOT** contain any `Hooks`. + ## Environment Variables When Helm executes a plugin, it passes the outer environment to the plugin, and diff --git a/pkg/getter/plugingetter.go b/pkg/getter/plugingetter.go index 6f5b6969e..5b36eaa5a 100644 --- a/pkg/getter/plugingetter.go +++ b/pkg/getter/plugingetter.go @@ -30,7 +30,8 @@ import ( // collectPlugins scans for getter plugins. // This will load plugins according to the environment. func collectPlugins(settings environment.EnvSettings) (Providers, error) { - plugins, err := plugin.FindPlugins(settings.PluginDirs()) + pluginsDirs := []string{settings.PluginDirs(), settings.SystemPluginsDir} + plugins, err := plugin.FindPlugins(pluginsDirs) if err != nil { return nil, err } diff --git a/pkg/helm/environment/environment.go b/pkg/helm/environment/environment.go index 5bc6f70ca..382e39614 100644 --- a/pkg/helm/environment/environment.go +++ b/pkg/helm/environment/environment.go @@ -28,7 +28,6 @@ import ( "github.com/spf13/pflag" "k8s.io/client-go/util/homedir" - "k8s.io/helm/pkg/helm/helmpath" ) @@ -47,6 +46,8 @@ type EnvSettings struct { KubeContext string // Debug indicates whether or not Helm is running in Debug mode. Debug bool + // SystemPluginsDir system dir plugin + SystemPluginsDir string } // AddFlags binds flags to the given flagset. @@ -55,6 +56,7 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { fs.StringVarP(&s.Namespace, "namespace", "n", "", "namespace scope for this request") fs.StringVar(&s.KubeConfig, "kubeconfig", "", "path to the kubeconfig file") fs.StringVar(&s.KubeContext, "kube-context", "", "name of the kubeconfig context to use") + fs.StringVar(&s.SystemPluginsDir, "system-plugins-dir", "/usr/share", "path system plugins configfiles") fs.BoolVar(&s.Debug, "debug", false, "enable verbose output") } diff --git a/pkg/helm/environment/environment_test.go b/pkg/helm/environment/environment_test.go index 7c8b57829..2a47af625 100644 --- a/pkg/helm/environment/environment_test.go +++ b/pkg/helm/environment/environment_test.go @@ -18,12 +18,12 @@ package environment import ( "os" + "reflect" "strings" "testing" - "k8s.io/helm/pkg/helm/helmpath" - "github.com/spf13/pflag" + "k8s.io/helm/pkg/helm/helmpath" ) func TestEnvSettings(t *testing.T) { @@ -90,7 +90,7 @@ func TestEnvSettings(t *testing.T) { if settings.Home != helmpath.Home(tt.home) { t.Errorf("expected home %q, got %q", tt.home, settings.Home) } - if settings.PluginDirs() != tt.plugins { + if !reflect.DeepEqual(settings.PluginDirs(), tt.plugins) { t.Errorf("expected plugins %q, got %q", tt.plugins, settings.PluginDirs()) } if settings.Debug != tt.debug { diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index f5cd3efb7..f035a1c2a 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -21,9 +21,8 @@ import ( "path/filepath" "strings" - helm_env "k8s.io/helm/pkg/helm/environment" - "github.com/ghodss/yaml" + helm_env "k8s.io/helm/pkg/helm/environment" ) const pluginFileName = "plugin.yaml" @@ -148,15 +147,17 @@ func LoadAll(basedir string) ([]*Plugin, error) { } // FindPlugins returns a list of YAML files that describe plugins. -func FindPlugins(plugdirs string) ([]*Plugin, error) { +func FindPlugins(plugdirs []string) ([]*Plugin, error) { found := []*Plugin{} // Let's get all UNIXy and allow path separators - for _, p := range filepath.SplitList(plugdirs) { - matches, err := LoadAll(p) - if err != nil { - return matches, err + for _, pd := range plugdirs { + for _, p := range filepath.SplitList(pd) { + matches, err := LoadAll(p) + if err != nil { + return matches, err + } + found = append(found, matches...) } - found = append(found, matches...) } return found, nil }