feat(helm): add plugin system backend

This adds a backend for a plugin system.

Closes #1572
pull/1573/head
Matt Butcher 8 years ago
parent 04f203d2d0
commit fad755e7ae
No known key found for this signature in database
GPG Key ID: DCD5F5E5EF32C345

@ -51,6 +51,7 @@ including installing pre-releases.
- [Kubernetes Distribution Notes](docs/kubernetes_distros.md)
- [Frequently Asked Questions](docs/install_faq.md)
- [Using Helm](docs/using_helm.md)
- [Plugins](docs/plugins.md)
- [Developing Charts](docs/charts.md)
- [Chart Lifecycle Hooks](docs/charts_hooks.md)
- [Chart Tips and Tricks](docs/charts_tips_and_tricks.md)

@ -31,6 +31,7 @@ import (
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/kube"
)
@ -91,7 +92,7 @@ func newRootCmd(out io.Writer) *cobra.Command {
p.StringVar(&helmHome, "home", home, "location of your Helm config. Overrides $HELM_HOME")
p.StringVar(&tillerHost, "host", thost, "address of tiller. Overrides $HELM_HOST")
p.StringVar(&kubeContext, "kube-context", "", "name of the kubeconfig context to use")
p.BoolVarP(&flagDebug, "debug", "", false, "enable verbose output")
p.BoolVar(&flagDebug, "debug", false, "enable verbose output")
// Tell gRPC not to log to console.
grpclog.SetLogger(log.New(ioutil.Discard, "", log.LstdFlags))
@ -124,6 +125,10 @@ func newRootCmd(out io.Writer) *cobra.Command {
// Deprecated
rup,
)
// Find and add plugins
loadPlugins(cmd, helmpath.Home(homePath()), out)
return cmd
}
@ -141,7 +146,7 @@ func setupConnection(c *cobra.Command, args []string) error {
return err
}
tillerHost = fmt.Sprintf(":%d", tunnel.Local)
tillerHost = fmt.Sprintf("localhost:%d", tunnel.Local)
if flagDebug {
fmt.Printf("Created tunnel using local port: '%d'\n", tunnel.Local)
}
@ -151,6 +156,7 @@ func setupConnection(c *cobra.Command, args []string) error {
if flagDebug {
fmt.Printf("SERVER: %q\n", tillerHost)
}
// Plugin support.
return nil
}

