kubectl/pkg/cmd/plugin.go
Maciej Szulik 7a3ec922e4 Move PluginHandler to separate file
Signed-off-by: Maciej Szulik <soltysh@gmail.com>

Kubernetes-commit: ac9120f6076217a6a033cf9a32dd89a61713c59f
2025-11-03 11:25:04 +01:00

162 lines
4.8 KiB
Go

/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
)
// PluginHandler is capable of parsing command line arguments
// and performing executable filename lookups to search
// for valid plugin files, and execute found plugins.
type PluginHandler interface {
// exists at the given filename, or a boolean false.
// Lookup will iterate over a list of given prefixes
// in order to recognize valid plugin filenames.
// The first filepath to match a prefix is returned.
Lookup(filename string) (string, bool)
// Execute receives an executable's filepath, a slice
// of arguments, and a slice of environment variables
// to relay to the executable.
Execute(executablePath string, cmdArgs, environment []string) error
}
// DefaultPluginHandler implements PluginHandler
type DefaultPluginHandler struct {
ValidPrefixes []string
}
// NewDefaultPluginHandler instantiates the DefaultPluginHandler with a list of
// given filename prefixes used to identify valid plugin filenames.
func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler {
return &DefaultPluginHandler{
ValidPrefixes: validPrefixes,
}
}
// Lookup implements PluginHandler
func (h *DefaultPluginHandler) Lookup(filename string) (string, bool) {
for _, prefix := range h.ValidPrefixes {
path, err := exec.LookPath(fmt.Sprintf("%s-%s", prefix, filename))
if shouldSkipOnLookPathErr(err) || len(path) == 0 {
continue
}
return path, true
}
return "", false
}
// Execute implements PluginHandler
func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
// Windows does not support exec syscall.
if runtime.GOOS == "windows" {
cmd := command(executablePath, cmdArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Env = environment
err := cmd.Run()
if err == nil {
os.Exit(0)
}
return err
}
// invoke cmd binary relaying the environment and args given
// append executablePath to cmdArgs, as execve will make first argument the "binary name".
return syscall.Exec(executablePath, append([]string{executablePath}, cmdArgs...), environment)
}
func command(name string, arg ...string) *exec.Cmd {
cmd := &exec.Cmd{
Path: name,
Args: append([]string{name}, arg...),
}
if filepath.Base(name) == name {
lp, err := exec.LookPath(name)
if lp != "" && !shouldSkipOnLookPathErr(err) {
// Update cmd.Path even if err is non-nil.
// If err is ErrDot (especially on Windows), lp may include a resolved
// extension (like .exe or .bat) that should be preserved.
cmd.Path = lp
}
}
return cmd
}
// HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find
// a plugin executable on the PATH that satisfies the given arguments.
func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string, minArgs int) error {
var remainingArgs []string // all "non-flag" arguments
for _, arg := range cmdArgs {
if strings.HasPrefix(arg, "-") {
break
}
remainingArgs = append(remainingArgs, strings.Replace(arg, "-", "_", -1))
}
if len(remainingArgs) == 0 {
// the length of cmdArgs is at least 1
return fmt.Errorf("flags cannot be placed before plugin name: %s", cmdArgs[0])
}
foundBinaryPath := ""
// attempt to find binary, starting at longest possible name with given cmdArgs
for len(remainingArgs) > 0 {
path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-"))
if !found {
remainingArgs = remainingArgs[:len(remainingArgs)-1]
if len(remainingArgs) < minArgs {
// we shouldn't continue searching with shorter names.
// this is especially for not searching kubectl-create plugin
// when kubectl-create-foo plugin is not found.
break
}
continue
}
foundBinaryPath = path
break
}
if len(foundBinaryPath) == 0 {
return nil
}
// invoke cmd binary relaying the current environment and args given
if err := pluginHandler.Execute(foundBinaryPath, cmdArgs[len(remainingArgs):], os.Environ()); err != nil {
return err
}
return nil
}
// IsSubcommandPluginAllowed returns the given command is allowed
// to use plugin as subcommand if the subcommand does not exist as builtin.
func IsSubcommandPluginAllowed(foundCmd string) bool {
allowedCmds := map[string]struct{}{"create": {}}
_, ok := allowedCmds[foundCmd]
return ok
}