github.1git.de/docker/cli@v26.1.3+incompatible/cli-plugins/manager/plugin.go (about) 1 package manager 2 3 import ( 4 "encoding/json" 5 "os" 6 "os/exec" 7 "path/filepath" 8 "regexp" 9 "strings" 10 11 "github.com/pkg/errors" 12 "github.com/spf13/cobra" 13 ) 14 15 var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$") 16 17 // Plugin represents a potential plugin with all it's metadata. 18 type Plugin struct { 19 Metadata 20 21 Name string `json:",omitempty"` 22 Path string `json:",omitempty"` 23 24 // Err is non-nil if the plugin failed one of the candidate tests. 25 Err error `json:",omitempty"` 26 27 // ShadowedPaths contains the paths of any other plugins which this plugin takes precedence over. 28 ShadowedPaths []string `json:",omitempty"` 29 } 30 31 // newPlugin determines if the given candidate is valid and returns a 32 // Plugin. If the candidate fails one of the tests then `Plugin.Err` 33 // is set, and is always a `pluginError`, but the `Plugin` is still 34 // returned with no error. An error is only returned due to a 35 // non-recoverable error. 36 func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { 37 path := c.Path() 38 if path == "" { 39 return Plugin{}, errors.New("plugin candidate path cannot be empty") 40 } 41 42 // The candidate listing process should have skipped anything 43 // which would fail here, so there are all real errors. 44 fullname := filepath.Base(path) 45 if fullname == "." { 46 return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path) 47 } 48 var err error 49 if fullname, err = trimExeSuffix(fullname); err != nil { 50 return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path) 51 } 52 if !strings.HasPrefix(fullname, NamePrefix) { 53 return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix) 54 } 55 56 p := Plugin{ 57 Name: strings.TrimPrefix(fullname, NamePrefix), 58 Path: path, 59 } 60 61 // Now apply the candidate tests, so these update p.Err. 62 if !pluginNameRe.MatchString(p.Name) { 63 p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String()) 64 return p, nil 65 } 66 67 for _, cmd := range cmds { 68 // Ignore conflicts with commands which are 69 // just plugin stubs (i.e. from a previous 70 // call to AddPluginCommandStubs). 71 if IsPluginCommand(cmd) { 72 continue 73 } 74 if cmd.Name() == p.Name { 75 p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name) 76 return p, nil 77 } 78 if cmd.HasAlias(p.Name) { 79 p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name()) 80 return p, nil 81 } 82 } 83 84 // We are supposed to check for relevant execute permissions here. Instead we rely on an attempt to execute. 85 meta, err := c.Metadata() 86 if err != nil { 87 p.Err = wrapAsPluginError(err, "failed to fetch metadata") 88 return p, nil 89 } 90 91 if err := json.Unmarshal(meta, &p.Metadata); err != nil { 92 p.Err = wrapAsPluginError(err, "invalid metadata") 93 return p, nil 94 } 95 if p.Metadata.SchemaVersion != "0.1.0" { 96 p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion) 97 return p, nil 98 } 99 if p.Metadata.Vendor == "" { 100 p.Err = NewPluginError("plugin metadata does not define a vendor") 101 return p, nil 102 } 103 return p, nil 104 } 105 106 // RunHook executes the plugin's hooks command 107 // and returns its unprocessed output. 108 func (p *Plugin) RunHook(hookData HookPluginData) ([]byte, error) { 109 hDataBytes, err := json.Marshal(hookData) 110 if err != nil { 111 return nil, wrapAsPluginError(err, "failed to marshall hook data") 112 } 113 114 pCmd := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes)) 115 pCmd.Env = os.Environ() 116 pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0]) 117 hookCmdOutput, err := pCmd.Output() 118 if err != nil { 119 return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand") 120 } 121 122 return hookCmdOutput, nil 123 }