github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/plugin/upgrade.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  	"strings"
    26  
    27  	"github.com/pkg/errors"
    28  	"github.com/spf13/cobra"
    29  	k8sver "k8s.io/apimachinery/pkg/util/version"
    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  
    36  var (
    37  	pluginUpgradeExample = templates.Examples(`
    38  	# upgrade installed plugins with specified name
    39  	kbcli plugin upgrade myplugin
    40  
    41  	# upgrade installed plugin to a newer version
    42  	kbcli plugin upgrade --all
    43  	`)
    44  )
    45  
    46  type UpgradeOptions struct {
    47  	//	common user flags
    48  	all bool
    49  
    50  	pluginNames []string
    51  	genericiooptions.IOStreams
    52  }
    53  
    54  func NewPluginUpgradeCmd(streams genericiooptions.IOStreams) *cobra.Command {
    55  	o := &UpgradeOptions{
    56  		IOStreams: streams,
    57  	}
    58  
    59  	cmd := &cobra.Command{
    60  		Use:     "upgrade",
    61  		Short:   "Upgrade kbcli or kubectl plugins",
    62  		Example: pluginUpgradeExample,
    63  		Run: func(cmd *cobra.Command, args []string) {
    64  			cmdutil.CheckErr(o.Complete(args))
    65  			cmdutil.CheckErr(o.Run())
    66  		},
    67  	}
    68  
    69  	cmd.Flags().BoolVar(&o.all, "all", o.all, "Upgrade all installed plugins")
    70  
    71  	return cmd
    72  }
    73  
    74  func (o *UpgradeOptions) Complete(args []string) error {
    75  	if o.all {
    76  		installed, err := GetInstalledPluginReceipts(paths.InstallReceiptsPath())
    77  		if err != nil {
    78  			return err
    79  		}
    80  		for _, receipt := range installed {
    81  			o.pluginNames = append(o.pluginNames, receipt.Status.Source.Name+"/"+receipt.Name)
    82  		}
    83  	} else {
    84  		if len(args) == 0 {
    85  			return errors.New("no plugin name specified")
    86  		}
    87  		for _, arg := range args {
    88  			receipt, err := ReadReceiptFromFile(paths.PluginInstallReceiptPath(arg))
    89  			if err != nil {
    90  				return err
    91  			}
    92  			o.pluginNames = append(o.pluginNames, receipt.Status.Source.Name+"/"+receipt.Name)
    93  		}
    94  	}
    95  
    96  	return nil
    97  }
    98  
    99  func (o *UpgradeOptions) Run() error {
   100  	for _, name := range o.pluginNames {
   101  		indexName, pluginName := CanonicalPluginName(name)
   102  
   103  		plugin, err := LoadPluginByName(paths.IndexPluginsPath(indexName), pluginName)
   104  		if err != nil {
   105  			return err
   106  		}
   107  
   108  		fmt.Fprintf(o.Out, "Upgrading plugin: %s\n", name)
   109  		if err := Upgrade(paths, plugin, indexName); err != nil {
   110  			if err == ErrIsAlreadyUpgraded {
   111  				fmt.Fprintf(o.Out, "Plugin %q is already upgraded\n", name)
   112  				continue
   113  			}
   114  			return err
   115  		}
   116  	}
   117  	return nil
   118  }
   119  
   120  // Upgrade reinstalls and deletes the old plugin. The operation tries
   121  // to keep dir in a healthy state if it fails during the process.
   122  func Upgrade(p *Paths, plugin Plugin, indexName string) error {
   123  	installReceipt, err := ReadReceiptFromFile(p.PluginInstallReceiptPath(plugin.Name))
   124  	if err != nil {
   125  		return errors.Wrapf(err, "failed to load install receipt for plugin %q", plugin.Name)
   126  	}
   127  
   128  	curVersion := installReceipt.Spec.Version
   129  	curv, err := parseVersion(curVersion)
   130  	if err != nil {
   131  		return errors.Wrapf(err, "failed to parse installed plugin version (%q) as a semver value", curVersion)
   132  	}
   133  
   134  	// Find available installation candidate
   135  	candidate, ok, err := GetMatchingPlatform(plugin.Spec.Platforms)
   136  	if err != nil {
   137  		return errors.Wrap(err, "failed trying to find a matching platform in plugin spec")
   138  	}
   139  	if !ok {
   140  		return errors.Errorf("plugin %q does not offer installation for this platform (%s)",
   141  			plugin.Name, OSArch())
   142  	}
   143  
   144  	newVersion := plugin.Spec.Version
   145  	newv, err := parseVersion(newVersion)
   146  	if err != nil {
   147  		return errors.Wrapf(err, "failed to parse candidate version spec (%q)", newVersion)
   148  	}
   149  	klog.V(2).Infof("Comparing versions: current=%s target=%s", curv, newv)
   150  
   151  	// See if it's a newer version
   152  	if !curv.LessThan(newv) {
   153  		klog.V(3).Infof("Plugin does not need upgrade (%s ≥ %s)", curv, newv)
   154  		return ErrIsAlreadyUpgraded
   155  	}
   156  	klog.V(1).Infof("Plugin needs upgrade (%s < %s)", curv, newv)
   157  
   158  	// Re-Install
   159  	klog.V(1).Infof("Installing new version %s", newVersion)
   160  	if err := install(installOperation{
   161  		pluginName: plugin.Name,
   162  		platform:   candidate,
   163  
   164  		installDir: p.PluginVersionInstallPath(plugin.Name, newVersion),
   165  		binDir:     p.BinPath(),
   166  	}, InstallOpts{}); err != nil {
   167  		return errors.Wrap(err, "failed to install new version")
   168  	}
   169  
   170  	klog.V(2).Infof("Upgrading install receipt for plugin %s", plugin.Name)
   171  	if err = StoreReceipt(NewReceipt(plugin, indexName, installReceipt.CreationTimestamp), p.PluginInstallReceiptPath(plugin.Name)); err != nil {
   172  		return errors.Wrap(err, "installation receipt could not be stored, uninstall may fail")
   173  	}
   174  
   175  	// Clean old installations
   176  	klog.V(2).Infof("Starting old version cleanup")
   177  	return cleanupInstallation(p, plugin, curVersion)
   178  }
   179  
   180  // cleanupInstallation removes a plugin directly
   181  func cleanupInstallation(p *Paths, plugin Plugin, oldVersion string) error {
   182  	klog.V(1).Infof("Remove old plugin installation under %q", p.PluginVersionInstallPath(plugin.Name, oldVersion))
   183  	return os.RemoveAll(p.PluginVersionInstallPath(plugin.Name, oldVersion))
   184  }
   185  
   186  func parseVersion(s string) (*k8sver.Version, error) {
   187  	var vv *k8sver.Version
   188  	if !strings.HasPrefix(s, "v") {
   189  		return vv, errors.Errorf("version string %q does not start with 'v'", s)
   190  	}
   191  	vv, err := k8sver.ParseSemantic(s)
   192  	if err != nil {
   193  		return vv, err
   194  	}
   195  	return vv, nil
   196  }