1 package manager
2
3 import (
4 "context"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "sort"
9 "strings"
10 "sync"
11
12 "github.com/docker/cli/cli/command"
13 "github.com/docker/cli/cli/config"
14 "github.com/docker/cli/cli/config/configfile"
15 "github.com/fvbommel/sortorder"
16 "github.com/spf13/cobra"
17 "golang.org/x/sync/errgroup"
18 )
19
20
21
22
23
24 const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
25
26
27 type errPluginNotFound string
28
29 func (e errPluginNotFound) NotFound() {}
30
31 func (e errPluginNotFound) Error() string {
32 return "Error: No such CLI plugin: " + string(e)
33 }
34
35 type notFound interface{ NotFound() }
36
37
38 func IsNotFound(err error) bool {
39 if e, ok := err.(*pluginError); ok {
40 err = e.Cause()
41 }
42 _, ok := err.(notFound)
43 return ok
44 }
45
46 func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
47 var pluginDirs []string
48
49 if cfg != nil {
50 pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
51 }
52 pluginDir, err := config.Path("cli-plugins")
53 if err != nil {
54 return nil, err
55 }
56
57 pluginDirs = append(pluginDirs, pluginDir)
58 pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
59 return pluginDirs, nil
60 }
61
62 func addPluginCandidatesFromDir(res map[string][]string, d string) error {
63 dentries, err := os.ReadDir(d)
64 if err != nil {
65 return err
66 }
67 for _, dentry := range dentries {
68 switch dentry.Type() & os.ModeType {
69 case 0, os.ModeSymlink:
70
71 default:
72
73 continue
74 }
75 name := dentry.Name()
76 if !strings.HasPrefix(name, NamePrefix) {
77 continue
78 }
79 name = strings.TrimPrefix(name, NamePrefix)
80 var err error
81 if name, err = trimExeSuffix(name); err != nil {
82 continue
83 }
84 res[name] = append(res[name], filepath.Join(d, dentry.Name()))
85 }
86 return nil
87 }
88
89
90 func listPluginCandidates(dirs []string) (map[string][]string, error) {
91 result := make(map[string][]string)
92 for _, d := range dirs {
93
94
95
96 if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
97 continue
98 }
99 if err := addPluginCandidatesFromDir(result, d); err != nil {
100
101 if os.IsNotExist(err) {
102 continue
103 }
104 return nil, err
105 }
106 }
107 return result, nil
108 }
109
110
111 func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
112 pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
113 if err != nil {
114 return nil, err
115 }
116
117 candidates, err := listPluginCandidates(pluginDirs)
118 if err != nil {
119 return nil, err
120 }
121
122 if paths, ok := candidates[name]; ok {
123 if len(paths) == 0 {
124 return nil, errPluginNotFound(name)
125 }
126 c := &candidate{paths[0]}
127 p, err := newPlugin(c, rootcmd.Commands())
128 if err != nil {
129 return nil, err
130 }
131 if !IsNotFound(p.Err) {
132 p.ShadowedPaths = paths[1:]
133 }
134 return &p, nil
135 }
136
137 return nil, errPluginNotFound(name)
138 }
139
140
141 func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
142 pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
143 if err != nil {
144 return nil, err
145 }
146
147 candidates, err := listPluginCandidates(pluginDirs)
148 if err != nil {
149 return nil, err
150 }
151
152 var plugins []Plugin
153 var mu sync.Mutex
154 eg, _ := errgroup.WithContext(context.TODO())
155 cmds := rootcmd.Commands()
156 for _, paths := range candidates {
157 func(paths []string) {
158 eg.Go(func() error {
159 if len(paths) == 0 {
160 return nil
161 }
162 c := &candidate{paths[0]}
163 p, err := newPlugin(c, cmds)
164 if err != nil {
165 return err
166 }
167 if !IsNotFound(p.Err) {
168 p.ShadowedPaths = paths[1:]
169 mu.Lock()
170 defer mu.Unlock()
171 plugins = append(plugins, p)
172 }
173 return nil
174 })
175 }(paths)
176 }
177 if err := eg.Wait(); err != nil {
178 return nil, err
179 }
180
181 sort.Slice(plugins, func(i, j int) bool {
182 return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
183 })
184
185 return plugins, nil
186 }
187
188
189
190
191 func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
192
193
194
195 args := os.Args[1:]
196 if !pluginNameRe.MatchString(name) {
197
198
199 return nil, errPluginNotFound(name)
200 }
201 exename := addExeSuffix(NamePrefix + name)
202 pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
203 if err != nil {
204 return nil, err
205 }
206
207 for _, d := range pluginDirs {
208 path := filepath.Join(d, exename)
209
210
211
212
213
214 if _, err := os.Stat(path); os.IsNotExist(err) {
215 continue
216 }
217
218 c := &candidate{path: path}
219 plugin, err := newPlugin(c, rootcmd.Commands())
220 if err != nil {
221 return nil, err
222 }
223 if plugin.Err != nil {
224
225 return nil, errPluginNotFound(name)
226 }
227 cmd := exec.Command(plugin.Path, args...)
228
229
230
231
232
233 cmd.Stdin = os.Stdin
234 cmd.Stdout = os.Stdout
235 cmd.Stderr = os.Stderr
236
237 cmd.Env = os.Environ()
238 cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
239
240 return cmd, nil
241 }
242 return nil, errPluginNotFound(name)
243 }
244
245
246 func IsPluginCommand(cmd *cobra.Command) bool {
247 return cmd.Annotations[CommandAnnotationPlugin] == "true"
248 }
249
View as plain text