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