github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "fmt" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "regexp" 13 "sort" 14 "strings" 15 "syscall" 16 17 "github.com/juju/cmd" 18 "github.com/juju/gnuflag" 19 "github.com/juju/utils" 20 "github.com/juju/utils/set" 21 22 "github.com/juju/juju/juju/osenv" 23 ) 24 25 const JujuPluginPrefix = "juju-" 26 const JujuPluginPattern = "^juju-[a-zA-Z]" 27 28 var jujuArgNames = set.NewStrings("-m", "--model", "-c", "--controller") 29 30 // This is a very rudimentary method used to extract common Juju 31 // arguments from the full list passed to the plugin. 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 !jujuArgNames.Contains(arg) { 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 := &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.NewFlagSetWithFlagKnownAs(cmdName, gnuflag.ContinueOnError, "option") 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 cmd.CommandBase 79 name string 80 81 controllerName string 82 modelName string 83 84 args []string 85 } 86 87 // Info is just a stub so that PluginCommand implements cmd.Command. 88 // Since this is never actually called, we can happily return nil. 89 func (*PluginCommand) Info() *cmd.Info { 90 return nil 91 } 92 93 func (c *PluginCommand) Init(args []string) error { 94 c.args = args 95 return nil 96 } 97 98 func (c *PluginCommand) SetFlags(f *gnuflag.FlagSet) { 99 f.StringVar(&c.modelName, "m", "", "Model to operate in. Accepts [<controller name>:]<model name>") 100 f.StringVar(&c.modelName, "model", "", "") 101 f.StringVar(&c.controllerName, "c", "", "Controller to operate in") 102 f.StringVar(&c.controllerName, "controller", "", "") 103 c.CommandBase.SetFlags(f) 104 } 105 106 func (c *PluginCommand) Run(ctx *cmd.Context) error { 107 command := exec.Command(c.name, c.args...) 108 109 env := os.Environ() 110 if c.controllerName != "" { 111 env = utils.Setenv(env, osenv.JujuControllerEnvKey+"="+c.controllerName) 112 } 113 if c.modelName != "" { 114 env = utils.Setenv(env, osenv.JujuModelEnvKey+"="+c.modelName) 115 } 116 command.Env = env 117 118 // Now hook up stdin, stdout, stderr 119 command.Stdin = ctx.Stdin 120 command.Stdout = ctx.Stdout 121 command.Stderr = ctx.Stderr 122 // And run it! 123 err := command.Run() 124 125 if exitError, ok := err.(*exec.ExitError); ok && exitError != nil { 126 status := exitError.ProcessState.Sys().(syscall.WaitStatus) 127 if status.Exited() { 128 return cmd.NewRcPassthroughError(status.ExitStatus()) 129 } 130 } 131 return err 132 } 133 134 type PluginDescription struct { 135 name string 136 description string 137 } 138 139 // GetPluginDescriptions runs each plugin with "--description". The calls to 140 // the plugins are run in parallel, so the function should only take as long 141 // as the longest call. 142 func GetPluginDescriptions() []PluginDescription { 143 plugins := findPlugins() 144 results := []PluginDescription{} 145 if len(plugins) == 0 { 146 return results 147 } 148 // create a channel with enough backing for each plugin 149 description := make(chan PluginDescription, len(plugins)) 150 151 // exec the command, and wait only for the timeout before killing the process 152 for _, plugin := range plugins { 153 go func(plugin string) { 154 result := PluginDescription{name: plugin} 155 defer func() { 156 description <- result 157 }() 158 desccmd := exec.Command(plugin, "--description") 159 output, err := desccmd.CombinedOutput() 160 161 if err == nil { 162 // trim to only get the first line 163 result.description = strings.SplitN(string(output), "\n", 2)[0] 164 } else { 165 result.description = fmt.Sprintf("error occurred running '%s --description'", plugin) 166 logger.Errorf("'%s --description': %s", plugin, err) 167 } 168 }(plugin) 169 } 170 resultMap := map[string]PluginDescription{} 171 // gather the results at the end 172 for range plugins { 173 result := <-description 174 resultMap[result.name] = result 175 } 176 // plugins array is already sorted, use this to get the results in order 177 for _, plugin := range plugins { 178 // Strip the 'juju-' off the start of the plugin name in the results 179 result := resultMap[plugin] 180 result.name = result.name[len(JujuPluginPrefix):] 181 results = append(results, result) 182 } 183 return results 184 } 185 186 // findPlugins searches the current PATH for executable files that match 187 // JujuPluginPattern. 188 func findPlugins() []string { 189 re := regexp.MustCompile(JujuPluginPattern) 190 path := os.Getenv("PATH") 191 plugins := []string{} 192 for _, name := range filepath.SplitList(path) { 193 entries, err := ioutil.ReadDir(name) 194 if err != nil { 195 continue 196 } 197 for _, entry := range entries { 198 if re.Match([]byte(entry.Name())) && (entry.Mode()&0111) != 0 { 199 plugins = append(plugins, entry.Name()) 200 } 201 } 202 } 203 sort.Strings(plugins) 204 return plugins 205 }