github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 "regexp" 14 "sort" 15 "strings" 16 "syscall" 17 18 "github.com/juju/cmd" 19 "github.com/juju/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.JujuModelEnvKey + "=" + c.ConnectionName()}..., 98 ) 99 100 // Now hook up stdin, stdout, stderr 101 command.Stdin = ctx.Stdin 102 command.Stdout = ctx.Stdout 103 command.Stderr = ctx.Stderr 104 // And run it! 105 err := command.Run() 106 107 if exitError, ok := err.(*exec.ExitError); ok && exitError != nil { 108 status := exitError.ProcessState.Sys().(syscall.WaitStatus) 109 if status.Exited() { 110 return cmd.NewRcPassthroughError(status.ExitStatus()) 111 } 112 } 113 return err 114 } 115 116 type PluginDescription struct { 117 name string 118 description string 119 } 120 121 const PluginTopicText = `Juju Plugins 122 123 Plugins are implemented as stand-alone executable files somewhere in the user's PATH. 124 The executable command must be of the format juju-<plugin name>. 125 126 ` 127 128 func PluginHelpTopic() string { 129 output := &bytes.Buffer{} 130 fmt.Fprintf(output, PluginTopicText) 131 132 existingPlugins := GetPluginDescriptions() 133 134 if len(existingPlugins) == 0 { 135 fmt.Fprintf(output, "No plugins found.\n") 136 } else { 137 longest := 0 138 for _, plugin := range existingPlugins { 139 if len(plugin.name) > longest { 140 longest = len(plugin.name) 141 } 142 } 143 for _, plugin := range existingPlugins { 144 fmt.Fprintf(output, "%-*s %s\n", longest, plugin.name, plugin.description) 145 } 146 } 147 148 return output.String() 149 } 150 151 // GetPluginDescriptions runs each plugin with "--description". The calls to 152 // the plugins are run in parallel, so the function should only take as long 153 // as the longest call. 154 func GetPluginDescriptions() []PluginDescription { 155 plugins := findPlugins() 156 results := []PluginDescription{} 157 if len(plugins) == 0 { 158 return results 159 } 160 // create a channel with enough backing for each plugin 161 description := make(chan PluginDescription, len(plugins)) 162 163 // exec the command, and wait only for the timeout before killing the process 164 for _, plugin := range plugins { 165 go func(plugin string) { 166 result := PluginDescription{name: plugin} 167 defer func() { 168 description <- result 169 }() 170 desccmd := exec.Command(plugin, "--description") 171 output, err := desccmd.CombinedOutput() 172 173 if err == nil { 174 // trim to only get the first line 175 result.description = strings.SplitN(string(output), "\n", 2)[0] 176 } else { 177 result.description = fmt.Sprintf("error occurred running '%s --description'", plugin) 178 logger.Errorf("'%s --description': %s", plugin, err) 179 } 180 }(plugin) 181 } 182 resultMap := map[string]PluginDescription{} 183 // gather the results at the end 184 for _ = range plugins { 185 result := <-description 186 resultMap[result.name] = result 187 } 188 // plugins array is already sorted, use this to get the results in order 189 for _, plugin := range plugins { 190 // Strip the 'juju-' off the start of the plugin name in the results 191 result := resultMap[plugin] 192 result.name = result.name[len(JujuPluginPrefix):] 193 results = append(results, result) 194 } 195 return results 196 } 197 198 // findPlugins searches the current PATH for executable files that match 199 // JujuPluginPattern. 200 func findPlugins() []string { 201 re := regexp.MustCompile(JujuPluginPattern) 202 path := os.Getenv("PATH") 203 plugins := []string{} 204 for _, name := range filepath.SplitList(path) { 205 entries, err := ioutil.ReadDir(name) 206 if err != nil { 207 continue 208 } 209 for _, entry := range entries { 210 if re.Match([]byte(entry.Name())) && (entry.Mode()&0111) != 0 { 211 plugins = append(plugins, entry.Name()) 212 } 213 } 214 } 215 sort.Strings(plugins) 216 return plugins 217 }