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