github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/util/helm/helm.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 helm
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"fmt"
    26  	"io"
    27  	"os"
    28  	"os/signal"
    29  	"path/filepath"
    30  	"strings"
    31  	"syscall"
    32  	"time"
    33  
    34  	"github.com/Masterminds/semver/v3"
    35  	"github.com/containers/common/pkg/retry"
    36  	"github.com/ghodss/yaml"
    37  	"github.com/pkg/errors"
    38  	"github.com/spf13/pflag"
    39  	"helm.sh/helm/v3/pkg/action"
    40  	"helm.sh/helm/v3/pkg/chart/loader"
    41  	"helm.sh/helm/v3/pkg/chartutil"
    42  	"helm.sh/helm/v3/pkg/cli"
    43  	"helm.sh/helm/v3/pkg/cli/values"
    44  	"helm.sh/helm/v3/pkg/getter"
    45  	"helm.sh/helm/v3/pkg/helmpath"
    46  	kubefake "helm.sh/helm/v3/pkg/kube/fake"
    47  	"helm.sh/helm/v3/pkg/registry"
    48  	"helm.sh/helm/v3/pkg/release"
    49  	"helm.sh/helm/v3/pkg/repo"
    50  	"helm.sh/helm/v3/pkg/storage"
    51  	"helm.sh/helm/v3/pkg/storage/driver"
    52  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    53  	"k8s.io/cli-runtime/pkg/genericclioptions"
    54  	"k8s.io/client-go/rest"
    55  	"k8s.io/klog/v2"
    56  
    57  	"github.com/1aal/kubeblocks/pkg/cli/testing"
    58  	"github.com/1aal/kubeblocks/pkg/cli/types"
    59  	"github.com/1aal/kubeblocks/pkg/cli/util/breakingchange"
    60  )
    61  
    62  const defaultTimeout = time.Second * 600
    63  
    64  type InstallOpts struct {
    65  	Name            string
    66  	Chart           string
    67  	Namespace       string
    68  	Wait            bool
    69  	Version         string
    70  	TryTimes        int
    71  	Login           bool
    72  	CreateNamespace bool
    73  	ValueOpts       *values.Options
    74  	Timeout         time.Duration
    75  	Atomic          bool
    76  	DisableHooks    bool
    77  	ForceUninstall  bool
    78  	Upgrader        breakingchange.Upgrader
    79  
    80  	// for helm template
    81  	DryRun     *bool
    82  	OutputDir  string
    83  	IncludeCRD bool
    84  }
    85  
    86  type Option func(*cli.EnvSettings)
    87  
    88  // AddRepo adds a repo
    89  func AddRepo(r *repo.Entry) error {
    90  	if r.Name == testing.KubeBlocksChartName {
    91  		return nil
    92  	}
    93  	settings := cli.New()
    94  	repoFile := settings.RepositoryConfig
    95  	b, err := os.ReadFile(repoFile)
    96  	if err != nil && !os.IsNotExist(err) {
    97  		return err
    98  	}
    99  
   100  	var f repo.File
   101  	if err = yaml.Unmarshal(b, &f); err != nil {
   102  		return err
   103  	}
   104  
   105  	// Check if the repo Name is legal
   106  	if strings.Contains(r.Name, "/") {
   107  		return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", r.Name)
   108  	}
   109  
   110  	if f.Has(r.Name) {
   111  		existing := f.Get(r.Name)
   112  		if *r != *existing && r.Name != types.KubeBlocksChartName {
   113  			// The input Name is different from the existing one, return an error
   114  			return errors.Errorf("repository name (%s) already exists, please specify a different name", r.Name)
   115  		}
   116  	}
   117  
   118  	cp, err := repo.NewChartRepository(r, getter.All(settings))
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	if _, err := cp.DownloadIndexFile(); err != nil {
   124  		return errors.Wrapf(err, "looks like %q is not a valid Chart repository or cannot be reached", r.URL)
   125  	}
   126  
   127  	f.Update(r)
   128  
   129  	if err = f.WriteFile(repoFile, 0644); err != nil {
   130  		return err
   131  	}
   132  	return nil
   133  }
   134  
   135  // RemoveRepo removes a repo
   136  func RemoveRepo(r *repo.Entry) error {
   137  	settings := cli.New()
   138  	repoFile := settings.RepositoryConfig
   139  	b, err := os.ReadFile(repoFile)
   140  	if err != nil && !os.IsNotExist(err) {
   141  		return err
   142  	}
   143  
   144  	var f repo.File
   145  	if err = yaml.Unmarshal(b, &f); err != nil {
   146  		return err
   147  	}
   148  
   149  	if f.Has(r.Name) {
   150  		f.Remove(r.Name)
   151  		if err = f.WriteFile(repoFile, 0644); err != nil {
   152  			return err
   153  		}
   154  	}
   155  	return nil
   156  }
   157  
   158  // GetInstalled gets helm package release info if installed.
   159  func (i *InstallOpts) GetInstalled(cfg *action.Configuration) (*release.Release, error) {
   160  	res, err := action.NewGet(cfg).Run(i.Name)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	if res == nil {
   165  		return nil, driver.ErrReleaseNotFound
   166  	}
   167  	if !statusDeployed(res) {
   168  		return nil, errors.Wrapf(ErrReleaseNotDeployed, "current version not in right status, try to fix it first, \n"+
   169  			"uninstall and install kubeblocks could be a way to fix error")
   170  	}
   171  	return res, nil
   172  }
   173  
   174  // Install installs a Chart
   175  func (i *InstallOpts) Install(cfg *Config) (*release.Release, error) {
   176  	ctx := context.Background()
   177  	opts := retry.Options{
   178  		MaxRetry: 1 + i.TryTimes,
   179  	}
   180  
   181  	actionCfg, err := NewActionConfig(cfg)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	var rel *release.Release
   187  	if err = retry.IfNecessary(ctx, func() error {
   188  		release, err1 := i.tryInstall(actionCfg)
   189  		if err1 != nil {
   190  			return err1
   191  		}
   192  		rel = release
   193  		return nil
   194  	}, &opts); err != nil {
   195  		return nil, errors.Errorf("install chart %s error: %s", i.Name, err.Error())
   196  	}
   197  
   198  	return rel, nil
   199  }
   200  
   201  func (i *InstallOpts) tryInstall(cfg *action.Configuration) (*release.Release, error) {
   202  	if i.DryRun == nil || !*i.DryRun {
   203  		released, err := i.GetInstalled(cfg)
   204  		if released != nil {
   205  			return released, nil
   206  		}
   207  		if err != nil && !ReleaseNotFound(err) {
   208  			return nil, err
   209  		}
   210  	}
   211  	settings := cli.New()
   212  
   213  	// TODO: Does not work now
   214  	// If a release does not exist, install it.
   215  	histClient := action.NewHistory(cfg)
   216  	histClient.Max = 1
   217  	if _, err := histClient.Run(i.Name); err != nil &&
   218  		!errors.Is(err, driver.ErrReleaseNotFound) {
   219  		return nil, err
   220  	}
   221  
   222  	client := action.NewInstall(cfg)
   223  	client.ReleaseName = i.Name
   224  	client.Namespace = i.Namespace
   225  	client.CreateNamespace = i.CreateNamespace
   226  	client.Wait = i.Wait
   227  	client.WaitForJobs = i.Wait
   228  	client.Timeout = i.Timeout
   229  	client.Version = i.Version
   230  	client.Atomic = i.Atomic
   231  
   232  	// for helm template
   233  	if i.DryRun != nil {
   234  		client.DryRun = *i.DryRun
   235  		client.OutputDir = i.OutputDir
   236  		client.IncludeCRDs = i.IncludeCRD
   237  		client.Replace = true
   238  		client.ClientOnly = true
   239  	}
   240  
   241  	if client.Timeout == 0 {
   242  		client.Timeout = defaultTimeout
   243  	}
   244  
   245  	cp, err := client.ChartPathOptions.LocateChart(i.Chart, settings)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	p := getter.All(settings)
   251  	vals, err := i.ValueOpts.MergeValues(p)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	// Check Chart dependencies to make sure all are present in /charts
   257  	chartRequested, err := loader.Load(cp)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	// Create context and prepare the handle of SIGTERM
   263  	ctx := context.Background()
   264  	_, cancel := context.WithCancel(ctx)
   265  
   266  	// Set up channel through which to send signal notifications.
   267  	// We must use a buffered channel or risk missing the signal
   268  	// if we're not ready to receive when the signal is sent.
   269  	cSignal := make(chan os.Signal, 2)
   270  	signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
   271  	go func() {
   272  		<-cSignal
   273  		fmt.Println("Install has been cancelled")
   274  		cancel()
   275  	}()
   276  
   277  	released, err := client.RunWithContext(ctx, chartRequested, vals)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	return released, nil
   282  }
   283  
   284  // Uninstall uninstalls a Chart
   285  func (i *InstallOpts) Uninstall(cfg *Config) error {
   286  	ctx := context.Background()
   287  	opts := retry.Options{
   288  		MaxRetry: 1 + i.TryTimes,
   289  	}
   290  	if cfg.Namespace() == "" {
   291  		cfg.SetNamespace(i.Namespace)
   292  	}
   293  
   294  	actionCfg, err := NewActionConfig(cfg)
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	if err := retry.IfNecessary(ctx, func() error {
   300  		if err := i.tryUninstall(actionCfg); err != nil {
   301  			return err
   302  		}
   303  		return nil
   304  	}, &opts); err != nil {
   305  		return err
   306  	}
   307  	return nil
   308  }
   309  
   310  func (i *InstallOpts) tryUninstall(cfg *action.Configuration) error {
   311  	client := action.NewUninstall(cfg)
   312  	client.Wait = i.Wait
   313  	client.Timeout = defaultTimeout
   314  	client.DisableHooks = i.DisableHooks
   315  
   316  	// Create context and prepare the handle of SIGTERM
   317  	ctx := context.Background()
   318  	_, cancel := context.WithCancel(ctx)
   319  
   320  	// Set up channel through which to send signal notifications.
   321  	// We must use a buffered channel or risk missing the signal
   322  	// if we're not ready to receive when the signal is sent.
   323  	cSignal := make(chan os.Signal, 2)
   324  	signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
   325  	go func() {
   326  		<-cSignal
   327  		fmt.Println("Install has been cancelled")
   328  		cancel()
   329  	}()
   330  
   331  	if _, err := client.Run(i.Name); err != nil {
   332  		if i.ForceUninstall {
   333  			// Remove secrets left over when uninstalling kubeblocks, when addon CRD is uninstalled before kubeblocks.
   334  			secretCount, errRemove := i.RemoveRemainSecrets(cfg)
   335  			if secretCount == 0 {
   336  				return err
   337  			}
   338  			if errRemove != nil {
   339  				errMsg := fmt.Sprintf("failed to remove remain secrets, please remove them manually, %v", errRemove)
   340  				return errors.Wrap(err, errMsg)
   341  			}
   342  		} else {
   343  			return err
   344  		}
   345  	}
   346  	return nil
   347  }
   348  
   349  func (i *InstallOpts) RemoveRemainSecrets(cfg *action.Configuration) (int, error) {
   350  	clientSet, err := cfg.KubernetesClientSet()
   351  	if err != nil {
   352  		return -1, err
   353  	}
   354  
   355  	labelSelector := metav1.LabelSelector{
   356  		MatchExpressions: []metav1.LabelSelectorRequirement{
   357  			{
   358  				Key:      "name",
   359  				Operator: metav1.LabelSelectorOpIn,
   360  				Values:   []string{i.Name},
   361  			},
   362  			{
   363  				Key:      "owner",
   364  				Operator: metav1.LabelSelectorOpIn,
   365  				Values:   []string{"helm"},
   366  			},
   367  			{
   368  				Key:      "status",
   369  				Operator: metav1.LabelSelectorOpIn,
   370  				Values:   []string{"uninstalling", "superseded"},
   371  			},
   372  		},
   373  	}
   374  
   375  	selector, err := metav1.LabelSelectorAsSelector(&labelSelector)
   376  	if err != nil {
   377  		fmt.Printf("Failed to build label selector: %v\n", err)
   378  		return -1, err
   379  	}
   380  	options := metav1.ListOptions{
   381  		LabelSelector: selector.String(),
   382  	}
   383  
   384  	secrets, err := clientSet.CoreV1().Secrets(i.Namespace).List(context.TODO(), options)
   385  	if err != nil {
   386  		return -1, err
   387  	}
   388  	secretCount := len(secrets.Items)
   389  	if secretCount == 0 {
   390  		return 0, nil
   391  	}
   392  
   393  	for _, secret := range secrets.Items {
   394  		err := clientSet.CoreV1().Secrets(i.Namespace).Delete(context.TODO(), secret.Name, metav1.DeleteOptions{})
   395  		if err != nil {
   396  			klog.V(1).Info(err)
   397  			return -1, fmt.Errorf("failed to delete Secret %s: %v", secret.Name, err)
   398  		}
   399  	}
   400  	return secretCount, nil
   401  }
   402  
   403  func NewActionConfig(cfg *Config) (*action.Configuration, error) {
   404  	if cfg.fake {
   405  		return fakeActionConfig(), nil
   406  	}
   407  
   408  	var err error
   409  	settings := cli.New()
   410  	actionCfg := new(action.Configuration)
   411  	settings.SetNamespace(cfg.namespace)
   412  	settings.KubeConfig = cfg.kubeConfig
   413  	if cfg.kubeContext != "" {
   414  		settings.KubeContext = cfg.kubeContext
   415  	}
   416  	settings.Debug = cfg.debug
   417  
   418  	if actionCfg.RegistryClient, err = registry.NewClient(
   419  		registry.ClientOptDebug(settings.Debug),
   420  		registry.ClientOptEnableCache(true),
   421  		registry.ClientOptWriter(io.Discard),
   422  		registry.ClientOptCredentialsFile(settings.RegistryConfig),
   423  	); err != nil {
   424  		return nil, err
   425  	}
   426  
   427  	// do not output warnings
   428  	getter := settings.RESTClientGetter()
   429  	getter.(*genericclioptions.ConfigFlags).WrapConfigFn = func(c *rest.Config) *rest.Config {
   430  		c.WarningHandler = rest.NoWarnings{}
   431  		return c
   432  	}
   433  
   434  	if err = actionCfg.Init(settings.RESTClientGetter(),
   435  		settings.Namespace(),
   436  		os.Getenv("HELM_DRIVER"),
   437  		cfg.logFn); err != nil {
   438  		return nil, err
   439  	}
   440  	return actionCfg, nil
   441  }
   442  
   443  func fakeActionConfig() *action.Configuration {
   444  	registryClient, err := registry.NewClient()
   445  	if err != nil {
   446  		return nil
   447  	}
   448  
   449  	res := &action.Configuration{
   450  		Releases:       storage.Init(driver.NewMemory()),
   451  		KubeClient:     &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}},
   452  		Capabilities:   chartutil.DefaultCapabilities,
   453  		RegistryClient: registryClient,
   454  		Log:            func(format string, v ...interface{}) {},
   455  	}
   456  	// to template the kubeblocks manifest, dry-run install will check and valida the KubeVersion in Capabilities is bigger than
   457  	// the KubeVersion in Chart.yaml.
   458  	// in helm v3.11.1 the DefaultCapabilities KubeVersion is 1.20 which lower than the kubeblocks Chart claimed '>=1.22.0-0'
   459  	res.Capabilities.KubeVersion.Version = "v99.99.0"
   460  	return res
   461  }
   462  
   463  // Upgrade will upgrade a Chart
   464  func (i *InstallOpts) Upgrade(cfg *Config) error {
   465  	if i.Name == testing.KubeBlocksChartName {
   466  		return nil
   467  	}
   468  	ctx := context.Background()
   469  	opts := retry.Options{
   470  		MaxRetry: 1 + i.TryTimes,
   471  	}
   472  
   473  	actionCfg, err := NewActionConfig(cfg)
   474  	if err != nil {
   475  		return err
   476  	}
   477  
   478  	if err = retry.IfNecessary(ctx, func() error {
   479  		var err1 error
   480  		if _, err1 = i.tryUpgrade(actionCfg); err1 != nil {
   481  			return err1
   482  		}
   483  		return nil
   484  	}, &opts); err != nil {
   485  		return err
   486  	}
   487  
   488  	return nil
   489  }
   490  
   491  func (i *InstallOpts) tryUpgrade(cfg *action.Configuration) (*release.Release, error) {
   492  	installed, err := i.GetInstalled(cfg)
   493  	if err != nil {
   494  		return nil, err
   495  	}
   496  
   497  	settings := cli.New()
   498  
   499  	client := action.NewUpgrade(cfg)
   500  	client.Namespace = i.Namespace
   501  	client.Wait = i.Wait
   502  	client.WaitForJobs = i.Wait
   503  	client.Timeout = i.Timeout
   504  	if client.Timeout == 0 {
   505  		client.Timeout = defaultTimeout
   506  	}
   507  
   508  	if len(i.Version) > 0 {
   509  		client.Version = i.Version
   510  	} else {
   511  		client.Version = installed.Chart.AppVersion()
   512  	}
   513  	// do not use helm's ReuseValues, do it ourselves, helm's default upgrade also set it to false
   514  	// if ReuseValues set to true, helm will use old values instead of new ones, which will cause nil pointer error if new values added.
   515  	client.ReuseValues = false
   516  
   517  	cp, err := client.ChartPathOptions.LocateChart(i.Chart, settings)
   518  	if err != nil {
   519  		return nil, err
   520  	}
   521  
   522  	p := getter.All(settings)
   523  	vals, err := i.ValueOpts.MergeValues(p)
   524  	if err != nil {
   525  		return nil, err
   526  	}
   527  	// get coalesced values of current chart
   528  	currentValues, err := chartutil.CoalesceValues(installed.Chart, installed.Config)
   529  	if err != nil {
   530  		return nil, err
   531  	}
   532  	// merge current values into vals, so current release's user values can be kept
   533  	installed.Chart.Values = currentValues
   534  	vals, err = chartutil.CoalesceValues(installed.Chart, vals)
   535  	if err != nil {
   536  		return nil, err
   537  	}
   538  
   539  	// Check Chart dependencies to make sure all are present in /charts
   540  	chartRequested, err := loader.Load(cp)
   541  	if err != nil {
   542  		return nil, err
   543  	}
   544  
   545  	// Create context and prepare the handle of SIGTERM
   546  	ctx := context.Background()
   547  	_, cancel := context.WithCancel(ctx)
   548  
   549  	// Set up channel through which to send signal notifications.
   550  	// We must use a buffered channel or risk missing the signal
   551  	// if we're not ready to receive when the signal is sent.
   552  	cSignal := make(chan os.Signal, 2)
   553  	signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
   554  	go func() {
   555  		<-cSignal
   556  		fmt.Println("Upgrade has been cancelled")
   557  		cancel()
   558  	}()
   559  
   560  	// save resources of old version
   561  	if err = i.Upgrader.SaveOldResources(); err != nil {
   562  		return nil, err
   563  	}
   564  
   565  	// update crds before helm upgrade
   566  	for _, obj := range chartRequested.CRDObjects() {
   567  		// Read in the resources
   568  		target, err := cfg.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false)
   569  		if err != nil {
   570  			return nil, errors.Wrapf(err, "failed to update CRD %s", obj.Name)
   571  		}
   572  
   573  		// helm only use the original.Info part for looking up original CRD in Update interface
   574  		// so set original with target as they have same .Info part
   575  		original := target
   576  		if _, err := cfg.KubeClient.Update(original, target, false); err != nil {
   577  			return nil, errors.Wrapf(err, "failed to update CRD %s", obj.Name)
   578  		}
   579  	}
   580  
   581  	// transform old resources to new resources and clear the tmp dir which saved the old resources.
   582  	if err = i.Upgrader.TransformResourcesAndClear(); err != nil {
   583  		return nil, err
   584  	}
   585  
   586  	released, err := client.RunWithContext(ctx, i.Name, chartRequested, vals)
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  	return released, nil
   591  }
   592  
   593  func GetChartVersions(chartName string) ([]*semver.Version, error) {
   594  	if chartName == testing.KubeBlocksChartName {
   595  		return nil, nil
   596  	}
   597  	settings := cli.New()
   598  	rf, err := repo.LoadFile(settings.RepositoryConfig)
   599  	if err != nil {
   600  		if os.IsNotExist(errors.Cause(err)) {
   601  			return nil, nil
   602  		} else {
   603  			return nil, err
   604  		}
   605  	}
   606  
   607  	var ind *repo.IndexFile
   608  	for _, re := range rf.Repositories {
   609  		n := re.Name
   610  		if n != types.KubeBlocksRepoName {
   611  			continue
   612  		}
   613  
   614  		// load index file
   615  		f := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(n))
   616  		ind, err = repo.LoadIndexFile(f)
   617  		if err != nil {
   618  			return nil, err
   619  		}
   620  		break
   621  	}
   622  
   623  	// cannot find any index file
   624  	if ind == nil {
   625  		return nil, nil
   626  	}
   627  
   628  	var versions []*semver.Version
   629  	for chart, entry := range ind.Entries {
   630  		if len(entry) == 0 || chart != chartName {
   631  			continue
   632  		}
   633  		for _, v := range entry {
   634  			ver, err := semver.NewVersion(v.Version)
   635  			if err != nil {
   636  				return nil, err
   637  			}
   638  			versions = append(versions, ver)
   639  		}
   640  	}
   641  	return versions, nil
   642  }
   643  
   644  // AddValueOptionsFlags add helm value flags
   645  func AddValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
   646  	f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)")
   647  	f.StringArrayVar(&v.Values, "set", []string{}, "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
   648  	f.StringArrayVar(&v.StringValues, "set-string", []string{}, "Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
   649  	f.StringArrayVar(&v.FileValues, "set-file", []string{}, "Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
   650  	f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "Set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)")
   651  }
   652  
   653  func ValueOptsIsEmpty(valueOpts *values.Options) bool {
   654  	if valueOpts == nil {
   655  		return true
   656  	}
   657  	return len(valueOpts.ValueFiles) == 0 &&
   658  		len(valueOpts.StringValues) == 0 &&
   659  		len(valueOpts.Values) == 0 &&
   660  		len(valueOpts.FileValues) == 0 &&
   661  		len(valueOpts.JSONValues) == 0
   662  }
   663  
   664  func GetQuiteLog() action.DebugLog {
   665  	return func(format string, v ...interface{}) {}
   666  }
   667  
   668  func GetVerboseLog() action.DebugLog {
   669  	return func(format string, v ...interface{}) {
   670  		klog.Infof(format+"\n", v...)
   671  	}
   672  }
   673  
   674  // GetValues gives an implementation of 'helm get values' for target release
   675  func GetValues(release string, cfg *Config) (map[string]interface{}, error) {
   676  	actionConfig, err := NewActionConfig(cfg)
   677  	if err != nil {
   678  		return nil, err
   679  	}
   680  	client := action.NewGetValues(actionConfig)
   681  	client.AllValues = true
   682  	return client.Run(release)
   683  }
   684  
   685  // GetTemplateInstallOps build a helm InstallOpts with dryrun to implement helm template
   686  func GetTemplateInstallOps(name, chart, version, namespace string) *InstallOpts {
   687  	dryrun := true
   688  	return &InstallOpts{
   689  		Name:       name,
   690  		Chart:      chart,
   691  		Version:    version,
   692  		Namespace:  namespace,
   693  		TryTimes:   2,
   694  		Atomic:     true,
   695  		IncludeCRD: true,
   696  		DryRun:     &dryrun,
   697  	}
   698  }