feat(plugin): refactor `helm plugin`

Signed-off-by: Matthew Fisher <matt.fisher@microsoft.com>
pull/5514/head
Matthew Fisher 7 years ago
parent 0b1caa14a7
commit bdfe016b09
No known key found for this signature in database
GPG Key ID: 92AA783CBAAE8E3B

@ -19,19 +19,14 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"github.com/Masterminds/semver"
"github.com/ghodss/yaml"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/require" "helm.sh/helm/cmd/helm/require"
"helm.sh/helm/pkg/getter" "helm.sh/helm/pkg/getter"
"helm.sh/helm/pkg/helmpath" "helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/plugin"
"helm.sh/helm/pkg/plugin/installer"
"helm.sh/helm/pkg/repo" "helm.sh/helm/pkg/repo"
) )
@ -47,20 +42,10 @@ const (
type initOptions struct { type initOptions struct {
skipRefresh bool // --skip-refresh skipRefresh bool // --skip-refresh
stableRepositoryURL string // --stable-repo-url stableRepositoryURL string // --stable-repo-url
pluginsFilename string // --plugins
home helmpath.Home home helmpath.Home
} }
type pluginsFileEntry struct {
URL string `json:"url"`
Version string `json:"version,omitempty"`
}
type pluginsFile struct {
Plugins []*pluginsFileEntry `json:"plugins"`
}
func newInitCmd(out io.Writer) *cobra.Command { func newInitCmd(out io.Writer) *cobra.Command {
o := &initOptions{} o := &initOptions{}
@ -78,7 +63,6 @@ func newInitCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.BoolVar(&o.skipRefresh, "skip-refresh", false, "do not refresh (download) the local repository cache") f.BoolVar(&o.skipRefresh, "skip-refresh", false, "do not refresh (download) the local repository cache")
f.StringVar(&o.stableRepositoryURL, "stable-repo-url", defaultStableRepositoryURL, "URL for stable repository") f.StringVar(&o.stableRepositoryURL, "stable-repo-url", defaultStableRepositoryURL, "URL for stable repository")
f.StringVar(&o.pluginsFilename, "plugins", "", "a YAML file specifying plugins to install")
return cmd return cmd
} }
@ -94,11 +78,6 @@ func (o *initOptions) run(out io.Writer) error {
if err := ensureRepoFileFormat(o.home.RepositoryFile(), out); err != nil { if err := ensureRepoFileFormat(o.home.RepositoryFile(), out); err != nil {
return err return err
} }
if o.pluginsFilename != "" {
if err := ensurePluginsInstalled(o.pluginsFilename, out); err != nil {
return err
}
}
fmt.Fprintf(out, "$HELM_HOME has been configured at %s.\n", settings.Home) fmt.Fprintf(out, "$HELM_HOME has been configured at %s.\n", settings.Home)
fmt.Fprintln(out, "Happy Helming!") fmt.Fprintln(out, "Happy Helming!")
return nil return nil
@ -184,71 +163,3 @@ func ensureRepoFileFormat(file string, out io.Writer) error {
} }
return nil return nil
} }
func ensurePluginsInstalled(pluginsFilename string, out io.Writer) error {
bytes, err := ioutil.ReadFile(pluginsFilename)
if err != nil {
return err
}
pf := new(pluginsFile)
if err := yaml.Unmarshal(bytes, &pf); err != nil {
return errors.Wrapf(err, "failed to parse %s", pluginsFilename)
}
for _, requiredPlugin := range pf.Plugins {
if err := ensurePluginInstalled(requiredPlugin, pluginsFilename, out); err != nil {
return errors.Wrapf(err, "failed to install plugin from %s", requiredPlugin.URL)
}
}
return nil
}
func ensurePluginInstalled(requiredPlugin *pluginsFileEntry, pluginsFilename string, out io.Writer) error {
i, err := installer.NewForSource(requiredPlugin.URL, requiredPlugin.Version, settings.Home)
if err != nil {
return err
}
if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) {
if err := installer.Install(i); err != nil {
return err
}
p, err := plugin.LoadDir(i.Path())
if err != nil {
return err
}
if err := runHook(p, plugin.Install); err != nil {
return err
}
fmt.Fprintf(out, "Installed plugin: %s\n", p.Metadata.Name)
} else if requiredPlugin.Version != "" {
p, err := plugin.LoadDir(i.Path())
if err != nil {
return err
}
if p.Metadata.Version != "" {
pluginVersion, err := semver.NewVersion(p.Metadata.Version)
if err != nil {
return err
}
constraint, err := semver.NewConstraint(requiredPlugin.Version)
if err != nil {
return err
}
if !constraint.Check(pluginVersion) {
fmt.Fprintf(out, "WARNING: Installed plugin '%s' is at version %s, while %s specifies %s\n",
p.Metadata.Name, p.Metadata.Version, pluginsFilename, requiredPlugin.Version)
}
}
}
return nil
}

@ -20,12 +20,12 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/pkg/getter"
"helm.sh/helm/pkg/plugin" "helm.sh/helm/pkg/plugin"
) )
@ -41,8 +41,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
return return
} }
// debug("HELM_PLUGIN_DIRS=%s", settings.PluginDirs()) found, err := plugin.FindAll(os.Getenv("PATH"))
found, err := findPlugins(settings.PluginDirs())
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err) fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err)
return return
@ -59,15 +58,19 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
// Now we create commands for all of these. // Now we create commands for all of these.
for _, plug := range found { for _, plug := range found {
plug := plug plug := plug
md := plug.Metadata // skip downloader plugins
if md.Usage == "" { if strings.HasPrefix(plug.Name, getter.PluginDownloaderPrefix) {
md.Usage = fmt.Sprintf("the %q plugin", md.Name) continue
} }
// pull out the name using the last hyphen. We want the cobra command to be the
// last keyword of the plugin. For example, `helm-plugin-install` should show up
// under `helm plugin` as `install`.
plugName := plug.Name[strings.LastIndex(plug.Name, "-")+1:]
c := &cobra.Command{ c := &cobra.Command{
Use: md.Name, Use: plugName,
Short: md.Usage, Short: fmt.Sprintf("the %q plugin", plugName),
Long: md.Description,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
u, err := processParent(cmd, args) u, err := processParent(cmd, args)
if err != nil { if err != nil {
@ -77,14 +80,9 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
// Call setupEnv before PrepareCommand because // Call setupEnv before PrepareCommand because
// PrepareCommand uses os.ExpandEnv and expects the // PrepareCommand uses os.ExpandEnv and expects the
// setupEnv vars. // setupEnv vars.
plugin.SetupPluginEnv(settings, md.Name, plug.Dir) plugin.SetupPluginEnv(settings, plug.Name, plug.Dir)
main, argv, prepCmdErr := plug.PrepareCommand(u)
if prepCmdErr != nil {
os.Stderr.WriteString(prepCmdErr.Error())
return errors.Errorf("plugin %q exited with error", md.Name)
}
prog := exec.Command(main, argv...) prog := exec.Command(plugin.PluginNamePrefix+plug.Name, u...)
prog.Env = os.Environ() prog.Env = os.Environ()
prog.Stdin = os.Stdin prog.Stdin = os.Stdin
prog.Stdout = out prog.Stdout = out
@ -92,7 +90,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
if err := prog.Run(); err != nil { if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok { if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr) os.Stderr.Write(eerr.Stderr)
return errors.Errorf("plugin %q exited with error", md.Name) return errors.Errorf("plugin %q exited with error", plug.Name)
} }
return err return err
} }
@ -102,8 +100,22 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
DisableFlagParsing: true, DisableFlagParsing: true,
} }
// TODO: Make sure a command with this name does not already exist. // Check if a command with this name does not already exist. If it does, replace it with the plugin
baseCmd.AddCommand(c) cmd, _, err := baseCmd.Find(strings.Split(plug.Name, "-"))
if err != nil {
panic(err)
}
if cmd == baseCmd {
// if we're back at the root, then we never found an existing command. In that case, add
// it to the command tree.
baseCmd.AddCommand(c)
} else if cmd.Name() == c.Name() {
baseCmd.RemoveCommand(cmd)
baseCmd.AddCommand(c)
} else {
cmd.AddCommand(c)
}
} }
} }
@ -139,17 +151,3 @@ func manuallyProcessArgs(args []string) ([]string, []string) {
} }
return known, unknown return known, unknown
} }
// findPlugins returns a list of YAML files that describe plugins.
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
}
found = append(found, matches...)
}
return found, nil
}

