github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/cli/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 // ListPlugins produces a list of the plugins available on the system 107 func ListPlugins(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 var plugins []Plugin 119 for _, paths := range candidates { 120 if len(paths) == 0 { 121 continue 122 } 123 c := &candidate{paths[0]} 124 p, err := newPlugin(c, rootcmd) 125 if err != nil { 126 return nil, err 127 } 128 if !IsNotFound(p.Err) { 129 p.ShadowedPaths = paths[1:] 130 plugins = append(plugins, p) 131 } 132 } 133 134 sort.Slice(plugins, func(i, j int) bool { 135 return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name) 136 }) 137 138 return plugins, nil 139 } 140 141 // PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin. 142 // The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts. 143 // The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow. 144 func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) { 145 // This uses the full original args, not the args which may 146 // have been provided by cobra to our caller. This is because 147 // they lack e.g. global options which we must propagate here. 148 args := os.Args[1:] 149 if !pluginNameRe.MatchString(name) { 150 // We treat this as "not found" so that callers will 151 // fallback to their "invalid" command path. 152 return nil, errPluginNotFound(name) 153 } 154 exename := addExeSuffix(NamePrefix + name) 155 pluginDirs, err := getPluginDirs(dockerCli) 156 if err != nil { 157 return nil, err 158 } 159 160 for _, d := range pluginDirs { 161 path := filepath.Join(d, exename) 162 163 // We stat here rather than letting the exec tell us 164 // ENOENT because the latter does not distinguish a 165 // file not existing from its dynamic loader or one of 166 // its libraries not existing. 167 if _, err := os.Stat(path); os.IsNotExist(err) { 168 continue 169 } 170 171 c := &candidate{path: path} 172 plugin, err := newPlugin(c, rootcmd) 173 if err != nil { 174 return nil, err 175 } 176 if plugin.Err != nil { 177 // TODO: why are we not returning plugin.Err? 178 return nil, errPluginNotFound(name) 179 } 180 cmd := exec.Command(plugin.Path, args...) 181 // Using dockerCli.{In,Out,Err}() here results in a hang until something is input. 182 // See: - https://github.com/golang/go/issues/10338 183 // - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab 184 // os.Stdin is a *os.File which avoids this behaviour. We don't need the functionality 185 // of the wrappers here anyway. 186 cmd.Stdin = os.Stdin 187 cmd.Stdout = os.Stdout 188 cmd.Stderr = os.Stderr 189 190 cmd.Env = os.Environ() 191 cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0]) 192 193 return cmd, nil 194 } 195 return nil, errPluginNotFound(name) 196 }