github.com/cspotcode/docker-cli@v20.10.0-rc1.0.20201201121459-3faad7acc5b8+incompatible/cli-plugins/manager/manager.go (about) 1 package manager 2 3 import ( 4 "io/ioutil" 5 "os" 6 "os/exec" 7 "path/filepath" 8 "sort" 9 "strings" 10 11 "github.com/docker/cli/cli/command" 12 "github.com/docker/cli/cli/config" 13 "github.com/fvbommel/sortorder" 14 "github.com/spf13/cobra" 15 ) 16 17 // ReexecEnvvar is the name of an ennvar which is set to the command 18 // used to originally invoke the docker CLI when executing a 19 // plugin. Assuming $PATH and $CWD remain unchanged this should allow 20 // the plugin to re-execute the original CLI. 21 const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND" 22 23 // errPluginNotFound is the error returned when a plugin could not be found. 24 type errPluginNotFound string 25 26 func (e errPluginNotFound) NotFound() {} 27 28 func (e errPluginNotFound) Error() string { 29 return "Error: No such CLI plugin: " + string(e) 30 } 31 32 type notFound interface{ NotFound() } 33 34 // IsNotFound is true if the given error is due to a plugin not being found. 35 func IsNotFound(err error) bool { 36 if e, ok := err.(*pluginError); ok { 37 err = e.Cause() 38 } 39 _, ok := err.(notFound) 40 return ok 41 } 42 43 func getPluginDirs(dockerCli command.Cli) ([]string, error) { 44 var pluginDirs []string 45 46 if cfg := dockerCli.ConfigFile(); cfg != nil { 47 pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...) 48 } 49 pluginDir, err := config.Path("cli-plugins") 50 if err != nil { 51 return nil, err 52 } 53 54 pluginDirs = append(pluginDirs, pluginDir) 55 pluginDirs = append(pluginDirs, defaultSystemPluginDirs...) 56 return pluginDirs, nil 57 } 58 59 func addPluginCandidatesFromDir(res map[string][]string, d string) error { 60 dentries, err := ioutil.ReadDir(d) 61 if err != nil { 62 return err 63 } 64 for _, dentry := range dentries { 65 switch dentry.Mode() & os.ModeType { 66 case 0, os.ModeSymlink: 67 // Regular file or symlink, keep going 68 default: 69 // Something else, ignore. 70 continue 71 } 72 name := dentry.Name() 73 if !strings.HasPrefix(name, NamePrefix) { 74 continue 75 } 76 name = strings.TrimPrefix(name, NamePrefix) 77 var err error 78 if name, err = trimExeSuffix(name); err != nil { 79 continue 80 } 81 res[name] = append(res[name], filepath.Join(d, dentry.Name())) 82 } 83 return nil 84 } 85 86 // listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority. 87 func listPluginCandidates(dirs []string) (map[string][]string, error) { 88 result := make(map[string][]string) 89 for _, d := range dirs { 90 // Silently ignore any directories which we cannot 91 // Stat (e.g. due to permissions or anything else) or 92 // which is not a directory. 93 if fi, err := os.Stat(d); err != nil || !fi.IsDir() { 94 continue 95 } 96 if err := addPluginCandidatesFromDir(result, d); err != nil { 97 // Silently ignore paths which don't exist. 98 if os.IsNotExist(err) { 99 continue 100 } 101 return nil, err // Or return partial result? 102 } 103 } 104 return result, nil 105 } 106 107 // ListPlugins produces a list of the plugins available on the system 108 func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) { 109 pluginDirs, err := getPluginDirs(dockerCli) 110 if err != nil { 111 return nil, err 112 } 113 114 candidates, err := listPluginCandidates(pluginDirs) 115 if err != nil { 116 return nil, err 117 } 118 119 var plugins []Plugin 120 for _, paths := range candidates { 121 if len(paths) == 0 { 122 continue 123 } 124 c := &candidate{paths[0]} 125 p, err := newPlugin(c, rootcmd) 126 if err != nil { 127 return nil, err 128 } 129 if !IsNotFound(p.Err) { 130 p.ShadowedPaths = paths[1:] 131 plugins = append(plugins, p) 132 } 133 } 134 135 sort.Slice(plugins, func(i, j int) bool { 136 return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name) 137 }) 138 139 return plugins, nil 140 } 141 142 // PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin. 143 // The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts. 144 // The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow. 145 func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) { 146 // This uses the full original args, not the args which may 147 // have been provided by cobra to our caller. This is because 148 // they lack e.g. global options which we must propagate here. 149 args := os.Args[1:] 150 if !pluginNameRe.MatchString(name) { 151 // We treat this as "not found" so that callers will 152 // fallback to their "invalid" command path. 153 return nil, errPluginNotFound(name) 154 } 155 exename := addExeSuffix(NamePrefix + name) 156 pluginDirs, err := getPluginDirs(dockerCli) 157 if err != nil { 158 return nil, err 159 } 160 161 for _, d := range pluginDirs { 162 path := filepath.Join(d, exename) 163 164 // We stat here rather than letting the exec tell us 165 // ENOENT because the latter does not distinguish a 166 // file not existing from its dynamic loader or one of 167 // its libraries not existing. 168 if _, err := os.Stat(path); os.IsNotExist(err) { 169 continue 170 } 171 172 c := &candidate{path: path} 173 plugin, err := newPlugin(c, rootcmd) 174 if err != nil { 175 return nil, err 176 } 177 if plugin.Err != nil { 178 // TODO: why are we not returning plugin.Err? 179 return nil, errPluginNotFound(name) 180 } 181 cmd := exec.Command(plugin.Path, args...) 182 // Using dockerCli.{In,Out,Err}() here results in a hang until something is input. 183 // See: - https://github.com/golang/go/issues/10338 184 // - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab 185 // os.Stdin is a *os.File which avoids this behaviour. We don't need the functionality 186 // of the wrappers here anyway. 187 cmd.Stdin = os.Stdin 188 cmd.Stdout = os.Stdout 189 cmd.Stderr = os.Stderr 190 191 cmd.Env = os.Environ() 192 cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0]) 193 194 return cmd, nil 195 } 196 return nil, errPluginNotFound(name) 197 }