@ -241,7 +241,7 @@ func tempHelmHome(t *testing.T) (string, error) {
//
// t is used only for logging.
func ensureTestHome(home helmpath.Home, t *testing.T) error {
configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Starters()}
configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Plugins(), home.Starters()}
for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil {
if err := os.MkdirAll(p, 0755); err != nil {

@ -67,3 +67,8 @@ func (h Home) LocalRepository(paths ...string) string {
frag := append([]string{string(h), "repository/local"}, paths...)
return filepath.Join(frag...)
}
// Plugins returns the path to the plugins directory.
func (h Home) Plugins() string {
return filepath.Join(string(h), "plugins")
}

@ -143,7 +143,7 @@ func (i *initCmd) run() error {
//
// If $HELM_HOME does not exist, this function will create it.
func ensureHome(home helmpath.Home, out io.Writer) error {
configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Starters()}
configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Plugins(), home.Starters()}
for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil {
fmt.Fprintf(out, "Creating %s \n", p)

@ -0,0 +1,180 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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"
"os/exec"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/plugin"
)
const pluginEnvVar = "HELM_PLUGIN"
// loadPlugins loads plugins into the command list.
//
// This follows a different pattern than the other commands because it has
// to inspect its environment and then add commands to the base command
// as it finds them.
func loadPlugins(baseCmd *cobra.Command, home helmpath.Home, out io.Writer) {
plugdirs := os.Getenv(pluginEnvVar)
if plugdirs == "" {
plugdirs = home.Plugins()
}
found, err := findPlugins(plugdirs)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err)
return
}
// Now we create commands for all of these.
for _, plug := range found {
plug := plug
md := plug.Metadata
if md.Usage == "" {
md.Usage = fmt.Sprintf("the %q plugin", md.Name)
}
c := &cobra.Command{
Use: md.Name,
Short: md.Usage,
Long: md.Description,
RunE: func(cmd *cobra.Command, args []string) error {
k, u := manuallyProcessArgs(args)
if err := cmd.ParseFlags(k); err != nil {
return err
}
// Call setupEnv before PrepareCommand because
// PrepareCommand uses os.ExpandEnv and expects the
// setupEnv vars.
setupEnv(md.Name, plug.Dir, plugdirs, home)
main, argv := plug.PrepareCommand(u)
prog := exec.Command(main, argv...)
prog.Env = os.Environ()
prog.Stdout = out
prog.Stderr = os.Stderr
if err := prog.Run(); err != nil {
eerr := err.(*exec.ExitError)
os.Stderr.Write(eerr.Stderr)
return fmt.Errorf("plugin %q exited with error", md.Name)
}
return nil
},
// This passes all the flags to the subcommand.
DisableFlagParsing: true,
}
if md.UseTunnel {
c.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// Parse the parent flag, but not the local flags.
k, _ := manuallyProcessArgs(args)
if err := c.Parent().ParseFlags(k); err != nil {
return err
}
return setupConnection(cmd, args)
}
}
// TODO: Make sure a command with this name does not already exist.
baseCmd.AddCommand(c)
}
}
// manuallyProcessArgs processes an arg array, removing special args.
//
// Returns two sets of args: known and unknown (in that order)
func manuallyProcessArgs(args []string) ([]string, []string) {
known := []string{}
unknown := []string{}
kvargs := []string{"--host", "--kube-context", "--home"}
knownArg := func(a string) bool {
for _, pre := range kvargs {
if strings.HasPrefix(a, pre+"=") {
return true
}
}
return false
}
for i := 0; i < len(args); i++ {
switch a := args[i]; a {
case "--debug":
known = append(known, a)
case "--host", "--kube-context", "--home":
known = append(known, a, args[i+1])
i++
default:
if knownArg(a) {
known = append(known, a)
continue
}
unknown = append(unknown, a)
}
}
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
}
// setupEnv prepares os.Env for plugins. It operates on os.Env because
// the plugin subsystem itself needs access to the environment variables
// created here.
func setupEnv(shortname, base, plugdirs string, home helmpath.Home) {
// Set extra env vars:
for key, val := range map[string]string{
"HELM_PLUGIN_NAME": shortname,
"HELM_PLUGIN_DIR": base,
"HELM_BIN": os.Args[0],
// Set vars that may not have been set, and save client the
// trouble of re-parsing.
pluginEnvVar: plugdirs,
homeEnvVar: home.String(),
// Set vars that convey common information.
"HELM_PATH_REPOSITORY": home.Repository(),
"HELM_PATH_REPOSITORY_FILE": home.RepositoryFile(),
"HELM_PATH_CACHE": home.Cache(),
"HELM_PATH_LOCAL_REPOSITORY": home.LocalRepository(),
//"HELM_PATH_STARTER": home.Starter(),
"TILLER_HOST": tillerHost,
} {
os.Setenv(key, val)
}
}

