github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/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  	"launchpad.net/juju-core/cmd"
    19  	"launchpad.net/juju-core/juju/osenv"
    20  	"launchpad.net/juju-core/log"
    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 := &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{subcommand}
    72  }
    73  
    74  type PluginCommand struct {
    75  	cmd.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) SetFlags(f *gnuflag.FlagSet) {
    92  	c.EnvCommandBase.SetFlags(f)
    93  }
    94  
    95  func (c *PluginCommand) Run(ctx *cmd.Context) error {
    96  	command := exec.Command(c.name, c.args...)
    97  	command.Env = append(os.Environ(), []string{
    98  		osenv.JujuHomeEnvKey + "=" + osenv.JujuHome(),
    99  		osenv.JujuEnvEnvKey + "=" + c.EnvironName()}...,
   100  	)
   101  
   102  	// Now hook up stdin, stdout, stderr
   103  	command.Stdin = ctx.Stdin
   104  	command.Stdout = ctx.Stdout
   105  	command.Stderr = ctx.Stderr
   106  	// And run it!
   107  	return command.Run()
   108  }
   109  
   110  type PluginDescription struct {
   111  	name        string
   112  	description string
   113  }
   114  
   115  const PluginTopicText = `Juju Plugins
   116  
   117  Plugins are implemented as stand-alone executable files somewhere in the user's PATH.
   118  The executable command must be of the format juju-<plugin name>.
   119  
   120  `
   121  
   122  func PluginHelpTopic() string {
   123  	output := &bytes.Buffer{}
   124  	fmt.Fprintf(output, PluginTopicText)
   125  
   126  	existingPlugins := GetPluginDescriptions()
   127  
   128  	if len(existingPlugins) == 0 {
   129  		fmt.Fprintf(output, "No plugins found.\n")
   130  	} else {
   131  		longest := 0
   132  		for _, plugin := range existingPlugins {
   133  			if len(plugin.name) > longest {
   134  				longest = len(plugin.name)
   135  			}
   136  		}
   137  		for _, plugin := range existingPlugins {
   138  			fmt.Fprintf(output, "%-*s  %s\n", longest, plugin.name, plugin.description)
   139  		}
   140  	}
   141  
   142  	return output.String()
   143  }
   144  
   145  // GetPluginDescriptions runs each plugin with "--description".  The calls to
   146  // the plugins are run in parallel, so the function should only take as long
   147  // as the longest call.
   148  func GetPluginDescriptions() []PluginDescription {
   149  	plugins := findPlugins()
   150  	results := []PluginDescription{}
   151  	if len(plugins) == 0 {
   152  		return results
   153  	}
   154  	// create a channel with enough backing for each plugin
   155  	description := make(chan PluginDescription, len(plugins))
   156  
   157  	// exec the command, and wait only for the timeout before killing the process
   158  	for _, plugin := range plugins {
   159  		go func(plugin string) {
   160  			result := PluginDescription{name: plugin}
   161  			defer func() {
   162  				description <- result
   163  			}()
   164  			desccmd := exec.Command(plugin, "--description")
   165  			output, err := desccmd.CombinedOutput()
   166  
   167  			if err == nil {
   168  				// trim to only get the first line
   169  				result.description = strings.SplitN(string(output), "\n", 2)[0]
   170  			} else {
   171  				result.description = fmt.Sprintf("error occurred running '%s --description'", plugin)
   172  				log.Errorf("'%s --description': %s", plugin, err)
   173  			}
   174  		}(plugin)
   175  	}
   176  	resultMap := map[string]PluginDescription{}
   177  	// gather the results at the end
   178  	for _ = range plugins {
   179  		result := <-description
   180  		resultMap[result.name] = result
   181  	}
   182  	// plugins array is already sorted, use this to get the results in order
   183  	for _, plugin := range plugins {
   184  		// Strip the 'juju-' off the start of the plugin name in the results
   185  		result := resultMap[plugin]
   186  		result.name = result.name[len(JujuPluginPrefix):]
   187  		results = append(results, result)
   188  	}
   189  	return results
   190  }
   191  
   192  // findPlugins searches the current PATH for executable files that start with
   193  // JujuPluginPrefix.
   194  func findPlugins() []string {
   195  	path := os.Getenv("PATH")
   196  	plugins := []string{}
   197  	for _, name := range filepath.SplitList(path) {
   198  		entries, err := ioutil.ReadDir(name)
   199  		if err != nil {
   200  			continue
   201  		}
   202  		for _, entry := range entries {
   203  			if strings.HasPrefix(entry.Name(), JujuPluginPrefix) && (entry.Mode()&0111) != 0 {
   204  				plugins = append(plugins, entry.Name())
   205  			}
   206  		}
   207  	}
   208  	sort.Strings(plugins)
   209  	return plugins
   210  }