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 // PrepareCommand uses os.ExpandEnv and expects the
// setupEnv vars. // setupEnv vars.
plugin.SetupPluginEnv(settings, md.Name, plug.Dir) 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 := exec.Command(main, argv...)
prog.Env = os.Environ() 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: Here is a plugin YAML for a plugin that adds support for Keybase operations:
``` ```
name: "keybase" name: "last"
version: "0.1.0" version: "0.1.0"
usage: "Integrate Keybase.io tools with Helm" usage: "get the last release name"
description: |- description: "get the last release name""
This plugin provides Keybase services to Helm.
ignoreFlags: false 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 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` plugin is called with `helm myplugin --foo` and `ignoreFlags: true`, then `--foo`
is silently discarded. is silently discarded.
Finally, and most importantly, `command` is the command that this plugin will Finally, and most importantly, `platformCommand` or `command` is the command
execute when it is called. Environment variables are interpolated before the plugin that this plugin will execute when it is called. The `platformCommand` section
is executed. The pattern above illustrates the preferred way to indicate where defines the OS/Architecture specific variations of a command. The following
the plugin program lives. 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: There are some strategies for working with plugin commands:
- If a plugin includes an executable, the executable for a `command:` should be - If a plugin includes an executable, the executable for a
packaged in the plugin directory. `platformCommand:` or a `command:` should be packaged in the plugin directory.
- The `command:` line will have any environment variables expanded before - The `platformCommand:` or `command:` line will have any environment
execution. `$HELM_PLUGIN_DIR` will point to the plugin directory. 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. - 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 - Helm injects lots of configuration into environment variables. Take a look at
the environment to see what information is available. the environment to see what information is available.

@ -16,9 +16,11 @@ limitations under the License.
package plugin // import "k8s.io/helm/pkg/plugin" package plugin // import "k8s.io/helm/pkg/plugin"
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
helm_env "k8s.io/helm/pkg/helm/environment" helm_env "k8s.io/helm/pkg/helm/environment"
@ -38,6 +40,13 @@ type Downloaders struct {
Command string `json:"command"` 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. // Metadata describes a plugin.
// //
// This is the plugin equivalent of a chart.Metadata. // 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 // Note that command is not executed in a shell. To do so, we suggest
// pointing the command to a shell script. // 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 // IgnoreFlags ignores any flags passed in from Helm
// //
@ -87,14 +104,47 @@ type Plugin struct {
Dir string 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 // It merges extraArgs into any arguments supplied in the plugin. It
// returns the name of the command and an args array. // returns the name of the command and an args array.
// //
// The result is suitable to pass to exec.Command. // The result is suitable to pass to exec.Command.
func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string) { func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) {
parts := strings.Split(os.ExpandEnv(p.Metadata.Command), " ") 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] main := parts[0]
baseArgs := []string{} baseArgs := []string{}
if len(parts) > 1 { if len(parts) > 1 {
@ -103,7 +153,7 @@ func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string) {
if !p.Metadata.IgnoreFlags { if !p.Metadata.IgnoreFlags {
baseArgs = append(baseArgs, extraArgs...) baseArgs = append(baseArgs, extraArgs...)
} }
return main, baseArgs return main, baseArgs, nil
} }
// LoadDir loads a plugin from the given directory. // LoadDir loads a plugin from the given directory.

@ -17,20 +17,15 @@ package plugin // import "k8s.io/helm/pkg/plugin"
import ( import (
"reflect" "reflect"
"runtime"
"testing" "testing"
) )
func TestPrepareCommand(t *testing.T) { func checkCommand(p *Plugin, extraArgs []string, osStrCmp string, t *testing.T) {
p := &Plugin{ cmd, args, err := p.PrepareCommand(extraArgs)
Dir: "/tmp", // Unused if err != nil {
Metadata: &Metadata{ t.Errorf(err.Error())
Name: "test",
Command: "echo -n foo",
},
} }
argv := []string{"--debug", "--foo", "bar"}
cmd, args := p.PrepareCommand(argv)
if cmd != "echo" { if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd) t.Errorf("Expected echo, got %q", cmd)
} }
@ -39,7 +34,7 @@ func TestPrepareCommand(t *testing.T) {
t.Errorf("expected 5 args, got %d", l) 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++ { for i := 0; i < len(args); i++ {
if expect[i] != args[i] { if expect[i] != args[i] {
t.Errorf("Expected arg=%q, got %q", 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 // Test with IgnoreFlags. This should omit --debug, --foo, bar
p.Metadata.IgnoreFlags = true 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" { if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd) t.Errorf("Expected echo, got %q", cmd)
} }
if l := len(args); l != 2 { if l := len(args); l != 2 {
t.Errorf("expected 2 args, got %d", l) t.Errorf("expected 2 args, got %d", l)
} }
expect = []string{"-n", "foo"} expect = []string{"-n", osStrCmp}
for i := 0; i < len(args); i++ { for i := 0; i < len(args); i++ {
if expect[i] != args[i] { if expect[i] != args[i] {
t.Errorf("Expected arg=%q, got %q", 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) { func TestLoadDir(t *testing.T) {
dirname := "testdata/plugdir/hello" dirname := "testdata/plugdir/hello"
plug, err := LoadDir(dirname) plug, err := LoadDir(dirname)

Loading…
Cancel
Save