github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/plugin/plugin.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package plugin
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"github.com/spf13/cobra"
    31  	"k8s.io/cli-runtime/pkg/genericiooptions"
    32  	"k8s.io/klog/v2"
    33  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    34  	"k8s.io/kubectl/pkg/util/templates"
    35  
    36  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    37  	"github.com/1aal/kubeblocks/pkg/cli/util"
    38  )
    39  
    40  var (
    41  	pluginLong = templates.LongDesc(`
    42  	Provides utilities for interacting with plugins.
    43  		
    44  	Plugins provide extended functionality that is not part of the major command-line distribution.
    45  	`)
    46  
    47  	pluginListExample = templates.Examples(`
    48  	# List all available plugins file on a user's PATH.
    49  	kbcli plugin list
    50  	`)
    51  
    52  	ValidPluginFilenamePrefixes = []string{"kbcli", "kubectl"}
    53  	paths                       = GetKbcliPluginPath()
    54  )
    55  
    56  func NewPluginCmd(streams genericiooptions.IOStreams) *cobra.Command {
    57  	cmd := &cobra.Command{
    58  		Use:   "plugin",
    59  		Short: "Provides utilities for interacting with plugins.",
    60  		Long:  pluginLong,
    61  		PersistentPreRun: func(cmd *cobra.Command, args []string) {
    62  			InitPlugin()
    63  		},
    64  	}
    65  
    66  	cmd.AddCommand(
    67  		NewPluginListCmd(streams),
    68  		NewPluginIndexCmd(streams),
    69  		NewPluginInstallCmd(streams),
    70  		NewPluginUninstallCmd(streams),
    71  		NewPluginSearchCmd(streams),
    72  		NewPluginDescribeCmd(streams),
    73  		NewPluginUpgradeCmd(streams),
    74  	)
    75  	return cmd
    76  }
    77  
    78  type PluginListOptions struct {
    79  	Verifier PathVerifier
    80  
    81  	PluginPaths []string
    82  
    83  	genericiooptions.IOStreams
    84  }
    85  
    86  func NewPluginListCmd(streams genericiooptions.IOStreams) *cobra.Command {
    87  	o := &PluginListOptions{
    88  		IOStreams: streams,
    89  	}
    90  	cmd := &cobra.Command{
    91  		Use:                   "list",
    92  		DisableFlagsInUseLine: true,
    93  		Short:                 "List all visible plugin executables on a user's PATH",
    94  		Example:               pluginListExample,
    95  		Run: func(cmd *cobra.Command, args []string) {
    96  			cmdutil.CheckErr(o.Complete(cmd))
    97  			cmdutil.CheckErr(o.Run())
    98  		},
    99  	}
   100  	return cmd
   101  }
   102  
   103  func (o *PluginListOptions) Complete(cmd *cobra.Command) error {
   104  	o.Verifier = &CommandOverrideVerifier{
   105  		root:        cmd.Root(),
   106  		seenPlugins: map[string]string{},
   107  	}
   108  
   109  	o.PluginPaths = filepath.SplitList(os.Getenv("PATH"))
   110  	return nil
   111  }
   112  
   113  func (o *PluginListOptions) Run() error {
   114  	plugins, pluginErrors := o.ListPlugins()
   115  
   116  	if len(plugins) == 0 {
   117  		pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kbcli or kubectl plugins in your PATH"))
   118  	}
   119  
   120  	pluginWarnings := 0
   121  	p := NewPluginPrinter(o.IOStreams.Out)
   122  	errMsg := ""
   123  	for _, pluginPath := range plugins {
   124  		name := filepath.Base(pluginPath)
   125  		path := filepath.Dir(pluginPath)
   126  		if errs := o.Verifier.Verify(pluginPath); len(errs) != 0 {
   127  			for _, err := range errs {
   128  				errMsg += fmt.Sprintf("%s\n", err)
   129  				pluginWarnings++
   130  			}
   131  		}
   132  		addPluginRow(name, path, p)
   133  	}
   134  	p.Print()
   135  	klog.V(1).Info(errMsg)
   136  
   137  	if pluginWarnings > 0 {
   138  		if pluginWarnings == 1 {
   139  			pluginErrors = append(pluginErrors, fmt.Errorf("error: one plugin warining was found"))
   140  		} else {
   141  			pluginErrors = append(pluginErrors, fmt.Errorf("error: %d plugin warnings were found", pluginWarnings))
   142  		}
   143  	}
   144  	if len(pluginErrors) > 0 {
   145  		errs := bytes.NewBuffer(nil)
   146  		for _, e := range pluginErrors {
   147  			fmt.Fprintln(errs, e)
   148  		}
   149  		return fmt.Errorf("%s", errs.String())
   150  	}
   151  
   152  	return nil
   153  }
   154  
   155  func (o *PluginListOptions) ListPlugins() ([]string, []error) {
   156  	var plugins []string
   157  	var errors []error
   158  
   159  	for _, dir := range uniquePathsList(o.PluginPaths) {
   160  		if len(strings.TrimSpace(dir)) == 0 {
   161  			continue
   162  		}
   163  
   164  		files, err := os.ReadDir(dir)
   165  		if err != nil {
   166  			if _, ok := err.(*os.PathError); ok {
   167  				klog.V(1).Info("Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err)
   168  				continue
   169  			}
   170  
   171  			errors = append(errors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err))
   172  			continue
   173  		}
   174  
   175  		for _, f := range files {
   176  			if f.IsDir() {
   177  				continue
   178  			}
   179  			if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) {
   180  				continue
   181  			}
   182  
   183  			plugins = append(plugins, filepath.Join(dir, f.Name()))
   184  		}
   185  	}
   186  
   187  	return plugins, errors
   188  }
   189  
   190  // PathVerifier receives a path and validates it.
   191  type PathVerifier interface {
   192  	Verify(path string) []error
   193  }
   194  
   195  type CommandOverrideVerifier struct {
   196  	root        *cobra.Command
   197  	seenPlugins map[string]string
   198  }
   199  
   200  // Verify implements PathVerifier and determines if a given path
   201  // is valid depending on whether it overwrites an existing
   202  // kbcli command path, or a previously seen plugin.
   203  func (v *CommandOverrideVerifier) Verify(path string) []error {
   204  	if v.root == nil {
   205  		return []error{fmt.Errorf("unable to verify path with nil root")}
   206  	}
   207  
   208  	// extract the plugin binary name
   209  	binName := filepath.Base(path)
   210  
   211  	cmdPath := strings.Split(binName, "-")
   212  	if len(cmdPath) > 1 {
   213  		// the first argument is always "kbcli" or "kubectl" for a plugin binary
   214  		cmdPath = cmdPath[1:]
   215  	}
   216  
   217  	var errors []error
   218  	if isExec, err := isExecutable(path); err == nil && !isExec {
   219  		errors = append(errors, fmt.Errorf("warning: %q identified as a kbcli or kubectl plugin, but it is not executable", path))
   220  	} else if err != nil {
   221  		errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err))
   222  	}
   223  
   224  	if existingPath, ok := v.seenPlugins[binName]; ok {
   225  		errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath))
   226  	} else {
   227  		v.seenPlugins[binName] = path
   228  	}
   229  
   230  	if cmd, _, err := v.root.Find(cmdPath); err == nil {
   231  		errors = append(errors, fmt.Errorf("warning: %q overwrites existing kbcli command: %q", path, cmd.CommandPath()))
   232  	}
   233  
   234  	return errors
   235  }
   236  
   237  func isExecutable(fullPath string) (bool, error) {
   238  	info, err := os.Stat(fullPath)
   239  	if err != nil {
   240  		return false, err
   241  	}
   242  
   243  	if util.IsWindows() {
   244  		fileExt := strings.ToLower(filepath.Ext(fullPath))
   245  
   246  		switch fileExt {
   247  		case ".bat", ".cmd", ".com", ".exe", ".ps1":
   248  			return true, nil
   249  		}
   250  		return false, nil
   251  	}
   252  
   253  	if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
   254  		return true, nil
   255  	}
   256  
   257  	return false, nil
   258  }
   259  
   260  func uniquePathsList(paths []string) []string {
   261  	var newPaths []string
   262  	seen := map[string]bool{}
   263  
   264  	for _, path := range paths {
   265  		if !seen[path] {
   266  			newPaths = append(newPaths, path)
   267  			seen[path] = true
   268  		}
   269  	}
   270  	return newPaths
   271  }
   272  
   273  func hasValidPrefix(filepath string, validPrefixes []string) bool {
   274  	for _, prefix := range validPrefixes {
   275  		if strings.HasPrefix(filepath, prefix+"-") {
   276  			return true
   277  		}
   278  	}
   279  	return false
   280  }
   281  
   282  func NewPluginPrinter(out io.Writer) *printer.TablePrinter {
   283  	t := printer.NewTablePrinter(out)
   284  	t.SetHeader("NAME", "PATH")
   285  	return t
   286  }
   287  
   288  func addPluginRow(name, path string, p *printer.TablePrinter) {
   289  	p.AddRow(name, path)
   290  }
   291  
   292  func InitPlugin() {
   293  	// Ensure that the base directories exist
   294  	if err := EnsureDirs(paths.BasePath(),
   295  		paths.BinPath(),
   296  		paths.InstallPath(),
   297  		paths.IndexBase(),
   298  		paths.InstallReceiptsPath()); err != nil {
   299  		klog.Fatal(err)
   300  	}
   301  
   302  	// check if index exists, if indexes don't exist, download default index
   303  	indexes, err := ListIndexes(paths)
   304  	if err != nil {
   305  		klog.Fatal(err)
   306  	}
   307  	if len(indexes) == 0 {
   308  		klog.V(1).Info("no index found, downloading default index")
   309  		if err := AddIndex(paths, DefaultIndexName, DefaultIndexURI); err != nil {
   310  			klog.Fatal("failed to download default index", err)
   311  		}
   312  		if err := AddIndex(paths, KrewIndexName, KrewIndexURI); err != nil {
   313  			klog.Fatal("failed to download krew index", err)
   314  		}
   315  	}
   316  }