~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/cmd/juju/commands/plugin.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
        "launchpad.net/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.JujuXDGDataHomeEnvKey + "=" + osenv.JujuXDGDataHome(),
 
98
                osenv.JujuModelEnvKey + "=" + c.ConnectionName()}...,
 
99
        )
 
100
 
 
101
        // Now hook up stdin, stdout, stderr
 
102
        command.Stdin = ctx.Stdin
 
103
        command.Stdout = ctx.Stdout
 
104
        command.Stderr = ctx.Stderr
 
105
        // And run it!
 
106
        err := command.Run()
 
107
 
 
108
        if exitError, ok := err.(*exec.ExitError); ok && exitError != nil {
 
109
                status := exitError.ProcessState.Sys().(syscall.WaitStatus)
 
110
                if status.Exited() {
 
111
                        return cmd.NewRcPassthroughError(status.ExitStatus())
 
112
                }
 
113
        }
 
114
        return err
 
115
}
 
116
 
 
117
type PluginDescription struct {
 
118
        name        string
 
119
        description string
 
120
}
 
121
 
 
122
const PluginTopicText = `Juju Plugins
 
123
 
 
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>.
 
126
 
 
127
`
 
128
 
 
129
func PluginHelpTopic() string {
 
130
        output := &bytes.Buffer{}
 
131
        fmt.Fprintf(output, PluginTopicText)
 
132
 
 
133
        existingPlugins := GetPluginDescriptions()
 
134
 
 
135
        if len(existingPlugins) == 0 {
 
136
                fmt.Fprintf(output, "No plugins found.\n")
 
137
        } else {
 
138
                longest := 0
 
139
                for _, plugin := range existingPlugins {
 
140
                        if len(plugin.name) > longest {
 
141
                                longest = len(plugin.name)
 
142
                        }
 
143
                }
 
144
                for _, plugin := range existingPlugins {
 
145
                        fmt.Fprintf(output, "%-*s  %s\n", longest, plugin.name, plugin.description)
 
146
                }
 
147
        }
 
148
 
 
149
        return output.String()
 
150
}
 
151
 
 
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 {
 
159
                return results
 
160
        }
 
161
        // create a channel with enough backing for each plugin
 
162
        description := make(chan PluginDescription, len(plugins))
 
163
 
 
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}
 
168
                        defer func() {
 
169
                                description <- result
 
170
                        }()
 
171
                        desccmd := exec.Command(plugin, "--description")
 
172
                        output, err := desccmd.CombinedOutput()
 
173
 
 
174
                        if err == nil {
 
175
                                // trim to only get the first line
 
176
                                result.description = strings.SplitN(string(output), "\n", 2)[0]
 
177
                        } else {
 
178
                                result.description = fmt.Sprintf("error occurred running '%s --description'", plugin)
 
179
                                logger.Errorf("'%s --description': %s", plugin, err)
 
180
                        }
 
181
                }(plugin)
 
182
        }
 
183
        resultMap := map[string]PluginDescription{}
 
184
        // gather the results at the end
 
185
        for _ = range plugins {
 
186
                result := <-description
 
187
                resultMap[result.name] = result
 
188
        }
 
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)
 
195
        }
 
196
        return results
 
197
}
 
198
 
 
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)
 
207
                if err != nil {
 
208
                        continue
 
209
                }
 
210
                for _, entry := range entries {
 
211
                        if re.Match([]byte(entry.Name())) && (entry.Mode()&0111) != 0 {
 
212
                                plugins = append(plugins, entry.Name())
 
213
                        }
 
214
                }
 
215
        }
 
216
        sort.Strings(plugins)
 
217
        return plugins
 
218
}