Feature(Plugins): Enable platform specific commands (#5176)

* Add logic for platform specific commands to plugins
* Add plugins doc updated to incorporate platform specific commands
* Add condition for os match:  If OS matches and there is no more specific match, the command
will be executed
pull/5188/head
Martin Hickey 7 years ago committed by Adam Reese
parent dee2a1a000
commit 86d8596763

@ -78,7 +78,11 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
// PrepareCommand uses os.ExpandEnv and expects the
// setupEnv vars.
plugin.SetupPluginEnv(settings, md.Name, plug.Dir)
main, argv := plug.PrepareCommand(u)
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.Env = os.Environ()

@ -64,13 +64,22 @@ 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"
name: "last"
version: "0.1.0"
usage: "Integrate Keybase.io tools with Helm"
description: |-
This plugin provides Keybase services to Helm.
usage: "get the last release name"
description: "get the last release name""
ignoreFlags: false
command: "$HELM_PLUGIN_DIR/keybase.sh"
command: "$HELM_BIN --host $TILLER_HOST list --short --max 1 --date -r"
platformCommand:
- os: linux
arch: i386
command: "$HELM_BIN list --short --max 1 --date -r"
- os: linux
arch: amd64
command: "$HELM_BIN list --short --max 1 --date -r"
- os: windows
arch: amd64
command: "$HELM_BIN list --short --max 1 --date -r"
```
The `name` is the name of the plugin. When Helm executes it plugin, this is the
@ -91,17 +100,31 @@ 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, `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.
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 `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.
- 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.

@ -16,9 +16,11 @@ limitations under the License.
package plugin // import "k8s.io/helm/pkg/plugin"
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
helm_env "k8s.io/helm/pkg/helm/environment"
@ -38,6 +40,13 @@ type Downloaders struct {
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.
@ -62,7 +71,15 @@ type Metadata struct {
//
// 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"`
//
// 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
//
@ -87,14 +104,47 @@ type Plugin struct {
Dir string
}
// PrepareCommand takes a Plugin.Command and prepares it for execution.
// 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) {
parts := strings.Split(os.ExpandEnv(p.Metadata.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 parts == nil || len(parts) == 0 || parts[0] == "" {
return "", nil, fmt.Errorf("No plugin command is applicable")
}
main := parts[0]
baseArgs := []string{}
if len(parts) > 1 {
@ -103,7 +153,7 @@ func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string) {
if !p.Metadata.IgnoreFlags {
baseArgs = append(baseArgs, extraArgs...)
}
return main, baseArgs
return main, baseArgs, nil
}
// LoadDir loads a plugin from the given directory.

@ -17,20 +17,15 @@ package plugin // import "k8s.io/helm/pkg/plugin"
import (
"reflect"
"runtime"
"testing"
)
func TestPrepareCommand(t *testing.T) {
p := &Plugin{
Dir: "/tmp", // Unused
Metadata: &Metadata{
Name: "test",
Command: "echo -n foo",
},
func checkCommand(p *Plugin, extraArgs []string, osStrCmp string, t *testing.T) {
cmd, args, err := p.PrepareCommand(extraArgs)
if err != nil {
t.Errorf(err.Error())
}
argv := []string{"--debug", "--foo", "bar"}
cmd, args := p.PrepareCommand(argv)
if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd)
}
@ -39,7 +34,7 @@ func TestPrepareCommand(t *testing.T) {
t.Errorf("expected 5 args, got %d", l)
}
expect := []string{"-n", "foo", "--debug", "--foo", "bar"}
expect := []string{"-n", osStrCmp, "--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])
@ -48,14 +43,17 @@ func TestPrepareCommand(t *testing.T) {
// Test with IgnoreFlags. This should omit --debug, --foo, bar
p.Metadata.IgnoreFlags = true
cmd, args = p.PrepareCommand(argv)
cmd, args, err = p.PrepareCommand(extraArgs)
if err != nil {
t.Errorf(err.Error())
}
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"}
expect = []string{"-n", osStrCmp}
for i := 0; i < len(args); i++ {
if expect[i] != args[i] {
t.Errorf("Expected arg=%q, got %q", expect[i], args[i])
@ -63,6 +61,109 @@ func TestPrepareCommand(t *testing.T) {
}
}
func TestPrepareCommand(t *testing.T) {
p := &Plugin{
Dir: "/tmp", // Unused
Metadata: &Metadata{
Name: "test",
Command: "echo -n foo",
},
}
argv := []string{"--debug", "--foo", "bar"}
checkCommand(p, argv, "foo", t)
}
func TestPlatformPrepareCommand(t *testing.T) {
p := &Plugin{
Dir: "/tmp", // Unused
Metadata: &Metadata{
Name: "test",
Command: "echo -n os-arch",
PlatformCommand: []PlatformCommand{
{OperatingSystem: "linux", Architecture: "i386", Command: "echo -n linux-i386"},
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo -n linux-amd64"},
{OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"},
},
},
}
argv := []string{"--debug", "--foo", "bar"}
var osStrCmp string
os := runtime.GOOS
arch := runtime.GOARCH
if os == "linux" && arch == "i386" {
osStrCmp = "linux-i386"
} else if os == "linux" && arch == "amd64" {
osStrCmp = "linux-amd64"
} else if os == "windows" && arch == "amd64" {
osStrCmp = "win-64"
} else {
osStrCmp = "os-arch"
}
checkCommand(p, argv, osStrCmp, t)
}
func TestPartialPlatformPrepareCommand(t *testing.T) {
p := &Plugin{
Dir: "/tmp", // Unused
Metadata: &Metadata{
Name: "test",
Command: "echo -n os-arch",
PlatformCommand: []PlatformCommand{
{OperatingSystem: "linux", Architecture: "i386", Command: "echo -n linux-i386"},
{OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"},
},
},
}
argv := []string{"--debug", "--foo", "bar"}
var osStrCmp string
os := runtime.GOOS
arch := runtime.GOARCH
if os == "linux" {
osStrCmp = "linux-i386"
} else if os == "windows" && arch == "amd64" {
osStrCmp = "win-64"
} else {
osStrCmp = "os-arch"
}
checkCommand(p, argv, osStrCmp, t)
}
func TestNoPrepareCommand(t *testing.T) {
p := &Plugin{
Dir: "/tmp", // Unused
Metadata: &Metadata{
Name: "test",
},
}
argv := []string{"--debug", "--foo", "bar"}
_, _, err := p.PrepareCommand(argv)
if err == nil {
t.Errorf("Expected error to be returned")
}
}
func TestNoMatchPrepareCommand(t *testing.T) {
p := &Plugin{
Dir: "/tmp", // Unused
Metadata: &Metadata{
Name: "test",
PlatformCommand: []PlatformCommand{
{OperatingSystem: "no-os", Architecture: "amd64", Command: "echo -n linux-i386"},
},
},
}
argv := []string{"--debug", "--foo", "bar"}
_, _, err := p.PrepareCommand(argv)
if err == nil {
t.Errorf("Expected error to be returned")
}
}
func TestLoadDir(t *testing.T) {
dirname := "testdata/plugdir/hello"
plug, err := LoadDir(dirname)

Loading…
Cancel
Save