Add logic for platform specific commands to plugins

Logic and unit tests added

Signed-off-by: Martin Hickey <martin.hickey@ie.ibm.com>
pull/5176/head
Martin Hickey 7 years ago
parent dee2a1a000
commit 66059c9a63

@ -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()

@ -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,43 @@ 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 {
for _, platformCommand := range platformCommands {
if strings.EqualFold(platformCommand.OperatingSystem, runtime.GOOS) && strings.EqualFold(platformCommand.Architecture, runtime.GOARCH) {
return strings.Split(os.ExpandEnv(platformCommand.Command), " ")
}
}
return nil
}
// 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 +149,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,6 +17,7 @@ package plugin // import "k8s.io/helm/pkg/plugin"
import (
"reflect"
"runtime"
"testing"
)
@ -30,7 +31,10 @@ func TestPrepareCommand(t *testing.T) {
}
argv := []string{"--debug", "--foo", "bar"}
cmd, args := p.PrepareCommand(argv)
cmd, args, err := p.PrepareCommand(argv)
if err != nil {
t.Errorf(err.Error())
}
if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd)
}
@ -48,7 +52,10 @@ 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(argv)
if err != nil {
t.Errorf(err.Error())
}
if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd)
}
@ -63,6 +70,105 @@ func TestPrepareCommand(t *testing.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"}
cmd, args, err := p.PrepareCommand(argv)
if err != nil {
t.Errorf(err.Error())
}
if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd)
}
if l := len(args); l != 5 {
t.Errorf("expected 5 args, got %d", l)
}
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"
}
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])
}
}
// Test with IgnoreFlags. This should omit --debug, --foo, bar
p.Metadata.IgnoreFlags = true
cmd, args, err = p.PrepareCommand(argv)
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", osStrCmp}
for i := 0; i < len(args); i++ {
if expect[i] != args[i] {
t.Errorf("Expected arg=%q, got %q", expect[i], args[i])
}
}
}
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: "linux", Architecture: "no-arch", 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