...

Source file src/k8s.io/kubectl/pkg/cmd/plugin/plugin.go

Documentation: k8s.io/kubectl/pkg/cmd/plugin

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package plugin
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strings"
    26  
    27  	"github.com/spf13/cobra"
    28  
    29  	"k8s.io/cli-runtime/pkg/genericiooptions"
    30  
    31  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    32  	"k8s.io/kubectl/pkg/util/i18n"
    33  	"k8s.io/kubectl/pkg/util/templates"
    34  )
    35  
    36  var (
    37  	pluginLong = templates.LongDesc(i18n.T(`
    38  		Provides utilities for interacting with plugins.
    39  
    40  		Plugins provide extended functionality that is not part of the major command-line distribution.
    41  		Please refer to the documentation and examples for more information about how write your own plugins.
    42  
    43  		The easiest way to discover and install plugins is via the kubernetes sub-project krew.
    44  		To install krew, visit [krew.sigs.k8s.io](https://krew.sigs.k8s.io/docs/user-guide/setup/install/)`))
    45  
    46  	pluginExample = templates.Examples(i18n.T(`
    47  		# List all available plugins
    48  		kubectl plugin list`))
    49  
    50  	pluginListLong = templates.LongDesc(i18n.T(`
    51  		List all available plugin files on a user's PATH.
    52  
    53  		Available plugin files are those that are:
    54  		- executable
    55  		- anywhere on the user's PATH
    56  		- begin with "kubectl-"
    57  `))
    58  
    59  	ValidPluginFilenamePrefixes = []string{"kubectl"}
    60  )
    61  
    62  func NewCmdPlugin(streams genericiooptions.IOStreams) *cobra.Command {
    63  	cmd := &cobra.Command{
    64  		Use:                   "plugin [flags]",
    65  		DisableFlagsInUseLine: true,
    66  		Short:                 i18n.T("Provides utilities for interacting with plugins"),
    67  		Long:                  pluginLong,
    68  		Run: func(cmd *cobra.Command, args []string) {
    69  			cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
    70  		},
    71  	}
    72  
    73  	cmd.AddCommand(NewCmdPluginList(streams))
    74  	return cmd
    75  }
    76  
    77  type PluginListOptions struct {
    78  	Verifier PathVerifier
    79  	NameOnly bool
    80  
    81  	PluginPaths []string
    82  
    83  	genericiooptions.IOStreams
    84  }
    85  
    86  // NewCmdPluginList provides a way to list all plugin executables visible to kubectl
    87  func NewCmdPluginList(streams genericiooptions.IOStreams) *cobra.Command {
    88  	o := &PluginListOptions{
    89  		IOStreams: streams,
    90  	}
    91  
    92  	cmd := &cobra.Command{
    93  		Use:     "list",
    94  		Short:   i18n.T("List all visible plugin executables on a user's PATH"),
    95  		Example: pluginExample,
    96  		Long:    pluginListLong,
    97  		Run: func(cmd *cobra.Command, args []string) {
    98  			cmdutil.CheckErr(o.Complete(cmd))
    99  			cmdutil.CheckErr(o.Run())
   100  		},
   101  	}
   102  
   103  	cmd.Flags().BoolVar(&o.NameOnly, "name-only", o.NameOnly, "If true, display only the binary name of each plugin, rather than its full path")
   104  	return cmd
   105  }
   106  
   107  func (o *PluginListOptions) Complete(cmd *cobra.Command) error {
   108  	o.Verifier = &CommandOverrideVerifier{
   109  		root:        cmd.Root(),
   110  		seenPlugins: make(map[string]string),
   111  	}
   112  
   113  	o.PluginPaths = filepath.SplitList(os.Getenv("PATH"))
   114  	return nil
   115  }
   116  
   117  func (o *PluginListOptions) Run() error {
   118  	plugins, pluginErrors := o.ListPlugins()
   119  
   120  	if len(plugins) > 0 {
   121  		fmt.Fprintf(o.Out, "The following compatible plugins are available:\n\n")
   122  	} else {
   123  		pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kubectl plugins in your PATH"))
   124  	}
   125  
   126  	pluginWarnings := 0
   127  	for _, pluginPath := range plugins {
   128  		if o.NameOnly {
   129  			fmt.Fprintf(o.Out, "%s\n", filepath.Base(pluginPath))
   130  		} else {
   131  			fmt.Fprintf(o.Out, "%s\n", pluginPath)
   132  		}
   133  		if errs := o.Verifier.Verify(pluginPath); len(errs) != 0 {
   134  			for _, err := range errs {
   135  				fmt.Fprintf(o.ErrOut, "  - %s\n", err)
   136  				pluginWarnings++
   137  			}
   138  		}
   139  	}
   140  
   141  	if pluginWarnings > 0 {
   142  		if pluginWarnings == 1 {
   143  			pluginErrors = append(pluginErrors, fmt.Errorf("error: one plugin warning was found"))
   144  		} else {
   145  			pluginErrors = append(pluginErrors, fmt.Errorf("error: %v plugin warnings were found", pluginWarnings))
   146  		}
   147  	}
   148  	if len(pluginErrors) > 0 {
   149  		errs := bytes.NewBuffer(nil)
   150  		for _, e := range pluginErrors {
   151  			fmt.Fprintln(errs, e)
   152  		}
   153  		return fmt.Errorf("%s", errs.String())
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  // ListPlugins returns list of plugin paths.
   160  func (o *PluginListOptions) ListPlugins() ([]string, []error) {
   161  	plugins := []string{}
   162  	errors := []error{}
   163  
   164  	for _, dir := range uniquePathsList(o.PluginPaths) {
   165  		if len(strings.TrimSpace(dir)) == 0 {
   166  			continue
   167  		}
   168  
   169  		files, err := os.ReadDir(dir)
   170  		if err != nil {
   171  			if _, ok := err.(*os.PathError); ok {
   172  				fmt.Fprintf(o.ErrOut, "Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err)
   173  				continue
   174  			}
   175  
   176  			errors = append(errors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err))
   177  			continue
   178  		}
   179  
   180  		for _, f := range files {
   181  			if f.IsDir() {
   182  				continue
   183  			}
   184  			if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) {
   185  				continue
   186  			}
   187  
   188  			plugins = append(plugins, filepath.Join(dir, f.Name()))
   189  		}
   190  	}
   191  
   192  	return plugins, errors
   193  }
   194  
   195  // pathVerifier receives a path and determines if it is valid or not
   196  type PathVerifier interface {
   197  	// Verify determines if a given path is valid
   198  	Verify(path string) []error
   199  }
   200  
   201  type CommandOverrideVerifier struct {
   202  	root        *cobra.Command
   203  	seenPlugins map[string]string
   204  }
   205  
   206  // Verify implements PathVerifier and determines if a given path
   207  // is valid depending on whether or not it overwrites an existing
   208  // kubectl command path, or a previously seen plugin.
   209  func (v *CommandOverrideVerifier) Verify(path string) []error {
   210  	if v.root == nil {
   211  		return []error{fmt.Errorf("unable to verify path with nil root")}
   212  	}
   213  
   214  	// extract the plugin binary name
   215  	segs := strings.Split(path, "/")
   216  	binName := segs[len(segs)-1]
   217  
   218  	cmdPath := strings.Split(binName, "-")
   219  	if len(cmdPath) > 1 {
   220  		// the first argument is always "kubectl" for a plugin binary
   221  		cmdPath = cmdPath[1:]
   222  	}
   223  
   224  	errors := []error{}
   225  
   226  	if isExec, err := isExecutable(path); err == nil && !isExec {
   227  		errors = append(errors, fmt.Errorf("warning: %s identified as a kubectl plugin, but it is not executable", path))
   228  	} else if err != nil {
   229  		errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err))
   230  	}
   231  
   232  	if existingPath, ok := v.seenPlugins[binName]; ok {
   233  		errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath))
   234  	} else {
   235  		v.seenPlugins[binName] = path
   236  	}
   237  
   238  	if cmd, _, err := v.root.Find(cmdPath); err == nil {
   239  		errors = append(errors, fmt.Errorf("warning: %s overwrites existing command: %q", binName, cmd.CommandPath()))
   240  	}
   241  
   242  	return errors
   243  }
   244  
   245  func isExecutable(fullPath string) (bool, error) {
   246  	info, err := os.Stat(fullPath)
   247  	if err != nil {
   248  		return false, err
   249  	}
   250  
   251  	if runtime.GOOS == "windows" {
   252  		fileExt := strings.ToLower(filepath.Ext(fullPath))
   253  
   254  		switch fileExt {
   255  		case ".bat", ".cmd", ".com", ".exe", ".ps1":
   256  			return true, nil
   257  		}
   258  		return false, nil
   259  	}
   260  
   261  	if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
   262  		return true, nil
   263  	}
   264  
   265  	return false, nil
   266  }
   267  
   268  // uniquePathsList deduplicates a given slice of strings without
   269  // sorting or otherwise altering its order in any way.
   270  func uniquePathsList(paths []string) []string {
   271  	seen := map[string]bool{}
   272  	newPaths := []string{}
   273  	for _, p := range paths {
   274  		if seen[p] {
   275  			continue
   276  		}
   277  		seen[p] = true
   278  		newPaths = append(newPaths, p)
   279  	}
   280  	return newPaths
   281  }
   282  
   283  func hasValidPrefix(filepath string, validPrefixes []string) bool {
   284  	for _, prefix := range validPrefixes {
   285  		if !strings.HasPrefix(filepath, prefix+"-") {
   286  			continue
   287  		}
   288  		return true
   289  	}
   290  	return false
   291  }
   292  

View as plain text