github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/addon/addon.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 addon
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"fmt"
    26  	"math"
    27  	"path"
    28  	"sort"
    29  	"strconv"
    30  	"strings"
    31  
    32  	"github.com/jedib0t/go-pretty/v6/table"
    33  	"github.com/spf13/cobra"
    34  	"github.com/spf13/pflag"
    35  	corev1 "k8s.io/api/core/v1"
    36  	"k8s.io/apimachinery/pkg/api/resource"
    37  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    38  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    39  	"k8s.io/apimachinery/pkg/runtime"
    40  	"k8s.io/cli-runtime/pkg/genericiooptions"
    41  	discoverycli "k8s.io/client-go/discovery"
    42  	"k8s.io/client-go/dynamic"
    43  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    44  	"k8s.io/kubectl/pkg/util/templates"
    45  	"k8s.io/utils/strings/slices"
    46  
    47  	extensionsv1alpha1 "github.com/1aal/kubeblocks/apis/extensions/v1alpha1"
    48  	"github.com/1aal/kubeblocks/pkg/cli/cmd/plugin"
    49  	"github.com/1aal/kubeblocks/pkg/cli/list"
    50  	"github.com/1aal/kubeblocks/pkg/cli/patch"
    51  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    52  	"github.com/1aal/kubeblocks/pkg/cli/types"
    53  	"github.com/1aal/kubeblocks/pkg/cli/util"
    54  	"github.com/1aal/kubeblocks/pkg/constant"
    55  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    56  )
    57  
    58  type addonEnableFlags struct {
    59  	MemorySets       []string
    60  	CPUSets          []string
    61  	StorageSets      []string
    62  	ReplicaCountSets []string
    63  	StorageClassSets []string
    64  	TolerationsSet   []string
    65  	SetValues        []string
    66  	Force            bool
    67  }
    68  
    69  func (r *addonEnableFlags) useDefault() bool {
    70  	return len(r.MemorySets) == 0 &&
    71  		len(r.CPUSets) == 0 &&
    72  		len(r.StorageSets) == 0 &&
    73  		len(r.ReplicaCountSets) == 0 &&
    74  		len(r.StorageClassSets) == 0 &&
    75  		len(r.TolerationsSet) == 0
    76  }
    77  
    78  type addonCmdOpts struct {
    79  	genericiooptions.IOStreams
    80  
    81  	Factory cmdutil.Factory
    82  	dynamic dynamic.Interface
    83  
    84  	addon extensionsv1alpha1.Addon
    85  
    86  	*patch.Options
    87  	addonEnableFlags *addonEnableFlags
    88  
    89  	complete func(self *addonCmdOpts, cmd *cobra.Command, args []string) error
    90  }
    91  
    92  // NewAddonCmd for addon functions
    93  func NewAddonCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    94  	cmd := &cobra.Command{
    95  		Use:   "addon COMMAND",
    96  		Short: "Addon command.",
    97  	}
    98  	cmd.AddCommand(
    99  		newListCmd(f, streams),
   100  		newDescribeCmd(f, streams),
   101  		newEnableCmd(f, streams),
   102  		newDisableCmd(f, streams),
   103  	)
   104  	return cmd
   105  }
   106  
   107  func newListCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   108  	o := list.NewListOptions(f, streams, types.AddonGVR())
   109  	cmd := &cobra.Command{
   110  		Use:               "list",
   111  		Short:             "List addons.",
   112  		Aliases:           []string{"ls"},
   113  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR),
   114  		Run: func(cmd *cobra.Command, args []string) {
   115  			o.Names = args
   116  			util.CheckErr(addonListRun(o))
   117  		},
   118  	}
   119  	o.AddFlags(cmd, true)
   120  	return cmd
   121  }
   122  
   123  func newDescribeCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   124  	o := &addonCmdOpts{
   125  		Options:   patch.NewOptions(f, streams, types.AddonGVR()),
   126  		Factory:   f,
   127  		IOStreams: streams,
   128  		complete:  addonDescribeHandler,
   129  	}
   130  	cmd := &cobra.Command{
   131  		Use:               "describe ADDON_NAME",
   132  		Short:             "Describe an addon specification.",
   133  		Args:              cobra.ExactArgs(1),
   134  		Aliases:           []string{"desc"},
   135  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.AddonGVR()),
   136  		Run: func(cmd *cobra.Command, args []string) {
   137  			util.CheckErr(o.init(args))
   138  			util.CheckErr(o.fetchAddonObj())
   139  			util.CheckErr(o.complete(o, cmd, args))
   140  		},
   141  	}
   142  	return cmd
   143  }
   144  
   145  func newEnableCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   146  	o := &addonCmdOpts{
   147  		Options:          patch.NewOptions(f, streams, types.AddonGVR()),
   148  		Factory:          f,
   149  		IOStreams:        streams,
   150  		addonEnableFlags: &addonEnableFlags{},
   151  		complete:         addonEnableDisableHandler,
   152  	}
   153  
   154  	o.Options.OutputOperation = func(didPatch bool) string {
   155  		if didPatch {
   156  			return "enabled"
   157  		}
   158  		return "enabled (no change)"
   159  	}
   160  
   161  	// # kbcli addon enable flags:
   162  	// # [--memory [extraName:]<request>/<limit> (can specify multiple if has extra items)]
   163  	// # [--cpu [extraName:]<request>/<limit> (can specify multiple if has extra items)]
   164  	// # [--storage [extraName:]<request> (can specify multiple if has extra items)]
   165  	// # [--replicas [extraName:]<number> (can specify multiple if has extra items)]
   166  	// # [--storage-class [extraName:]<storage class name> (can specify multiple if has extra items)]
   167  	// # [--tolerations [extraName:]<toleration JSON list items> (can specify multiple if has extra items)]
   168  	// # [--dry-run] # TODO
   169  
   170  	cmd := &cobra.Command{
   171  		Use:               "enable ADDON_NAME",
   172  		Short:             "Enable an addon.",
   173  		Args:              cobra.ExactArgs(1),
   174  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.AddonGVR()),
   175  		Example: templates.Examples(`
   176      	# Enabled "prometheus" addon
   177      	kbcli addon enable prometheus
   178      
   179          # Enabled "prometheus" addon with custom resources settings
   180      	kbcli addon enable prometheus --memory 512Mi/4Gi --storage 8Gi --replicas 2
   181      
   182          # Enabled "prometheus" addon and its extra alertmanager component with custom resources settings 
   183      	kbcli addon enable prometheus --memory 512Mi/4Gi --storage 8Gi --replicas 2 \
   184    			--memory alertmanager:16Mi/256Mi --storage alertmanager:1Gi --replicas alertmanager:2 
   185  
   186          # Enabled "prometheus" addon with tolerations 
   187      	kbcli addon enable prometheus \
   188  			--tolerations '[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]' \
   189  			--tolerations 'alertmanager:[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]'
   190  
   191  		# Enabled "prometheus" addon with helm like custom settings
   192  		kbcli addon enable prometheus --set prometheus.alertmanager.image.tag=v0.24.0
   193  
   194  		# Force enabled "csi-s3" addon
   195  		kbcli addon enable csi-s3 --force
   196  `),
   197  		Run: func(cmd *cobra.Command, args []string) {
   198  			util.CheckErr(o.init(args))
   199  			util.CheckErr(o.fetchAddonObj())
   200  			util.CheckErr(o.validate())
   201  			util.CheckErr(o.complete(o, cmd, args))
   202  			util.CheckErr(o.Run(cmd))
   203  		},
   204  	}
   205  	cmd.Flags().StringArrayVar(&o.addonEnableFlags.MemorySets, "memory", []string{},
   206  		"Sets addon memory resource values (--memory [extraName:]<request>/<limit>) (can specify multiple if has extra items))")
   207  	cmd.Flags().StringArrayVar(&o.addonEnableFlags.CPUSets, "cpu", []string{},
   208  		"Sets addon CPU resource values (--cpu [extraName:]<request>/<limit>) (can specify multiple if has extra items))")
   209  	cmd.Flags().StringArrayVar(&o.addonEnableFlags.StorageSets, "storage", []string{},
   210  		`Sets addon storage size (--storage [extraName:]<request>) (can specify multiple if has extra items)). 
   211  Additional notes:
   212  1. Specify '0' value will remove storage values settings and explicitly disable 'persistentVolumeEnabled' attribute.
   213  2. For Helm type Addon, that resizing storage will fail if modified value is a storage request size 
   214  that belongs to StatefulSet's volume claim template, to resolve 'Failed' Addon status possible action is disable and 
   215  re-enable the addon (More info on how-to resize a PVC: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources).
   216  `)
   217  	cmd.Flags().StringArrayVar(&o.addonEnableFlags.ReplicaCountSets, "replicas", []string{},
   218  		"Sets addon component replica count (--replicas [extraName:]<number>) (can specify multiple if has extra items))")
   219  	cmd.Flags().StringArrayVar(&o.addonEnableFlags.StorageClassSets, "storage-class", []string{},
   220  		"Sets addon storage class name (--storage-class [extraName:]<storage class name>) (can specify multiple if has extra items))")
   221  	cmd.Flags().StringArrayVar(&o.addonEnableFlags.TolerationsSet, "tolerations", []string{},
   222  		"Sets addon pod tolerations (--tolerations [extraName:]<toleration JSON list items>) (can specify multiple if has extra items))")
   223  	cmd.Flags().StringArrayVar(&o.addonEnableFlags.SetValues, "set", []string{},
   224  		"set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2), it's only being processed if addon's type is helm.")
   225  	cmd.Flags().BoolVar(&o.addonEnableFlags.Force, "force", false, "ignoring the installable restrictions and forcefully enabling.")
   226  
   227  	o.Options.AddFlags(cmd)
   228  	return cmd
   229  }
   230  
   231  func newDisableCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   232  	o := &addonCmdOpts{
   233  		Options:   patch.NewOptions(f, streams, types.AddonGVR()),
   234  		Factory:   f,
   235  		IOStreams: streams,
   236  		complete:  addonEnableDisableHandler,
   237  	}
   238  
   239  	o.Options.OutputOperation = func(didPatch bool) string {
   240  		if didPatch {
   241  			return "disabled"
   242  		}
   243  		return "disabled (no change)"
   244  	}
   245  
   246  	cmd := &cobra.Command{
   247  		Use:               "disable ADDON_NAME",
   248  		Short:             "Disable an addon.",
   249  		Args:              cobra.ExactArgs(1),
   250  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.AddonGVR()),
   251  		Run: func(cmd *cobra.Command, args []string) {
   252  			util.CheckErr(o.init(args))
   253  			util.CheckErr(o.fetchAddonObj())
   254  			util.CheckErr(o.complete(o, cmd, args))
   255  			util.CheckErr(o.Run(cmd))
   256  		},
   257  	}
   258  	o.Options.AddFlags(cmd)
   259  	return cmd
   260  }
   261  
   262  func (o *addonCmdOpts) init(args []string) error {
   263  	o.Names = args
   264  	if o.dynamic == nil {
   265  		var err error
   266  		if o.dynamic, err = o.Factory.DynamicClient(); err != nil {
   267  			return err
   268  		}
   269  	}
   270  
   271  	// setup _KUBE_SERVER_INFO
   272  	if viper.Get(constant.CfgKeyServerInfo) == nil {
   273  		cfg, _ := o.Factory.ToRESTConfig()
   274  		cli, err := discoverycli.NewDiscoveryClientForConfig(cfg)
   275  		if err != nil {
   276  			return err
   277  		}
   278  		ver, err := cli.ServerVersion()
   279  		if err != nil {
   280  			return err
   281  		}
   282  		viper.SetDefault(constant.CfgKeyServerInfo, *ver)
   283  	}
   284  
   285  	return nil
   286  }
   287  
   288  func (o *addonCmdOpts) fetchAddonObj() error {
   289  	ctx := context.TODO()
   290  	obj, err := o.dynamic.Resource(o.GVR).Get(ctx, o.Names[0], metav1.GetOptions{})
   291  	if err != nil {
   292  		return err
   293  	}
   294  	if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &o.addon); err != nil {
   295  		return err
   296  	}
   297  	return nil
   298  }
   299  
   300  func (o *addonCmdOpts) validate() error {
   301  	if o.addonEnableFlags.Force {
   302  		return nil
   303  	}
   304  	if o.addon.Spec.Installable == nil {
   305  		return nil
   306  	}
   307  	for _, s := range o.addon.Spec.Installable.Selectors {
   308  		if !s.MatchesFromConfig() {
   309  			return fmt.Errorf("addon %s INSTALLABLE-SELECTOR has no matching requirement", o.Names)
   310  		}
   311  	}
   312  
   313  	if err := o.installAndUpgradePlugins(); err != nil {
   314  		fmt.Fprintf(o.Out, "failed to install/upgrade plugins: %v\n", err)
   315  	}
   316  
   317  	return nil
   318  }
   319  
   320  func addonDescribeHandler(o *addonCmdOpts, cmd *cobra.Command, args []string) error {
   321  	printRow := func(tbl *printer.TablePrinter, name string, item *extensionsv1alpha1.AddonInstallSpecItem) {
   322  		pvEnabled := ""
   323  		replicas := ""
   324  
   325  		if item.PVEnabled != nil {
   326  			pvEnabled = fmt.Sprintf("%v", *item.PVEnabled)
   327  		}
   328  		if item.Replicas != nil {
   329  			replicas = fmt.Sprintf("%d", *item.Replicas)
   330  		}
   331  
   332  		printQuantity := func(q resource.Quantity, ok bool) string {
   333  			if ok {
   334  				return q.String()
   335  			}
   336  			return ""
   337  		}
   338  
   339  		q, ok := item.Resources.Requests[corev1.ResourceStorage]
   340  		storageVal := printQuantity(q, ok)
   341  
   342  		q, ok = item.Resources.Requests[corev1.ResourceCPU]
   343  		cpuVal := printQuantity(q, ok)
   344  		q, ok = item.Resources.Limits[corev1.ResourceCPU]
   345  		cpuVal = fmt.Sprintf("%s/%s", cpuVal, printQuantity(q, ok))
   346  
   347  		q, ok = item.Resources.Requests[corev1.ResourceMemory]
   348  		memVal := printQuantity(q, ok)
   349  		q, ok = item.Resources.Limits[corev1.ResourceMemory]
   350  		memVal = fmt.Sprintf("%s/%s", memVal, printQuantity(q, ok))
   351  
   352  		tbl.AddRow(name,
   353  			replicas,
   354  			storageVal,
   355  			cpuVal,
   356  			memVal,
   357  			item.StorageClass,
   358  			item.Tolerations,
   359  			pvEnabled,
   360  		)
   361  	}
   362  	printInstalled := func(tbl *printer.TablePrinter) error {
   363  		installSpec := o.addon.Spec.InstallSpec
   364  		printRow(tbl, "main", &installSpec.AddonInstallSpecItem)
   365  		for _, e := range installSpec.ExtraItems {
   366  			printRow(tbl, e.Name, &e.AddonInstallSpecItem)
   367  		}
   368  		return nil
   369  	}
   370  
   371  	var labels []string
   372  	for k, v := range o.addon.Labels {
   373  		if strings.Contains(k, constant.APIGroup) {
   374  			labels = append(labels, fmt.Sprintf("%s=%s", k, v))
   375  		}
   376  	}
   377  	printer.PrintPairStringToLine("Name", o.addon.Name, 0)
   378  	printer.PrintPairStringToLine("Description", o.addon.Spec.Description, 0)
   379  	printer.PrintPairStringToLine("Labels", strings.Join(labels, ","), 0)
   380  	printer.PrintPairStringToLine("Type", string(o.addon.Spec.Type), 0)
   381  	if len(o.addon.GetExtraNames()) > 0 {
   382  		printer.PrintPairStringToLine("Extras", strings.Join(o.addon.GetExtraNames(), ","), 0)
   383  	}
   384  	printer.PrintPairStringToLine("Status", string(o.addon.Status.Phase), 0)
   385  	var autoInstall bool
   386  	if o.addon.Spec.Installable != nil {
   387  		autoInstall = o.addon.Spec.Installable.AutoInstall
   388  	}
   389  	printer.PrintPairStringToLine("Auto-install", strconv.FormatBool(autoInstall), 0)
   390  	if len(o.addon.Spec.Installable.GetSelectorsStrings()) > 0 {
   391  		printer.PrintPairStringToLine("Auto-install selector", strings.Join(o.addon.Spec.Installable.GetSelectorsStrings(), ","), 0)
   392  	}
   393  
   394  	switch o.addon.Status.Phase {
   395  	case extensionsv1alpha1.AddonEnabled:
   396  		printer.PrintTitle("Installed Info")
   397  		printer.PrintLineWithTabSeparator()
   398  		if err := printer.PrintTable(o.Out, nil, printInstalled,
   399  			"NAME", "REPLICAS", "STORAGE", "CPU (REQ/LIMIT)", "MEMORY (REQ/LIMIT)", "STORAGE-CLASS",
   400  			"TOLERATIONS", "PV-ENABLED"); err != nil {
   401  			return err
   402  		}
   403  	default:
   404  		printer.PrintLineWithTabSeparator()
   405  		for _, di := range o.addon.Spec.GetSortedDefaultInstallValues() {
   406  			printInstallable := func(tbl *printer.TablePrinter) error {
   407  				if len(di.Selectors) == 0 {
   408  					printer.PrintLineWithTabSeparator(
   409  						printer.NewPair("Default install selector", "NONE"),
   410  					)
   411  				} else {
   412  					printer.PrintLineWithTabSeparator(
   413  						printer.NewPair("Default install selector", strings.Join(di.GetSelectorsStrings(), ",")),
   414  					)
   415  				}
   416  				installSpec := di.AddonInstallSpec
   417  				printRow(tbl, "main", &installSpec.AddonInstallSpecItem)
   418  				for _, e := range installSpec.ExtraItems {
   419  					printRow(tbl, e.Name, &e.AddonInstallSpecItem)
   420  				}
   421  				return nil
   422  			}
   423  			if err := printer.PrintTable(o.Out, nil, printInstallable,
   424  				"NAME", "REPLICAS", "STORAGE", "CPU (REQ/LIMIT)", "MEMORY (REQ/LIMIT)", "STORAGE-CLASS",
   425  				"TOLERATIONS", "PV-ENABLED"); err != nil {
   426  				return err
   427  			}
   428  			printer.PrintLineWithTabSeparator()
   429  		}
   430  	}
   431  
   432  	// print failed message
   433  	if o.addon.Status.Phase == extensionsv1alpha1.AddonFailed {
   434  		var tbl *printer.TablePrinter
   435  		printHeader := true
   436  		for _, c := range o.addon.Status.Conditions {
   437  			if c.Status == metav1.ConditionTrue {
   438  				continue
   439  			}
   440  			if printHeader {
   441  				fmt.Fprintln(o.Out, "Failed Message")
   442  				tbl = printer.NewTablePrinter(o.Out)
   443  				tbl.Tbl.SetColumnConfigs([]table.ColumnConfig{
   444  					{Number: 3, WidthMax: 120},
   445  				})
   446  				tbl.SetHeader("TIME", "REASON", "MESSAGE")
   447  				printHeader = false
   448  			}
   449  			tbl.AddRow(util.TimeFormat(&c.LastTransitionTime), c.Reason, c.Message)
   450  		}
   451  		tbl.Print()
   452  	}
   453  
   454  	return nil
   455  }
   456  
   457  func addonEnableDisableHandler(o *addonCmdOpts, cmd *cobra.Command, args []string) error {
   458  	// record the flags that been set by user
   459  	var flags []*pflag.Flag
   460  	cmd.Flags().Visit(func(flag *pflag.Flag) {
   461  		flags = append(flags, flag)
   462  	})
   463  	return o.buildPatch(flags)
   464  }
   465  
   466  func (o *addonCmdOpts) buildEnablePatch(flags []*pflag.Flag, spec, install map[string]interface{}) (err error) {
   467  	extraNames := o.addon.GetExtraNames()
   468  	installSpec := extensionsv1alpha1.AddonInstallSpec{
   469  		Enabled:              true,
   470  		AddonInstallSpecItem: extensionsv1alpha1.NewAddonInstallSpecItem(),
   471  	}
   472  	// only using named return value in defer function
   473  	defer func() {
   474  		if err != nil {
   475  			return
   476  		}
   477  		var b []byte
   478  		b, err = json.Marshal(&installSpec)
   479  		if err != nil {
   480  			return
   481  		}
   482  		if err = json.Unmarshal(b, &install); err != nil {
   483  			return
   484  		}
   485  	}()
   486  
   487  	if o.addonEnableFlags.useDefault() {
   488  		return nil
   489  	}
   490  
   491  	// extractInstallSpecExtraItem extracts extensionsv1alpha1.AddonInstallExtraItem
   492  	// for the matching arg name, if not found, appends extensionsv1alpha1.AddonInstallExtraItem
   493  	// item to installSpec.ExtraItems and returns its pointer.
   494  	extractInstallSpecExtraItem := func(name string) (*extensionsv1alpha1.AddonInstallExtraItem, error) {
   495  		var pItem *extensionsv1alpha1.AddonInstallExtraItem
   496  		for i, eItem := range installSpec.ExtraItems {
   497  			if eItem.Name == name {
   498  				pItem = &installSpec.ExtraItems[i]
   499  				break
   500  			}
   501  		}
   502  		if pItem == nil {
   503  			if !slices.Contains(extraNames, name) {
   504  				return nil, fmt.Errorf("invalid extra item name [%s]", name)
   505  			}
   506  			installSpec.ExtraItems = append(installSpec.ExtraItems, extensionsv1alpha1.AddonInstallExtraItem{
   507  				Name:                 name,
   508  				AddonInstallSpecItem: extensionsv1alpha1.NewAddonInstallSpecItem(),
   509  			})
   510  			pItem = &installSpec.ExtraItems[len(installSpec.ExtraItems)-1]
   511  		}
   512  		return pItem, nil
   513  	}
   514  
   515  	_tuplesProcessor := func(t []string, s, flag string,
   516  		valueTransformer func(s, flag string) (interface{}, error),
   517  		valueAssigner func(*extensionsv1alpha1.AddonInstallSpecItem, interface{}),
   518  	) error {
   519  		l := len(t)
   520  		var name string
   521  		var result interface{}
   522  		switch l {
   523  		case 2:
   524  			name = t[0]
   525  			fallthrough
   526  		case 1:
   527  			if valueTransformer != nil {
   528  				result, err = valueTransformer(t[l-1], flag)
   529  				if err != nil {
   530  					return err
   531  				}
   532  			} else {
   533  				result = t[l-1]
   534  			}
   535  		default:
   536  			return fmt.Errorf("wrong flag value --%s=%s", flag, s)
   537  		}
   538  		name = strings.TrimSpace(name)
   539  		if name == "" {
   540  			valueAssigner(&installSpec.AddonInstallSpecItem, result)
   541  		} else {
   542  			pItem, err := extractInstallSpecExtraItem(name)
   543  			if err != nil {
   544  				return err
   545  			}
   546  			valueAssigner(&pItem.AddonInstallSpecItem, result)
   547  		}
   548  		return nil
   549  	}
   550  
   551  	twoTuplesProcessor := func(s, flag string,
   552  		valueTransformer func(s, flag string) (interface{}, error),
   553  		valueAssigner func(*extensionsv1alpha1.AddonInstallSpecItem, interface{}),
   554  	) error {
   555  		t := strings.SplitN(s, ":", 2)
   556  		return _tuplesProcessor(t, s, flag, valueTransformer, valueAssigner)
   557  	}
   558  
   559  	twoTuplesJSONProcessor := func(s, flag string,
   560  		valueTransformer func(s, flag string) (interface{}, error),
   561  		valueAssigner func(*extensionsv1alpha1.AddonInstallSpecItem, interface{}),
   562  	) error {
   563  		var jsonArray []map[string]interface{}
   564  		var t []string
   565  
   566  		err := json.Unmarshal([]byte(s), &jsonArray)
   567  		if err != nil {
   568  			// not a valid JSON array treat it a 2 tuples
   569  			t = strings.SplitN(s, ":", 2)
   570  		} else {
   571  			t = []string{s}
   572  		}
   573  		return _tuplesProcessor(t, s, flag, valueTransformer, valueAssigner)
   574  	}
   575  
   576  	reqLimitResTransformer := func(s, flag string) (interface{}, error) {
   577  		t := strings.SplitN(s, "/", 2)
   578  		if len(t) != 2 {
   579  			return nil, fmt.Errorf("wrong flag value --%s=%s", flag, s)
   580  		}
   581  		reqLim := [2]resource.Quantity{}
   582  		processTuple := func(i int) error {
   583  			if t[i] == "" {
   584  				return nil
   585  			}
   586  			q, err := resource.ParseQuantity(t[i])
   587  			if err != nil {
   588  				return err
   589  			}
   590  			reqLim[i] = q
   591  			return nil
   592  		}
   593  		for i := range t {
   594  			if err := processTuple(i); err != nil {
   595  				return nil, fmt.Errorf("wrong flag value --%s=%s, with error %v", flag, s, err)
   596  			}
   597  		}
   598  		return reqLim, nil
   599  	}
   600  
   601  	f := o.addonEnableFlags
   602  	for _, v := range f.ReplicaCountSets {
   603  		if err := twoTuplesProcessor(v, "replicas", func(s, flag string) (interface{}, error) {
   604  			v, err := strconv.Atoi(s)
   605  			if err != nil {
   606  				return nil, fmt.Errorf("wrong flag value --%s=%s, with error %v", flag, s, err)
   607  			}
   608  			if v < 0 {
   609  				return nil, fmt.Errorf("wrong flag value --%s=%s replica count value", flag, s)
   610  			}
   611  			if v > math.MaxInt32 {
   612  				return nil, fmt.Errorf("wrong flag value --%s=%s replica count exceed max. value (%d) ", flag, s, math.MaxInt32)
   613  			}
   614  			r := int32(v)
   615  			return &r, nil
   616  		}, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) {
   617  			item.Replicas = i.(*int32)
   618  		}); err != nil {
   619  			return err
   620  		}
   621  	}
   622  
   623  	for _, v := range f.StorageClassSets {
   624  		if err := twoTuplesProcessor(v, "storage-class", nil, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) {
   625  			item.StorageClass = i.(string)
   626  		}); err != nil {
   627  			return err
   628  		}
   629  	}
   630  
   631  	for _, v := range f.TolerationsSet {
   632  		if err := twoTuplesJSONProcessor(v, "tolerations", nil, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) {
   633  			item.Tolerations = i.(string)
   634  		}); err != nil {
   635  			return err
   636  		}
   637  	}
   638  
   639  	for _, v := range f.StorageSets {
   640  		if err := twoTuplesProcessor(v, "storage", func(s, flag string) (interface{}, error) {
   641  			q, err := resource.ParseQuantity(s)
   642  			if err != nil {
   643  				return nil, fmt.Errorf("wrong flag value --%s=%s, with error %v", flag, s, err)
   644  			}
   645  			return q, nil
   646  		}, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) {
   647  			q := i.(resource.Quantity)
   648  			// for 0 storage size, remove storage request value and explicitly disable `persistentVolumeEnabled`
   649  			if v, _ := q.AsInt64(); v == 0 {
   650  				delete(item.Resources.Requests, corev1.ResourceStorage)
   651  				b := false
   652  				item.PVEnabled = &b
   653  				return
   654  			}
   655  			item.Resources.Requests[corev1.ResourceStorage] = q
   656  			// explicitly enable `persistentVolumeEnabled` if with provided storage size setting
   657  			b := true
   658  			item.PVEnabled = &b
   659  		}); err != nil {
   660  			return err
   661  		}
   662  	}
   663  
   664  	for _, v := range f.CPUSets {
   665  		if err := twoTuplesProcessor(v, "cpu", reqLimitResTransformer, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) {
   666  			reqLim := i.([2]resource.Quantity)
   667  			item.Resources.Requests[corev1.ResourceCPU] = reqLim[0]
   668  			item.Resources.Limits[corev1.ResourceCPU] = reqLim[1]
   669  		}); err != nil {
   670  			return err
   671  		}
   672  	}
   673  
   674  	for _, v := range f.MemorySets {
   675  		if err := twoTuplesProcessor(v, "memory", reqLimitResTransformer, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) {
   676  			reqLim := i.([2]resource.Quantity)
   677  			item.Resources.Requests[corev1.ResourceMemory] = reqLim[0]
   678  			item.Resources.Limits[corev1.ResourceMemory] = reqLim[1]
   679  		}); err != nil {
   680  			return err
   681  		}
   682  	}
   683  
   684  	return nil
   685  }
   686  
   687  func (o *addonCmdOpts) buildHelmPatch(result map[string]interface{}) error {
   688  	var helmSpec extensionsv1alpha1.HelmTypeInstallSpec
   689  	if o.addon.Spec.Helm == nil {
   690  		helmSpec = extensionsv1alpha1.HelmTypeInstallSpec{
   691  			InstallValues: extensionsv1alpha1.HelmInstallValues{
   692  				SetValues: o.addonEnableFlags.SetValues,
   693  			},
   694  		}
   695  	} else {
   696  		helmSpec = *o.addon.Spec.Helm
   697  		helmSpec.InstallValues.SetValues = o.addonEnableFlags.SetValues
   698  	}
   699  	b, err := json.Marshal(&helmSpec)
   700  	if err != nil {
   701  		return err
   702  	}
   703  	if err = json.Unmarshal(b, &result); err != nil {
   704  		return err
   705  	}
   706  	return nil
   707  }
   708  
   709  func (o *addonCmdOpts) buildPatch(flags []*pflag.Flag) error {
   710  	var err error
   711  	spec := map[string]interface{}{}
   712  	status := map[string]interface{}{}
   713  	install := map[string]interface{}{}
   714  	helm := map[string]interface{}{}
   715  
   716  	if o.addonEnableFlags != nil {
   717  		if o.addon.Status.Phase == extensionsv1alpha1.AddonFailed {
   718  			status["phase"] = nil
   719  		}
   720  		if err = o.buildEnablePatch(flags, spec, install); err != nil {
   721  			return err
   722  		}
   723  
   724  		if err = o.buildHelmPatch(helm); err != nil {
   725  			return err
   726  		}
   727  	} else {
   728  		if !o.addon.Spec.InstallSpec.GetEnabled() {
   729  			fmt.Fprintf(o.Out, "%s/%s is already disabled\n", o.GVR.GroupResource().String(), o.Names[0])
   730  			return cmdutil.ErrExit
   731  		}
   732  		install["enabled"] = false
   733  	}
   734  
   735  	if err = unstructured.SetNestedField(spec, install, "install"); err != nil {
   736  		return err
   737  	}
   738  
   739  	if err = unstructured.SetNestedField(spec, helm, "helm"); err != nil {
   740  		return err
   741  	}
   742  
   743  	obj := unstructured.Unstructured{
   744  		Object: map[string]interface{}{
   745  			"spec": spec,
   746  		},
   747  	}
   748  	if len(status) > 0 {
   749  		phase := ""
   750  		if p, ok := status["phase"]; ok && p != nil {
   751  			phase = p.(string)
   752  		}
   753  		fmt.Printf("patching addon 'status.phase=%s' to 'status.phase=%v' will result addon install spec (spec.install) not being updated\n",
   754  			o.addon.Status.Phase, phase)
   755  		obj.Object["status"] = status
   756  		o.Subresource = "status"
   757  	}
   758  	bytes, err := obj.MarshalJSON()
   759  	if err != nil {
   760  		return err
   761  	}
   762  	o.Patch = string(bytes)
   763  	return nil
   764  }
   765  
   766  func addonListRun(o *list.ListOptions) error {
   767  	// if format is JSON or YAML, use default printer to output the result.
   768  	if o.Format == printer.JSON || o.Format == printer.YAML {
   769  		_, err := o.Run()
   770  		return err
   771  	}
   772  
   773  	// get and output the result
   774  	o.Print = false
   775  	r, err := o.Run()
   776  	if err != nil {
   777  		return err
   778  	}
   779  
   780  	infos, err := r.Infos()
   781  	if err != nil {
   782  		return err
   783  	}
   784  
   785  	if len(infos) == 0 {
   786  		fmt.Fprintln(o.IOStreams.Out, "No addon found")
   787  		return nil
   788  	}
   789  
   790  	printRows := func(tbl *printer.TablePrinter) error {
   791  		// sort addons with .status.Phase then .metadata.name
   792  		sort.SliceStable(infos, func(i, j int) bool {
   793  			toAddon := func(idx int) *extensionsv1alpha1.Addon {
   794  				addon := &extensionsv1alpha1.Addon{}
   795  				if err := runtime.DefaultUnstructuredConverter.FromUnstructured(infos[idx].Object.(*unstructured.Unstructured).Object, addon); err != nil {
   796  					return nil
   797  				}
   798  				return addon
   799  			}
   800  			iAddon := toAddon(i)
   801  			jAddon := toAddon(j)
   802  			if iAddon == nil {
   803  				return true
   804  			}
   805  			if jAddon == nil {
   806  				return false
   807  			}
   808  			if iAddon.Status.Phase == jAddon.Status.Phase {
   809  				return iAddon.GetName() < jAddon.GetName()
   810  			}
   811  			return iAddon.Status.Phase < jAddon.Status.Phase
   812  		})
   813  		for _, info := range infos {
   814  			addon := &extensionsv1alpha1.Addon{}
   815  			obj := info.Object.(*unstructured.Unstructured)
   816  			if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, addon); err != nil {
   817  				return err
   818  			}
   819  			extraNames := addon.GetExtraNames()
   820  			var selectors []string
   821  			var autoInstall bool
   822  			if addon.Spec.Installable != nil {
   823  				selectors = addon.Spec.Installable.GetSelectorsStrings()
   824  				autoInstall = addon.Spec.Installable.AutoInstall
   825  			}
   826  			label := obj.GetLabels()
   827  			provider := label[constant.AddonProviderLabelKey]
   828  			tbl.AddRow(addon.Name,
   829  				addon.Spec.Type,
   830  				provider,
   831  				addon.Status.Phase,
   832  				autoInstall,
   833  				strings.Join(selectors, ";"),
   834  				strings.Join(extraNames, ","),
   835  			)
   836  		}
   837  		return nil
   838  	}
   839  
   840  	if err = printer.PrintTable(o.Out, nil, printRows,
   841  		"NAME", "TYPE", "PROVIDER", "STATUS", "AUTO-INSTALL", "AUTO-INSTALLABLE-SELECTOR", "EXTRAS"); err != nil {
   842  		return err
   843  	}
   844  	return nil
   845  }
   846  
   847  func (o *addonCmdOpts) installAndUpgradePlugins() error {
   848  	if len(o.addon.Spec.CliPlugins) == 0 {
   849  		return nil
   850  	}
   851  
   852  	plugin.InitPlugin()
   853  
   854  	paths := plugin.GetKbcliPluginPath()
   855  	indexes, err := plugin.ListIndexes(paths)
   856  	if err != nil {
   857  		return err
   858  	}
   859  
   860  	indexRepositoryToNme := make(map[string]string)
   861  	for _, index := range indexes {
   862  		indexRepositoryToNme[index.URL] = index.Name
   863  	}
   864  
   865  	var plugins []string
   866  	var names []string
   867  	for _, p := range o.addon.Spec.CliPlugins {
   868  		names = append(names, p.Name)
   869  		indexName, ok := indexRepositoryToNme[p.IndexRepository]
   870  		if !ok {
   871  			// index not found, add it
   872  			_, indexName = path.Split(p.IndexRepository)
   873  			if err := plugin.AddIndex(paths, indexName, p.IndexRepository); err != nil {
   874  				return err
   875  			}
   876  		}
   877  		plugins = append(plugins, fmt.Sprintf("%s/%s", indexName, p.Name))
   878  	}
   879  
   880  	installOption := &plugin.PluginInstallOption{
   881  		IOStreams: o.IOStreams,
   882  	}
   883  	upgradeOption := &plugin.UpgradeOptions{
   884  		IOStreams: o.IOStreams,
   885  	}
   886  
   887  	// install plugins
   888  	if err := installOption.Complete(plugins); err != nil {
   889  		return err
   890  	}
   891  	if err := installOption.Install(); err != nil {
   892  		return err
   893  	}
   894  
   895  	// upgrade existed plugins
   896  	if err := upgradeOption.Complete(names); err != nil {
   897  		return err
   898  	}
   899  	if err := upgradeOption.Run(); err != nil {
   900  		return err
   901  	}
   902  
   903  	return nil
   904  }