Skip to content

Commit

Permalink
feat: Added multi-platform plugin hook support
Browse files Browse the repository at this point in the history
Signed-off-by: Steve Hipwell <steve.hipwell@gmail.com>
  • Loading branch information
stevehipwell committed Apr 18, 2024
1 parent 14d0c13 commit acb80c5
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 30 deletions.
21 changes: 14 additions & 7 deletions cmd/helm/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, []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 {
Expand Down
3 changes: 3 additions & 0 deletions pkg/plugin/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
80 changes: 57 additions & 23 deletions pkg/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -91,7 +92,8 @@ type Metadata struct {
IgnoreFlags bool `json:"ignoreFlags"`

// Hooks are commands that will run on events.
Hooks Hooks
PlatformHooks PlatformHooks `json:"platformHooks"`
Hooks Hooks

// Downloaders field is used if the plugin supply downloader mechanism
// for special protocols.
Expand All @@ -116,21 +118,23 @@ 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 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
Expand All @@ -141,33 +145,63 @@ 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, command string, commandArgs []string, extraArgs []string) (string, []string, error) {
var cmdParts, args []string

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(command, " ")
args = commandArgs
}
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:] {
cmdExp := os.ExpandEnv(cmdPart)
baseArgs = append(baseArgs, cmdExp)
}
}
if !p.Metadata.IgnoreFlags {

for _, arg := range args {
argExp := os.ExpandEnv(arg)
baseArgs = append(baseArgs, argExp)
}

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{}, 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 ​-.
Expand Down

0 comments on commit acb80c5

Please sign in to comment.