github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/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  	"sort"
    14  	"strings"
    15  	"syscall"
    16  
    17  	"github.com/juju/cmd"
    18  	"launchpad.net/gnuflag"
    19  
    20  	"github.com/juju/juju/cmd/envcmd"
    21  	"github.com/juju/juju/juju/osenv"
    22  )
    23  
    24  const JujuPluginPrefix = "juju-"
    25  
    26  // This is a very rudimentary method used to extract common Juju
    27  // arguments from the full list passed to the plugin. Currently,
    28  // there is only one such argument: -e env
    29  // If more than just -e is required, the method can be improved then.
    30  func extractJujuArgs(args []string) []string {
    31  	var jujuArgs []string
    32  	nrArgs := len(args)
    33  	for nextArg := 0; nextArg < nrArgs; {
    34  		arg := args[nextArg]
    35  		nextArg++
    36  		if arg != "-e" {
    37  			continue
    38  		}
    39  		jujuArgs = append(jujuArgs, arg)
    40  		if nextArg < nrArgs {
    41  			jujuArgs = append(jujuArgs, args[nextArg])
    42  			nextArg++
    43  		}
    44  	}
    45  	return jujuArgs
    46  }
    47  
    48  func RunPlugin(ctx *cmd.Context, subcommand string, args []string) error {
    49  	cmdName := JujuPluginPrefix + subcommand
    50  	plugin := envcmd.Wrap(&PluginCommand{name: cmdName})
    51  
    52  	// We process common flags supported by Juju commands.
    53  	// To do this, we extract only those supported flags from the
    54  	// argument list to avoid confusing flags.Parse().
    55  	flags := gnuflag.NewFlagSet(cmdName, gnuflag.ContinueOnError)
    56  	flags.SetOutput(ioutil.Discard)
    57  	plugin.SetFlags(flags)
    58  	jujuArgs := extractJujuArgs(args)
    59  	if err := flags.Parse(false, jujuArgs); err != nil {
    60  		return err
    61  	}
    62  	if err := plugin.Init(args); err != nil {
    63  		return err
    64  	}
    65  	err := plugin.Run(ctx)
    66  	_, execError := err.(*exec.Error)
    67  	// exec.Error results are for when the executable isn't found, in
    68  	// those cases, drop through.
    69  	if !execError {
    70  		return err
    71  	}
    72  	return &cmd.UnrecognizedCommand{Name: subcommand}
    73  }
    74  
    75  type PluginCommand struct {
    76  	envcmd.EnvCommandBase
    77  	name string
    78  	args []string
    79  }
    80  
    81  // Info is just a stub so that PluginCommand implements cmd.Command.
    82  // Since this is never actually called, we can happily return nil.
    83  func (*PluginCommand) Info() *cmd.Info {
    84  	return nil
    85  }
    86  
    87  func (c *PluginCommand) Init(args []string) error {
    88  	c.args = args
    89  	return nil
    90  }
    91  
    92  func (c *PluginCommand) Run(ctx *cmd.Context) error {
    93  	command := exec.Command(c.name, c.args...)
    94  	command.Env = append(os.Environ(), []string{
    95  		osenv.JujuHomeEnvKey + "=" + osenv.JujuHome(),
    96  		osenv.JujuEnvEnvKey + "=" + c.ConnectionName()}...,
    97  	)
    98  
    99  	// Now hook up stdin, stdout, stderr
   100  	command.Stdin = ctx.Stdin
   101  	command.Stdout = ctx.Stdout
   102  	command.Stderr = ctx.Stderr
   103  	// And run it!
   104  	err := command.Run()
   105  
   106  	if exitError, ok := err.(*exec.ExitError); ok && exitError != nil {
   107  		status := exitError.ProcessState.Sys().(syscall.WaitStatus)
   108  		if status.Exited() {
   109  			return cmd.NewRcPassthroughError(status.ExitStatus())
   110  		}
   111  	}
   112  	return err
   113  }
   114  
   115  type PluginDescription struct {
   116  	name        string
   117  	description string
   118  }
   119  
   120  const PluginTopicText = `Juju Plugins
   121  
   122  Plugins are implemented as stand-alone executable files somewhere in the user's PATH.
   123  The executable command must be of the format juju-<plugin name>.
   124  
   125  `
   126  
   127  func PluginHelpTopic() string {
   128  	output := &bytes.Buffer{}
   129  	fmt.Fprintf(output, PluginTopicText)
   130  
   131  	existingPlugins := GetPluginDescriptions()
   132  
   133  	if len(existingPlugins) == 0 {
   134  		fmt.Fprintf(output, "No plugins found.\n")
   135  	} else {
   136  		longest := 0
   137  		for _, plugin := range existingPlugins {
   138  			if len(plugin.name) > longest {
   139  				longest = len(plugin.name)
   140  			}
   141  		}
   142  		for _, plugin := range existingPlugins {
   143  			fmt.Fprintf(output, "%-*s  %s\n", longest, plugin.name, plugin.description)
   144  		}
   145  	}
   146  
   147  	return output.String()
   148  }
   149  
   150  // GetPluginDescriptions runs each plugin with "--description".  The calls to
   151  // the plugins are run in parallel, so the function should only take as long
   152  // as the longest call.
   153  func GetPluginDescriptions() []PluginDescription {
   154  	plugins := findPlugins()
   155  	results := []PluginDescription{}
   156  	if len(plugins) == 0 {
   157  		return results
   158  	}
   159  	// create a channel with enough backing for each plugin
   160  	description := make(chan PluginDescription, len(plugins))
   161  
   162  	// exec the command, and wait only for the timeout before killing the process
   163  	for _, plugin := range plugins {
   164  		go func(plugin string) {
   165  			result := PluginDescription{name: plugin}
   166  			defer func() {
   167  				description <- result
   168  			}()
   169  			desccmd := exec.Command(plugin, "--description")
   170  			output, err := desccmd.CombinedOutput()
   171  
   172  			if err == nil {
   173  				// trim to only get the first line
   174  				result.description = strings.SplitN(string(output), "\n", 2)[0]
   175  			} else {
   176  				result.description = fmt.Sprintf("error occurred running '%s --description'", plugin)
   177  				logger.Errorf("'%s --description': %s", plugin, err)
   178  			}
   179  		}(plugin)
   180  	}
   181  	resultMap := map[string]PluginDescription{}
   182  	// gather the results at the end
   183  	for _ = range plugins {
   184  		result := <-description
   185  		resultMap[result.name] = result
   186  	}
   187  	// plugins array is already sorted, use this to get the results in order
   188  	for _, plugin := range plugins {
   189  		// Strip the 'juju-' off the start of the plugin name in the results
   190  		result := resultMap[plugin]
   191  		result.name = result.name[len(JujuPluginPrefix):]
   192  		results = append(results, result)
   193  	}
   194  	return results
   195  }
   196  
   197  // findPlugins searches the current PATH for executable files that start with
   198  // JujuPluginPrefix.
   199  func findPlugins() []string {
   200  	path := os.Getenv("PATH")
   201  	plugins := []string{}
   202  	for _, name := range filepath.SplitList(path) {
   203  		entries, err := ioutil.ReadDir(name)
   204  		if err != nil {
   205  			continue
   206  		}
   207  		for _, entry := range entries {
   208  			if strings.HasPrefix(entry.Name(), JujuPluginPrefix) && (entry.Mode()&0111) != 0 {
   209  				plugins = append(plugins, entry.Name())
   210  			}
   211  		}
   212  	}
   213  	sort.Strings(plugins)
   214  	return plugins
   215  }