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 }