1
// Copyright 2012, 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
19
"launchpad.net/gnuflag"
21
"github.com/juju/juju/cmd/modelcmd"
22
"github.com/juju/juju/juju/osenv"
25
const JujuPluginPrefix = "juju-"
26
const JujuPluginPattern = "^juju-[a-zA-Z]"
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 {
35
for nextArg := 0; nextArg < nrArgs; {
41
jujuArgs = append(jujuArgs, arg)
43
jujuArgs = append(jujuArgs, args[nextArg])
50
func RunPlugin(ctx *cmd.Context, subcommand string, args []string) error {
51
cmdName := JujuPluginPrefix + subcommand
52
plugin := modelcmd.Wrap(&PluginCommand{name: cmdName})
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 {
64
if err := plugin.Init(args); err != nil {
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.
74
return &cmd.UnrecognizedCommand{Name: subcommand}
77
type PluginCommand struct {
78
modelcmd.ModelCommandBase
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 {
89
func (c *PluginCommand) Init(args []string) error {
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.JujuXDGDataHomeEnvKey + "=" + osenv.JujuXDGDataHome(),
98
osenv.JujuModelEnvKey + "=" + c.ConnectionName()}...,
101
// Now hook up stdin, stdout, stderr
102
command.Stdin = ctx.Stdin
103
command.Stdout = ctx.Stdout
104
command.Stderr = ctx.Stderr
108
if exitError, ok := err.(*exec.ExitError); ok && exitError != nil {
109
status := exitError.ProcessState.Sys().(syscall.WaitStatus)
111
return cmd.NewRcPassthroughError(status.ExitStatus())
117
type PluginDescription struct {
122
const PluginTopicText = `Juju Plugins
124
Plugins are implemented as stand-alone executable files somewhere in the user's PATH.
125
The executable command must be of the format juju-<plugin name>.
129
func PluginHelpTopic() string {
130
output := &bytes.Buffer{}
131
fmt.Fprintf(output, PluginTopicText)
133
existingPlugins := GetPluginDescriptions()
135
if len(existingPlugins) == 0 {
136
fmt.Fprintf(output, "No plugins found.\n")
139
for _, plugin := range existingPlugins {
140
if len(plugin.name) > longest {
141
longest = len(plugin.name)
144
for _, plugin := range existingPlugins {
145
fmt.Fprintf(output, "%-*s %s\n", longest, plugin.name, plugin.description)
149
return output.String()
152
// GetPluginDescriptions runs each plugin with "--description". The calls to
153
// the plugins are run in parallel, so the function should only take as long
154
// as the longest call.
155
func GetPluginDescriptions() []PluginDescription {
156
plugins := findPlugins()
157
results := []PluginDescription{}
158
if len(plugins) == 0 {
161
// create a channel with enough backing for each plugin
162
description := make(chan PluginDescription, len(plugins))
164
// exec the command, and wait only for the timeout before killing the process
165
for _, plugin := range plugins {
166
go func(plugin string) {
167
result := PluginDescription{name: plugin}
169
description <- result
171
desccmd := exec.Command(plugin, "--description")
172
output, err := desccmd.CombinedOutput()
175
// trim to only get the first line
176
result.description = strings.SplitN(string(output), "\n", 2)[0]
178
result.description = fmt.Sprintf("error occurred running '%s --description'", plugin)
179
logger.Errorf("'%s --description': %s", plugin, err)
183
resultMap := map[string]PluginDescription{}
184
// gather the results at the end
185
for _ = range plugins {
186
result := <-description
187
resultMap[result.name] = result
189
// plugins array is already sorted, use this to get the results in order
190
for _, plugin := range plugins {
191
// Strip the 'juju-' off the start of the plugin name in the results
192
result := resultMap[plugin]
193
result.name = result.name[len(JujuPluginPrefix):]
194
results = append(results, result)
199
// findPlugins searches the current PATH for executable files that match
200
// JujuPluginPattern.
201
func findPlugins() []string {
202
re := regexp.MustCompile(JujuPluginPattern)
203
path := os.Getenv("PATH")
204
plugins := []string{}
205
for _, name := range filepath.SplitList(path) {
206
entries, err := ioutil.ReadDir(name)
210
for _, entry := range entries {
211
if re.Match([]byte(entry.Name())) && (entry.Mode()&0111) != 0 {
212
plugins = append(plugins, entry.Name())
216
sort.Strings(plugins)