@ -0,0 +1,125 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 (
"bytes"
"os"
"strings"
"testing"
"k8s.io/helm/cmd/helm/helmpath"
"github.com/spf13/cobra"
)
func TestManuallyProcessArgs(t *testing.T) {
input := []string{
"--debug",
"--foo",
"bar",
"--host",
"example.com",
"--kube-context",
"test1",
"--home=/tmp",
"command",
}
expectKnown := []string{
"--debug", "--host", "example.com", "--kube-context", "test1", "--home=/tmp",
}
expectUnknown := []string{
"--foo", "bar", "command",
}
known, unknown := manuallyProcessArgs(input)
for i, k := range known {
if k != expectKnown[i] {
t.Errorf("expected known flag %d to be %q, got %q", i, expectKnown[i], k)
}
}
for i, k := range unknown {
if k != expectUnknown[i] {
t.Errorf("expected unknown flag %d to be %q, got %q", i, expectUnknown[i], k)
}
}
}
func TestLoadPlugins(t *testing.T) {
// Set helm home to point to testdata
old := helmHome
helmHome = "testdata/helmhome"
defer func() {
helmHome = old
}()
hh := helmpath.Home(homePath())
out := bytes.NewBuffer(nil)
cmd := &cobra.Command{}
loadPlugins(cmd, hh, out)
envs := strings.Join([]string{
"fullenv",
"fullenv.yaml",
hh.Plugins(),
hh.String(),
hh.Repository(),
hh.RepositoryFile(),
hh.Cache(),
hh.LocalRepository(),
os.Args[0],
}, "\n")
// Test that the YAML file was correctly converted to a command.
tests := []struct {
use string
short string
long string
expect string
args []string
}{
{"args", "echo args", "This echos args", "-a -b -c\n", []string{"-a", "-b", "-c"}},
{"echo", "echo stuff", "This echos stuff", "hello\n", []string{}},
{"env", "env stuff", "show the env", hh.String() + "\n", []string{}},
{"fullenv", "show env vars", "show all env vars", envs + "\n", []string{}},
}
plugins := cmd.Commands()
for i := 0; i < len(plugins); i++ {
out.Reset()
tt := tests[i]
pp := plugins[i]
if pp.Use != tt.use {
t.Errorf("%d: Expected Use=%q, got %q", i, tt.use, pp.Use)
}
if pp.Short != tt.short {
t.Errorf("%d: Expected Use=%q, got %q", i, tt.short, pp.Short)
}
if pp.Long != tt.long {
t.Errorf("%d: Expected Use=%q, got %q", i, tt.long, pp.Long)
}
if err := pp.RunE(pp, tt.args); err != nil {
t.Errorf("Error running %s: %s", tt.use, err)
}
if out.String() != tt.expect {
t.Errorf("Expected %s to output:\n%s\ngot\n%s", tt.use, tt.expect, out.String())
}
}
}

@ -0,0 +1,2 @@
#!/bin/bash
echo $*

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

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

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

@ -0,0 +1,10 @@
#!/bin/sh
echo $HELM_PLUGIN_SHORTNAME
echo $HELM_PLUGIN_NAME
echo $HELM_PLUGIN
echo $HELM_HOME
echo $HELM_PATH_REPOSITORY
echo $HELM_PATH_REPOSITORY_FILE
echo $HELM_PATH_CACHE
echo $HELM_PATH_LOCAL_REPOSITORY
echo $HELM_BIN

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

