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