@ -17,56 +17,25 @@ package main
import ( import (
"io" "io"
"os"
"os/exec"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/pkg/plugin"
) )
const pluginHelp = ` const pluginHelp = `
Manage client-side Helm plugins. Provides utilities for interacting with plugins.
Plugins provide extended functionality that is not part of Helm. Please refer to the documentation
and examples for more information about how write your own plugins.
` `
func newPluginCmd(out io.Writer) *cobra.Command { func newPluginCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "plugin", Use: "plugin",
Short: "add, list, or remove Helm plugins", Short: "utilities for interacting with Helm plugins",
Long: pluginHelp, Long: pluginHelp,
} }
cmd.AddCommand( cmd.AddCommand(
newPluginInstallCmd(out),
newPluginListCmd(out), newPluginListCmd(out),
newPluginRemoveCmd(out),
newPluginUpdateCmd(out),
) )
return cmd return cmd
} }
// runHook will execute a plugin hook.
func runHook(p *plugin.Plugin, event string) error {
hook := p.Metadata.Hooks[event]
if hook == "" {
return nil
}
prog := exec.Command("sh", "-c", hook)
// TODO make this work on windows
// I think its ... ¯\_(ツ)_/¯
// prog := exec.Command("cmd", "/C", p.Metadata.Hooks.Install())
debug("running %s hook: %s", event, prog)
plugin.SetupPluginEnv(settings, p.Metadata.Name, p.Dir)
prog.Stdout, prog.Stderr = os.Stdout, os.Stderr
if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr)
return errors.Errorf("plugin %s hook for %q exited with error", event, p.Metadata.Name)
}
return err
}
return nil
}

@ -1,90 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"io"
"github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/require"
"helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/plugin"
"helm.sh/helm/pkg/plugin/installer"
)
type pluginInstallOptions struct {
source string
version string
home helmpath.Home
}
const pluginInstallDesc = `
This command allows you to install a plugin from a url to a VCS repo or a local path.
Example usage:
$ helm plugin install https://github.com/technosophos/helm-template
`
func newPluginInstallCmd(out io.Writer) *cobra.Command {
o := &pluginInstallOptions{}
cmd := &cobra.Command{
Use: "install [options] <path|url>...",
Short: "install one or more Helm plugins",
Long: pluginInstallDesc,
Args: require.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
return o.complete(args)
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out)
},
}
cmd.Flags().StringVar(&o.version, "version", "", "specify a version constraint. If this is not specified, the latest version is installed")
return cmd
}
func (o *pluginInstallOptions) complete(args []string) error {
o.source = args[0]
o.home = settings.Home
return nil
}
func (o *pluginInstallOptions) run(out io.Writer) error {
installer.Debug = settings.Debug
i, err := installer.NewForSource(o.source, o.version, o.home)
if err != nil {
return err
}
if err := installer.Install(i); err != nil {
return err
}
debug("loading plugin from %s", i.Path())
p, err := plugin.LoadDir(i.Path())
if err != nil {
return err
}
if err := runHook(p, plugin.Install); err != nil {
return err
}
fmt.Fprintf(out, "Installed plugin: %s\n", p.Metadata.Name)
return nil
}

@ -18,24 +18,31 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"os"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/pkg/plugin"
"helm.sh/helm/pkg/helmpath"
) )
const pluginListHelp = `
List all available plugin files on a user's PATH.
Available plugin files are those that are:
- executable
- anywhere on the user's PATH
- begin with "helm-"
`
type pluginListOptions struct { type pluginListOptions struct {
home helmpath.Home
} }
func newPluginListCmd(out io.Writer) *cobra.Command { func newPluginListCmd(out io.Writer) *cobra.Command {
o := &pluginListOptions{} o := &pluginListOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "list installed Helm plugins", Short: "list all installed Helm plugins",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.home = settings.Home
return o.run(out) return o.run(out)
}, },
} }
@ -43,16 +50,15 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
} }
func (o *pluginListOptions) run(out io.Writer) error { func (o *pluginListOptions) run(out io.Writer) error {
debug("pluginDirs: %s", settings.PluginDirs()) plugins, err := plugin.FindAll(os.Getenv("PATH"))
plugins, err := findPlugins(settings.PluginDirs())
if err != nil { if err != nil {
return err return err
} }
table := uitable.New() table := uitable.New()
table.AddRow("NAME", "VERSION", "DESCRIPTION") table.AddRow("NAME", "DIRECTORY")
for _, p := range plugins { for _, p := range plugins {
table.AddRow(p.Metadata.Name, p.Metadata.Version, p.Metadata.Description) table.AddRow(p.Name, p.Dir)
} }
fmt.Fprintln(out, table) fmt.Fprintln(out, table)
return nil return nil

@ -1,98 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"io"
"os"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/plugin"
)
type pluginRemoveOptions struct {
names []string
home helmpath.Home
}
func newPluginRemoveCmd(out io.Writer) *cobra.Command {
o := &pluginRemoveOptions{}
cmd := &cobra.Command{
Use: "remove <plugin>...",
Short: "remove one or more Helm plugins",
PreRunE: func(cmd *cobra.Command, args []string) error {
return o.complete(args)
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out)
},
}
return cmd
}
func (o *pluginRemoveOptions) complete(args []string) error {
if len(args) == 0 {
return errors.New("please provide plugin name to remove")
}
o.names = args
o.home = settings.Home
return nil
}
func (o *pluginRemoveOptions) run(out io.Writer) error {
debug("loading installed plugins from %s", settings.PluginDirs())
plugins, err := findPlugins(settings.PluginDirs())
if err != nil {
return err
}
var errorPlugins []string
for _, name := range o.names {
if found := findPlugin(plugins, name); found != nil {
if err := removePlugin(found); err != nil {
errorPlugins = append(errorPlugins, fmt.Sprintf("Failed to remove plugin %s, got error (%v)", name, err))
} else {
fmt.Fprintf(out, "Removed plugin: %s\n", name)
}
} else {
errorPlugins = append(errorPlugins, fmt.Sprintf("Plugin: %s not found", name))
}
}
if len(errorPlugins) > 0 {
return errors.Errorf(strings.Join(errorPlugins, "\n"))
}
return nil
}
func removePlugin(p *plugin.Plugin) error {
if err := os.RemoveAll(p.Dir); err != nil {
return err
}
return runHook(p, plugin.Delete)
}
func findPlugin(plugins []*plugin.Plugin, name string) *plugin.Plugin {
for _, p := range plugins {
if p.Metadata.Name == name {
return p
}
}
return nil
}

@ -1,112 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"io"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/plugin"
"helm.sh/helm/pkg/plugin/installer"
)
type pluginUpdateOptions struct {
names []string
home helmpath.Home
}
func newPluginUpdateCmd(out io.Writer) *cobra.Command {
o := &pluginUpdateOptions{}
cmd := &cobra.Command{
Use: "update <plugin>...",
Short: "update one or more Helm plugins",
PreRunE: func(cmd *cobra.Command, args []string) error {
return o.complete(args)
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out)
},
}
return cmd
}
func (o *pluginUpdateOptions) complete(args []string) error {
if len(args) == 0 {
return errors.New("please provide plugin name to update")
}
o.names = args
o.home = settings.Home
return nil
}
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())
if err != nil {
return err
}
var errorPlugins []string
for _, name := range o.names {
if found := findPlugin(plugins, name); found != nil {
if err := updatePlugin(found, o.home); err != nil {
errorPlugins = append(errorPlugins, fmt.Sprintf("Failed to update plugin %s, got error (%v)", name, err))
} else {
fmt.Fprintf(out, "Updated plugin: %s\n", name)
}
} else {
errorPlugins = append(errorPlugins, fmt.Sprintf("Plugin: %s not found", name))
}
}
if len(errorPlugins) > 0 {
return errors.Errorf(strings.Join(errorPlugins, "\n"))
}
return nil
}
func updatePlugin(p *plugin.Plugin, home helmpath.Home) error {
exactLocation, err := filepath.EvalSymlinks(p.Dir)
if err != nil {
return err
}
absExactLocation, err := filepath.Abs(exactLocation)
if err != nil {
return err
}
i, err := installer.FindSource(absExactLocation, home)
if err != nil {
return err
}
if err := installer.Update(i); err != nil {
return err
}
debug("loading plugin from %s", i.Path())
updatedPlugin, err := plugin.LoadDir(i.Path())
if err != nil {
return err
}
return runHook(updatedPlugin, plugin.Update)
}

