github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/commands/plugin.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"sort"
    14  	"strings"
    15  	"syscall"
    16  
    17  	"github.com/juju/cmd"
    18  	"github.com/juju/gnuflag"
    19  	"github.com/juju/utils"
    20  	"github.com/juju/utils/set"
    21  
    22  	"github.com/juju/juju/juju/osenv"
    23  )
    24  
    25  const JujuPluginPrefix = "juju-"
    26  const JujuPluginPattern = "^juju-[a-zA-Z]"
    27  
    28  var jujuArgNames = set.NewStrings("-m", "--model", "-c", "--controller")
    29  
    30  // This is a very rudimentary method used to extract common Juju
    31  // arguments from the full list passed to the plugin.
    32  func extractJujuArgs(args []string) []string {
    33  	var jujuArgs []string
    34  	nrArgs := len(args)
    35  	for nextArg := 0; nextArg < nrArgs; {
    36  		arg := args[nextArg]
    37  		nextArg++
    38  		if !jujuArgNames.Contains(arg) {
    39  			continue
    40  		}
    41  		jujuArgs = append(jujuArgs, arg)
    42  		if nextArg < nrArgs {
    43  			jujuArgs = append(jujuArgs, args[nextArg])
    44  			nextArg++
    45  		}
    46  	}
    47  	return jujuArgs
    48  }
    49  
    50  func RunPlugin(ctx *cmd.Context, subcommand string, args []string) error {
    51  	cmdName := JujuPluginPrefix + subcommand
    52  	plugin := &PluginCommand{name: cmdName}
    53  
    54  	// We process common flags supported by Juju commands.
    55  	// To do this, we extract only those supported flags from the
    56  	// argument list to avoid confusing flags.Parse().
    57  	flags := gnuflag.NewFlagSetWithFlagKnownAs(cmdName, gnuflag.ContinueOnError, "option")
    58  	flags.SetOutput(ioutil.Discard)
    59  	plugin.SetFlags(flags)
    60  	jujuArgs := extractJujuArgs(args)
    61  	if err := flags.Parse(false, jujuArgs); err != nil {
    62  		return err
    63  	}
    64  	if err := plugin.Init(args); err != nil {
    65  		return err
    66  	}
    67  	err := plugin.Run(ctx)
    68  	_, execError := err.(*exec.Error)
    69  	// exec.Error results are for when the executable isn't found, in
    70  	// those cases, drop through.
    71  	if !execError {
    72  		return err
    73  	}
    74  	return &cmd.UnrecognizedCommand{Name: subcommand}
    75  }
    76  
    77  type PluginCommand struct {
    78  	cmd.CommandBase
    79  	name string
    80  
    81  	controllerName string
    82  	modelName      string
    83  
    84  	args []string
    85  }
    86  
    87  // Info is just a stub so that PluginCommand implements cmd.Command.
    88  // Since this is never actually called, we can happily return nil.
    89  func (*PluginCommand) Info() *cmd.Info {
    90  	return nil
    91  }
    92  
    93  func (c *PluginCommand) Init(args []string) error {
    94  	c.args = args
    95  	return nil
    96  }
    97  
    98  func (c *PluginCommand) SetFlags(f *gnuflag.FlagSet) {
    99  	f.StringVar(&c.modelName, "m", "", "Model to operate in. Accepts [<controller name>:]<model name>")
   100  	f.StringVar(&c.modelName, "model", "", "")
   101  	f.StringVar(&c.controllerName, "c", "", "Controller to operate in")
   102  	f.StringVar(&c.controllerName, "controller", "", "")
   103  	c.CommandBase.SetFlags(f)
   104  }
   105  
   106  func (c *PluginCommand) Run(ctx *cmd.Context) error {
   107  	command := exec.Command(c.name, c.args...)
   108  
   109  	env := os.Environ()
   110  	if c.controllerName != "" {
   111  		env = utils.Setenv(env, osenv.JujuControllerEnvKey+"="+c.controllerName)
   112  	}
   113  	if c.modelName != "" {
   114  		env = utils.Setenv(env, osenv.JujuModelEnvKey+"="+c.modelName)
   115  	}
   116  	command.Env = env
   117  
   118  	// Now hook up stdin, stdout, stderr
   119  	command.Stdin = ctx.Stdin
   120  	command.Stdout = ctx.Stdout
   121  	command.Stderr = ctx.Stderr
   122  	// And run it!
   123  	err := command.Run()
   124  
   125  	if exitError, ok := err.(*exec.ExitError); ok && exitError != nil {
   126  		status := exitError.ProcessState.Sys().(syscall.WaitStatus)
   127  		if status.Exited() {
   128  			return cmd.NewRcPassthroughError(status.ExitStatus())
   129  		}
   130  	}
   131  	return err
   132  }
   133  
   134  type PluginDescription struct {
   135  	name        string
   136  	description string
   137  }
   138  
   139  // GetPluginDescriptions runs each plugin with "--description".  The calls to
   140  // the plugins are run in parallel, so the function should only take as long
   141  // as the longest call.
   142  func GetPluginDescriptions() []PluginDescription {
   143  	plugins := findPlugins()
   144  	results := []PluginDescription{}
   145  	if len(plugins) == 0 {
   146  		return results
   147  	}
   148  	// create a channel with enough backing for each plugin
   149  	description := make(chan PluginDescription, len(plugins))
   150  
   151  	// exec the command, and wait only for the timeout before killing the process
   152  	for _, plugin := range plugins {
   153  		go func(plugin string) {
   154  			result := PluginDescription{name: plugin}
   155  			defer func() {
   156  				description <- result
   157  			}()
   158  			desccmd := exec.Command(plugin, "--description")
   159  			output, err := desccmd.CombinedOutput()
   160  
   161  			if err == nil {
   162  				// trim to only get the first line
   163  				result.description = strings.SplitN(string(output), "\n", 2)[0]
   164  			} else {
   165  				result.description = fmt.Sprintf("error occurred running '%s --description'", plugin)
   166  				logger.Errorf("'%s --description': %s", plugin, err)
   167  			}
   168  		}(plugin)
   169  	}
   170  	resultMap := map[string]PluginDescription{}
   171  	// gather the results at the end
   172  	for range plugins {
   173  		result := <-description
   174  		resultMap[result.name] = result
   175  	}
   176  	// plugins array is already sorted, use this to get the results in order
   177  	for _, plugin := range plugins {
   178  		// Strip the 'juju-' off the start of the plugin name in the results
   179  		result := resultMap[plugin]
   180  		result.name = result.name[len(JujuPluginPrefix):]
   181  		results = append(results, result)
   182  	}
   183  	return results
   184  }
   185  
   186  // findPlugins searches the current PATH for executable files that match
   187  // JujuPluginPattern.
   188  func findPlugins() []string {
   189  	re := regexp.MustCompile(JujuPluginPattern)
   190  	path := os.Getenv("PATH")
   191  	plugins := []string{}
   192  	for _, name := range filepath.SplitList(path) {
   193  		entries, err := ioutil.ReadDir(name)
   194  		if err != nil {
   195  			continue
   196  		}
   197  		for _, entry := range entries {
   198  			if re.Match([]byte(entry.Name())) && (entry.Mode()&0111) != 0 {
   199  				plugins = append(plugins, entry.Name())
   200  			}
   201  		}
   202  	}
   203  	sort.Strings(plugins)
   204  	return plugins
   205  }