@ -0,0 +1,176 @@
# The Helm Plugins Guide
Helm 2.1.0 introduced the concept of a client-side Helm _plugin_. A plugin is a
tool that can be accessed through the `helm` CLI, but which is not part of the
built-in Helm codebase.
This guide explains how to use and create plugins.
## An Overview
Helm plugins are add-on tools that integrate seemlessly 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:
- They can be added and removed from a Helm installation without impacting the
core Helm tool.
- 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,
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
A Helm plugin management system is in the works. But in the short term, plugins
are installed by copying the plugin directory into `$(helm home)/plugins`.
```console
$ cp -a myplugin/ $(helm home)/plugins/
```
If you have a plugin tar distribution, simply untar the plugin into the
`$(helm home)/plugins` directory.
## Building Plugins
In many ways, a plugin is similar to a chart. Each plugin has a top-level
directory, and then a `plugin.yaml` file.
```
$(helm home)/plugins/
|- keybase/
|
|- plugin.yaml
|- keybase.sh
```
In the example above, the `keybase` plugin is contained inside of a directory
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`.
Here is a plugin YAML for a plugin that adds support for Keybase operations:
```
name: "keybase"
version: "0.1.0"
usage: "Integreate Keybase.io tools with Helm"
description: |-
This plugin provides Keybase services to Helm.
ignoreFlags: false
useTunnel: false
command: "$HELM_PLUGIN_DIR/keybase.sh"
```
The `name` is the name of the plugin. When Helm executes it 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
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.
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.
There are some strategies for working with plugin commands:
- If a plugin includes an executable, the executable for a `command:` should be
packaged in the plugin directory.
- The `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`.
## Environment Variables
When Helm executes a plugin, it passes the outer environment to the plugin, and
also injects some additional environment variables.
Variables like `KUBECONFIG` are set for the plugin if they are set in the
outer environment.
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_DIR`: The directory that contains the plugin.
- `HELM_BIN`: The path to the `helm` command (as executed by the user).
- `HELM_HOME`: The path to the Helm home.
- `HELM_PATH_*`: Paths to important Helm files and directories are stored in
environment variables prefixed by `HELM_PATH`.
- `TILLER_HOST`: The `domain:port` to Tiller. If a tunnel is created, this
will point to the local endpoint for the tunnel. Otherwise, it will point
to `$HELM_HOST`, `--host`, or the default host (according to Helm's rules of
precedence).
While `HELM_HOST` _may_ be set, there is no guarantee that it will point to the
correct Tiller instance. This is done to allow plugin developer to access
`HELM_HOST` in its raw state when the plugin itself needs to manually configure
a connection.
## A Note on `useTunnel`
If a plugin specifies `useTunnel: true`, Helm will do the following (in order):
1. Parse global flags and the environment
2. Create the tunnel
3. Set `TILLER_HOST`
4. Execute the plugin
5. Close the tunnel
The tunnel is removed as soon as the `command` returns. So, for example, a
command cannot background a process and assume that that process will be able
to use the tunnel.
## A Note on Flag Parsing
When executing a plugin, Helm will parse global flags for its own use, but pass
all flags to the plugin.
Plugins MUST NOT produce an error for the following flags:
- `--debug`
- `--home`
- `--host`
- `--kube-context`
- `-h`
- `--help`
Plugins _should_ display help text and then exit for `-h` and `--help`. In all
other cases, plugins may simply ignore the flags.

@ -0,0 +1,135 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 plugin
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/ghodss/yaml"
)
// PluginFileName is the name of a plugin file.
const PluginFileName = "plugin.yaml"
// 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.
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"`
// UseTunnel indicates that this command needs a tunnel.
// Setting this will cause a number of side effects, such as the
// automatic setting of HELM_HOST.
UseTunnel bool `json:"useTunnel"`
}
// Plugin represents a plugin.
type Plugin struct {
// Metadata is a parsed representation of a plugin.yaml
Metadata *Metadata
// Dir is the string path to the directory that holds the plugin.
Dir string
}
// PrepareCommand takes a Plugin.Command and prepares it for execution.
//
// 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) {
parts := strings.Split(os.ExpandEnv(p.Metadata.Command), " ")
main := parts[0]
baseArgs := []string{}
if len(parts) > 1 {
baseArgs = parts[1:]
}
if !p.Metadata.IgnoreFlags {
baseArgs = append(baseArgs, extraArgs...)
}
return main, baseArgs
}
// LoadDir loads a plugin from the given directory.
func LoadDir(dirname string) (*Plugin, error) {
data, err := ioutil.ReadFile(filepath.Join(dirname, PluginFileName))
if err != nil {
return nil, err
}
plug := &Plugin{Dir: dirname}
if err := yaml.Unmarshal(data, &plug.Metadata); err != nil {
return nil, err
}
return plug, nil
}
// LoadAll loads all plugins found beneath the base directory.
//
// This scans only one directory level.
func LoadAll(basedir string) ([]*Plugin, error) {
plugins := []*Plugin{}
// We want basedir/*/plugin.yaml
scanpath := filepath.Join(basedir, "*", PluginFileName)
matches, err := filepath.Glob(scanpath)
if err != nil {
return plugins, err
}
if matches == nil {
return plugins, nil
}
for _, yaml := range matches {
dir := filepath.Dir(yaml)
p, err := LoadDir(dir)
if err != nil {
return plugins, err
}
plugins = append(plugins, p)
}
return plugins, nil
}

