diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index 124ebb5c1..309fad3c1 100644 --- a/cmd/helm/load_plugins.go +++ b/cmd/helm/load_plugins.go @@ -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() diff --git a/docs/plugins.md b/docs/plugins.md index 185257bb8..aac26a0b4 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -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. diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index f5cd3efb7..546f66744 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -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. diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index 6327f32e9..de707c281 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -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)