From 7a3ec922e4d655d7e7029d77ca4f6c74fb9cc49b Mon Sep 17 00:00:00 2001 From: Maciej Szulik Date: Mon, 3 Nov 2025 11:25:04 +0100 Subject: [PATCH] Move PluginHandler to separate file Signed-off-by: Maciej Szulik Kubernetes-commit: ac9120f6076217a6a033cf9a32dd89a61713c59f --- pkg/cmd/cmd.go | 139 --------------------------------------- pkg/cmd/plugin.go | 162 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 139 deletions(-) create mode 100644 pkg/cmd/plugin.go diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 608e6a016..19156aa32 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -20,11 +20,7 @@ import ( "fmt" "net/http" "os" - "os/exec" - "path/filepath" - "runtime" "strings" - "syscall" "github.com/spf13/cobra" @@ -167,141 +163,6 @@ func NewDefaultKubectlCommandWithArgs(o KubectlOptions) *cobra.Command { return cmd } -// 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 -} - -// 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 -} - -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 -} - -// 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) -} - -// 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 -} - // NewKubectlCommand creates the `kubectl` command and its nested children. func NewKubectlCommand(o KubectlOptions) *cobra.Command { warningHandler := rest.NewWarningWriter(o.IOStreams.ErrOut, rest.WarningWriterOptions{Deduplicate: true, Color: term.AllowsColorOutput(o.IOStreams.ErrOut)}) diff --git a/pkg/cmd/plugin.go b/pkg/cmd/plugin.go new file mode 100644 index 000000000..36c9fcdca --- /dev/null +++ b/pkg/cmd/plugin.go @@ -0,0 +1,162 @@ +/* +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 +}