Files
docker-cli/cli-plugins/manager/plugin.go
Laura Brehm 5011759056 hooks: set expected environment when executing
During normal plugin execution (from the CLI), the CLI configures the
plugin command it's about to execute in order to pass all environment
variables on, as well as to set the ReExec env var that informs the
plugin about how it was executed, and which plugins rely on to check
whether they are being run standalone or not.

This commit adds the same behavior to hook invocations, which is
necessary for some plugins to know that they are not running standalone
so that they expose their root command at the correct level.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-04-17 16:57:44 +01:00

127 lines
3.7 KiB
Go

package manager
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
// Plugin represents a potential plugin with all it's metadata.
type Plugin struct {
Metadata
Name string `json:",omitempty"`
Path string `json:",omitempty"`
// Err is non-nil if the plugin failed one of the candidate tests.
Err error `json:",omitempty"`
// ShadowedPaths contains the paths of any other plugins which this plugin takes precedence over.
ShadowedPaths []string `json:",omitempty"`
}
// newPlugin determines if the given candidate is valid and returns a
// Plugin. If the candidate fails one of the tests then `Plugin.Err`
// is set, and is always a `pluginError`, but the `Plugin` is still
// returned with no error. An error is only returned due to a
// non-recoverable error.
func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
path := c.Path()
if path == "" {
return Plugin{}, errors.New("plugin candidate path cannot be empty")
}
// The candidate listing process should have skipped anything
// which would fail here, so there are all real errors.
fullname := filepath.Base(path)
if fullname == "." {
return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
}
var err error
if fullname, err = trimExeSuffix(fullname); err != nil {
return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
}
if !strings.HasPrefix(fullname, NamePrefix) {
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
}
p := Plugin{
Name: strings.TrimPrefix(fullname, NamePrefix),
Path: path,
}
// Now apply the candidate tests, so these update p.Err.
if !pluginNameRe.MatchString(p.Name) {
p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
return p, nil
}
for _, cmd := range cmds {
// Ignore conflicts with commands which are
// just plugin stubs (i.e. from a previous
// call to AddPluginCommandStubs).
if IsPluginCommand(cmd) {
continue
}
if cmd.Name() == p.Name {
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
return p, nil
}
if cmd.HasAlias(p.Name) {
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
return p, nil
}
}
// We are supposed to check for relevant execute permissions here. Instead we rely on an attempt to execute.
meta, err := c.Metadata()
if err != nil {
p.Err = wrapAsPluginError(err, "failed to fetch metadata")
return p, nil
}
if err := json.Unmarshal(meta, &p.Metadata); err != nil {
p.Err = wrapAsPluginError(err, "invalid metadata")
return p, nil
}
if p.Metadata.SchemaVersion != "0.1.0" {
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
return p, nil
}
if p.Metadata.Vendor == "" {
p.Err = NewPluginError("plugin metadata does not define a vendor")
return p, nil
}
return p, nil
}
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(cmdName string, flags map[string]string) ([]byte, error) {
hDataBytes, err := json.Marshal(HookPluginData{
RootCmd: cmdName,
Flags: flags,
})
if err != nil {
return nil, wrapAsPluginError(err, "failed to marshall hook data")
}
pCmd := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes))
pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output()
if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
}
return hookCmdOutput, nil
}