@ -0,0 +1,117 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 plugin
import (
"reflect"
"testing"
)
func TestPrepareCommand(t *testing.T) {
p := &Plugin{
Dir: "/tmp", // Unused
Metadata: &Metadata{
Name: "test",
Command: "echo -n foo",
},
}
argv := []string{"--debug", "--foo", "bar"}
cmd, args := p.PrepareCommand(argv)
if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd)
}
if l := len(args); l != 5 {
t.Errorf("expected 5 args, got %d", l)
}
expect := []string{"-n", "foo", "--debug", "--foo", "bar"}
for i := 0; i < len(args); i++ {
if expect[i] != args[i] {
t.Errorf("Expected arg=%q, got %q", expect[i], args[i])
}
}
// Test with IgnoreFlags. This should omit --debug, --foo, bar
p.Metadata.IgnoreFlags = true
cmd, args = p.PrepareCommand(argv)
if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd)
}
if l := len(args); l != 2 {
t.Errorf("expected 2 args, got %d", l)
}
expect = []string{"-n", "foo"}
for i := 0; i < len(args); i++ {
if expect[i] != args[i] {
t.Errorf("Expected arg=%q, got %q", expect[i], args[i])
}
}
}
func TestLoadDir(t *testing.T) {
dirname := "testdata/plugdir/hello"
plug, err := LoadDir(dirname)
if err != nil {
t.Fatalf("error loading Hello plugin: %s", err)
}
if plug.Dir != dirname {
t.Errorf("Expected dir %q, got %q", dirname, plug.Dir)
}
expect := Metadata{
Name: "hello",
Version: "0.1.0",
Usage: "usage",
Description: "description",
Command: "$HELM_PLUGIN_SELF/hello.sh",
UseTunnel: true,
IgnoreFlags: true,
}
if reflect.DeepEqual(expect, plug.Metadata) {
t.Errorf("Expected name %v, got %v", expect, plug.Metadata)
}
}
func TestLoadAll(t *testing.T) {
// Verify that empty dir loads:
if plugs, err := LoadAll("testdata"); err != nil {
t.Fatalf("error loading dir with no plugins: %s", err)
} else if len(plugs) > 0 {
t.Fatalf("expected empty dir to have 0 plugins")
}
basedir := "testdata/plugdir"
plugs, err := LoadAll(basedir)
if err != nil {
t.Fatalf("Could not load %q: %s", basedir, err)
}
if l := len(plugs); l != 2 {
t.Fatalf("expected 2 plugins, found %d", l)
}
if plugs[0].Metadata.Name != "echo" {
t.Errorf("Expected first plugin to be echo, got %q", plugs[0].Metadata.Name)
}
if plugs[1].Metadata.Name != "hello" {
t.Errorf("Expected second plugin to be hello, got %q", plugs[1].Metadata.Name)
}
}

@ -0,0 +1,6 @@
name: "echo"
version: "1.2.3"
usage: "echo something"
description: |-
This is a testing fixture.
command: "echo Hello"

@ -0,0 +1,13 @@
#!/bin/bash
echo "Hello from a Helm plugin"
echo "PARAMS"
echo $*
echo "ENVIRONMENT"
echo $TILLER_HOST
echo $HELM_HOME
$HELM_BIN --host $TILLER_HOST ls --all

@ -0,0 +1,8 @@
name: "hello"
version: "0.1.0"
usage: "usage"
description: |-
description
command: "$HELM_PLUGIN_SELF/helm-hello"
useTunnel: true
ignoreFlags: true
Loading…
Cancel
Save