github.com/zntrio/harp/v2@v2.0.9/pkg/sdk/cmdutil/plugin.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package cmdutil
    19  
    20  import (
    21  	"fmt"
    22  	"os"
    23  	"runtime"
    24  	"strings"
    25  	"syscall"
    26  
    27  	exec "golang.org/x/sys/execabs"
    28  )
    29  
    30  // PluginHandler is capable of parsing command line arguments
    31  // and performing executable filename lookups to search
    32  // for valid plugin files, and execute found plugins.
    33  type PluginHandler interface {
    34  	// exists at the given filename, or a boolean false.
    35  	// Lookup will iterate over a list of given prefixes
    36  	// in order to recognize valid plugin filenames.
    37  	// The first filepath to match a prefix is returned.
    38  	Lookup(filename string) (string, bool)
    39  	// Execute receives an executable's filepath, a slice
    40  	// of arguments, and a slice of environment variables
    41  	// to relay to the executable.
    42  	Execute(executablePath string, cmdArgs, environment []string) error
    43  }
    44  
    45  // DefaultPluginHandler implements PluginHandler.
    46  type DefaultPluginHandler struct {
    47  	ValidPrefixes []string
    48  }
    49  
    50  // NewDefaultPluginHandler instantiates the DefaultPluginHandler with a list of
    51  // given filename prefixes used to identify valid plugin filenames.
    52  func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler {
    53  	return &DefaultPluginHandler{
    54  		ValidPrefixes: validPrefixes,
    55  	}
    56  }
    57  
    58  // Lookup implements PluginHandler.
    59  func (h *DefaultPluginHandler) Lookup(filename string) (string, bool) {
    60  	for _, prefix := range h.ValidPrefixes {
    61  		path, err := exec.LookPath(fmt.Sprintf("%s-%s", prefix, filename))
    62  		if err != nil || path == "" {
    63  			continue
    64  		}
    65  		return path, true
    66  	}
    67  
    68  	return "", false
    69  }
    70  
    71  // Execute implements PluginHandler.
    72  func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
    73  	// Windows does not support exec syscall.
    74  	if runtime.GOOS == "windows" {
    75  		cmd := exec.Command(executablePath, cmdArgs...)
    76  		cmd.Stdout = os.Stdout
    77  		cmd.Stderr = os.Stderr
    78  		cmd.Stdin = os.Stdin
    79  		cmd.Env = environment
    80  		err := cmd.Run()
    81  		if err == nil {
    82  			os.Exit(0)
    83  		}
    84  		if err != nil {
    85  			return fmt.Errorf("unable to execute command: %w", err)
    86  		}
    87  
    88  		// No error
    89  		return nil
    90  	}
    91  
    92  	// invoke cmd binary relaying the environment and args given
    93  	// append executablePath to cmdArgs, as execve will make first argument the "binary name".
    94  	//nolint:gosec // controlled input
    95  	return syscall.Exec(executablePath, append([]string{executablePath}, cmdArgs...), environment)
    96  }
    97  
    98  // HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find
    99  // a plugin executable on the PATH that satisfies the given arguments.
   100  func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error {
   101  	remainingArgs := []string{} // all "non-flag" arguments
   102  
   103  	for idx := range cmdArgs {
   104  		if strings.HasPrefix(cmdArgs[idx], "-") {
   105  			break
   106  		}
   107  		remainingArgs = append(remainingArgs, strings.ReplaceAll(cmdArgs[idx], "-", "_"))
   108  	}
   109  
   110  	foundBinaryPath := ""
   111  
   112  	// attempt to find binary, starting at longest possible name with given cmdArgs
   113  	for len(remainingArgs) > 0 {
   114  		path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-"))
   115  		if !found {
   116  			remainingArgs = remainingArgs[:len(remainingArgs)-1]
   117  			continue
   118  		}
   119  
   120  		foundBinaryPath = path
   121  		break
   122  	}
   123  
   124  	if foundBinaryPath == "" {
   125  		return nil
   126  	}
   127  
   128  	// invoke cmd binary relaying the current environment and args given
   129  	if err := pluginHandler.Execute(foundBinaryPath, cmdArgs[len(remainingArgs):], os.Environ()); err != nil {
   130  		return fmt.Errorf("unable to execute %q plugin: %w", foundBinaryPath, err)
   131  	}
   132  
   133  	return nil
   134  }