297 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
| Copyright 2017 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 plugin
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"runtime"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/spf13/cobra"
 | |
| 
 | |
| 	"k8s.io/cli-runtime/pkg/genericiooptions"
 | |
| 
 | |
| 	cmdutil "k8s.io/kubectl/pkg/cmd/util"
 | |
| 	"k8s.io/kubectl/pkg/util/i18n"
 | |
| 	"k8s.io/kubectl/pkg/util/templates"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	pluginLong = templates.LongDesc(i18n.T(`
 | |
| 		Provides utilities for interacting with plugins.
 | |
| 
 | |
| 		Plugins provide extended functionality that is not part of the major command-line distribution.
 | |
| 		Please refer to the documentation and examples for more information about how write your own plugins.
 | |
| 
 | |
| 		The easiest way to discover and install plugins is via the kubernetes sub-project krew: [krew.sigs.k8s.io].
 | |
| 		To install krew, visit https://krew.sigs.k8s.io/docs/user-guide/installing-plugins/`))
 | |
| 
 | |
| 	pluginExample = templates.Examples(i18n.T(`
 | |
| 		# List all available plugins
 | |
| 		kubectl plugin list
 | |
| 		
 | |
| 		# List only binary names of available plugins without paths
 | |
| 		kubectl plugin list --name-only`))
 | |
| 
 | |
| 	pluginListLong = templates.LongDesc(i18n.T(`
 | |
| 		List all available plugin files on a user's PATH.
 | |
| 		To see plugins binary names without the full path use --name-only flag.
 | |
| 
 | |
| 		Available plugin files are those that are:
 | |
| 		- executable
 | |
| 		- anywhere on the user's PATH
 | |
| 		- begin with "kubectl-"
 | |
| `))
 | |
| 
 | |
| 	ValidPluginFilenamePrefixes = []string{"kubectl"}
 | |
| )
 | |
| 
 | |
