github.com/oam-dev/kubevela@v1.9.11/references/cli/addon.go (about)

     1  /*
     2  Copyright 2021 The KubeVela 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 cli
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"sort"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/fatih/color"
    30  	"github.com/getkin/kin-openapi/openapi3"
    31  	"github.com/gosuri/uitable"
    32  	"github.com/pkg/errors"
    33  	"github.com/spf13/cobra"
    34  	"helm.sh/helm/v3/pkg/strvals"
    35  	types2 "k8s.io/apimachinery/pkg/types"
    36  	"k8s.io/client-go/discovery"
    37  	"k8s.io/client-go/rest"
    38  	"sigs.k8s.io/controller-runtime/pkg/client"
    39  
    40  	common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    41  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    42  	"github.com/oam-dev/kubevela/apis/types"
    43  	pkgaddon "github.com/oam-dev/kubevela/pkg/addon"
    44  	"github.com/oam-dev/kubevela/pkg/oam"
    45  	addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
    46  	"github.com/oam-dev/kubevela/pkg/utils/apply"
    47  	"github.com/oam-dev/kubevela/pkg/utils/common"
    48  	cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
    49  )
    50  
    51  const (
    52  	statusEnabled  = "enabled"
    53  	statusDisabled = "disabled"
    54  	statusSuspend  = "suspend"
    55  )
    56  
    57  var enabledAddonColor = color.New(color.Bold, color.FgGreen)
    58  
    59  var (
    60  	forceDisable  bool
    61  	addonRegistry string
    62  	addonVersion  string
    63  	addonClusters string
    64  	verboseStatus bool
    65  	skipValidate  bool
    66  	overrideDefs  bool
    67  	dryRun        bool
    68  	yes2all       bool
    69  )
    70  
    71  // NewAddonCommand create `addon` command
    72  func NewAddonCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command {
    73  	cmd := &cobra.Command{
    74  		Use:   "addon",
    75  		Short: "Manage addons for extension.",
    76  		Long:  "Manage addons for extension.",
    77  		Annotations: map[string]string{
    78  			types.TagCommandOrder: order,
    79  			types.TagCommandType:  types.TypeApp,
    80  		},
    81  	}
    82  	cmd.AddCommand(
    83  		NewAddonListCommand(c),
    84  		NewAddonEnableCommand(c, ioStreams),
    85  		NewAddonDisableCommand(c, ioStreams),
    86  		NewAddonStatusCommand(c, ioStreams),
    87  		NewAddonRegistryCommand(c, ioStreams),
    88  		NewAddonUpgradeCommand(c, ioStreams),
    89  		NewAddonPackageCommand(c),
    90  		NewAddonInitCommand(),
    91  		NewAddonPushCommand(c),
    92  	)
    93  	return cmd
    94  }
    95  
    96  // NewAddonListCommand create addon list command
    97  func NewAddonListCommand(c common.Args) *cobra.Command {
    98  	cmd := &cobra.Command{
    99  		Use:     "list",
   100  		Aliases: []string{"ls"},
   101  		Short:   "List addons",
   102  		Long:    "List addons in KubeVela",
   103  		Example: `  List addon by:
   104  	vela addon ls
   105    List addons in a specific registry, useful to reveal addons with duplicated names:
   106      vela addon ls --registry <registry-name>
   107  `,
   108  		RunE: func(cmd *cobra.Command, args []string) error {
   109  			k8sClient, err := c.GetClient()
   110  			if err != nil {
   111  				return err
   112  			}
   113  			table, err := listAddons(context.Background(), k8sClient, addonRegistry)
   114  			if err != nil {
   115  				return err
   116  			}
   117  			fmt.Println(table.String())
   118  			return nil
   119  		},
   120  	}
   121  	cmd.Flags().StringVarP(&addonRegistry, "registry", "r", "", "specify the registry name to list")
   122  	return cmd
   123  }
   124  
   125  // NewAddonEnableCommand create addon enable command
   126  func NewAddonEnableCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Command {
   127  	ctx := context.Background()
   128  	cmd := &cobra.Command{
   129  		Use:     "enable",
   130  		Aliases: []string{"install"},
   131  		Short:   "enable an addon",
   132  		Long:    "enable an addon in cluster.",
   133  		Example: `  Enable addon by:
   134  	vela addon enable <addon-name>
   135    Enable addon with specify version:
   136  	vela addon enable <addon-name> --version <addon-version>
   137    Enable addon for specific clusters, (local means control plane):
   138  	vela addon enable <addon-name> --clusters={local,cluster1,cluster2}
   139    Enable addon locally:
   140  	vela addon enable <your-local-addon-path>
   141    Enable addon with specified args (the args should be defined in addon's parameters):
   142  	vela addon enable <addon-name> <my-parameter-of-addon>=<my-value>
   143    Enable addon with specified registry:
   144      vela addon enable <registryName>/<addonName>
   145  `,
   146  		RunE: func(cmd *cobra.Command, args []string) error {
   147  			var additionalInfo string
   148  			if len(args) < 1 {
   149  				return fmt.Errorf("must specify addon name")
   150  			}
   151  			addonArgs, err := parseAddonArgsToMap(args[1:])
   152  			if err != nil {
   153  				return err
   154  			}
   155  			clusterArgs := transClusters(addonClusters)
   156  			if len(clusterArgs) != 0 {
   157  				addonArgs[types.ClustersArg] = clusterArgs
   158  			}
   159  			config, err := c.GetConfig()
   160  			if err != nil {
   161  				return err
   162  			}
   163  			k8sClient, err := c.GetClient()
   164  			if err != nil {
   165  				return err
   166  			}
   167  			dc, err := c.GetDiscoveryClient()
   168  			if err != nil {
   169  				return err
   170  			}
   171  			addonOrDir := args[0]
   172  			var name = addonOrDir
   173  			// inject runtime info
   174  			addonArgs[pkgaddon.InstallerRuntimeOption] = map[string]interface{}{
   175  				"upgrade": false,
   176  			}
   177  			var addonName string
   178  			if file, err := os.Stat(addonOrDir); err == nil {
   179  				if !file.IsDir() {
   180  					return fmt.Errorf("%s is not addon dir", addonOrDir)
   181  				}
   182  				ioStream.Infof(color.New(color.FgYellow).Sprintf("enabling addon by local dir: %s \n", addonOrDir))
   183  				// args[0] is a local path install with local dir, use base dir name as addonName
   184  				abs, err := filepath.Abs(addonOrDir)
   185  				if err != nil {
   186  					return errors.Wrapf(err, "directory %s is invalid", addonOrDir)
   187  				}
   188  				addonName = filepath.Base(abs)
   189  				if !yes2all {
   190  					if err := checkUninstallFromClusters(ctx, k8sClient, addonName, addonArgs); err != nil {
   191  						return err
   192  					}
   193  				}
   194  				additionalInfo, err = enableAddonByLocal(ctx, addonName, addonOrDir, k8sClient, dc, config, addonArgs)
   195  				if err != nil {
   196  					return err
   197  				}
   198  			} else {
   199  				if filepath.IsAbs(addonOrDir) || strings.HasPrefix(addonOrDir, ".") || strings.HasSuffix(addonOrDir, "/") {
   200  					return fmt.Errorf("addon directory %s not found in local file system", addonOrDir)
   201  				}
   202  				_, addonName, err = splitSpecifyRegistry(name)
   203  				if err != nil {
   204  					return fmt.Errorf("failed to split addonName and addonRegistry: %w", err)
   205  				}
   206  				if !yes2all {
   207  					if err := checkUninstallFromClusters(ctx, k8sClient, addonName, addonArgs); err != nil {
   208  						return err
   209  					}
   210  				}
   211  				additionalInfo, err = enableAddon(ctx, k8sClient, dc, config, name, addonVersion, addonArgs)
   212  				if err != nil {
   213  					return err
   214  				}
   215  			}
   216  			if dryRun {
   217  				return nil
   218  			}
   219  			fmt.Printf("Addon %s enabled successfully.\n", addonName)
   220  			AdditionalEndpointPrinter(ctx, c, k8sClient, addonName, additionalInfo, false)
   221  			return nil
   222  		},
   223  	}
   224  
   225  	cmd.Flags().StringVarP(&addonVersion, "version", "v", "", "specify the addon version to enable")
   226  	cmd.Flags().StringVarP(&addonClusters, types.ClustersArg, "c", "", "specify the runtime-clusters to enable")
   227  	cmd.Flags().BoolVarP(&skipValidate, "skip-version-validating", "s", false, "skip validating system version requirement")
   228  	cmd.Flags().BoolVarP(&overrideDefs, "override-definitions", "", false, "override existing definitions if conflict with those contained in this addon")
   229  	cmd.Flags().BoolVarP(&dryRun, FlagDryRun, "", false, "render all yaml files out without real execute it")
   230  	cmd.Flags().BoolVarP(&yes2all, "yes", "y", false, "all checks will be skipped and the default answer is yes for all validation check.")
   231  	return cmd
   232  }
   233  
   234  // AdditionalEndpointPrinter will print endpoints
   235  func AdditionalEndpointPrinter(ctx context.Context, c common.Args, _ client.Client, name, info string, _ bool) {
   236  	err := printAppEndpoints(ctx, addonutil.Addon2AppName(name), types.DefaultKubeVelaNS, Filter{}, c, true)
   237  	if err != nil {
   238  		fmt.Println("Get application endpoints error:", err)
   239  		return
   240  	}
   241  	if len(info) > 0 {
   242  		fmt.Println(info)
   243  	}
   244  }
   245  
   246  // NewAddonUpgradeCommand create addon upgrade command
   247  func NewAddonUpgradeCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Command {
   248  	ctx := context.Background()
   249  	cmd := &cobra.Command{
   250  		Use:   "upgrade",
   251  		Short: "upgrade an addon",
   252  		Long:  "upgrade an addon in cluster.",
   253  		Example: `
   254    Upgrade addon by:
   255  	vela addon upgrade <addon-name>
   256    Upgrade addon with specify version:
   257  	vela addon upgrade <addon-name> --version <addon-version>
   258    Upgrade addon for specific clusters, (local means control plane):
   259  	vela addon upgrade <addon-name> --clusters={local,cluster1,cluster2}
   260    Upgrade addon locally:
   261  	vela addon upgrade <your-local-addon-path>
   262    Upgrade addon with specified args (the args should be defined in addon's parameters):
   263  	vela addon upgrade <addon-name> <my-parameter-of-addon>=<my-value>
   264    The specified args will be merged with legacy args, what user specified in 'vela addon enable', and non-empty legacy arg will be overridden by
   265  non-empty new arg
   266  `,
   267  		RunE: func(cmd *cobra.Command, args []string) error {
   268  			if len(args) < 1 {
   269  				return fmt.Errorf("must specify addon name")
   270  			}
   271  			config, err := c.GetConfig()
   272  			if err != nil {
   273  				return err
   274  			}
   275  			k8sClient, err := c.GetClient()
   276  			if err != nil {
   277  				return err
   278  			}
   279  			dc, err := c.GetDiscoveryClient()
   280  			if err != nil {
   281  				return err
   282  			}
   283  			addonInputArgs, err := parseAddonArgsToMap(args[1:])
   284  			if err != nil {
   285  				return err
   286  			}
   287  			clusterArgs := transClusters(addonClusters)
   288  			if len(clusterArgs) != 0 {
   289  				addonInputArgs[types.ClustersArg] = clusterArgs
   290  			}
   291  			addonOrDir := args[0]
   292  
   293  			// inject runtime info
   294  			addonInputArgs[pkgaddon.InstallerRuntimeOption] = map[string]interface{}{
   295  				"upgrade": true,
   296  			}
   297  
   298  			var name, additionalInfo string
   299  			if file, err := os.Stat(addonOrDir); err == nil {
   300  				if !file.IsDir() {
   301  					return fmt.Errorf("%s is not addon dir", addonOrDir)
   302  				}
   303  				ioStream.Infof(color.New(color.FgYellow).Sprintf("enabling addon by local dir: %s \n", addonOrDir))
   304  				// args[0] is a local path install with local dir
   305  				abs, err := filepath.Abs(addonOrDir)
   306  				if err != nil {
   307  					return errors.Wrapf(err, "cannot open directory %s", addonOrDir)
   308  				}
   309  				name = filepath.Base(abs)
   310  				_, err = pkgaddon.FetchAddonRelatedApp(context.Background(), k8sClient, name)
   311  				if err != nil {
   312  					return errors.Wrapf(err, "cannot fetch addon related addon %s", name)
   313  				}
   314  				addonArgs, err := pkgaddon.MergeAddonInstallArgs(ctx, k8sClient, name, addonInputArgs)
   315  				if err != nil {
   316  					return err
   317  				}
   318  				additionalInfo, err = enableAddonByLocal(ctx, name, addonOrDir, k8sClient, dc, config, addonArgs)
   319  				if err != nil {
   320  					return err
   321  				}
   322  			} else {
   323  				if filepath.IsAbs(addonOrDir) || strings.HasPrefix(addonOrDir, ".") || strings.HasSuffix(addonOrDir, "/") {
   324  					return fmt.Errorf("addon directory %s not found in local", addonOrDir)
   325  				}
   326  				name = addonOrDir
   327  				_, err = pkgaddon.FetchAddonRelatedApp(context.Background(), k8sClient, addonOrDir)
   328  				if err != nil {
   329  					return errors.Wrapf(err, "cannot fetch addon related addon %s", addonOrDir)
   330  				}
   331  				addonArgs, err := pkgaddon.MergeAddonInstallArgs(ctx, k8sClient, name, addonInputArgs)
   332  				if err != nil {
   333  					return err
   334  				}
   335  				additionalInfo, err = enableAddon(ctx, k8sClient, dc, config, addonOrDir, addonVersion, addonArgs)
   336  				if err != nil {
   337  					return err
   338  				}
   339  			}
   340  
   341  			fmt.Printf("Addon %s enabled successfully.\n", name)
   342  			AdditionalEndpointPrinter(ctx, c, k8sClient, name, additionalInfo, true)
   343  			return nil
   344  		},
   345  	}
   346  	cmd.Flags().StringVarP(&addonVersion, "version", "v", "", "specify the addon version to upgrade")
   347  	cmd.Flags().StringVarP(&addonClusters, types.ClustersArg, "c", "", "specify the runtime-clusters to upgrade")
   348  	cmd.Flags().BoolVarP(&skipValidate, "skip-version-validating", "s", false, "skip validating system version requirement")
   349  	cmd.Flags().BoolVarP(&overrideDefs, "override-definitions", "", false, "override existing definitions if conflict with those contained in this addon")
   350  	return cmd
   351  }
   352  
   353  func parseAddonArgsToMap(args []string) (map[string]interface{}, error) {
   354  	res := map[string]interface{}{}
   355  	for _, arg := range args {
   356  		if err := strvals.ParseInto(arg, res); err != nil {
   357  			return nil, err
   358  		}
   359  	}
   360  	return res, nil
   361  }
   362  
   363  // NewAddonDisableCommand create addon disable command
   364  func NewAddonDisableCommand(c common.Args, _ cmdutil.IOStreams) *cobra.Command {
   365  	cmd := &cobra.Command{
   366  		Use:     "disable",
   367  		Aliases: []string{"uninstall"},
   368  		Short:   "disable an addon",
   369  		Long:    "disable an addon in cluster.",
   370  		Example: "vela addon disable <addon-name>",
   371  		RunE: func(cmd *cobra.Command, args []string) error {
   372  			if len(args) < 1 {
   373  				return fmt.Errorf("must specify addon name")
   374  			}
   375  			name := args[0]
   376  			k8sClient, err := c.GetClient()
   377  			if err != nil {
   378  				return err
   379  			}
   380  			config, err := c.GetConfig()
   381  			if err != nil {
   382  				return err
   383  			}
   384  			err = disableAddon(k8sClient, name, config, forceDisable)
   385  			if err != nil {
   386  				return err
   387  			}
   388  			fmt.Printf("Successfully disable addon:%s\n", name)
   389  			return nil
   390  		},
   391  	}
   392  	cmd.Flags().BoolVarP(&forceDisable, "force", "f", false, "skip checking if applications are still using this addon")
   393  	return cmd
   394  }
   395  
   396  // NewAddonStatusCommand create addon status command
   397  func NewAddonStatusCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Command {
   398  	cmd := &cobra.Command{
   399  		Use:     "status",
   400  		Short:   "get an addon's status.",
   401  		Long:    "get an addon's status from cluster.",
   402  		Example: "vela addon status <addon-name>",
   403  		RunE: func(cmd *cobra.Command, args []string) error {
   404  			if len(args) < 1 {
   405  				return fmt.Errorf("must specify addon name")
   406  			}
   407  			name := args[0]
   408  			err := statusAddon(name, ioStream, cmd, c)
   409  			if err != nil {
   410  				return err
   411  			}
   412  			return nil
   413  		},
   414  	}
   415  	cmd.Flags().BoolVarP(&verboseStatus, "verbose", "v", false, "show addon descriptions and parameters in addition to status")
   416  	return cmd
   417  }
   418  
   419  // NewAddonInitCommand creates an addon scaffold
   420  func NewAddonInitCommand() *cobra.Command {
   421  	var path string
   422  	initCmd := pkgaddon.InitCmd{}
   423  
   424  	cmd := &cobra.Command{
   425  		Use:   "init",
   426  		Short: "create an addon scaffold",
   427  		Long:  "Create an addon scaffold for quick starting.",
   428  		Example: `  Store the scaffold in a different directory:
   429  	vela addon init mongodb -p path/to/addon
   430  
   431    Add a Helm component:
   432  	vela addon init mongodb --helm-repo https://marketplace.azurecr.io/helm/v1/repo --chart mongodb --chart-version 12.1.16
   433  
   434    Add resources from URL using ref-objects component
   435  	vela addon init my-addon --url https://domain.com/resource.yaml
   436  
   437    Use --no-samples options to skip creating sample files
   438  	vela addon init my-addon --no-sample
   439  
   440    You can combine all the options together.`,
   441  		RunE: func(cmd *cobra.Command, args []string) error {
   442  			if len(args) < 1 {
   443  				return fmt.Errorf("an addon name is required")
   444  			}
   445  
   446  			addonName := args[0]
   447  
   448  			// Scaffold will be created in ./addonName, unless the user specifies a path
   449  			// validity of addon names will be checked later
   450  			addonPath := addonName
   451  			if len(path) > 0 {
   452  				addonPath = path
   453  			}
   454  
   455  			if addonName == "" || addonPath == "" {
   456  				return fmt.Errorf("addon name or path should not be empty")
   457  			}
   458  
   459  			initCmd.AddonName = addonName
   460  			initCmd.Path = addonPath
   461  
   462  			return initCmd.CreateScaffold()
   463  		},
   464  	}
   465  
   466  	f := cmd.Flags()
   467  	f.StringVar(&initCmd.HelmRepoURL, "helm-repo", "", "URL that points to a Helm repo")
   468  	f.StringVar(&initCmd.HelmChartName, "chart", "", "Helm Chart name")
   469  	f.StringVar(&initCmd.HelmChartVersion, "chart-version", "", "version of the Chart")
   470  	f.StringVarP(&path, "path", "p", "", "path to the addon directory (default is ./<addon-name>)")
   471  	f.StringArrayVarP(&initCmd.RefObjURLs, "url", "u", []string{}, "add URL resources using ref-object component")
   472  	f.BoolVarP(&initCmd.NoSamples, "no-samples", "", false, "do not generate sample files")
   473  	f.BoolVarP(&initCmd.Overwrite, "force", "f", false, "overwrite existing addon files")
   474  
   475  	return cmd
   476  }
   477  
   478  // NewAddonPushCommand pushes an addon dir/package to a ChartMuseum
   479  func NewAddonPushCommand(c common.Args) *cobra.Command {
   480  	p := &pkgaddon.PushCmd{}
   481  	cmd := &cobra.Command{
   482  		Use:   "push",
   483  		Short: "uploads an addon package to ChartMuseum",
   484  		Long: `Uploads an addon package to ChartMuseum.
   485  
   486  Two arguments are needed <addon directory/package> and <name/URL of ChartMuseum>.
   487  
   488  The first argument <addon directory/package> can be:
   489  	- your conventional addon directory (containing metadata.yaml). We will package it for you.
   490  	- packaged addon (.tgz) generated by 'vela addon package' command
   491  
   492  The second argument <name/URL of ChartMuseum> can be:
   493  	- registry name (helm type). You can add your ChartMuseum registry using 'vela addon registry add'.
   494  	- ChartMuseum URL, e.g. http://localhost:8080`,
   495  		Example: `# Push the addon in directory <your-addon> to a ChartMuseum registry named <localcm>
   496  $ vela addon push your-addon localcm
   497  
   498  # Push packaged addon mongo-1.0.0.tgz to a ChartMuseum registry at http://localhost:8080
   499  $ vela addon push mongo-1.0.0.tgz http://localhost:8080
   500  
   501  # Force push, overwriting existing ones
   502  $ vela addon push your-addon localcm -f
   503  
   504  # If you already written your own Chart.yaml and don't want us to generate it for you:
   505  $ vela addon push your-addon localcm --keep-chartmeta
   506  # Note: when using .tgz packages, we will always keep the original Chart.yaml
   507  
   508  # In addition to cli flags, you can also use environment variables
   509  $ HELM_REPO_USERNAME=name HELM_REPO_PASSWORD=pswd vela addon push mongo-1.0.0.tgz http://localhost:8080`,
   510  		RunE: func(cmd *cobra.Command, args []string) error {
   511  			if len(args) != 2 {
   512  				return fmt.Errorf("two arguments are needed: addon directory/package, name/URL of Chart repository")
   513  			}
   514  
   515  			c, err := c.GetClient()
   516  			if err != nil {
   517  				return err
   518  			}
   519  
   520  			p.Client = c
   521  			p.Out = cmd.OutOrStdout()
   522  			p.ChartName = args[0]
   523  			p.RepoName = args[1]
   524  			p.SetFieldsFromEnv()
   525  
   526  			return p.Push(context.Background())
   527  		},
   528  	}
   529  
   530  	f := cmd.Flags()
   531  	f.StringVarP(&p.ChartVersion, "version", "v", "", "override chart version pre-push")
   532  	f.StringVarP(&p.AppVersion, "app-version", "a", "", "override app version pre-push")
   533  	f.StringVarP(&p.Username, "username", "u", "", "override HTTP basic auth username [$HELM_REPO_USERNAME]")
   534  	f.StringVarP(&p.Password, "password", "p", "", "override HTTP basic auth password [$HELM_REPO_PASSWORD]")
   535  	f.StringVarP(&p.AccessToken, "access-token", "", "", "send token in Authorization header [$HELM_REPO_ACCESS_TOKEN]")
   536  	f.StringVarP(&p.AuthHeader, "auth-header", "", "", "alternative header to use for token auth [$HELM_REPO_AUTH_HEADER]")
   537  	f.StringVarP(&p.ContextPath, "context-path", "", "", "ChartMuseum context path [$HELM_REPO_CONTEXT_PATH]")
   538  	f.StringVarP(&p.CaFile, "ca-file", "", "", "verify certificates of HTTPS-enabled servers using this CA bundle [$HELM_REPO_CA_FILE]")
   539  	f.StringVarP(&p.CertFile, "cert-file", "", "", "identify HTTPS client using this SSL certificate file [$HELM_REPO_CERT_FILE]")
   540  	f.StringVarP(&p.KeyFile, "key-file", "", "", "identify HTTPS client using this SSL key file [$HELM_REPO_KEY_FILE]")
   541  	f.BoolVarP(&p.InsecureSkipVerify, "insecure", "", false, "connect to server with an insecure way by skipping certificate verification [$HELM_REPO_INSECURE]")
   542  	f.BoolVarP(&p.ForceUpload, "force", "f", false, "force upload even if chart version exists")
   543  	f.BoolVarP(&p.UseHTTP, "use-http", "", false, "use HTTP")
   544  	f.BoolVarP(&p.KeepChartMetadata, "keep-chartmeta", "", false, "do not update Chart.yaml automatically according to addon metadata (only when addon dir provided)")
   545  	f.Int64VarP(&p.Timeout, "timeout", "t", 30, "The duration (in seconds) vela cli will wait to get response from ChartMuseum")
   546  
   547  	return cmd
   548  }
   549  
   550  func enableAddon(ctx context.Context, k8sClient client.Client, dc *discovery.DiscoveryClient, config *rest.Config, name string, version string, args map[string]interface{}) (string, error) {
   551  	var err error
   552  	var additionalInfo string
   553  	registryDS := pkgaddon.NewRegistryDataStore(k8sClient)
   554  	registries, err := registryDS.ListRegistries(ctx)
   555  	if err != nil {
   556  		return "", err
   557  	}
   558  	registryName, addonName, err := splitSpecifyRegistry(name)
   559  	if err != nil {
   560  		return "", err
   561  	}
   562  	if len(registryName) != 0 {
   563  		foundRegistry := false
   564  		for _, registry := range registries {
   565  			if registry.Name == registryName {
   566  				foundRegistry = true
   567  			}
   568  		}
   569  		if !foundRegistry {
   570  			return "", fmt.Errorf("specified registry %s not exist", registryName)
   571  		}
   572  	}
   573  	for i, registry := range registries {
   574  		opts := addonOptions()
   575  		if len(registryName) != 0 && registryName != registry.Name {
   576  			continue
   577  		}
   578  		additionalInfo, err = pkgaddon.EnableAddon(ctx, addonName, version, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, registry, args, nil, pkgaddon.FilterDependencyRegistries(i, registries), opts...)
   579  		if errors.Is(err, pkgaddon.ErrNotExist) || errors.Is(err, pkgaddon.ErrFetch) {
   580  			continue
   581  		}
   582  		if unMatchErr := new(pkgaddon.VersionUnMatchError); errors.As(err, unMatchErr) {
   583  			// Get available version of the addon
   584  			availableVersion, err := unMatchErr.GetAvailableVersion()
   585  			if err != nil {
   586  				return "", err
   587  			}
   588  			input := NewUserInput()
   589  			if input.AskBool(unMatchErr.Error(), &UserInputOptions{AssumeYes: false}) {
   590  				return pkgaddon.EnableAddon(ctx, addonName, availableVersion, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, registry, args, nil, pkgaddon.FilterDependencyRegistries(i, registries))
   591  			}
   592  			// The user does not agree to use the version provided by us
   593  			return "", fmt.Errorf("you can try another version by command: \"vela addon enable %s --version <version> \" ", addonName)
   594  		}
   595  		if err != nil {
   596  			return "", err
   597  		}
   598  		if err = waitApplicationRunning(k8sClient, addonName); err != nil {
   599  			return "", err
   600  		}
   601  		return additionalInfo, nil
   602  	}
   603  	if len(registryName) != 0 {
   604  		return "", fmt.Errorf("addon: %s not found in registry %s", addonName, registryName)
   605  	}
   606  	return "", fmt.Errorf("addon: %s not found in all candidate registries", addonName)
   607  }
   608  
   609  func addonOptions() []pkgaddon.InstallOption {
   610  	var opts []pkgaddon.InstallOption
   611  	if skipValidate || yes2all {
   612  		opts = append(opts, pkgaddon.SkipValidateVersion)
   613  	}
   614  	if overrideDefs || yes2all {
   615  		opts = append(opts, pkgaddon.OverrideDefinitions)
   616  	}
   617  	if dryRun {
   618  		opts = append(opts, pkgaddon.DryRunAddon)
   619  	}
   620  	return opts
   621  }
   622  
   623  // enableAddonByLocal enable addon in local dir and return the addon name
   624  func enableAddonByLocal(ctx context.Context, name string, dir string, k8sClient client.Client, dc *discovery.DiscoveryClient, config *rest.Config, args map[string]interface{}) (string, error) {
   625  	opts := addonOptions()
   626  	info, err := pkgaddon.EnableAddonByLocalDir(ctx, name, dir, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, args, opts...)
   627  	if err != nil {
   628  		return "", err
   629  	}
   630  	if err = waitApplicationRunning(k8sClient, name); err != nil {
   631  		return "", err
   632  	}
   633  	return info, nil
   634  }
   635  
   636  func disableAddon(client client.Client, name string, config *rest.Config, force bool) error {
   637  	if err := pkgaddon.DisableAddon(context.Background(), client, name, config, force); err != nil {
   638  		return err
   639  	}
   640  	return nil
   641  }
   642  
   643  func statusAddon(name string, ioStreams cmdutil.IOStreams, cmd *cobra.Command, c common.Args) error {
   644  	k8sClient, err := c.GetClient()
   645  	if err != nil {
   646  		return err
   647  	}
   648  
   649  	statusString, status, err := generateAddonInfo(k8sClient, name)
   650  	if err != nil {
   651  		return err
   652  	}
   653  
   654  	fmt.Print(statusString)
   655  
   656  	if status.AddonPhase != statusEnabled && status.AddonPhase != statusDisabled {
   657  		fmt.Printf("diagnose addon info from application %s", addonutil.Addon2AppName(name))
   658  		err := printAppStatus(context.Background(), k8sClient, ioStreams, addonutil.Addon2AppName(name), types.DefaultKubeVelaNS, cmd, c, false)
   659  		if err != nil {
   660  			return err
   661  		}
   662  	}
   663  	return nil
   664  }
   665  
   666  func addonNotExist(err error) bool {
   667  	if errors.Is(err, pkgaddon.ErrNotExist) || errors.Is(err, pkgaddon.ErrRegistryNotExist) {
   668  		return true
   669  	}
   670  	if strings.Contains(err.Error(), "not found") {
   671  		return true
   672  	}
   673  	return false
   674  }
   675  
   676  // generateAddonInfo will get addon status, description, version, dependencies (and whether they are installed),
   677  // and parameters (and their current values).
   678  // The first return value is the formatted string for printing.
   679  // The second return value is just for diagnostic purposes, as it is needed in statusAddon to print diagnostic info.
   680  func generateAddonInfo(c client.Client, name string) (string, pkgaddon.Status, error) {
   681  	var res string
   682  	var phase string
   683  	var installed bool
   684  	var addonPackage *pkgaddon.WholeAddonPackage
   685  
   686  	// Check current addon status
   687  	status, err := pkgaddon.GetAddonStatus(context.Background(), c, name)
   688  	if err != nil {
   689  		return res, status, err
   690  	}
   691  
   692  	// Get addon install package
   693  	if verboseStatus || status.AddonPhase == statusDisabled {
   694  		// We need the metadata to get descriptions about parameters
   695  		addonPackages, err := pkgaddon.FindAddonPackagesDetailFromRegistry(context.Background(), c, []string{name}, nil)
   696  		// If the state of addon is not disabled, we don't check the error, because it could be installed from local.
   697  		if status.AddonPhase == statusDisabled && err != nil {
   698  			if addonNotExist(err) {
   699  				return "", pkgaddon.Status{}, fmt.Errorf("addon '%s' not found in cluster or any registry", name)
   700  			}
   701  			return "", pkgaddon.Status{}, err
   702  		}
   703  		if len(addonPackages) != 0 {
   704  			addonPackage = addonPackages[0]
   705  			if status.InstalledRegistry != "" {
   706  				for _, ap := range addonPackages {
   707  					if ap.RegistryName == status.InstalledRegistry {
   708  						addonPackage = ap
   709  						break
   710  					}
   711  				}
   712  			}
   713  		}
   714  	}
   715  
   716  	switch status.AddonPhase {
   717  	case statusEnabled:
   718  		installed = true
   719  		c := color.New(color.FgGreen)
   720  		phase = c.Sprintf("%s", status.AddonPhase)
   721  	case statusSuspend:
   722  		installed = true
   723  		c := color.New(color.FgRed)
   724  		phase = c.Sprintf("%s", status.AddonPhase)
   725  	case statusDisabled:
   726  		c := color.New(color.Faint)
   727  		phase = c.Sprintf("%s", status.AddonPhase)
   728  		// If the addon is
   729  		// 1. disabled,
   730  		// 2. does not exist in the registry,
   731  		// 3. verbose is on (when off, it is not possible to know whether the addon is in registry or not),
   732  		// means the addon does not exist at all.
   733  		// So, no need to go further, we return error message saying that we can't find it.
   734  		if addonPackage == nil && verboseStatus {
   735  			return res, pkgaddon.Status{}, fmt.Errorf("addon %s is not found in registries nor locally installed", name)
   736  		}
   737  	default:
   738  		c := color.New(color.Faint)
   739  		phase = c.Sprintf("%s", status.AddonPhase)
   740  	}
   741  
   742  	// Addon name
   743  	res += color.New(color.Bold).Sprintf("%s", name)
   744  	res += fmt.Sprintf(": %s ", phase)
   745  	if installed {
   746  		res += fmt.Sprintf("(%s)", status.InstalledVersion)
   747  	}
   748  	res += "\n"
   749  
   750  	// Description
   751  	// Skip this if addon is installed from local sources.
   752  	// Description is fetched from the Internet, which is not useful for local sources.
   753  	if status.InstalledRegistry != pkgaddon.LocalAddonRegistryName && addonPackage != nil {
   754  		res += fmt.Sprintln(addonPackage.Description)
   755  	}
   756  
   757  	// Installed Clusters
   758  	if len(status.Clusters) != 0 {
   759  		res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprintln("Installed Clusters")
   760  		var ic []string
   761  		for c := range status.Clusters {
   762  			ic = append(ic, c)
   763  		}
   764  		sort.Strings(ic)
   765  		res += fmt.Sprintln(ic)
   766  	}
   767  
   768  	// Registry name
   769  	registryName := status.InstalledRegistry
   770  	// Disabled addons will have empty InstalledRegistry, so if the addon exists in the registry, we use the registry name.
   771  	if registryName == "" && addonPackage != nil {
   772  		registryName = addonPackage.RegistryName
   773  	}
   774  	if registryName != "" {
   775  		res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprintln("Registry Name")
   776  		res += fmt.Sprintln(registryName)
   777  	}
   778  
   779  	// If the addon is installed from local sources, or does not exist at all, stop here!
   780  	// The following information is fetched from the Internet, which is not useful for local sources.
   781  	if registryName == pkgaddon.LocalAddonRegistryName || registryName == "" || addonPackage == nil {
   782  		return res, status, nil
   783  	}
   784  
   785  	// Available Versions
   786  	res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprintln("Available Versions")
   787  	res += genAvailableVersionInfo(addonPackage.AvailableVersions, status.InstalledVersion, 8)
   788  	res += "\n"
   789  
   790  	// Dependencies
   791  	dependenciesString, allInstalled := generateDependencyString(c, addonPackage.Dependencies)
   792  	res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprint("Dependencies ")
   793  	if allInstalled {
   794  		res += color.GreenString("✔")
   795  	} else {
   796  		res += color.RedString("✘")
   797  	}
   798  	res += "\n"
   799  	res += dependenciesString
   800  	res += "\n"
   801  
   802  	// Parameters
   803  	parameterString := generateParameterString(status, addonPackage)
   804  	if len(parameterString) != 0 {
   805  		res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprintln("Parameters")
   806  		res += parameterString
   807  	}
   808  
   809  	return res, status, nil
   810  }
   811  
   812  func generateParameterString(status pkgaddon.Status, addonPackage *pkgaddon.WholeAddonPackage) string {
   813  	ret := ""
   814  
   815  	if addonPackage.APISchema == nil {
   816  		return ret
   817  	}
   818  	ret = printSchema(addonPackage.APISchema, status.Parameters, 0)
   819  
   820  	return ret
   821  }
   822  
   823  func convertInterface2StringList(l []interface{}) []string {
   824  	var strl []string
   825  	for _, s := range l {
   826  		str, ok := s.(string)
   827  		if !ok {
   828  			continue
   829  		}
   830  		strl = append(strl, str)
   831  	}
   832  	return strl
   833  }
   834  
   835  // printSchema prints the parameters in an addon recursively to a string
   836  // Deeper the parameter is nested, more the indentations.
   837  func printSchema(ref *openapi3.Schema, currentParams map[string]interface{}, indent int) string {
   838  	ret := ""
   839  
   840  	if ref == nil {
   841  		return ret
   842  	}
   843  
   844  	addIndent := func(n int) string {
   845  		r := ""
   846  		for i := 0; i < n; i++ {
   847  			r += "\t"
   848  		}
   849  		return r
   850  	}
   851  
   852  	// Required parameters
   853  	required := make(map[string]bool)
   854  	for _, k := range ref.Required {
   855  		required[k] = true
   856  	}
   857  
   858  	for propKey, propValue := range ref.Properties {
   859  		desc := propValue.Value.Description
   860  		defaultValue := propValue.Value.Default
   861  		if defaultValue == nil {
   862  			defaultValue = ""
   863  		}
   864  		required := required[propKey]
   865  
   866  		// Extra indentation on nested objects
   867  		addedIndent := addIndent(indent)
   868  
   869  		var currentValue string
   870  		thisParam, hasParam := currentParams[propKey]
   871  		if hasParam {
   872  			currentValue = fmt.Sprintf("%#v", thisParam)
   873  			switch thisParam.(type) {
   874  			case int:
   875  			case int64:
   876  			case int32:
   877  			case float32:
   878  			case float64:
   879  			case string:
   880  			case bool:
   881  			default:
   882  				if js, err := json.MarshalIndent(thisParam, "", "  "); err == nil {
   883  					currentValue = strings.ReplaceAll(string(js), "\n", "\n\t         "+addedIndent)
   884  				}
   885  			}
   886  		}
   887  
   888  		// Header: addon: description
   889  		ret += addedIndent
   890  		ret += color.New(color.FgCyan).Sprintf("-> ")
   891  		ret += color.New(color.Bold).Sprint(propKey) + ": "
   892  		ret += fmt.Sprintf("%s\n", desc)
   893  
   894  		// Show current value
   895  		if currentValue != "" {
   896  			ret += addedIndent
   897  			ret += "\tcurrent value: " + color.New(color.FgGreen).Sprintf("%s\n", currentValue)
   898  		}
   899  
   900  		// Show required or not
   901  		if required {
   902  			ret += addedIndent
   903  			ret += "\trequired: "
   904  			ret += color.GreenString("✔\n")
   905  		}
   906  		// Show Enum options
   907  		if len(propValue.Value.Enum) > 0 {
   908  			ret += addedIndent
   909  			ret += "\toptions: \"" + strings.Join(convertInterface2StringList(propValue.Value.Enum), "\", \"") + "\"\n"
   910  		}
   911  		// Show default value
   912  		if defaultValue != "" && currentValue == "" {
   913  			ret += addedIndent
   914  			ret += "\tdefault: " + fmt.Sprintf("%#v\n", defaultValue)
   915  		}
   916  
   917  		// Object type param, we will get inside the object.
   918  		// To show what's inside nested objects.
   919  		if propValue.Value.Type == "object" {
   920  			nestedParam := make(map[string]interface{})
   921  			if hasParam {
   922  				nestedParam = currentParams[propKey].(map[string]interface{})
   923  			}
   924  			ret += printSchema(propValue.Value, nestedParam, indent+1)
   925  		}
   926  	}
   927  
   928  	return ret
   929  }
   930  
   931  func generateDependencyString(c client.Client, dependencies []*pkgaddon.Dependency) (string, bool) {
   932  	if len(dependencies) == 0 {
   933  		return "[]", true
   934  	}
   935  
   936  	ret := "["
   937  	allDependenciesInstalled := true
   938  
   939  	for idx, d := range dependencies {
   940  		name := d.Name
   941  
   942  		// Checks if the dependency is enabled, and mark it
   943  		status, err := pkgaddon.GetAddonStatus(context.Background(), c, name)
   944  		if err != nil {
   945  			continue
   946  		}
   947  
   948  		var enabledString string
   949  		switch status.AddonPhase {
   950  		case statusEnabled:
   951  			enabledString = color.GreenString("✔")
   952  		case statusSuspend:
   953  			enabledString = color.RedString("✔")
   954  		default:
   955  			enabledString = color.RedString("✘")
   956  			allDependenciesInstalled = false
   957  		}
   958  		ret += fmt.Sprintf("%s %s", name, enabledString)
   959  
   960  		if idx != len(dependencies)-1 {
   961  			ret += ", "
   962  		}
   963  	}
   964  
   965  	ret += "]"
   966  
   967  	return ret, allDependenciesInstalled
   968  }
   969  
   970  func listAddons(ctx context.Context, clt client.Client, registry string) (*uitable.Table, error) {
   971  	var addons []*pkgaddon.UIData
   972  	var err error
   973  	registryDS := pkgaddon.NewRegistryDataStore(clt)
   974  	registries, err := registryDS.ListRegistries(ctx)
   975  	if err != nil {
   976  		return nil, err
   977  	}
   978  
   979  	for _, r := range registries {
   980  		if registry != "" && r.Name != registry {
   981  			continue
   982  		}
   983  		var addonList []*pkgaddon.UIData
   984  		var err error
   985  		if !pkgaddon.IsVersionRegistry(r) {
   986  			meta, err := r.ListAddonMeta()
   987  			if err != nil {
   988  				continue
   989  			}
   990  			addonList, err = r.ListUIData(meta, pkgaddon.CLIMetaOptions)
   991  			if err != nil {
   992  				continue
   993  			}
   994  		} else {
   995  			versionedRegistry := pkgaddon.BuildVersionedRegistry(r.Name, r.Helm.URL, &common.HTTPOption{
   996  				Username:        r.Helm.Username,
   997  				Password:        r.Helm.Password,
   998  				InsecureSkipTLS: r.Helm.InsecureSkipTLS,
   999  			})
  1000  			addonList, err = versionedRegistry.ListAddon()
  1001  			if err != nil {
  1002  				continue
  1003  			}
  1004  		}
  1005  		addons = mergeAddons(addons, addonList)
  1006  	}
  1007  
  1008  	table := uitable.New()
  1009  	table.AddRow("NAME", "REGISTRY", "DESCRIPTION", "AVAILABLE-VERSIONS", "STATUS")
  1010  
  1011  	// get locally installed addons first
  1012  	locallyInstalledAddons := map[string]bool{}
  1013  	appList := v1beta1.ApplicationList{}
  1014  	if err := clt.List(ctx, &appList, client.MatchingLabels{oam.LabelAddonRegistry: pkgaddon.LocalAddonRegistryName}); err != nil {
  1015  		return table, err
  1016  	}
  1017  	for _, app := range appList.Items {
  1018  		labels := app.GetLabels()
  1019  		addonName := labels[oam.LabelAddonName]
  1020  		addonVersion := labels[oam.LabelAddonVersion]
  1021  		table.AddRow(enabledAddonColor.Sprintf("%s", addonName), app.GetLabels()[oam.LabelAddonRegistry], "", genAvailableVersionInfo([]string{addonVersion}, addonVersion, 3), enabledAddonColor.Sprintf("%s", statusEnabled))
  1022  		locallyInstalledAddons[addonName] = true
  1023  	}
  1024  
  1025  	for _, addon := range addons {
  1026  		// if the addon with same name has already installed locally, display the registry one as not installed
  1027  		if locallyInstalledAddons[addon.Name] {
  1028  			table.AddRow(addon.Name, addon.RegistryName, limitStringLength(addon.Description, 60), genAvailableVersionInfo(addon.AvailableVersions, "", 3), "-")
  1029  			continue
  1030  		}
  1031  		status, err := pkgaddon.GetAddonStatus(ctx, clt, addon.Name)
  1032  		if err != nil {
  1033  			return table, err
  1034  		}
  1035  		statusRow := status.AddonPhase
  1036  		name := addon.Name
  1037  		if len(status.InstalledVersion) != 0 {
  1038  			statusRow = enabledAddonColor.Sprintf("%s (%s)", statusRow, status.InstalledVersion)
  1039  			name = enabledAddonColor.Sprintf("%s", name)
  1040  		}
  1041  		if statusRow == statusDisabled {
  1042  			statusRow = "-"
  1043  		}
  1044  		table.AddRow(name, addon.RegistryName, limitStringLength(addon.Description, 60), genAvailableVersionInfo(addon.AvailableVersions, status.InstalledVersion, 3), statusRow)
  1045  	}
  1046  
  1047  	return table, nil
  1048  }
  1049  
  1050  func waitApplicationRunning(k8sClient client.Client, addonName string) error {
  1051  	if dryRun {
  1052  		return nil
  1053  	}
  1054  	trackInterval := 5 * time.Second
  1055  	timeout := 600 * time.Second
  1056  	start := time.Now()
  1057  	ctx := context.Background()
  1058  	var app v1beta1.Application
  1059  	spinner := newTrackingSpinnerWithDelay("Waiting addon running ...", 1*time.Second)
  1060  	spinner.Start()
  1061  	defer spinner.Stop()
  1062  
  1063  	for {
  1064  		err := k8sClient.Get(ctx, types2.NamespacedName{Name: addonutil.Addon2AppName(addonName), Namespace: types.DefaultKubeVelaNS}, &app)
  1065  		if err != nil {
  1066  			return client.IgnoreNotFound(err)
  1067  		}
  1068  
  1069  		phase := app.Status.Phase
  1070  		if app.Generation > app.Status.ObservedGeneration {
  1071  			phase = common2.ApplicationStarting
  1072  		} else {
  1073  			switch app.Status.Phase {
  1074  			case common2.ApplicationRunning:
  1075  				return nil
  1076  			case common2.ApplicationWorkflowSuspending:
  1077  				fmt.Printf("Enabling suspend, please run \"vela workflow resume %s -n vela-system\" to continue", addonutil.Addon2AppName(addonName))
  1078  				return nil
  1079  			case common2.ApplicationWorkflowTerminated, common2.ApplicationWorkflowFailed:
  1080  				return errors.Errorf("Enabling failed, please run \"vela status %s -n vela-system\" to check the status of the addon", addonutil.Addon2AppName(addonName))
  1081  			default:
  1082  			}
  1083  		}
  1084  
  1085  		timeConsumed := int(time.Since(start).Seconds())
  1086  		applySpinnerNewSuffix(spinner, fmt.Sprintf("Waiting addon application running. It is now in phase: %s (timeout %d/%d seconds)...",
  1087  			phase, timeConsumed, int(timeout.Seconds())))
  1088  		if timeConsumed > int(timeout.Seconds()) {
  1089  			return errors.Errorf("Enabling timeout, please run \"vela status %s -n vela-system\" to check the status of the addon", addonutil.Addon2AppName(addonName))
  1090  		}
  1091  		time.Sleep(trackInterval)
  1092  	}
  1093  
  1094  }
  1095  
  1096  // generate the available version
  1097  // this func put the installed version as the first version and keep the origin order
  1098  // print ... if available version too much
  1099  func genAvailableVersionInfo(versions []string, installedVersion string, limit int) string {
  1100  	var v []string
  1101  
  1102  	// put installed-version as the first version and keep the origin order
  1103  	if len(installedVersion) != 0 {
  1104  		for i, version := range versions {
  1105  			if version == installedVersion {
  1106  				v = append(v, version)
  1107  				versions = append(versions[:i], versions[i+1:]...)
  1108  			}
  1109  		}
  1110  	}
  1111  	v = append(v, versions...)
  1112  
  1113  	res := "["
  1114  	var count int
  1115  	for _, version := range v {
  1116  		if count == limit {
  1117  			// just show  newest 3 versions
  1118  			res += "..."
  1119  			break
  1120  		}
  1121  		if version == installedVersion {
  1122  			res += enabledAddonColor.Sprintf("%s", version)
  1123  		} else {
  1124  			res += version
  1125  		}
  1126  		res += ", "
  1127  		count++
  1128  	}
  1129  	res = strings.TrimSuffix(res, ", ")
  1130  	res += "]"
  1131  	return res
  1132  }
  1133  
  1134  // limitStringLength limits the length of the string, and add ... if it is too long
  1135  func limitStringLength(str string, length int) string {
  1136  	if length <= 0 {
  1137  		return str
  1138  	}
  1139  	if len(str) > length {
  1140  		return str[:length] + "..."
  1141  	}
  1142  	return str
  1143  }
  1144  
  1145  // TransAddonName will turn addon's name from xxx/yyy to xxx-yyy
  1146  func TransAddonName(name string) string {
  1147  	return strings.ReplaceAll(name, "/", "-")
  1148  }
  1149  
  1150  func mergeAddons(a1, a2 []*pkgaddon.UIData) []*pkgaddon.UIData {
  1151  	for _, item := range a2 {
  1152  		if hasAddon(a1, item.Name) {
  1153  			continue
  1154  		}
  1155  		a1 = append(a1, item)
  1156  	}
  1157  	return a1
  1158  }
  1159  
  1160  func hasAddon(addons []*pkgaddon.UIData, name string) bool {
  1161  	for _, addon := range addons {
  1162  		if addon.Name == name {
  1163  			return true
  1164  		}
  1165  	}
  1166  	return false
  1167  }
  1168  
  1169  func transClusters(cstr string) []interface{} {
  1170  	if len(cstr) == 0 {
  1171  		return nil
  1172  	}
  1173  	cstr = strings.TrimPrefix(strings.TrimSuffix(cstr, "}"), "{")
  1174  	var clusterL []interface{}
  1175  	clusterList := strings.Split(cstr, ",")
  1176  	for _, v := range clusterList {
  1177  		clusterL = append(clusterL, strings.TrimSpace(v))
  1178  	}
  1179  	return clusterL
  1180  }
  1181  
  1182  // NewAddonPackageCommand create addon package command
  1183  func NewAddonPackageCommand(_ common.Args) *cobra.Command {
  1184  	cmd := &cobra.Command{
  1185  		Use:     "package",
  1186  		Short:   "package an addon directory",
  1187  		Long:    "package an addon directory into a helm chart archive.",
  1188  		Example: "vela addon package <addon directory>",
  1189  		RunE: func(cmd *cobra.Command, args []string) error {
  1190  			if len(args) < 1 {
  1191  				return fmt.Errorf("must specify addon directory path")
  1192  			}
  1193  			addonDict, err := filepath.Abs(args[0])
  1194  			if err != nil {
  1195  				return err
  1196  			}
  1197  
  1198  			archive, err := pkgaddon.PackageAddon(addonDict)
  1199  			if err != nil {
  1200  				return errors.Wrapf(err, "fail to package %s into helm chart archive", addonDict)
  1201  			}
  1202  
  1203  			fmt.Printf("Successfully package addon to: %s\n", archive)
  1204  			return nil
  1205  		},
  1206  	}
  1207  	return cmd
  1208  }
  1209  
  1210  func splitSpecifyRegistry(name string) (string, string, error) {
  1211  	res := strings.Split(name, "/")
  1212  	switch len(res) {
  1213  	case 2:
  1214  		return res[0], res[1], nil
  1215  	case 1:
  1216  		return "", res[0], nil
  1217  	default:
  1218  		return "", "", fmt.Errorf("invalid addon name, you should specify name only  <addonName>  or with registry as prefix <registryName>/<addonName>")
  1219  	}
  1220  }
  1221  
  1222  func checkUninstallFromClusters(ctx context.Context, k8sClient client.Client, addonName string, args map[string]interface{}) error {
  1223  	status, err := pkgaddon.GetAddonStatus(ctx, k8sClient, addonName)
  1224  	if err != nil {
  1225  		return fmt.Errorf("failed to check addon status: %w", err)
  1226  	}
  1227  	if status.AddonPhase == statusDisabled {
  1228  		return nil
  1229  	}
  1230  	if _, ok := args["clusters"]; !ok {
  1231  		return nil
  1232  	}
  1233  	cList, ok := args["clusters"].([]interface{})
  1234  	if !ok {
  1235  		return fmt.Errorf("clusters parameter must be a list of string")
  1236  	}
  1237  
  1238  	clusters := map[string]struct{}{}
  1239  	for _, c := range cList {
  1240  		clusterName := c.(string)
  1241  		clusters[clusterName] = struct{}{}
  1242  	}
  1243  	var disableClusters, installedClusters []string
  1244  	for c := range status.Clusters {
  1245  		if _, ok := clusters[c]; !ok {
  1246  			disableClusters = append(disableClusters, c)
  1247  		}
  1248  		installedClusters = append(installedClusters, c)
  1249  	}
  1250  	if len(disableClusters) == 0 {
  1251  		return nil
  1252  	}
  1253  	fmt.Println(color.New(color.FgRed).Sprintf("'%s' addon was currently installed on clusters %s, but this operation will uninstall from these clusters: %s \n", addonName, generateClustersInfo(installedClusters), generateClustersInfo(disableClusters)))
  1254  	input := NewUserInput()
  1255  	if !input.AskBool("Do you want to continue?", &UserInputOptions{AssumeYes: false}) {
  1256  		return fmt.Errorf("operation abort")
  1257  	}
  1258  	return nil
  1259  }
  1260  
  1261  func generateClustersInfo(clusters []string) string {
  1262  	ret := "["
  1263  	for i, cluster := range clusters {
  1264  		ret += cluster
  1265  		if i < len(clusters)-1 {
  1266  			ret += ","
  1267  		}
  1268  	}
  1269  	ret += "]"
  1270  	return ret
  1271  }