github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/plugin/install.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  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  
    27  	"github.com/pkg/errors"
    28  	"github.com/spf13/cobra"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/cli-runtime/pkg/genericiooptions"
    31  	"k8s.io/klog/v2"
    32  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    33  	"k8s.io/kubectl/pkg/util/templates"
    34  
    35  	"github.com/1aal/kubeblocks/pkg/cli/cmd/plugin/download"
    36  )
    37  
    38  var (
    39  	pluginInstallExample = templates.Examples(`
    40  	# install a kbcli or kubectl plugin by name
    41  	kbcli plugin install [PLUGIN]
    42  
    43  	# install a kbcli or kubectl plugin by name and index
    44  	kbcli plugin install [INDEX/PLUGIN]
    45  	`)
    46  )
    47  
    48  type PluginInstallOption struct {
    49  	plugins []pluginEntry
    50  
    51  	genericiooptions.IOStreams
    52  }
    53  
    54  type pluginEntry struct {
    55  	index  string
    56  	plugin Plugin
    57  }
    58  
    59  func NewPluginInstallCmd(streams genericiooptions.IOStreams) *cobra.Command {
    60  	o := &PluginInstallOption{
    61  		IOStreams: streams,
    62  	}
    63  	cmd := &cobra.Command{
    64  		Use:     "install",
    65  		Short:   "Install kbcli or kubectl plugins",
    66  		Example: pluginInstallExample,
    67  		Run: func(cmd *cobra.Command, args []string) {
    68  			cmdutil.CheckErr(o.Complete(args))
    69  			cmdutil.CheckErr(o.Install())
    70  		},
    71  	}
    72  	return cmd
    73  }
    74  
    75  func (o *PluginInstallOption) Complete(names []string) error {
    76  	for _, name := range names {
    77  		indexName, pluginName := CanonicalPluginName(name)
    78  
    79  		// check whether the plugin exists
    80  		if _, err := os.Stat(paths.PluginInstallReceiptPath(pluginName)); err == nil {
    81  			fmt.Fprintf(o.Out, "plugin %q is already installed\n", name)
    82  			continue
    83  		}
    84  
    85  		plugin, err := LoadPluginByName(paths.IndexPluginsPath(indexName), pluginName)
    86  		if err != nil {
    87  			if os.IsNotExist(err) {
    88  				return errors.Errorf("plugin %q does not exist in the %s plugin index", name, indexName)
    89  			}
    90  			return errors.Wrapf(err, "failed to load plugin %q from the %s plugin index", name, indexName)
    91  		}
    92  		o.plugins = append(o.plugins, pluginEntry{
    93  			index:  indexName,
    94  			plugin: plugin,
    95  		})
    96  	}
    97  	return nil
    98  }
    99  
   100  func (o *PluginInstallOption) Install() error {
   101  	var failed []string
   102  	var returnErr error
   103  	for _, entry := range o.plugins {
   104  		plugin := entry.plugin
   105  		fmt.Fprintf(o.Out, "Installing plugin: %s\n", plugin.Name)
   106  		err := Install(paths, plugin, entry.index, InstallOpts{})
   107  		if err == ErrIsAlreadyInstalled {
   108  			continue
   109  		}
   110  		if err != nil {
   111  			klog.Warningf("failed to install plugin %q: %v", plugin.Name, err)
   112  			if returnErr == nil {
   113  				returnErr = err
   114  			}
   115  			failed = append(failed, plugin.Name)
   116  			continue
   117  		}
   118  		fmt.Fprintf(o.Out, "Installed plugin: %s\n", plugin.Name)
   119  		output := fmt.Sprintf("Use this plugin:\n\tkubectl %s\n", plugin.Name)
   120  		if plugin.Spec.Homepage != "" {
   121  			output += fmt.Sprintf("Documentation:\n\t%s\n", plugin.Spec.Homepage)
   122  		}
   123  		if plugin.Spec.Caveats != "" {
   124  			output += fmt.Sprintf("Caveats:\n%s\n", indent(plugin.Spec.Caveats))
   125  		}
   126  		fmt.Fprintln(o.Out, indent(output))
   127  	}
   128  	if len(failed) > 0 {
   129  		return errors.Wrapf(returnErr, "failed to install some plugins: %+v", failed)
   130  	}
   131  	return nil
   132  }
   133  
   134  // Install downloads and installs a plugin. The operation tries
   135  // to keep the plugin dir in a healthy state if it fails during the process.
   136  func Install(p *Paths, plugin Plugin, indexName string, opts InstallOpts) error {
   137  	klog.V(2).Infof("Looking for installed versions")
   138  	_, err := ReadReceiptFromFile(p.PluginInstallReceiptPath(plugin.Name))
   139  	if err == nil {
   140  		return ErrIsAlreadyInstalled
   141  	} else if !os.IsNotExist(err) {
   142  		return errors.Wrap(err, "failed to look up plugin receipt")
   143  	}
   144  
   145  	// Find available installation candidate
   146  	candidate, ok, err := GetMatchingPlatform(plugin.Spec.Platforms)
   147  	if err != nil {
   148  		return errors.Wrap(err, "failed trying to find a matching platform in plugin spec")
   149  	}
   150  	if !ok {
   151  		return errors.Errorf("plugin %q does not offer installation for this platform", plugin.Name)
   152  	}
   153  
   154  	// The actual install should be the last action so that a failure during receipt
   155  	// saving does not result in an installed plugin without receipt.
   156  	klog.V(3).Infof("Install plugin %s at version=%s", plugin.Name, plugin.Spec.Version)
   157  	if err := install(installOperation{
   158  		pluginName: plugin.Name,
   159  		platform:   candidate,
   160  
   161  		binDir:     p.BinPath(),
   162  		installDir: p.PluginVersionInstallPath(plugin.Name, plugin.Spec.Version),
   163  	}, opts); err != nil {
   164  		return errors.Wrap(err, "install failed")
   165  	}
   166  
   167  	klog.V(3).Infof("Storing install receipt for plugin %s", plugin.Name)
   168  	err = StoreReceipt(NewReceipt(plugin, indexName, metav1.Now()), p.PluginInstallReceiptPath(plugin.Name))
   169  	return errors.Wrap(err, "installation receipt could not be stored, uninstall may fail")
   170  }
   171  
   172  func install(op installOperation, opts InstallOpts) error {
   173  	// Download and extract
   174  	klog.V(3).Infof("Creating download staging directory")
   175  	downloadStagingDir, err := os.MkdirTemp("", "kbcli-downloads")
   176  	if err != nil {
   177  		return errors.Wrapf(err, "could not create staging dir %q", downloadStagingDir)
   178  	}
   179  	klog.V(3).Infof("Successfully created download staging directory %q", downloadStagingDir)
   180  	defer func() {
   181  		klog.V(3).Infof("Deleting the download staging directory %s", downloadStagingDir)
   182  		if err := os.RemoveAll(downloadStagingDir); err != nil {
   183  			klog.Warningf("failed to clean up download staging directory: %s", err)
   184  		}
   185  	}()
   186  	if err := download.DownloadAndExtract(downloadStagingDir, op.platform.URI, op.platform.Sha256, opts.ArchiveFileOverride); err != nil {
   187  		return errors.Wrap(err, "failed to unpack into staging dir")
   188  	}
   189  
   190  	applyDefaults(&op.platform)
   191  	if err := moveToInstallDir(downloadStagingDir, op.installDir, op.platform.Files); err != nil {
   192  		return errors.Wrap(err, "failed while moving files to the installation directory")
   193  	}
   194  
   195  	subPathAbs, err := filepath.Abs(op.installDir)
   196  	if err != nil {
   197  		return errors.Wrapf(err, "failed to get the absolute fullPath of %q", op.installDir)
   198  	}
   199  	fullPath := filepath.Join(op.installDir, filepath.FromSlash(op.platform.Bin))
   200  	pathAbs, err := filepath.Abs(fullPath)
   201  	if err != nil {
   202  		return errors.Wrapf(err, "failed to get the absolute fullPath of %q", fullPath)
   203  	}
   204  	if _, ok := IsSubPath(subPathAbs, pathAbs); !ok {
   205  		return errors.Wrapf(err, "the fullPath %q does not extend the sub-fullPath %q", fullPath, op.installDir)
   206  	}
   207  	err = createOrUpdateLink(op.binDir, fullPath, op.pluginName)
   208  	return errors.Wrap(err, "failed to link installed plugin")
   209  }