github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/test/testutils/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 testutils
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"os/signal"
    28  	"strings"
    29  	"syscall"
    30  	"time"
    31  
    32  	"github.com/containers/common/pkg/retry"
    33  	"github.com/pkg/errors"
    34  	"gopkg.in/yaml.v2"
    35  	"helm.sh/helm/v3/pkg/action"
    36  	"helm.sh/helm/v3/pkg/chart/loader"
    37  	"helm.sh/helm/v3/pkg/chartutil"
    38  	"helm.sh/helm/v3/pkg/cli"
    39  	"helm.sh/helm/v3/pkg/cli/values"
    40  	"helm.sh/helm/v3/pkg/getter"
    41  	kubefake "helm.sh/helm/v3/pkg/kube/fake"
    42  	"helm.sh/helm/v3/pkg/registry"
    43  	"helm.sh/helm/v3/pkg/release"
    44  	"helm.sh/helm/v3/pkg/repo"
    45  	"helm.sh/helm/v3/pkg/storage"
    46  	"helm.sh/helm/v3/pkg/storage/driver"
    47  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    48  	"k8s.io/cli-runtime/pkg/genericclioptions"
    49  	"k8s.io/client-go/rest"
    50  	"k8s.io/klog/v2"
    51  )
    52  
    53  const defaultTimeout = time.Second * 600
    54  
    55  type Config struct {
    56  	namespace   string
    57  	kubeConfig  string
    58  	debug       bool
    59  	kubeContext string
    60  	logFn       action.DebugLog
    61  	fake        bool
    62  }
    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  
    79  	// for helm template
    80  	DryRun     *bool
    81  	OutputDir  string
    82  	IncludeCRD bool
    83  }
    84  
    85  // AddRepo adds a repo
    86  func AddRepo(r *repo.Entry) error {
    87  
    88  	settings := cli.New()
    89  	repoFile := settings.RepositoryConfig
    90  	b, err := os.ReadFile(repoFile)
    91  	if err != nil && !os.IsNotExist(err) {
    92  		return err
    93  	}
    94  
    95  	var f repo.File
    96  	if err = yaml.Unmarshal(b, &f); err != nil {
    97  		return err
    98  	}
    99  
   100  	// Check if the repo Name is legal
   101  	if strings.Contains(r.Name, "/") {
   102  		return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", r.Name)
   103  	}
   104  
   105  	if f.Has(r.Name) {
   106  		existing := f.Get(r.Name)
   107  		if *r != *existing && r.Name != KubeBlocksChartName {
   108  			// The input Name is different from the existing one, return an error
   109  			return errors.Errorf("repository name (%s) already exists, please specify a different name", r.Name)
   110  		}
   111  	}
   112  
   113  	cp, err := repo.NewChartRepository(r, getter.All(settings))
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	if _, err := cp.DownloadIndexFile(); err != nil {
   119  		return errors.Wrapf(err, "looks like %q is not a valid Chart repository or cannot be reached", r.URL)
   120  	}
   121  
   122  	f.Update(r)
   123  
   124  	if err = f.WriteFile(repoFile, 0644); err != nil {
   125  		return err
   126  	}
   127  	return nil
   128  }
   129  
   130  func statusDeployed(rl *release.Release) bool {
   131  	if rl == nil {
   132  		return false
   133  	}
   134  	return release.StatusDeployed == rl.Info.Status
   135  }
   136  
   137  // GetInstalled gets helm package release info if installed.
   138  func (i *InstallOpts) GetInstalled(cfg *action.Configuration) (*release.Release, error) {
   139  	var ErrReleaseNotDeployed = fmt.Errorf("release: not in deployed status")
   140  	res, err := action.NewGet(cfg).Run(i.Name)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	if res == nil {
   145  		return nil, driver.ErrReleaseNotFound
   146  	}
   147  	if !statusDeployed(res) {
   148  		return nil, errors.Wrapf(ErrReleaseNotDeployed, "current version not in right status, try to fix it first, \n"+
   149  			"uninstall and install kubeblocks could be a way to fix error")
   150  	}
   151  	return res, nil
   152  }
   153  
   154  // Install installs a Chart
   155  func (i *InstallOpts) Install(cfg *Config) (*release.Release, error) {
   156  	ctx := context.Background()
   157  	opts := retry.Options{
   158  		MaxRetry: 1 + i.TryTimes,
   159  	}
   160  
   161  	actionCfg, err := NewActionConfig(cfg)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	var rel *release.Release
   167  	if err = retry.IfNecessary(ctx, func() error {
   168  		release, err1 := i.tryInstall(actionCfg)
   169  		if err1 != nil {
   170  			return err1
   171  		}
   172  		rel = release
   173  		return nil
   174  	}, &opts); err != nil {
   175  		return nil, errors.Errorf("install chart %s error: %s", i.Name, err.Error())
   176  	}
   177  
   178  	return rel, nil
   179  }
   180  
   181  func ReleaseNotFound(err error) bool {
   182  	if err == nil {
   183  		return false
   184  	}
   185  	return errors.Is(err, driver.ErrReleaseNotFound) ||
   186  		strings.Contains(err.Error(), driver.ErrReleaseNotFound.Error())
   187  }
   188  
   189  func (i *InstallOpts) tryInstall(cfg *action.Configuration) (*release.Release, error) {
   190  	if i.DryRun == nil || !*i.DryRun {
   191  		released, err := i.GetInstalled(cfg)
   192  		if released != nil {
   193  			return released, nil
   194  		}
   195  		if err != nil && !ReleaseNotFound(err) {
   196  			return nil, err
   197  		}
   198  	}
   199  	settings := cli.New()
   200  
   201  	// TODO: Does not work now
   202  	// If a release does not exist, install it.
   203  	histClient := action.NewHistory(cfg)
   204  	histClient.Max = 1
   205  	if _, err := histClient.Run(i.Name); err != nil &&
   206  		!errors.Is(err, driver.ErrReleaseNotFound) {
   207  		return nil, err
   208  	}
   209  
   210  	client := action.NewInstall(cfg)
   211  	client.ReleaseName = i.Name
   212  	client.Namespace = i.Namespace
   213  	client.CreateNamespace = i.CreateNamespace
   214  	client.Wait = i.Wait
   215  	client.WaitForJobs = i.Wait
   216  	client.Timeout = i.Timeout
   217  	client.Version = i.Version
   218  	client.Atomic = i.Atomic
   219  
   220  	// for helm template
   221  	if i.DryRun != nil {
   222  		client.DryRun = *i.DryRun
   223  		client.OutputDir = i.OutputDir
   224  		client.IncludeCRDs = i.IncludeCRD
   225  		client.Replace = true
   226  		client.ClientOnly = true
   227  	}
   228  
   229  	if client.Timeout == 0 {
   230  		client.Timeout = defaultTimeout
   231  	}
   232  
   233  	cp, err := client.ChartPathOptions.LocateChart(i.Chart, settings)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	p := getter.All(settings)
   239  	vals, err := i.ValueOpts.MergeValues(p)
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	// Check Chart dependencies to make sure all are present in /charts
   245  	chartRequested, err := loader.Load(cp)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	// Create context and prepare the handle of SIGTERM
   251  	ctx := context.Background()
   252  	_, cancel := context.WithCancel(ctx)
   253  
   254  	// Set up channel through which to send signal notifications.
   255  	// We must use a buffered channel or risk missing the signal
   256  	// if we're not ready to receive when the signal is sent.
   257  	cSignal := make(chan os.Signal, 2)
   258  	signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
   259  	go func() {
   260  		<-cSignal
   261  		fmt.Println("Install has been cancelled")
   262  		cancel()
   263  	}()
   264  
   265  	released, err := client.RunWithContext(ctx, chartRequested, vals)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	return released, nil
   270  }
   271  
   272  // Uninstall uninstalls a Chart
   273  func (i *InstallOpts) Uninstall(cfg *Config) error {
   274  	ctx := context.Background()
   275  	opts := retry.Options{
   276  		MaxRetry: 1 + i.TryTimes,
   277  	}
   278  	if cfg.Namespace() == "" {
   279  		cfg.SetNamespace(i.Namespace)
   280  	}
   281  
   282  	actionCfg, err := NewActionConfig(cfg)
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	if err := retry.IfNecessary(ctx, func() error {
   288  		if err := i.tryUninstall(actionCfg); err != nil {
   289  			return err
   290  		}
   291  		return nil
   292  	}, &opts); err != nil {
   293  		return err
   294  	}
   295  	return nil
   296  }
   297  func (i *InstallOpts) tryUninstall(cfg *action.Configuration) error {
   298  	client := action.NewUninstall(cfg)
   299  	client.Wait = i.Wait
   300  	client.Timeout = defaultTimeout
   301  	client.DisableHooks = i.DisableHooks
   302  
   303  	// Create context and prepare the handle of SIGTERM
   304  	ctx := context.Background()
   305  	_, cancel := context.WithCancel(ctx)
   306  
   307  	// Set up channel through which to send signal notifications.
   308  	// We must use a buffered channel or risk missing the signal
   309  	// if we're not ready to receive when the signal is sent.
   310  	cSignal := make(chan os.Signal, 2)
   311  	signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
   312  	go func() {
   313  		<-cSignal
   314  		fmt.Println("Install has been cancelled")
   315  		cancel()
   316  	}()
   317  
   318  	if _, err := client.Run(i.Name); err != nil {
   319  		if i.ForceUninstall {
   320  			// Remove secrets left over when uninstalling kubeblocks, when addon CRD is uninstalled before kubeblocks.
   321  			secretCount, errRemove := i.RemoveRemainSecrets(cfg)
   322  			if secretCount == 0 {
   323  				return err
   324  			}
   325  			if errRemove != nil {
   326  				errMsg := fmt.Sprintf("failed to remove remain secrets, please remove them manually, %v", errRemove)
   327  				return errors.Wrap(err, errMsg)
   328  			}
   329  		} else {
   330  			return err
   331  		}
   332  	}
   333  	return nil
   334  }
   335  
   336  func (i *InstallOpts) RemoveRemainSecrets(cfg *action.Configuration) (int, error) {
   337  	clientSet, err := cfg.KubernetesClientSet()
   338  	if err != nil {
   339  		return -1, err
   340  	}
   341  
   342  	labelSelector := metav1.LabelSelector{
   343  		MatchExpressions: []metav1.LabelSelectorRequirement{
   344  			{
   345  				Key:      "name",
   346  				Operator: metav1.LabelSelectorOpIn,
   347  				Values:   []string{i.Name},
   348  			},
   349  			{
   350  				Key:      "owner",
   351  				Operator: metav1.LabelSelectorOpIn,
   352  				Values:   []string{"helm"},
   353  			},
   354  			{
   355  				Key:      "status",
   356  				Operator: metav1.LabelSelectorOpIn,
   357  				Values:   []string{"uninstalling", "superseded"},
   358  			},
   359  		},
   360  	}
   361  
   362  	selector, err := metav1.LabelSelectorAsSelector(&labelSelector)
   363  	if err != nil {
   364  		fmt.Printf("Failed to build label selector: %v\n", err)
   365  		return -1, err
   366  	}
   367  	options := metav1.ListOptions{
   368  		LabelSelector: selector.String(),
   369  	}
   370  
   371  	secrets, err := clientSet.CoreV1().Secrets(i.Namespace).List(context.TODO(), options)
   372  	if err != nil {
   373  		return -1, err
   374  	}
   375  	secretCount := len(secrets.Items)
   376  	if secretCount == 0 {
   377  		return 0, nil
   378  	}
   379  
   380  	for _, secret := range secrets.Items {
   381  		err := clientSet.CoreV1().Secrets(i.Namespace).Delete(context.TODO(), secret.Name, metav1.DeleteOptions{})
   382  		if err != nil {
   383  			klog.V(1).Info(err)
   384  			return -1, fmt.Errorf("failed to delete Secret %s: %v", secret.Name, err)
   385  		}
   386  	}
   387  	return secretCount, nil
   388  }
   389  
   390  func fakeActionConfig() *action.Configuration {
   391  	registryClient, err := registry.NewClient()
   392  	if err != nil {
   393  		return nil
   394  	}
   395  
   396  	res := &action.Configuration{
   397  		Releases:       storage.Init(driver.NewMemory()),
   398  		KubeClient:     &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}},
   399  		Capabilities:   chartutil.DefaultCapabilities,
   400  		RegistryClient: registryClient,
   401  		Log:            func(format string, v ...interface{}) {},
   402  	}
   403  	// to template the kubeblocks manifest, dry-run install will check and valida the KubeVersion in Capabilities is bigger than
   404  	// the KubeVersion in Chart.yaml.
   405  	// in helm v3.11.1 the DefaultCapabilities KubeVersion is 1.20 which lower than the kubeblocks Chart claimed '>=1.22.0-0'
   406  	res.Capabilities.KubeVersion.Version = "v99.99.0"
   407  	return res
   408  }
   409  
   410  func NewActionConfig(cfg *Config) (*action.Configuration, error) {
   411  	if cfg.fake {
   412  		return fakeActionConfig(), nil
   413  	}
   414  
   415  	var err error
   416  	settings := cli.New()
   417  	actionCfg := new(action.Configuration)
   418  	settings.SetNamespace(cfg.namespace)
   419  	settings.KubeConfig = cfg.kubeConfig
   420  	if cfg.kubeContext != "" {
   421  		settings.KubeContext = cfg.kubeContext
   422  	}
   423  	settings.Debug = cfg.debug
   424  
   425  	if actionCfg.RegistryClient, err = registry.NewClient(
   426  		registry.ClientOptDebug(settings.Debug),
   427  		registry.ClientOptEnableCache(true),
   428  		registry.ClientOptWriter(io.Discard),
   429  		registry.ClientOptCredentialsFile(settings.RegistryConfig),
   430  	); err != nil {
   431  		return nil, err
   432  	}
   433  
   434  	// do not output warnings
   435  	getter := settings.RESTClientGetter()
   436  	getter.(*genericclioptions.ConfigFlags).WrapConfigFn = func(c *rest.Config) *rest.Config {
   437  		c.WarningHandler = rest.NoWarnings{}
   438  		return c
   439  	}
   440  
   441  	if err = actionCfg.Init(settings.RESTClientGetter(),
   442  		settings.Namespace(),
   443  		os.Getenv("HELM_DRIVER"),
   444  		cfg.logFn); err != nil {
   445  		return nil, err
   446  	}
   447  	return actionCfg, nil
   448  }
   449  
   450  func NewConfig(namespace string, kubeConfig string, ctx string, debug bool) *Config {
   451  	cfg := &Config{
   452  		namespace:   namespace,
   453  		debug:       debug,
   454  		kubeConfig:  kubeConfig,
   455  		kubeContext: ctx,
   456  	}
   457  
   458  	if debug {
   459  		cfg.logFn = GetVerboseLog()
   460  	} else {
   461  		cfg.logFn = GetQuiteLog()
   462  	}
   463  	return cfg
   464  }
   465  func (o *Config) SetNamespace(namespace string) {
   466  	o.namespace = namespace
   467  }
   468  
   469  func (o *Config) Namespace() string {
   470  	return o.namespace
   471  }
   472  
   473  func GetQuiteLog() action.DebugLog {
   474  	return func(format string, v ...interface{}) {}
   475  }
   476  
   477  func GetVerboseLog() action.DebugLog {
   478  	return func(format string, v ...interface{}) {
   479  		klog.Infof(format+"\n", v...)
   480  	}
   481  }