2018-12-11 09:03:47 -05:00
package manager
import (
2024-04-26 07:03:56 -04:00
"context"
2018-12-11 09:03:47 -05:00
"encoding/json"
2024-04-17 11:57:44 -04:00
"os"
2023-07-20 11:25:36 -04:00
"os/exec"
2018-12-11 09:03:47 -05:00
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
2022-09-29 11:21:51 -04:00
var pluginNameRe = regexp . MustCompile ( "^[a-z][a-z0-9]*$" )
2018-12-11 09:03:47 -05:00
// Plugin represents a potential plugin with all it's metadata.
type Plugin struct {
Metadata
2018-12-19 09:49:20 -05:00
Name string ` json:",omitempty" `
Path string ` json:",omitempty" `
2018-12-11 09:03:47 -05:00
// 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`
2018-12-19 09:49:20 -05:00
// 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.
2023-04-01 09:40:32 -04:00
func newPlugin ( c Candidate , cmds [ ] * cobra . Command ) ( Plugin , error ) {
2018-12-11 09:03:47 -05:00
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 )
}
2019-01-14 12:53:19 -05:00
var err error
if fullname , err = trimExeSuffix ( fullname ) ; err != nil {
return Plugin { } , errors . Wrapf ( err , "plugin candidate %q" , path )
2018-12-11 09:03:47 -05:00
}
if ! strings . HasPrefix ( fullname , NamePrefix ) {
2019-01-14 12:53:19 -05:00
return Plugin { } , errors . Errorf ( "plugin candidate %q: does not have %q prefix" , path , NamePrefix )
2018-12-11 09:03:47 -05:00
}
p := Plugin {
Name : strings . TrimPrefix ( fullname , NamePrefix ) ,
Path : path ,
}
// Now apply the candidate tests, so these update p.Err.
if ! pluginNameRe . MatchString ( p . Name ) {
2018-12-19 09:49:20 -05:00
p . Err = NewPluginError ( "plugin candidate %q did not match %q" , p . Name , pluginNameRe . String ( ) )
2018-12-11 09:03:47 -05:00
return p , nil
}
2023-04-01 09:40:32 -04:00
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
2018-12-11 09:03:47 -05:00
}
}
// 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 {
2018-12-19 09:49:20 -05:00
p . Err = wrapAsPluginError ( err , "failed to fetch metadata" )
2018-12-11 09:03:47 -05:00
return p , nil
}
if err := json . Unmarshal ( meta , & p . Metadata ) ; err != nil {
2018-12-19 09:49:20 -05:00
p . Err = wrapAsPluginError ( err , "invalid metadata" )
2018-12-11 09:03:47 -05:00
return p , nil
}
if p . Metadata . SchemaVersion != "0.1.0" {
2018-12-19 09:49:20 -05:00
p . Err = NewPluginError ( "plugin SchemaVersion %q is not valid, must be 0.1.0" , p . Metadata . SchemaVersion )
2018-12-11 09:03:47 -05:00
return p , nil
}
if p . Metadata . Vendor == "" {
2018-12-19 09:49:20 -05:00
p . Err = NewPluginError ( "plugin metadata does not define a vendor" )
2018-12-11 09:03:47 -05:00
return p , nil
}
return p , nil
}
2023-07-20 11:25:36 -04:00
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
2024-04-26 07:03:56 -04:00
func ( p * Plugin ) RunHook ( ctx context . Context , hookData HookPluginData ) ( [ ] byte , error ) {
2024-04-22 12:12:53 -04:00
hDataBytes , err := json . Marshal ( hookData )
2023-07-20 11:25:36 -04:00
if err != nil {
return nil , wrapAsPluginError ( err , "failed to marshall hook data" )
}
2024-10-30 10:30:46 -04:00
pCmd := exec . CommandContext ( ctx , p . Path , p . Name , HookSubcommandName , string ( hDataBytes ) ) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
2024-04-17 11:57:44 -04:00
pCmd . Env = os . Environ ( )
pCmd . Env = append ( pCmd . Env , ReexecEnvvar + "=" + os . Args [ 0 ] )
hookCmdOutput , err := pCmd . Output ( )
2023-07-20 11:25:36 -04:00
if err != nil {
return nil , wrapAsPluginError ( err , "failed to execute plugin hook subcommand" )
}
return hookCmdOutput , nil
}