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 }