From 5790a0f34b80f9552a129f47e229479208c4640c Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Thu, 18 Apr 2024 18:17:39 +0100 Subject: [PATCH] feat: Added multi-platform plugin hook support Signed-off-by: Steve Hipwell --- cmd/helm/plugin.go | 21 +- pkg/plugin/hooks.go | 3 + pkg/plugin/plugin.go | 135 ++++-- pkg/plugin/plugin_test.go | 410 ++++++++++++++---- .../testdata/plugdir/good/hello/hello.ps1 | 3 + .../testdata/plugdir/good/hello/plugin.yaml | 21 +- 6 files changed, 468 insertions(+), 125 deletions(-) create mode 100644 pkg/plugin/testdata/plugdir/good/hello/hello.ps1 diff --git a/cmd/helm/plugin.go b/cmd/helm/plugin.go index 8e1044f54..97adbd142 100644 --- a/cmd/helm/plugin.go +++ b/cmd/helm/plugin.go @@ -47,19 +47,26 @@ func newPluginCmd(out io.Writer) *cobra.Command { // runHook will execute a plugin hook. func runHook(p *plugin.Plugin, event string) error { - hook := p.Metadata.Hooks[event] - if hook == "" { + var cmd string + var cmdArgs []string + + plugin.SetupPluginEnv(settings, p.Metadata.Name, p.Dir) + + command := p.Metadata.Hooks[event] + if command != "" { + cmd = "sh" + cmdArgs = []string{"-c", command} + } + + main, argv, err := plugin.PrepareCommands(p.Metadata.PlatformHooks[event], cmd, cmdArgs, false, []string{}) + if err != nil { return nil } - prog := exec.Command("sh", "-c", hook) - // TODO make this work on windows - // I think its ... ¯\_(ツ)_/¯ - // prog := exec.Command("cmd", "/C", p.Metadata.Hooks.Install()) + prog := exec.Command(main, argv...) debug("running %s hook: %s", event, prog) - plugin.SetupPluginEnv(settings, p.Metadata.Name, p.Dir) prog.Stdout, prog.Stderr = os.Stdout, os.Stderr if err := prog.Run(); err != nil { if eerr, ok := err.(*exec.ExitError); ok { diff --git a/pkg/plugin/hooks.go b/pkg/plugin/hooks.go index e3481515f..34d3163a4 100644 --- a/pkg/plugin/hooks.go +++ b/pkg/plugin/hooks.go @@ -25,5 +25,8 @@ const ( Update = "update" ) +// PlatformHooks is a map of events to a command for a particular operating system and architecture. +type PlatformHooks map[string][]PlatformCommand + // Hooks is a map of events to commands. type Hooks map[string]string diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 5bb743481..7c704e9b9 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -44,9 +44,10 @@ type Downloaders struct { // 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"` + OperatingSystem string `json:"os"` + Architecture string `json:"arch"` + Command string `json:"command"` + Args []string `json:"args"` } // Metadata describes a plugin. @@ -65,23 +66,32 @@ type Metadata struct { // Description is a long description shown in places like `helm help` Description string `json:"description"` - // Command is the command, as a single string. + // PlatformCommand is the plugin command, with a platform selector and support for args. // - // The command will be passed through environment expansion, so env vars can + // The command and args will be passed through environment expansion, so env vars can // be present in this command. Unless IgnoreFlags is set, this will // also merge the flags passed from Helm. // - // Note that command is not executed in a shell. To do so, we suggest + // Note that the command is not executed in a shell. To do so, we suggest // pointing the command to a shell script. // - // The following rules will apply to processing commands: - // - If platformCommand is present, it will be searched first + // The following rules will apply to processing platform 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"` + + // Command is the plugin command, as a single string. The command will be ignored if a valid PlatformCommand is found. + // + // The command will be passed through environment expansion, so env vars can + // be present in this command. Unless IgnoreFlags is set, this will + // also merge the flags passed from Helm. + // + // 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"` // IgnoreFlags ignores any flags passed in from Helm // @@ -90,7 +100,28 @@ type Metadata struct { // the `--debug` flag will be discarded. IgnoreFlags bool `json:"ignoreFlags"` - // Hooks are commands that will run on events. + // PlatformHooks are commands that will run on plugin events, with a platform selector and support for args. + // + // The command and args will be passed through environment expansion, so env vars can + // be present in the command. + // + // Note that the command is not executed in a shell. To do so, we suggest + // pointing the command to a shell script. + // + // The following rules will apply to processing platform commands: + // - If PlatformHooks 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 skip the event + PlatformHooks PlatformHooks `json:"platformHooks"` + + // Hooks are commands that will run on plugin events, as a single string. + // + // The command will be passed through environment expansion, so env vars can + // be present in this command. + // + // Note that the command is executed in the sh shell. Hooks Hooks // Downloaders field is used if the plugin supply downloader mechanism @@ -116,21 +147,29 @@ type Plugin struct { // - 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(cmds []PlatformCommand) []string { - var command []string +func getPlatformCommand(cmds []PlatformCommand) ([]string, []string) { + var command, args []string + eq := strings.EqualFold for _, c := range cmds { + if len(c.Architecture) > 0 && !eq(c.Architecture, runtime.GOARCH) { + continue + } + if eq(c.OperatingSystem, runtime.GOOS) { command = strings.Split(c.Command, " ") + args = c.Args } + if eq(c.OperatingSystem, runtime.GOOS) && eq(c.Architecture, runtime.GOARCH) { - return strings.Split(c.Command, " ") + return strings.Split(c.Command, " "), c.Args } } - return command + + return command, args } -// PrepareCommand takes a Plugin.PlatformCommand.Command, a Plugin.Command and will applying the following processing: +// PrepareCommands takes a []Plugin.PlatformCommand, 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 @@ -141,33 +180,71 @@ func getPlatformCommand(cmds []PlatformCommand) []string { // 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, error) { - var parts []string - platCmdLen := len(p.Metadata.PlatformCommand) - if platCmdLen > 0 { - parts = getPlatformCommand(p.Metadata.PlatformCommand) +func PrepareCommands(cmds []PlatformCommand, legacyCmd string, legacyCmdArgs []string, expandLegacyCmdArgs bool, extraArgs []string) (string, []string, error) { + var cmdParts, args []string + expandArgs := true + + cmdsLen := len(cmds) + if cmdsLen > 0 { + cmdParts, args = getPlatformCommand(cmds) } - if platCmdLen == 0 || parts == nil { - parts = strings.Split(p.Metadata.Command, " ") + if cmdsLen == 0 || cmdParts == nil { + cmdParts = strings.Split(legacyCmd, " ") + args = legacyCmdArgs + expandArgs = expandLegacyCmdArgs } - if len(parts) == 0 || parts[0] == "" { + if len(cmdParts) == 0 || cmdParts[0] == "" { return "", nil, fmt.Errorf("no plugin command is applicable") } - main := os.ExpandEnv(parts[0]) + main := os.ExpandEnv(cmdParts[0]) baseArgs := []string{} - if len(parts) > 1 { - for _, cmdpart := range parts[1:] { - cmdexp := os.ExpandEnv(cmdpart) - baseArgs = append(baseArgs, cmdexp) + if len(cmdParts) > 1 { + for _, cmdPart := range cmdParts[1:] { + if expandArgs { + baseArgs = append(baseArgs, os.ExpandEnv(cmdPart)) + } else { + baseArgs = append(baseArgs, cmdPart) + } } } - if !p.Metadata.IgnoreFlags { + + for _, arg := range args { + if expandArgs { + baseArgs = append(baseArgs, os.ExpandEnv(arg)) + } else { + baseArgs = append(baseArgs, arg) + } + } + + if len(extraArgs) > 0 { baseArgs = append(baseArgs, extraArgs...) } + return main, baseArgs, 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, error) { + var extraArgsIn []string + + if !p.Metadata.IgnoreFlags { + extraArgsIn = extraArgs + } + + return PrepareCommands(p.Metadata.PlatformCommand, p.Metadata.Command, []string{}, true, extraArgsIn) +} + // validPluginName is a regular expression that validates plugin names. // // Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, ​_​ and ​-. diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index 725052346..a6bfe1f09 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -21,165 +21,389 @@ import ( "path/filepath" "reflect" "runtime" + "strings" "testing" "helm.sh/helm/v3/pkg/cli" ) -func checkCommand(p *Plugin, extraArgs []string, osStrCmp string, t *testing.T) { - cmd, args, err := p.PrepareCommand(extraArgs) +func TestPrepareCommand(t *testing.T) { + p := &Plugin{ + Dir: "/tmp", // Unused + Metadata: &Metadata{ + Name: "test", + Command: "echo \"error\"", + PlatformCommand: []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: runtime.GOARCH, Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + }, + }, + } + + expectedIndex := 1 + + cmd, args, err := p.PrepareCommand([]string{}) if err != nil { t.Fatal(err) } - if cmd != "echo" { - t.Fatalf("Expected echo, got %q", cmd) + if cmd != p.Metadata.PlatformCommand[expectedIndex].Command { + t.Fatalf("Expected %q, got %q", p.Metadata.PlatformCommand[expectedIndex].Command, cmd) } - - if l := len(args); l != 5 { - t.Fatalf("expected 5 args, got %d", l) + if !reflect.DeepEqual(args, p.Metadata.PlatformCommand[expectedIndex].Args) { + t.Fatalf("Expected %v, got %v", p.Metadata.PlatformCommand[expectedIndex].Args, args) } +} - 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]) - } +func TestPrepareCommandExtraArgs(t *testing.T) { + p := &Plugin{ + Dir: "/tmp", // Unused + Metadata: &Metadata{ + Name: "test", + Command: "echo \"error\"", + PlatformCommand: []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: runtime.GOARCH, Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + }, + }, } + extraArgs := []string{"--debug", "--foo", "bar"} + + expectedIndex := 1 + resultArgs := append(p.Metadata.PlatformCommand[expectedIndex].Args, extraArgs...) - // Test with IgnoreFlags. This should omit --debug, --foo, bar - p.Metadata.IgnoreFlags = true - cmd, args, err = p.PrepareCommand(extraArgs) + cmd, args, err := p.PrepareCommand(extraArgs) if err != nil { t.Fatal(err) } - if cmd != "echo" { - t.Fatalf("Expected echo, got %q", cmd) - } - if l := len(args); l != 2 { - t.Fatalf("expected 2 args, got %d", l) + if cmd != p.Metadata.PlatformCommand[expectedIndex].Command { + t.Fatalf("Expected %q, got %q", p.Metadata.PlatformCommand[expectedIndex].Command, cmd) } - 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]) - } + if !reflect.DeepEqual(args, resultArgs) { + t.Fatalf("Expected %v, got %v", resultArgs, args) } } -func TestPrepareCommand(t *testing.T) { +func TestPrepareCommandExtraArgsIgnored(t *testing.T) { p := &Plugin{ Dir: "/tmp", // Unused Metadata: &Metadata{ Name: "test", - Command: "echo -n foo", + Command: "echo \"error\"", + PlatformCommand: []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: runtime.GOARCH, Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + }, + IgnoreFlags: true, }, } - argv := []string{"--debug", "--foo", "bar"} + extraArgs := []string{"--debug", "--foo", "bar"} + + expectedIndex := 1 - checkCommand(p, argv, "foo", t) + cmd, args, err := p.PrepareCommand(extraArgs) + if err != nil { + t.Fatal(err) + } + if cmd != p.Metadata.PlatformCommand[expectedIndex].Command { + t.Fatalf("Expected %q, got %q", p.Metadata.PlatformCommand[expectedIndex].Command, cmd) + } + if !reflect.DeepEqual(args, p.Metadata.PlatformCommand[expectedIndex].Args) { + t.Fatalf("Expected %v, got %v", p.Metadata.PlatformCommand[expectedIndex].Args, args) + } } -func TestPlatformPrepareCommand(t *testing.T) { +func TestPrepareCommandNoArgs(t *testing.T) { + command := "echo" + commandArgs := []string{"-n", "\"test\""} + p := &Plugin{ Dir: "/tmp", // Unused Metadata: &Metadata{ Name: "test", - Command: "echo -n os-arch", + Command: "echo \"error\"", PlatformCommand: []PlatformCommand{ - {OperatingSystem: "linux", Architecture: "386", Command: "echo -n linux-386"}, - {OperatingSystem: "linux", Architecture: "amd64", Command: "echo -n linux-amd64"}, - {OperatingSystem: "linux", Architecture: "arm64", Command: "echo -n linux-arm64"}, - {OperatingSystem: "linux", Architecture: "ppc64le", Command: "echo -n linux-ppc64le"}, - {OperatingSystem: "linux", Architecture: "s390x", Command: "echo -n linux-s390x"}, - {OperatingSystem: "linux", Architecture: "riscv64", Command: "echo -n linux-riscv64"}, - {OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"}, + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "echo \"error\""}, + {OperatingSystem: runtime.GOOS, Architecture: runtime.GOARCH, Command: strings.Join(append([]string{command}, commandArgs...), " ")}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "echo \"error\""}, + {OperatingSystem: runtime.GOOS, Architecture: "", Command: "echo \"error\""}, }, }, } - var osStrCmp string - os := runtime.GOOS - arch := runtime.GOARCH - if os == "linux" && arch == "386" { - osStrCmp = "linux-386" - } else if os == "linux" && arch == "amd64" { - osStrCmp = "linux-amd64" - } else if os == "linux" && arch == "arm64" { - osStrCmp = "linux-arm64" - } else if os == "linux" && arch == "ppc64le" { - osStrCmp = "linux-ppc64le" - } else if os == "linux" && arch == "s390x" { - osStrCmp = "linux-s390x" - } else if os == "linux" && arch == "riscv64" { - osStrCmp = "linux-riscv64" - } else if os == "windows" && arch == "amd64" { - osStrCmp = "win-64" - } else { - osStrCmp = "os-arch" - } - - argv := []string{"--debug", "--foo", "bar"} - checkCommand(p, argv, osStrCmp, t) + + cmd, args, err := p.PrepareCommand([]string{}) + if err != nil { + t.Fatal(err) + } + if cmd != command { + t.Fatalf("Expected %q, got %q", command, cmd) + } + if !reflect.DeepEqual(args, commandArgs) { + t.Fatalf("Expected %v, got %v", commandArgs, args) + } } -func TestPartialPlatformPrepareCommand(t *testing.T) { +func TestPrepareCommandFallback(t *testing.T) { + command := "echo" + commandArgs := []string{"-n", "foo"} + p := &Plugin{ Dir: "/tmp", // Unused Metadata: &Metadata{ Name: "test", - Command: "echo -n os-arch", - PlatformCommand: []PlatformCommand{ - {OperatingSystem: "linux", Architecture: "386", Command: "echo -n linux-386"}, - {OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"}, - }, + Command: strings.Join(append([]string{command}, commandArgs...), " "), }, } - var osStrCmp string - os := runtime.GOOS - arch := runtime.GOARCH - if os == "linux" { - osStrCmp = "linux-386" - } else if os == "windows" && arch == "amd64" { - osStrCmp = "win-64" - } else { - osStrCmp = "os-arch" + + cmd, args, err := p.PrepareCommand([]string{}) + if err != nil { + t.Fatal(err) + } + if cmd != command { + t.Fatalf("Expected %q, got %q", command, cmd) + } + if !reflect.DeepEqual(commandArgs, args) { + t.Fatalf("Expected %v, got %v", args, args) + } +} + +func TestPrepareCommandFallbackExtraArgs(t *testing.T) { + command := "echo" + commandArgs := []string{"-n", "foo"} + + p := &Plugin{ + Dir: "/tmp", // Unused + Metadata: &Metadata{ + Name: "test", + Command: strings.Join(append([]string{command}, commandArgs...), " "), + }, } + extraArgs := []string{"--debug", "--foo", "bar"} - argv := []string{"--debug", "--foo", "bar"} - checkCommand(p, argv, osStrCmp, t) + resultArgs := append(commandArgs, extraArgs...) + + cmd, args, err := p.PrepareCommand(extraArgs) + if err != nil { + t.Fatal(err) + } + if cmd != command { + t.Fatalf("Expected %q, got %q", command, cmd) + } + if !reflect.DeepEqual(args, resultArgs) { + t.Fatalf("Expected %v, got %v", resultArgs, args) + } } -func TestNoPrepareCommand(t *testing.T) { +func TestPrepareCommandFallbackExtraArgsIgnored(t *testing.T) { + command := "echo" + commandArgs := []string{"-n", "foo"} + + p := &Plugin{ + Dir: "/tmp", // Unused + Metadata: &Metadata{ + Name: "test", + Command: strings.Join(append([]string{command}, commandArgs...), " "), + IgnoreFlags: true, + }, + } + extraArgs := []string{"--debug", "--foo", "bar"} + + cmd, args, err := p.PrepareCommand(extraArgs) + if err != nil { + t.Fatal(err) + } + if cmd != command { + t.Fatalf("Expected %q, got %q", command, cmd) + } + if !reflect.DeepEqual(args, commandArgs) { + t.Fatalf("Expected %v, got %v", commandArgs, args) + } +} + +func TestPrepareCommandNoMatch(t *testing.T) { p := &Plugin{ Dir: "/tmp", // Unused Metadata: &Metadata{ Name: "test", + PlatformCommand: []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: "no-os", Architecture: runtime.GOARCH, Command: "sh", Args: []string{"-c", "echo \"test\""}}, + }, }, } - argv := []string{"--debug", "--foo", "bar"} - _, _, err := p.PrepareCommand(argv) + _, _, err := p.PrepareCommand([]string{}) if err == nil { t.Fatalf("Expected error to be returned") } } -func TestNoMatchPrepareCommand(t *testing.T) { +func TestPrepareCommandNoCommand(t *testing.T) { p := &Plugin{ Dir: "/tmp", // Unused Metadata: &Metadata{ Name: "test", - PlatformCommand: []PlatformCommand{ - {OperatingSystem: "no-os", Architecture: "amd64", Command: "echo -n linux-386"}, - }, }, } - argv := []string{"--debug", "--foo", "bar"} - if _, _, err := p.PrepareCommand(argv); err == nil { + _, _, err := p.PrepareCommand([]string{}) + if err == nil { t.Fatalf("Expected error to be returned") } } +func TestPrepareCommands(t *testing.T) { + cmds := []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: runtime.GOARCH, Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + } + + expectedIndex := 1 + + cmd, args, err := PrepareCommands(cmds, "pwsh", []string{"-c", "echo \"error\""}, true, []string{}) + if err != nil { + t.Fatal(err) + } + if cmd != cmds[expectedIndex].Command { + t.Fatalf("Expected %q, got %q", cmds[expectedIndex].Command, cmd) + } + if !reflect.DeepEqual(args, cmds[expectedIndex].Args) { + t.Fatalf("Expected %v, got %v", cmds[expectedIndex].Args, args) + } +} + +func TestPrepareCommandsExtraArgs(t *testing.T) { + cmds := []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: runtime.GOARCH, Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + } + extraArgs := []string{"--debug", "--foo", "bar"} + + expectedIndex := 1 + resultArgs := append(cmds[expectedIndex].Args, extraArgs...) + + cmd, args, err := PrepareCommands(cmds, "pwsh", []string{"-c", "echo \"error\""}, true, extraArgs) + if err != nil { + t.Fatal(err) + } + if cmd != cmds[expectedIndex].Command { + t.Fatalf("Expected %q, got %q", cmds[expectedIndex].Command, cmd) + } + if !reflect.DeepEqual(args, resultArgs) { + t.Fatalf("Expected %v, got %v", resultArgs, args) + } +} + +func TestPrepareCommandsNoArch(t *testing.T) { + cmds := []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "", Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "pwsh", Args: []string{"-c", "echo \"error\""}}, + } + + expectedIndex := 1 + + cmd, args, err := PrepareCommands(cmds, "pwsh", []string{"-c", "echo \"error\""}, true, []string{}) + if err != nil { + t.Fatal(err) + } + if cmd != cmds[expectedIndex].Command { + t.Fatalf("Expected %q, got %q", cmds[expectedIndex].Command, cmd) + } + if !reflect.DeepEqual(args, cmds[expectedIndex].Args) { + t.Fatalf("Expected %v, got %v", cmds[expectedIndex].Args, args) + } +} + +func TestPrepareCommandsFallback(t *testing.T) { + cmds := []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: "no-os", Architecture: runtime.GOARCH, Command: "sh", Args: []string{"-c", "echo \"test\""}}, + } + + command := "sh" + commandArgs := []string{"-c", "echo \"test\""} + + cmd, args, err := PrepareCommands(cmds, command, commandArgs, true, []string{}) + if err != nil { + t.Fatal(err) + } + if cmd != command { + t.Fatalf("Expected %q, got %q", command, cmd) + } + if !reflect.DeepEqual(args, commandArgs) { + t.Fatalf("Expected %v, got %v", commandArgs, args) + } +} + +func TestPrepareCommandsNoMatch(t *testing.T) { + cmds := []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "no-arch", Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: runtime.GOOS, Architecture: "no-arch", Command: "sh", Args: []string{"-c", "echo \"test\""}}, + {OperatingSystem: "no-os", Architecture: runtime.GOARCH, Command: "sh", Args: []string{"-c", "echo \"test\""}}, + } + + if _, _, err := PrepareCommands(cmds, "", []string{}, true, []string{}); err == nil { + t.Fatalf("Expected error to be returned") + } +} + +func TestPrepareCommandsNoCommands(t *testing.T) { + cmds := []PlatformCommand{} + + if _, _, err := PrepareCommands(cmds, "", []string{}, true, []string{}); err == nil { + t.Fatalf("Expected error to be returned") + } +} + +func TestPrepareCommandsExpand(t *testing.T) { + cmds := []PlatformCommand{} + + command := "echo" + commandArgs := []string{"${TEST}"} + commandArgsExpanded := []string{""} + + cmd, args, err := PrepareCommands(cmds, command, commandArgs, true, []string{}) + if err != nil { + t.Fatal(err) + } + if cmd != command { + t.Fatalf("Expected %q, got %q", command, cmd) + } + if !reflect.DeepEqual(args, commandArgsExpanded) { + t.Fatalf("Expected %v, got %v", commandArgsExpanded, args) + } +} + +func TestPrepareCommandsNoExpand(t *testing.T) { + cmds := []PlatformCommand{} + + command := "echo" + commandArgs := []string{"${TEST}"} + + cmd, args, err := PrepareCommands(cmds, command, commandArgs, false, []string{}) + if err != nil { + t.Fatal(err) + } + if cmd != command { + t.Fatalf("Expected %q, got %q", command, cmd) + } + if !reflect.DeepEqual(args, commandArgs) { + t.Fatalf("Expected %v, got %v", commandArgs, args) + } +} + func TestLoadDir(t *testing.T) { dirname := "testdata/plugdir/good/hello" plug, err := LoadDir(dirname) @@ -196,8 +420,18 @@ func TestLoadDir(t *testing.T) { Version: "0.1.0", Usage: "usage", Description: "description", - Command: "$HELM_PLUGIN_DIR/hello.sh", + PlatformCommand: []PlatformCommand{ + {OperatingSystem: "linux", Architecture: "", Command: "sh", Args: []string{"-c", "${HELM_PLUGIN_DIR}/hello.sh"}}, + {OperatingSystem: "windows", Architecture: "", Command: "pwsh", Args: []string{"-c", "${HELM_PLUGIN_DIR}/hello.ps1"}}, + }, + Command: "${HELM_PLUGIN_DIR}/hello.sh", IgnoreFlags: true, + PlatformHooks: map[string][]PlatformCommand{ + Install: { + {OperatingSystem: "linux", Architecture: "", Command: "sh", Args: []string{"-c", "echo \"installing...\""}}, + {OperatingSystem: "windows", Architecture: "", Command: "pwsh", Args: []string{"-c", "echo \"installing...\""}}, + }, + }, Hooks: map[string]string{ Install: "echo installing...", }, diff --git a/pkg/plugin/testdata/plugdir/good/hello/hello.ps1 b/pkg/plugin/testdata/plugdir/good/hello/hello.ps1 new file mode 100644 index 000000000..bee61f27d --- /dev/null +++ b/pkg/plugin/testdata/plugdir/good/hello/hello.ps1 @@ -0,0 +1,3 @@ +#!/usr/bin/env pwsh + +Write-Host "Hello, world!" diff --git a/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml b/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml index b857b55ee..2446f91c0 100644 --- a/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml +++ b/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml @@ -3,7 +3,26 @@ version: "0.1.0" usage: "usage" description: |- description -command: "$HELM_PLUGIN_DIR/hello.sh" +platformCommand: + - os: linux + arch: + command: "sh" + args: ["-c", "${HELM_PLUGIN_DIR}/hello.sh"] + - os: windows + arch: + command: "pwsh" + args: ["-c", "${HELM_PLUGIN_DIR}/hello.ps1"] +command: "${HELM_PLUGIN_DIR}/hello.sh" ignoreFlags: true +PlatformHooks: + install: + - os: linux + arch: + command: "sh" + args: ["-c", 'echo "installing..."'] + - os: windows + arch: + command: "pwsh" + args: ["-c", 'echo "installing..."'] hooks: install: "echo installing..."