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/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index f5cd3efb7..cf5c72f74 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,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. diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index 6327f32e9..9885ffd3c 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -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)