| func NewCmdPlugin(streams genericiooptions.IOStreams) *cobra.Command {
 | |
| 	cmd := &cobra.Command{
 | |
| 		Use:                   "plugin [flags]",
 | |
| 		DisableFlagsInUseLine: true,
 | |
| 		Short:                 i18n.T("Provides utilities for interacting with plugins"),
 | |
| 		Long:                  pluginLong,
 | |
| 		Example:               pluginExample,
 | |
| 		Run: func(cmd *cobra.Command, args []string) {
 | |
| 			cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	cmd.AddCommand(NewCmdPluginList(streams))
 | |
| 	return cmd
 | |
| }
 | |
| 
 | |
| type PluginListOptions struct {
 | |
| 	Verifier PathVerifier
 | |
| 	NameOnly bool
 | |
| 
 | |
| 	PluginPaths []string
 | |
| 
 | |
| 	genericiooptions.IOStreams
 | |
| }
 | |
| 
 | |
| // NewCmdPluginList provides a way to list all plugin executables visible to kubectl
 | |
| func NewCmdPluginList(streams genericiooptions.IOStreams) *cobra.Command {
 | |
| 	o := &PluginListOptions{
 | |
| 		IOStreams: streams,
 | |
| 	}
 | |
| 
 | |
| 	cmd := &cobra.Command{
 | |
| 		Use:     "list",
 | |
| 		Short:   i18n.T("List all visible plugin executables on a user's PATH"),
 | |
| 		Example: pluginExample,
 | |
| 		Long:    pluginListLong,
 | |
| 		Run: func(cmd *cobra.Command, args []string) {
 | |
| 			cmdutil.CheckErr(o.Complete(cmd))
 | |
| 			cmdutil.CheckErr(o.Run())
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	cmd.Flags().BoolVar(&o.NameOnly, "name-only", o.NameOnly, "If true, display only the binary name of each plugin, rather than its full path")
 | |
| 	return cmd
 | |
| }
 | |
| 
 | |
| func (o *PluginListOptions) Complete(cmd *cobra.Command) error {
 | |
| 	o.Verifier = &CommandOverrideVerifier{
 | |
| 		root:        cmd.Root(),
 | |
| 		seenPlugins: make(map[string]string),
 | |
| 	}
 | |
| 
 | |
| 	o.PluginPaths = filepath.SplitList(os.Getenv("PATH"))
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (o *PluginListOptions) Run() error {
 | |
| 	plugins, pluginErrors := o.ListPlugins()
 | |
| 
 | |
| 	if len(plugins) > 0 {
 | |
| 		fmt.Fprintf(o.Out, "The following compatible plugins are available:\n\n")
 | |
| 	} else {
 | |
| 		pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kubectl plugins in your PATH"))
 | |
| 	}
 | |
| 
 | |
| 	pluginWarnings := 0
 | |
| 	for _, pluginPath := range plugins {
 | |
| 		if o.NameOnly {
 | |
| 			fmt.Fprintf(o.Out, "%s\n", filepath.Base(pluginPath))
 | |
| 		} else {
 | |
| 			fmt.Fprintf(o.Out, "%s\n", pluginPath)
 | |
| 		}
 | |
| 		if errs := o.Verifier.Verify(pluginPath); len(errs) != 0 {
 | |
| 			for _, err := range errs {
 | |
| 				fmt.Fprintf(o.ErrOut, "  - %s\n", err)
 | |
| 				pluginWarnings++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if pluginWarnings > 0 {
 | |
| 		if pluginWarnings == 1 {
 | |
| 			pluginErrors = append(pluginErrors, fmt.Errorf("error: one plugin warning was found"))
 | |
| 		} else {
 | |
| 			pluginErrors = append(pluginErrors, fmt.Errorf("error: %v plugin warnings were found", pluginWarnings))
 | |
| 		}
 | |
| 	}
 | |
| 	if len(pluginErrors) > 0 {
 | |
| 		errs := bytes.NewBuffer(nil)
 | |
| 		for _, e := range pluginErrors {
 | |
| 			fmt.Fprintln(errs, e)
 | |
| 		}
 | |
| 		return fmt.Errorf("%s", errs.String())
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // ListPlugins returns list of plugin paths.
 | |
| func (o *PluginListOptions) ListPlugins() ([]string, []error) {
 | |
| 	plugins := []string{}
 | |
| 	errors := []error{}
 | |
| 
 | |
| 	for _, dir := range uniquePathsList(o.PluginPaths) {
 | |
| 		if len(strings.TrimSpace(dir)) == 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		files, err := os.ReadDir(dir)
 | |
| 		if err != nil {
 | |
| 			if _, ok := err.(*os.PathError); ok {
 | |
| 				fmt.Fprintf(o.ErrOut, "Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err)
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			errors = append(errors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err))
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		for _, f := range files {
 | |
| 			if f.IsDir() {
 | |
| 				continue
 | |
| 			}
 | |
| 			if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			plugins = append(plugins, filepath.Join(dir, f.Name()))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return plugins, errors
 | |
| }
 | |
| 
 | |
| // pathVerifier receives a path and determines if it is valid or not
 | |
| type PathVerifier interface {
 | |
| 	// Verify determines if a given path is valid
 | |
| 	Verify(path string) []error
 | |
| }
 | |
| 
 | |
| type CommandOverrideVerifier struct {
 | |
| 	root        *cobra.Command
 | |
| 	seenPlugins map[string]string
 | |
| }
 | |
| 
 | |
| // Verify implements PathVerifier and determines if a given path
 | |
| // is valid depending on whether or not it overwrites an existing
 | |
| // kubectl command path, or a previously seen plugin.
 | |
| func (v *CommandOverrideVerifier) Verify(path string) []error {
 | |
| 	if v.root == nil {
 | |
| 		return []error{fmt.Errorf("unable to verify path with nil root")}
 | |
| 	}
 | |
| 
 | |
| 	// extract the plugin binary name
 | |
| 	segs := strings.Split(path, "/")
 | |
| 	binName := segs[len(segs)-1]
 | |
| 
 | |
| 	cmdPath := strings.Split(binName, "-")
 | |
| 	if len(cmdPath) > 1 {
 | |
| 		// the first argument is always "kubectl" for a plugin binary
 | |
| 		cmdPath = cmdPath[1:]
 | |
| 	}
 | |
| 
 | |
| 	errors := []error{}
 | |
| 
 | |
| 	if isExec, err := isExecutable(path); err == nil && !isExec {
 | |
| 		errors = append(errors, fmt.Errorf("warning: %s identified as a kubectl plugin, but it is not executable", path))
 | |
| 	} else if err != nil {
 | |
| 		errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err))
 | |
| 	}
 | |
| 
 | |
| 	if existingPath, ok := v.seenPlugins[binName]; ok {
 | |
| 		errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath))
 | |
| 	} else {
 | |
| 		v.seenPlugins[binName] = path
 | |
| 	}
 | |
| 
 | |
| 	if cmd, _, err := v.root.Find(cmdPath); err == nil {
 | |
| 		errors = append(errors, fmt.Errorf("warning: %s overwrites existing command: %q", binName, cmd.CommandPath()))
 | |
| 	}
 | |
| 
 | |
| 	return errors
 | |
| }
 | |
| 
 | |
| func isExecutable(fullPath string) (bool, error) {
 | |
| 	info, err := os.Stat(fullPath)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	if runtime.GOOS == "windows" {
 | |
| 		fileExt := strings.ToLower(filepath.Ext(fullPath))
 | |
| 
 | |
| 		switch fileExt {
 | |
| 		case ".bat", ".cmd", ".com", ".exe", ".ps1":
 | |
| 			return true, nil
 | |
| 		}
 | |
| 		return false, nil
 | |
| 	}
 | |
| 
 | |
| 	if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
 | |
| 		return true, nil
 | |
| 	}
 | |
| 
 | |
| 	return false, nil
 | |
| }
 | |
| 
 | |
| // uniquePathsList deduplicates a given slice of strings without
 | |
| // sorting or otherwise altering its order in any way.
 | |
| func uniquePathsList(paths []string) []string {
 | |
| 	seen := map[string]bool{}
 | |
| 	newPaths := []string{}
 | |
| 	for _, p := range paths {
 | |
| 		if seen[p] {
 | |
| 			continue
 | |
| 		}
 | |
| 		seen[p] = true
 | |
| 		newPaths = append(newPaths, p)
 | |
| 	}
 | |
| 	return newPaths
 | |
| }
 | |
| 
 | |
| func hasValidPrefix(filepath string, validPrefixes []string) bool {
 | |
| 	for _, prefix := range validPrefixes {
 | |
| 		if !strings.HasPrefix(filepath, prefix+"-") {
 | |
| 			continue
 | |
| 		}
 | |
| 		return true
 | |
| 	}
 | |
| 	return false
 | |
| }
 |