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