github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/cmd/juju/plugin.go (about)

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