github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	"bytes"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"regexp"
    14  	"sort"
    15  	"strings"
    16  	"syscall"
    17  
    18  	"github.com/juju/cmd"
    19  	"github.com/juju/gnuflag"
    20  
    21  	"github.com/juju/juju/cmd/modelcmd"
    22  	"github.com/juju/juju/juju/osenv"
    23  )
    24  
    25  const JujuPluginPrefix = "juju-"
    26  const JujuPluginPattern = "^juju-[a-zA-Z]"
    27  
    28  // This is a very rudimentary method used to extract common Juju
    29  // arguments from the full list passed to the plugin. Currently,
    30  // there is only one such argument: -m env
    31  // If more than just -e is required, the method can be improved then.
    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 arg != "-m" {
    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 := modelcmd.Wrap(&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.NewFlagSet(cmdName, gnuflag.ContinueOnError)
    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  	modelcmd.ModelCommandBase
    79  	name string
    80  	args []string
    81  }
    82  
    83  // Info is just a stub so that PluginCommand implements cmd.Command.
    84  // Since this is never actually called, we can happily return nil.
    85  func (*PluginCommand) Info() *cmd.Info {
    86  	return nil
    87  }
    88  
    89  func (c *PluginCommand) Init(args []string) error {
    90  	c.args = args
    91  	return nil
    92  }
    93  
    94  func (c *PluginCommand) Run(ctx *cmd.Context) error {
    95  	command := exec.Command(c.name, c.args...)
    96  	command.Env = append(os.Environ(), []string{
    97  		osenv.JujuModelEnvKey + "=" + c.ConnectionName()}...,
    98  	)
    99  
   100  	// Now hook up stdin, stdout, stderr
   101  	command.Stdin = ctx.Stdin
   102  	command.Stdout = ctx.Stdout
   103  	command.Stderr = ctx.Stderr
   104  	// And run it!
   105  	err := command.Run()
   106  
   107  	if exitError, ok := err.(*exec.ExitError); ok && exitError != nil {
   108  		status := exitError.ProcessState.Sys().(syscall.WaitStatus)
   109  		if status.Exited() {
   110  			return cmd.NewRcPassthroughError(status.ExitStatus())
   111  		}
   112  	}
   113  	return err
   114  }
   115  
   116  type PluginDescription struct {
   117  	name        string
   118  	description string
   119  }
   120  
   121  const PluginTopicText = `Juju Plugins
   122  
   123  Plugins are implemented as stand-alone executable files somewhere in the user's PATH.
   124  The executable command must be of the format juju-<plugin name>.
   125  
   126  `
   127  
   128  func PluginHelpTopic() string {
   129  	output := &bytes.Buffer{}
   130  	fmt.Fprintf(output, PluginTopicText)
   131  
   132  	existingPlugins := GetPluginDescriptions()
   133  
   134  	if len(existingPlugins) == 0 {
   135  		fmt.Fprintf(output, "No plugins found.\n")
   136  	} else {
   137  		longest := 0
   138  		for _, plugin := range existingPlugins {
   139  			if len(plugin.name) > longest {
   140  				longest = len(plugin.name)
   141  			}
   142  		}
   143  		for _, plugin := range existingPlugins {
   144  			fmt.Fprintf(output, "%-*s  %s\n", longest, plugin.name, plugin.description)
   145  		}
   146  	}
   147  
   148  	return output.String()
   149  }
   150  
   151  // GetPluginDescriptions runs each plugin with "--description".  The calls to
   152  // the plugins are run in parallel, so the function should only take as long
   153  // as the longest call.
   154  func GetPluginDescriptions() []PluginDescription {
   155  	plugins := findPlugins()
   156  	results := []PluginDescription{}
   157  	if len(plugins) == 0 {
   158  		return results
   159  	}
   160  	// create a channel with enough backing for each plugin
   161  	description := make(chan PluginDescription, len(plugins))
   162  
   163  	// exec the command, and wait only for the timeout before killing the process
   164  	for _, plugin := range plugins {
   165  		go func(plugin string) {
   166  			result := PluginDescription{name: plugin}
   167  			defer func() {
   168  				description <- result
   169  			}()
   170  			desccmd := exec.Command(plugin, "--description")
   171  			output, err := desccmd.CombinedOutput()
   172  
   173  			if err == nil {
   174  				// trim to only get the first line
   175  				result.description = strings.SplitN(string(output), "\n", 2)[0]
   176  			} else {
   177  				result.description = fmt.Sprintf("error occurred running '%s --description'", plugin)
   178  				logger.Errorf("'%s --description': %s", plugin, err)
   179  			}
   180  		}(plugin)
   181  	}
   182  	resultMap := map[string]PluginDescription{}
   183  	// gather the results at the end
   184  	for _ = range plugins {
   185  		result := <-description
   186  		resultMap[result.name] = result
   187  	}
   188  	// plugins array is already sorted, use this to get the results in order
   189  	for _, plugin := range plugins {
   190  		// Strip the 'juju-' off the start of the plugin name in the results
   191  		result := resultMap[plugin]
   192  		result.name = result.name[len(JujuPluginPrefix):]
   193  		results = append(results, result)
   194  	}
   195  	return results
   196  }
   197  
   198  // findPlugins searches the current PATH for executable files that match
   199  // JujuPluginPattern.
   200  func findPlugins() []string {
   201  	re := regexp.MustCompile(JujuPluginPattern)
   202  	path := os.Getenv("PATH")
   203  	plugins := []string{}
   204  	for _, name := range filepath.SplitList(path) {
   205  		entries, err := ioutil.ReadDir(name)
   206  		if err != nil {
   207  			continue
   208  		}
   209  		for _, entry := range entries {
   210  			if re.Match([]byte(entry.Name())) && (entry.Mode()&0111) != 0 {
   211  				plugins = append(plugins, entry.Name())
   212  			}
   213  		}
   214  	}
   215  	sort.Strings(plugins)
   216  	return plugins
   217  }