@ -1,202 +1,136 @@
# The Helm Plugins Guide # The Helm Plugin Guide
Helm 2.1.0 introduced the concept of a client-side Helm _plugin_. A plugin is a This guide demonstrates how to install and write extensions for Helm. By thinking of core Helm commands as essential building blocks for interacting with a chart, a chart developer can think of plugins as a means of utilizing these building blocks to create more complex behavior. Plugins extend Helm with new sub-commands, allowing for new and custom features not included in Helm itself.
tool that can be accessed through the `helm` CLI, but which is not part of the
built-in Helm codebase.
Existing plugins can be found on [related](related.md#helm-plugins) section or by searching [Github](https://github.com/search?q=topic%3Ahelm-plugin&type=Repositories).
This guide explains how to use and create plugins.
## An Overview ## An Overview
Helm plugins are add-on tools that integrate seamlessly with Helm. They provide A plugin is nothing more than a standalone executable file, whose name begins with `helm-`. To install a plugin, simply move this executable file to anywhere on your PATH.
a way to extend the core feature set of Helm, but without requiring every new
feature to be written in Go and added to the core tool. Helm plugins are add-on tools that integrate seamlessly with Helm. They provide a way to extend the core feature set of Helm, but without requiring every new feature to be written in Go and added to the core tool.
Helm plugins have the following features: Helm plugins have the following features:
- They can be added and removed from a Helm installation without impacting the - They can be added and removed from a Helm installation without impacting the core Helm tool.
core Helm tool.
- They can be written in any programming language. - They can be written in any programming language.
- They integrate with Helm, and will show up in `helm help` and other places.
Helm plugins live in `$(helm home)/plugins`.
The Helm plugin model is partially modeled on Git's plugin model. To that end, The Helm plugin model is partially modeled on Git's plugin model. To that end, you may sometimes hear `helm` referred to as the _porcelain_ layer, with 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.
you may sometimes hear `helm` referred to as the _porcelain_ layer, with
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 ## Installing a Plugin
Plugins are installed using the `$ helm plugin install <path|url>` 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` Helm does not provide a plugin manager or anything similar to install or update plugins. It is your responsibility to ensure that plugin executables have a filename that begins with `helm-`, and that they are placed somewhere on your `$PATH`.
```console ## Discovering Plugins
$ helm plugin install https://github.com/technosophos/helm-template
```
If you have a plugin tar distribution, simply untar the plugin into the `$(helm home)/plugins` directory. You can also install tarball plugins directly from url by issuing `helm plugin install http://domain/path/to/plugin.tar.gz` `helm plugin list` searches your PATH for plugins. Executing this command causes a traversal of all files in your PATH. Any files that are executable and begin with `helm-` will show up in the order in which they are present in your PATH in this commands output. Any files beginning with `helm-` that are not executable will not be shown. Similar to how bash interprets duplicate commands in PATH, the first plugin that conflicts with anothers name will take precedence.
Alternatively, a set of plugins can be installed during the `helm init` process by using the `--plugins <file.yaml>` flag, where `file.yaml` looks like this: ## Overriding Helm Commands
``` It is possible to create plugins that overwrite existing Helm commands. Creating a plugin called `helm-version` will take ownership of the command `helm version`, allowing you to extend Helm's capabilities or replace existing functionality with your own.
plugins:
- name: helm-template
url: https://github.com/technosophos/helm-template
- name: helm-diff
url: https://github.com/databus23/helm-diff
version: 2.11.0+3
```
The `name` field only exists to allow you to easily identify plugins, and does not serve a functional purpose. If a plugin specified in the file is already installed, it maintains its current version. It is also possible to use plugins to add new subcommands to existing Helm commands. For example, adding a subcommand `helm create foo` by naming your plugin `helm-create-foo` will take ownership of the command `helm create foo`.
## Building Plugins Do keep in mind that _plugins override all child subcommands as well, unless they were written as a plugin._ See more in the Limitations section listed below.
In many ways, a plugin is similar to a chart. Each plugin has a top-level For example, if you write a plugin called `helm-dependency` to override `helm dependency`'s default behaviour, commands like `helm dependency build` are shadowed and unavailable to the user.
directory, and then a `plugin.yaml` file.
``` 1. The `helm-dependency` plugin accepts the `build` argument
$(helm home)/plugins/ 2. Another plugin called `help-dependency-build` is introduced.
|- keybase/
|
|- plugin.yaml
|- keybase.sh
``` ## Limitations
Unless the plugin is overriding an existing command, Helm plugins can only be loaded one level deep from the root command tree.
This is because of how Helm loads plugins. Internally, Helm does a recursive search in its command subtree to determine where to inject the plugin into the CLI. If no existing command is found, Helm adds the plugin to the root of the command tree.
As an example, when Helm loads in a plugin called `helm-dependency-build`, it will find that `helm dependency build` already exists and will replace that command with the plugin.
if you write a plugin called `helm-dependency` to override `helm dependency`'s default behaviour, commands like `helm dependency build` are shadowed and unavailable to the user.
However, if *another* plugin implements `helm-dependency-build`, then `helm-dependency-build` will become available as `helm dependency build`, regardless if the parent command was overridden.
One last edge case with the plugin loader exists: unless another plugin implements the parent command, plugins two levels deep in the command tree will be loaded at the root level.
In the example above, the `keybase` plugin is contained inside of a directory For example, if a plugin implements `helm-foo-bar` (where `helm-foo` is a Helm command that doesn't exist), then it will be loaded as `helm bar`. Again, this is because of how Helm loads plugins: If no existing command is found, Helm adds the plugin to the root of the command tree.
named `keybase`. It has two files: `plugin.yaml` (required) and an executable
script, `keybase.sh` (optional).
The core of a plugin is a simple YAML file named `plugin.yaml`. However, if another plugin implements `helm-foo`, then `helm-foo-bar` will be loaded as `helm foo bar`.
Here is a plugin YAML for a plugin that adds support for Keybase operations:
Because of this limitation, it is best to write plugins at the root level of the command subtree *unless* you are overriding the behaviour of a particular command, or you're introducing/replacing new commands to a particular plugin.
## Writing Plugins
You can write a plugin in any programming language or script that allows you to write command-line commands.
There is no plugin installation or pre-loading required. Plugin executables receive the inherited environment from Helm. A plugin determines which command path it wishes to implement based on its name. For example, a plugin wanting to provide a new command `helm foo`, would simply be named `helm-foo`, and live somewhere in the users PATH.
For example, you could write a bash script called `helm-foo`:
``` ```
name: "last" #!/bin/bash
version: "0.1.0"
usage: "get the last release name" # optional argument handling
description: "get the last release name"" if [[ "$1" == "version" ]]
ignoreFlags: false then
command: "$HELM_BIN --host $TILLER_HOST list --short --max 1 --date -r" echo "1.0.0"
platformCommand: exit 0
- os: linux fi
arch: i386
command: "$HELM_BIN list --short --max 1 --date -r" # optional argument handling
- os: linux if [[ "$1" == "config" ]]
arch: amd64 then
command: "$HELM_BIN list --short --max 1 --date -r" echo $KUBECONFIG
- os: windows exit 0
arch: amd64 fi
command: "$HELM_BIN list --short --max 1 --date -r"
echo "I am a plugin named helm-foo"
``` ```
The `name` is the name of the plugin. When Helm executes it plugin, this is the In the example above, the `helm-foo` plugin will accept `helm foo`, `helm foo version` and `helm foo config`.
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
plugin with `name: keybase` should be contained in a directory named `keybase`.
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.
Finally, and most importantly, `platformCommand` or `command` is the command
that this plugin will execute when it is called. The `platformCommand` section
defines the OS/Architecture specific variations of a command. The following
rules will apply in deciding which command to use:
- If `platformCommand` is present, it will be searched first.
- If both `os` and `arch` match the current platform, search will stop and the
command will be used.
- If `os` matches and there is no more specific `arch` match, the command
will be used.
- If no `platformCommand` match is found, the default `command` will be used.
- If no matches are found in `platformCommand` and no `command` is present,
Helm will exit with an error.
Environment variables are interpolated before the plugin is executed. The
pattern above illustrates the preferred way to indicate where the plugin
program lives.
There are some strategies for working with plugin commands:
- If a plugin includes an executable, the executable for a
`platformCommand:` or a `command:` should be packaged in the plugin directory.
- The `platformCommand:` or `command:` line will have any environment
variables expanded before execution. `$HELM_PLUGIN_DIR` will point to the
plugin directory.
- The command itself is not executed in a shell. So you can't oneline a shell script.
- Helm injects lots of configuration into environment variables. Take a look at
the environment to see what information is available.
- Helm makes no assumptions about the language of the plugin. You can write it
in whatever you prefer.
- Commands are responsible for implementing specific help text for `-h` and `--help`.
Helm will use `usage` and `description` for `helm help` and `helm help myplugin`,
but will not handle `helm myplugin --help`.
## Downloader Plugins ## Downloader Plugins
By default, Helm is able to pull 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): By default, Helm is able to pull Charts using HTTP/S. However, plugins can extend Helm's capability to download Charts from arbitrary sources by registering as a downloader plugin.
``` Plugins can register themselves as a downloader plugin if the name begins with `helm-downloader-`.
downloaders:
- command: "bin/mydownloader"
protocols:
- "myprotocol"
- "myprotocols"
```
If such plugin is installed, Helm can interact with the repository using the specified If such plugin is installed, Helm can interact with the repository using the specified protocol scheme by invoking the plugin. The special repository shall be added similarly to the regular ones: `helm repo add favorite myprotocol://example.com/` The rules for the special repos are the same to the regular ones: Helm must be able to download the `index.yaml` file in order to discover and cache the list of available Charts.
protocol scheme by invoking the `command`. The special repository shall be added
similarly to the regular ones: `helm repo add favorite myprotocol://example.com/`
The rules for the special repos are the same to the regular ones: Helm must be able
to download the `index.yaml` file in order to discover and cache the list of
available Charts.
The defined command will be invoked with the following scheme: The defined command will be invoked with the following scheme: `helm-downloader-myprotocol certFile keyFile caFile full-URL`. The SSL credentials are coming from the repo definition, stored in `$HELM_HOME/repository/repositories.yaml`. The downloader plugin is expected to dump the raw content to stdout and report errors on stderr.
`command certFile keyFile caFile full-URL`. The SSL credentials are coming from the
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.
## Environment Variables ## Environment Variables
When Helm executes a plugin, it passes the outer environment to the plugin, and When Helm executes a plugin, it passes the outer environment to the plugin, and also injects some additional environment variables.
also injects some additional environment variables.
Variables like `KUBECONFIG` are set for the plugin if they are set in the Variables like `KUBECONFIG` are set for the plugin if they are set in the outer environment.
outer environment.
The following variables are guaranteed to be set: The following variables are guaranteed to be set:
- `HELM_PLUGIN`: The path to the plugins directory - `HELM_PLUGIN_NAME`: The name of the plugin, as invoked by `helm`. So `helm myplug` will have the short name `myplug`.
- `HELM_PLUGIN_NAME`: The name of the plugin, as invoked by `helm`. So
`helm myplug` will have the short name `myplug`.
- `HELM_PLUGIN_DIR`: The directory that contains the plugin.
- `HELM_BIN`: The path to the `helm` command (as executed by the user). - `HELM_BIN`: The path to the `helm` command (as executed by the user).
- `HELM_HOME`: The path to the Helm home. - `HELM_HOME`: The path to the Helm home.
- `HELM_PATH_*`: Paths to important Helm files and directories are stored in - `HELM_PATH_*`: Paths to important Helm files and directories are stored in environment variables prefixed by `HELM_PATH`.
environment variables prefixed by `HELM_PATH`.
## A Note on Flag Parsing ## A Note on Flag Parsing
When executing a plugin, Helm will parse global flags for its own use. Some of When executing a plugin, Helm will parse global flags for its own use. Some of these flags are _not_ passed on to the plugin.
these flags are _not_ passed on to the plugin.
- `--debug`: If this is specified, `$HELM_DEBUG` is set to `1` - `--debug`: If this is specified, `$HELM_DEBUG` is set to `1`
- `--home`: This is converted to `$HELM_HOME` - `--home`: This is converted to `$HELM_HOME`.
- `--kube-context`: This is simply dropped.
Plugins _should_ display help text and then exit for `-h` and `--help`. In all other cases, plugins may use flags as appropriate.
## Changes from Helm 2
In Helm 2, plugins were installed using the `$ helm plugin install <path|url>` command. You could pass in a path to a plugin on your local file system or a url of a remote VCS repo. `helm plugin install` would clone or copy the plugin into `$(helm home)/plugins`.
Plugins also included a a `plugin.yaml` file which would define how to install, upgrade and invoke the plugin, along with hooks for the `helm` CLI to integrate the plugin into `helm help`.
This approach came with a few limitations, however:
- `helm plugin install --version 1.0.0` may fetch a plugin.yaml that did not match with the intended version number, leaving Helm in an erroneous state
- most VCS repositories only contained the source code, but not the binary. Most plugins just invoked a bash/powershell script to fetch the actual binary.
We were, in effect, re-building all of the functionality of a traditional package manager.
In the end, we decided to split out Helm's plugin manager from Helm 3, relying on the community to distribute their plugins through more traditional package managers.
Plugins _should_ display help text and then exit for `-h` and `--help`. In all This has the trade-off of plugins being unable to integrate with `helm help`, but the ease of development and ease of integrations with tools like `apt`/`brew` for package management and `man` for documentation made for a simpler plugin development experience.
other cases, plugins may use flags as appropriate.

@ -20,6 +20,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -27,24 +28,23 @@ import (
"helm.sh/helm/pkg/plugin" "helm.sh/helm/pkg/plugin"
) )
// PluginDownloaderPrefix is the command name prefix for all helm downloader plugins.
const PluginDownloaderPrefix = "downloader-"
// collectPlugins scans for getter plugins. // collectPlugins scans for getter plugins.
// This will load plugins according to the cli. // This will load plugins according to the cli.
func collectPlugins(settings cli.EnvSettings) (Providers, error) { func collectPlugins(settings cli.EnvSettings) (Providers, error) {
plugins, err := plugin.FindPlugins(settings.PluginDirs()) plugins, err := plugin.FindAll(os.Getenv("PATH"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
var result Providers var result Providers
for _, plugin := range plugins { for _, plugin := range plugins {
for _, downloader := range plugin.Metadata.Downloaders { if strings.HasPrefix(plugin.Name, PluginDownloaderPrefix) {
scheme := strings.TrimPrefix(plugin.Name, PluginDownloaderPrefix)
result = append(result, Provider{ result = append(result, Provider{
Schemes: downloader.Protocols, Schemes: []string{scheme},
New: newPluginGetter( New: newPluginGetter(plugin, settings),
downloader.Command,
settings,
plugin.Metadata.Name,
plugin.Dir,
),
}) })
} }
} }
@ -81,16 +81,16 @@ func (p *pluginGetter) Get(href string) (*bytes.Buffer, error) {
} }
// newPluginGetter constructs a valid plugin getter // newPluginGetter constructs a valid plugin getter
func newPluginGetter(command string, settings cli.EnvSettings, name, base string) Constructor { func newPluginGetter(plugin *plugin.Plugin, settings cli.EnvSettings) Constructor {
return func(URL, CertFile, KeyFile, CAFile string) (Getter, error) { return func(URL, CertFile, KeyFile, CAFile string) (Getter, error) {
result := &pluginGetter{ result := &pluginGetter{
command: command, command: plugin.Name,
certFile: CertFile, certFile: CertFile,
keyFile: KeyFile, keyFile: KeyFile,
cAFile: CAFile, cAFile: CAFile,
settings: settings, settings: settings,
name: name, name: plugin.Name,
base: base, base: plugin.Dir,
} }
return result, nil return result, nil
} }

@ -0,0 +1,48 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package osutil // import "helm.sh/helm/pkg/osutil"
import (
"os"
"path/filepath"
"runtime"
"strings"
)
// IsExecutable determines whether or not a particular file is executable. If there is an error, it
// will be of type *PathError.
func IsExecutable(fullPath string) (bool, error) {
info, err := os.Stat(fullPath)
if err != nil {
return false, err
}
if runtime.GOOS == "windows" {
fileExt := strings.ToLower(filepath.Ext(fullPath))
switch fileExt {
case ".bat", ".cmd", ".com", ".exe", ".ps1":
return true, nil
}
return false, nil
}
if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
return true, nil
}
return false, nil
}

@ -1,74 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package cache provides a key generator for vcs urls.
package cache // import "helm.sh/helm/pkg/plugin/cache"
import (
"net/url"
"regexp"
"strings"
)
// Thanks glide!
// scpSyntaxRe matches the SCP-like addresses used to access repos over SSH.
var scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`)
// Key generates a cache key based on a url or scp string. The key is file
// system safe.
func Key(repo string) (string, error) {
var u *url.URL
var err error
var strip bool
if m := scpSyntaxRe.FindStringSubmatch(repo); m != nil {
// Match SCP-like syntax and convert it to a URL.
// Eg, "git@github.com:user/repo" becomes
// "ssh://git@github.com/user/repo".
u = &url.URL{
Scheme: "ssh",
User: url.User(m[1]),
Host: m[2],
Path: "/" + m[3],
}
strip = true
} else {
u, err = url.Parse(repo)
if err != nil {
return "", err
}
}
if strip {
u.Scheme = ""
}
var key string
if u.Scheme != "" {
key = u.Scheme + "-"
}
if u.User != nil && u.User.Username() != "" {
key = key + u.User.Username() + "-"
}
key = key + u.Host
if u.Path != "" {
key = key + strings.Replace(u.Path, "/", "-", -1)
}
key = strings.Replace(key, ":", "-", -1)
return key, nil
}

@ -1,48 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/pkg/plugin/installer"
import (
"os"
"path/filepath"
"helm.sh/helm/pkg/helmpath"
)
type base struct {
// Source is the reference to a plugin
Source string
// HelmHome is the $HELM_HOME directory
HelmHome helmpath.Home
}
func newBase(source string, home helmpath.Home) base {
return base{source, home}
}
// link creates a symlink from the plugin source to $HELM_HOME.
func (b *base) link(from string) error {
debug("symlinking %s to %s", from, b.Path())
return os.Symlink(from, b.Path())
}
// Path is where the plugin will be symlinked to.
func (b *base) Path() string {
if b.Source == "" {
return ""
}
return filepath.Join(b.HelmHome.Plugins(), filepath.Base(b.Source))
}

@ -1,17 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package installer provides an interface for installing Helm plugins.
package installer // import "helm.sh/helm/pkg/plugin/installer"

@ -1,210 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/pkg/plugin/installer"
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/pkg/cli"
"helm.sh/helm/pkg/getter"
"helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/plugin/cache"
)
// HTTPInstaller installs plugins from an archive served by a web server.
type HTTPInstaller struct {
CacheDir string
PluginName string
base
extractor Extractor
getter getter.Getter
}
// TarGzExtractor extracts gzip compressed tar archives
type TarGzExtractor struct{}
// Extractor provides an interface for extracting archives
type Extractor interface {
Extract(buffer *bytes.Buffer, targetDir string) error
}
// Extractors contains a map of suffixes and matching implementations of extractor to return
var Extractors = map[string]Extractor{
".tar.gz": &TarGzExtractor{},
".tgz": &TarGzExtractor{},
}
// NewExtractor creates a new extractor matching the source file name
func NewExtractor(source string) (Extractor, error) {
for suffix, extractor := range Extractors {
if strings.HasSuffix(source, suffix) {
return extractor, nil
}
}
return nil, errors.Errorf("no extractor implemented yet for %s", source)
}
// NewHTTPInstaller creates a new HttpInstaller.
func NewHTTPInstaller(source string, home helmpath.Home) (*HTTPInstaller, error) {
key, err := cache.Key(source)
if err != nil {
return nil, err
}
extractor, err := NewExtractor(source)
if err != nil {
return nil, err
}
getConstructor, err := getter.ByScheme("http", cli.EnvSettings{})
if err != nil {
return nil, err
}
get, err := getConstructor.New(source, "", "", "")
if err != nil {
return nil, err
}
i := &HTTPInstaller{
CacheDir: home.Path("cache", "plugins", key),
PluginName: stripPluginName(filepath.Base(source)),
base: newBase(source, home),
extractor: extractor,
getter: get,
}
return i, nil
}
// helper that relies on some sort of convention for plugin name (plugin-name-<version>)
func stripPluginName(name string) string {
var strippedName string
for suffix := range Extractors {
if strings.HasSuffix(name, suffix) {
strippedName = strings.TrimSuffix(name, suffix)
break
}
}
re := regexp.MustCompile(`(.*)-[0-9]+\..*`)
return re.ReplaceAllString(strippedName, `$1`)
}
// Install downloads and extracts the tarball into the cache directory and creates a symlink to the plugin directory in $HELM_HOME.
//
// Implements Installer.
func (i *HTTPInstaller) Install() error {
pluginData, err := i.getter.Get(i.Source)
if err != nil {
return err
}
err = i.extractor.Extract(pluginData, i.CacheDir)
if err != nil {
return err
}
if !isPlugin(i.CacheDir) {
return ErrMissingMetadata
}
src, err := filepath.Abs(i.CacheDir)
if err != nil {
return err
}
return i.link(src)
}
// Update updates a local repository
// Not implemented for now since tarball most likely will be packaged by version
func (i *HTTPInstaller) Update() error {
return errors.Errorf("method Update() not implemented for HttpInstaller")
}
// Override link because we want to use HttpInstaller.Path() not base.Path()
func (i *HTTPInstaller) link(from string) error {
debug("symlinking %s to %s", from, i.Path())
return os.Symlink(from, i.Path())
}
// Path is overridden because we want to join on the plugin name not the file name
func (i HTTPInstaller) Path() string {
if i.base.Source == "" {
return ""
}
return filepath.Join(i.base.HelmHome.Plugins(), i.PluginName)
}
// Extract extracts compressed archives
//
// Implements Extractor.
func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error {
uncompressedStream, err := gzip.NewReader(buffer)
if err != nil {
return err
}
tarReader := tar.NewReader(uncompressedStream)
os.MkdirAll(targetDir, 0755)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
path := filepath.Join(targetDir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(path, 0755); err != nil {
return err
}
case tar.TypeReg:
outFile, err := os.Create(path)
if err != nil {
return err
}
if _, err := io.Copy(outFile, tarReader); err != nil {
outFile.Close()
return err
}
outFile.Close()
default:
return errors.Errorf("unknown type: %b in %s", header.Typeflag, header.Name)
}
}
return nil
}

@ -1,191 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/pkg/plugin/installer"
import (
"bytes"
"encoding/base64"
"io/ioutil"
"os"
"testing"
"github.com/pkg/errors"
"helm.sh/helm/pkg/helmpath"
)
var _ Installer = new(HTTPInstaller)
// Fake http client
type TestHTTPGetter struct {
MockResponse *bytes.Buffer
MockError error
}
func (t *TestHTTPGetter) Get(href string) (*bytes.Buffer, error) { return t.MockResponse, t.MockError }
// Fake plugin tarball data
var fakePluginB64 = "H4sIAKRj51kAA+3UX0vCUBgGcC9jn+Iwuk3Peza3GeyiUlJQkcogCOzgli7dJm4TvYk+a5+k479UqquUCJ/fLs549sLO2TnvWnJa9aXnjwujYdYLovxMhsPcfnHOLdNkOXthM/IVQQYjg2yyLLJ4kXGhLp5j0z3P41tZksqxmspL3B/O+j/XtZu1y8rdYzkOZRCxduKPk53ny6Wwz/GfIIf1As8lxzGJSmoHNLJZphKHG4YpTCE0wVk3DULfpSJ3DMMqkj3P5JfMYLdX1Vr9Ie/5E5cstcdC8K04iGLX5HaJuKpWL17F0TCIBi5pf/0pjtLhun5j3f9v6r7wfnI/H0eNp9d1/5P6Gez0vzo7wsoxfrAZbTny/o9k6J8z/VkO/LPlWdC1iVpbEEcq5nmeJ13LEtmbV0k2r2PrOs9PuuNglC5rL1Y5S/syXRQmutaNw1BGnnp8Wq3UG51WvX1da3bKtZtCN/R09DwAAAAAAAAAAAAAAAAAAADAb30AoMczDwAoAAA="
func TestStripName(t *testing.T) {
if stripPluginName("fake-plugin-0.0.1.tar.gz") != "fake-plugin" {
t.Errorf("name does not match expected value")
}
if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" {
t.Errorf("name does not match expected value")
}
if stripPluginName("fake-plugin.tgz") != "fake-plugin" {
t.Errorf("name does not match expected value")
}
if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" {
t.Errorf("name does not match expected value")
}
}
func TestHTTPInstaller(t *testing.T) {
source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz"
hh, err := ioutil.TempDir("", "helm-home-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
home := helmpath.Home(hh)
if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
t.Fatalf("Could not create %s: %s", home.Plugins(), err)
}
i, err := NewForSource(source, "0.0.1", home)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
// ensure a HTTPInstaller was returned
httpInstaller, ok := i.(*HTTPInstaller)
if !ok {
t.Error("expected a HTTPInstaller")
}
// inject fake http client responding with minimal plugin tarball
mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64)
if err != nil {
t.Fatalf("Could not decode fake tgz plugin: %s", err)
}
httpInstaller.getter = &TestHTTPGetter{
MockResponse: bytes.NewBuffer(mockTgz),
}
// install the plugin
if err := Install(i); err != nil {
t.Error(err)
}
if i.Path() != home.Path("plugins", "fake-plugin") {
t.Errorf("expected path '$HELM_HOME/plugins/fake-plugin', got %q", i.Path())
}
// Install again to test plugin exists error
if err := Install(i); err == nil {
t.Error("expected error for plugin exists, got none")
} else if err.Error() != "plugin already exists" {
t.Errorf("expected error for plugin exists, got (%v)", err)
}
}
func TestHTTPInstallerNonExistentVersion(t *testing.T) {
source := "https://repo.localdomain/plugins/fake-plugin-0.0.2.tar.gz"
hh, err := ioutil.TempDir("", "helm-home-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
home := helmpath.Home(hh)
if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
t.Fatalf("Could not create %s: %s", home.Plugins(), err)
}
i, err := NewForSource(source, "0.0.2", home)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
// ensure a HTTPInstaller was returned
httpInstaller, ok := i.(*HTTPInstaller)
if !ok {
t.Error("expected a HTTPInstaller")
}
// inject fake http client responding with error
httpInstaller.getter = &TestHTTPGetter{
MockError: errors.Errorf("failed to download plugin for some reason"),
}
// attempt to install the plugin
if err := Install(i); err == nil {
t.Error("expected error from http client")
}
}
func TestHTTPInstallerUpdate(t *testing.T) {
source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz"
hh, err := ioutil.TempDir("", "helm-home-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
home := helmpath.Home(hh)
if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
t.Fatalf("Could not create %s: %s", home.Plugins(), err)
}
i, err := NewForSource(source, "0.0.1", home)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
// ensure a HTTPInstaller was returned
httpInstaller, ok := i.(*HTTPInstaller)
if !ok {
t.Error("expected a HTTPInstaller")
}
// inject fake http client responding with minimal plugin tarball
mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64)
if err != nil {
t.Fatalf("Could not decode fake tgz plugin: %s", err)
}
httpInstaller.getter = &TestHTTPGetter{
MockResponse: bytes.NewBuffer(mockTgz),
}
// install the plugin before updating
if err := Install(i); err != nil {
t.Error(err)
}
if i.Path() != home.Path("plugins", "fake-plugin") {
t.Errorf("expected path '$HELM_HOME/plugins/fake-plugin', got %q", i.Path())
}
// Update plugin, should fail because it is not implemented
if err := Update(i); err == nil {
t.Error("update method not implemented for http installer")
}
}

@ -1,117 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package installer
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/pkg/helmpath"
)
// ErrMissingMetadata indicates that plugin.yaml is missing.
var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing")
// Debug enables verbose output.
var Debug bool
// Installer provides an interface for installing helm client plugins.
type Installer interface {
// Install adds a plugin to $HELM_HOME.
Install() error
// Path is the directory of the installed plugin.
Path() string
// Update updates a plugin to $HELM_HOME.
Update() error
}
// Install installs a plugin to $HELM_HOME.
func Install(i Installer) error {
if _, pathErr := os.Stat(path.Dir(i.Path())); os.IsNotExist(pathErr) {
return errors.New(`plugin home "$HELM_HOME/plugins" does not exist`)
}
if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) {
return errors.New("plugin already exists")
}
return i.Install()
}
// Update updates a plugin in $HELM_HOME.
func Update(i Installer) error {
if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) {
return errors.New("plugin does not exist")
}
return i.Update()
}
// NewForSource determines the correct Installer for the given source.
func NewForSource(source, version string, home helmpath.Home) (Installer, error) {
// Check if source is a local directory
if isLocalReference(source) {
return NewLocalInstaller(source, home)
} else if isRemoteHTTPArchive(source) {
return NewHTTPInstaller(source, home)
}
return NewVCSInstaller(source, version, home)
}
// FindSource determines the correct Installer for the given source.
func FindSource(location string, home helmpath.Home) (Installer, error) {
installer, err := existingVCSRepo(location, home)
if err != nil && err.Error() == "Cannot detect VCS" {
return installer, errors.New("cannot get information about plugin source")
}
return installer, err
}
// isLocalReference checks if the source exists on the filesystem.
func isLocalReference(source string) bool {
_, err := os.Stat(source)
return err == nil
}
// isRemoteHTTPArchive checks if the source is a http/https url and is an archive
func isRemoteHTTPArchive(source string) bool {
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
for suffix := range Extractors {
if strings.HasSuffix(source, suffix) {
return true
}
}
}
return false
}
// isPlugin checks if the directory contains a plugin.yaml file.
func isPlugin(dirname string) bool {
_, err := os.Stat(filepath.Join(dirname, "plugin.yaml"))
return err == nil
}
func debug(format string, args ...interface{}) {
if Debug {
format = fmt.Sprintf("[debug] %s\n", format)
fmt.Printf(format, args...)
}
}

@ -1,57 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/pkg/plugin/installer"
import (
"path/filepath"
"github.com/pkg/errors"
"helm.sh/helm/pkg/helmpath"
)
// LocalInstaller installs plugins from the filesystem.
type LocalInstaller struct {
base
}
// NewLocalInstaller creates a new LocalInstaller.
func NewLocalInstaller(source string, home helmpath.Home) (*LocalInstaller, error) {
src, err := filepath.Abs(source)
if err != nil {
return nil, errors.Wrap(err, "unable to get absolute path to plugin")
}
i := &LocalInstaller{
base: newBase(src, home),
}
return i, nil
}
// Install creates a symlink to the plugin directory in $HELM_HOME.
//
// Implements Installer.
func (i *LocalInstaller) Install() error {
if !isPlugin(i.Source) {
return ErrMissingMetadata
}
return i.link(i.Source)
}
// Update updates a local repository
func (i *LocalInstaller) Update() error {
debug("local repository is auto-updated")
return nil
}

@ -1,64 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/pkg/plugin/installer"
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"helm.sh/helm/pkg/helmpath"
)
var _ Installer = new(LocalInstaller)
func TestLocalInstaller(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-home-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
home := helmpath.Home(hh)
if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
t.Fatalf("Could not create %s: %s", home.Plugins(), err)
}
// Make a temp dir
tdir, err := ioutil.TempDir("", "helm-installer-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tdir)
if err := ioutil.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil {
t.Fatal(err)
}
source := "../testdata/plugdir/echo"
i, err := NewForSource(source, "", home)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if err := Install(i); err != nil {
t.Error(err)
}
if i.Path() != home.Path("plugins", "echo") {
t.Errorf("expected path '$HELM_HOME/plugins/helm-env', got %q", i.Path())
}
}

@ -1,174 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/pkg/plugin/installer"
import (
"os"
"sort"
"github.com/Masterminds/semver"
"github.com/Masterminds/vcs"
"github.com/pkg/errors"
"helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/plugin/cache"
)
// VCSInstaller installs plugins from remote a repository.
type VCSInstaller struct {
Repo vcs.Repo
Version string
base
}
func existingVCSRepo(location string, home helmpath.Home) (Installer, error) {
repo, err := vcs.NewRepo("", location)
if err != nil {
return nil, err
}
i := &VCSInstaller{
Repo: repo,
base: newBase(repo.Remote(), home),
}
return i, err
}
// NewVCSInstaller creates a new VCSInstaller.
func NewVCSInstaller(source, version string, home helmpath.Home) (*VCSInstaller, error) {
key, err := cache.Key(source)
if err != nil {
return nil, err
}
cachedpath := home.Path("cache", "plugins", key)
repo, err := vcs.NewRepo(source, cachedpath)
if err != nil {
return nil, err
}
i := &VCSInstaller{
Repo: repo,
Version: version,
base: newBase(source, home),
}
return i, err
}
// Install clones a remote repository and creates a symlink to the plugin directory in HELM_HOME.
//
// Implements Installer.
func (i *VCSInstaller) Install() error {
if err := i.sync(i.Repo); err != nil {
return err
}
ref, err := i.solveVersion(i.Repo)
if err != nil {
return err
}
if ref != "" {
if err := i.setVersion(i.Repo, ref); err != nil {
return err
}
}
if !isPlugin(i.Repo.LocalPath()) {
return ErrMissingMetadata
}
return i.link(i.Repo.LocalPath())
}
// Update updates a remote repository
func (i *VCSInstaller) Update() error {
debug("updating %s", i.Repo.Remote())
if i.Repo.IsDirty() {
return errors.New("plugin repo was modified")
}
if err := i.Repo.Update(); err != nil {
return err
}
if !isPlugin(i.Repo.LocalPath()) {
return ErrMissingMetadata
}
return nil
}
func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) {
if i.Version == "" {
return "", nil
}
if repo.IsReference(i.Version) {
return i.Version, nil
}
// Create the constraint first to make sure it's valid before
// working on the repo.
constraint, err := semver.NewConstraint(i.Version)
if err != nil {
return "", err
}
// Get the tags
refs, err := repo.Tags()
if err != nil {
return "", err
}
debug("found refs: %s", refs)
// Convert and filter the list to semver.Version instances
semvers := getSemVers(refs)
// Sort semver list
sort.Sort(sort.Reverse(semver.Collection(semvers)))
for _, v := range semvers {
if constraint.Check(v) {
// If the constrint passes get the original reference
ver := v.Original()
debug("setting to %s", ver)
return ver, nil
}
}
return "", errors.Errorf("requested version %q does not exist for plugin %q", i.Version, i.Repo.Remote())
}
// setVersion attempts to checkout the version
func (i *VCSInstaller) setVersion(repo vcs.Repo, ref string) error {
debug("setting version to %q", i.Version)
return repo.UpdateVersion(ref)
}
// sync will clone or update a remote repo.
func (i *VCSInstaller) sync(repo vcs.Repo) error {
if _, err := os.Stat(repo.LocalPath()); os.IsNotExist(err) {
debug("cloning %s to %s", repo.Remote(), repo.LocalPath())
return repo.Get()
}
debug("updating %s", repo.Remote())
return repo.Update()
}
// Filter a list of versions to only included semantic versions. The response
// is a mapping of the original version to the semantic version.
func getSemVers(refs []string) []*semver.Version {
var sv []*semver.Version
for _, r := range refs {
if v, err := semver.NewVersion(r); err == nil {
sv = append(sv, v)
}
}
return sv
}

@ -1,203 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/pkg/plugin/installer"
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/Masterminds/vcs"
"helm.sh/helm/pkg/helmpath"
)
var _ Installer = new(VCSInstaller)
type testRepo struct {
local, remote, current string
tags, branches []string
err error
vcs.Repo
}
func (r *testRepo) LocalPath() string { return r.local }
func (r *testRepo) Remote() string { return r.remote }
func (r *testRepo) Update() error { return r.err }
func (r *testRepo) Get() error { return r.err }
func (r *testRepo) IsReference(string) bool { return false }
func (r *testRepo) Tags() ([]string, error) { return r.tags, r.err }
func (r *testRepo) Branches() ([]string, error) { return r.branches, r.err }
func (r *testRepo) UpdateVersion(version string) error {
r.current = version
return r.err
}
func TestVCSInstaller(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-home-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
home := helmpath.Home(hh)
if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
t.Fatalf("Could not create %s: %s", home.Plugins(), err)
}
source := "https://github.com/adamreese/helm-env"
testRepoPath, _ := filepath.Abs("../testdata/plugdir/echo")
repo := &testRepo{
local: testRepoPath,
tags: []string{"0.1.0", "0.1.1"},
}
i, err := NewForSource(source, "~0.1.0", home)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
// ensure a VCSInstaller was returned
vcsInstaller, ok := i.(*VCSInstaller)
if !ok {
t.Fatal("expected a VCSInstaller")
}
// set the testRepo in the VCSInstaller
vcsInstaller.Repo = repo
if err := Install(i); err != nil {
t.Fatal(err)
}
if repo.current != "0.1.1" {
t.Errorf("expected version '0.1.1', got %q", repo.current)
}
if i.Path() != home.Path("plugins", "helm-env") {
t.Errorf("expected path '$HELM_HOME/plugins/helm-env', got %q", i.Path())
}
// Install again to test plugin exists error
if err := Install(i); err == nil {
t.Error("expected error for plugin exists, got none")
} else if err.Error() != "plugin already exists" {
t.Errorf("expected error for plugin exists, got (%v)", err)
}
//Testing FindSource method, expect error because plugin code is not a cloned repository
if _, err := FindSource(i.Path(), home); err == nil {
t.Error("expected error for inability to find plugin source, got none")
} else if err.Error() != "cannot get information about plugin source" {
t.Errorf("expected error for inability to find plugin source, got (%v)", err)
}
}
func TestVCSInstallerNonExistentVersion(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-home-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
home := helmpath.Home(hh)
if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
t.Fatalf("Could not create %s: %s", home.Plugins(), err)
}
source := "https://github.com/adamreese/helm-env"
version := "0.2.0"
i, err := NewForSource(source, version, home)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
// ensure a VCSInstaller was returned
_, ok := i.(*VCSInstaller)
if !ok {
t.Fatal("expected a VCSInstaller")
}
if err := Install(i); err == nil {
t.Error("expected error for version does not exists, got none")
} else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", version, source) {
t.Errorf("expected error for version does not exists, got (%v)", err)
}
}
func TestVCSInstallerUpdate(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-home-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
home := helmpath.Home(hh)
if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
t.Fatalf("Could not create %s: %s", home.Plugins(), err)
}
source := "https://github.com/adamreese/helm-env"
i, err := NewForSource(source, "", home)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
// ensure a VCSInstaller was returned
_, ok := i.(*VCSInstaller)
if !ok {
t.Fatal("expected a VCSInstaller")
}
if err := Update(i); err == nil {
t.Fatal("expected error for plugin does not exist, got none")
} else if err.Error() != "plugin does not exist" {
t.Fatalf("expected error for plugin does not exist, got (%v)", err)
}
// Install plugin before update
if err := Install(i); err != nil {
t.Fatal(err)
}
// Test FindSource method for positive result
pluginInfo, err := FindSource(i.Path(), home)
if err != nil {
t.Fatal(err)
}
repoRemote := pluginInfo.(*VCSInstaller).Repo.Remote()
if repoRemote != source {
t.Fatalf("invalid source found, expected %q got %q", source, repoRemote)
}
// Update plugin
if err := Update(i); err != nil {
t.Fatal(err)
}
// Test update failure
os.Remove(filepath.Join(i.Path(), "plugin.yaml"))
// Testing update for error
if err := Update(i); err == nil {
t.Error("expected error for plugin modified, got none")
} else if err.Error() != "plugin repo was modified" {
t.Errorf("expected error for plugin modified, got (%v)", err)
}
}

@ -17,205 +17,74 @@ package plugin // import "helm.sh/helm/pkg/plugin"
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"github.com/ghodss/yaml" cli "helm.sh/helm/pkg/cli"
"helm.sh/helm/pkg/osutil"
helm_env "helm.sh/helm/pkg/cli"
) )
const pluginFileName = "plugin.yaml" // PluginNamePrefix is the prefix for all helm plugins.
const PluginNamePrefix = "helm-"
// Downloaders represents the plugins capability if it can retrieve
// charts from special sources
type Downloaders struct {
// Protocols are the list of schemes from the charts URL.
Protocols []string `json:"protocols"`
// Command is the executable path with which the plugin performs
// the actual download for the corresponding Protocols
Command string `json:"command"`
}
// PlatformCommand represents a command for a particular operating system and architecture
type PlatformCommand struct {
OperatingSystem string `json:"os"`
Architecture string `json:"arch"`
Command string `json:"command"`
}
// Metadata describes a plugin.
//
// This is the plugin equivalent of a chart.Metadata.
type Metadata struct {
// Name is the name of the plugin
Name string `json:"name"`
// Version is a SemVer 2 version of the plugin.
Version string `json:"version"`
// Usage is the single-line usage text shown in help
Usage string `json:"usage"`
// Description is a long description shown in places like `helm help`
Description string `json:"description"`
// Command is the command, as a single string.
//
// The command will be passed through environment expansion, so env vars can
// be present in this command. Unless IgnoreFlags is set, this will
// also merge the flags passed from Helm.
//
// Note that command is not executed in a shell. To do so, we suggest
// pointing the command to a shell script.
//
// The following rules will apply to processing commands:
// - If platformCommand is present, it will be searched first
// - If both OS and Arch match the current platform, search will stop and the command will be executed
// - If OS matches and there is no more specific match, the command will be executed
// - If no OS/Arch match is found, the default command will be executed
// - If no command is present and no matches are found in platformCommand, Helm will exit with an error
PlatformCommand []PlatformCommand `json:"platformCommand"`
Command string `json:"command"`
// IgnoreFlags ignores any flags passed in from Helm
//
// For example, if the plugin is invoked as `helm --debug myplugin`, if this
// is false, `--debug` will be appended to `--command`. If this is true,
// the `--debug` flag will be discarded.
IgnoreFlags bool `json:"ignoreFlags"`
// Hooks are commands that will run on events.
Hooks Hooks
// Downloaders field is used if the plugin supply downloader mechanism
// for special protocols.
Downloaders []Downloaders `json:"downloaders"`
}
// Plugin represents a plugin. // Plugin represents a plugin.
type Plugin struct { type Plugin struct {
// Metadata is a parsed representation of a plugin.yaml // Name is the name of the plugin.
Metadata *Metadata Name string `json:"name"`
// Dir is the string path to the directory that holds the plugin. // Dir is the string path to the directory that holds the plugin.
Dir string Dir string `json:"dir"`
}
// The following rules will apply to processing the Plugin.PlatformCommand.Command:
// - If both OS and Arch match the current platform, search will stop and the command will be prepared for execution
// - If OS matches and there is no more specific match, the command will be prepared for execution
// - If no OS/Arch match is found, return nil
func getPlatformCommand(platformCommands []PlatformCommand) []string {
var command []string
for _, platformCommand := range platformCommands {
if strings.EqualFold(platformCommand.OperatingSystem, runtime.GOOS) {
command = strings.Split(os.ExpandEnv(platformCommand.Command), " ")
}
if strings.EqualFold(platformCommand.OperatingSystem, runtime.GOOS) && strings.EqualFold(platformCommand.Architecture, runtime.GOARCH) {
return strings.Split(os.ExpandEnv(platformCommand.Command), " ")
}
}
return command
}
// PrepareCommand takes a Plugin.PlatformCommand.Command, a Plugin.Command and will applying the following processing:
// - If platformCommand is present, it will be searched first
// - If both OS and Arch match the current platform, search will stop and the command will be prepared for execution
// - If OS matches and there is no more specific match, the command will be prepared for execution
// - If no OS/Arch match is found, the default command will be prepared for execution
// - If no command is present and no matches are found in platformCommand, will exit with an error
//
// It merges extraArgs into any arguments supplied in the plugin. It
// returns the name of the command and an args array.
//
// The result is suitable to pass to exec.Command.
func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) {
var parts []string
platCmdLen := len(p.Metadata.PlatformCommand)
if platCmdLen > 0 {
parts = getPlatformCommand(p.Metadata.PlatformCommand)
}
if platCmdLen == 0 || parts == nil {
parts = strings.Split(os.ExpandEnv(p.Metadata.Command), " ")
}
if len(parts) == 0 || parts[0] == "" {
return "", nil, fmt.Errorf("No plugin command is applicable")
}
main := parts[0]
baseArgs := []string{}
if len(parts) > 1 {
baseArgs = parts[1:]
}
if !p.Metadata.IgnoreFlags {
baseArgs = append(baseArgs, extraArgs...)
}
return main, baseArgs, nil
} }
// LoadDir loads a plugin from the given directory. // FindAll returns a list of executables that can be used as helm plugins.
func LoadDir(dirname string) (*Plugin, error) { func FindAll(plugdirs string) ([]*Plugin, error) {
data, err := ioutil.ReadFile(filepath.Join(dirname, pluginFileName)) found := []*Plugin{}
if err != nil {
return nil, err
}
plug := &Plugin{Dir: dirname} // Let's get all UNIXy and allow path separators
if err := yaml.Unmarshal(data, &plug.Metadata); err != nil { for _, dir := range filepath.SplitList(plugdirs) {
return nil, err matches, err := LoadAll(dir)
if err != nil {
return found, err
}
found = append(found, matches...)
} }
return plug, nil return found, nil
} }
// LoadAll loads all plugins found beneath the base directory. // LoadAll loads all plugins found beneath the base directory that are executable.
// //
// This scans only one directory level. // This scans only one directory level deep.
func LoadAll(basedir string) ([]*Plugin, error) { func LoadAll(basedir string) ([]*Plugin, error) {
plugins := []*Plugin{} plugins := []*Plugin{}
// We want basedir/*/plugin.yaml // We want basedir/helm-*
scanpath := filepath.Join(basedir, "*", pluginFileName) scanpath := filepath.Join(basedir, PluginNamePrefix+"*")
matches, err := filepath.Glob(scanpath) matches, err := filepath.Glob(scanpath)
if err != nil { if err != nil {
return plugins, err return plugins, err
} }
if matches == nil { for i := range matches {
return plugins, nil if isExec, err := osutil.IsExecutable(matches[i]); err == nil && !isExec {
} // swap the element to delete with the one at the end of the slice, then return n-1 elements
matches[len(matches)-1], matches[i] = matches[i], matches[len(matches)-1]
for _, yaml := range matches { matches = matches[:len(matches)-1]
dir := filepath.Dir(yaml) } else if err != nil {
p, err := LoadDir(dir) return nil, fmt.Errorf("error: unable to identify %s as an executable file: %v", matches[i], err)
if err != nil {
return plugins, err
} }
plugins = append(plugins, p)
}
return plugins, nil
}
// FindPlugins returns a list of YAML files that describe plugins. plugins = append(plugins, &Plugin{
func FindPlugins(plugdirs string) ([]*Plugin, error) { Name: strings.TrimPrefix(filepath.Base(matches[i]), PluginNamePrefix),
found := []*Plugin{} Dir: filepath.Dir(matches[i]),
// 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
}
found = append(found, matches...)
} }
return found, nil
return plugins, nil
} }
// SetupPluginEnv prepares os.Env for plugins. It operates on os.Env because // SetupPluginEnv prepares os.Env for plugins. It operates on os.Env because
// the plugin subsystem itself needs access to the environment variables // the plugin subsystem itself needs access to the environment variables
// created here. // created here.
func SetupPluginEnv(settings helm_env.EnvSettings, func SetupPluginEnv(settings cli.EnvSettings, shortName, base string) {
shortName, base string) {
for key, val := range map[string]string{ for key, val := range map[string]string{
"HELM_PLUGIN_NAME": shortName, "HELM_PLUGIN_NAME": shortName,
"HELM_PLUGIN_DIR": base, "HELM_PLUGIN_DIR": base,

Loading…
Cancel
Save