mirror of https://github.com/helm/helm
This adds a backend for a plugin system. Closes #1572pull/1573/head
parent
04f203d2d0
commit
fad755e7ae
@ -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…
Reference in new issue