github.com/metaprov/modela-operator@v0.0.0-20240118193048-f378be8b74d2/pkg/helm/helm_chart.go (about)

     1  /*
     2   * Copyright (c) 2021.
     3   *
     4   * Metaprov.com
     5   */
     6  
     7  package helm
     8  
     9  import (
    10  	"bufio"
    11  	"bytes"
    12  	"context"
    13  	"fmt"
    14  	"github.com/metaprov/modela-operator/pkg/kube"
    15  	"k8s.io/cli-runtime/pkg/genericclioptions"
    16  	"regexp"
    17  	ctrl "sigs.k8s.io/controller-runtime"
    18  	"sigs.k8s.io/kustomize/kyaml/kio"
    19  
    20  	"k8s.io/klog/v2"
    21  	"sigs.k8s.io/controller-runtime/pkg/log"
    22  
    23  	"github.com/pkg/errors"
    24  	helmaction "helm.sh/helm/v3/pkg/action"
    25  	helmchart "helm.sh/helm/v3/pkg/chart"
    26  	helmloader "helm.sh/helm/v3/pkg/chart/loader"
    27  	helmcli "helm.sh/helm/v3/pkg/cli"
    28  	helmrelease "helm.sh/helm/v3/pkg/release"
    29  )
    30  
    31  var settings = helmcli.New()
    32  
    33  type LabelPostRenderer struct {
    34  	Labels map[string]string
    35  }
    36  
    37  func (lb LabelPostRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
    38  	var output bytes.Buffer
    39  	var writer = bufio.NewWriter(&output)
    40  	rw := &kio.ByteReadWriter{
    41  		Reader:                bytes.NewReader(renderedManifests.Bytes()),
    42  		Writer:                writer,
    43  		OmitReaderAnnotations: true,
    44  		KeepReaderAnnotations: true,
    45  	}
    46  	p := kio.Pipeline{
    47  		Inputs:  []kio.Reader{rw},
    48  		Filters: []kio.Filter{kube.LabelFilter{Labels: lb.Labels}},
    49  		Outputs: []kio.Writer{rw},
    50  	}
    51  
    52  	if err := p.Execute(); err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	_ = writer.Flush()
    57  	return bytes.NewBuffer(output.Bytes()), nil
    58  }
    59  
    60  type HelmChart struct {
    61  	Name            string // chart name
    62  	Namespace       string // chart namespace
    63  	ReleaseName     string // release name
    64  	ChartVersion    string // chart version
    65  	DryRun          bool
    66  	CreateNamespace bool
    67  	Values          map[string]interface{}
    68  	chart           *helmchart.Chart
    69  }
    70  
    71  func NewHelmChart(name, namespace, releaseName string, dryRun bool) *HelmChart {
    72  	return &HelmChart{
    73  		Name:            name,
    74  		Namespace:       namespace,
    75  		ReleaseName:     releaseName,
    76  		DryRun:          dryRun,
    77  		CreateNamespace: false,
    78  		Values:          make(map[string]interface{}),
    79  	}
    80  }
    81  
    82  func (chart *HelmChart) GetConfig() (*helmaction.Configuration, error) {
    83  	var kubeConfig *genericclioptions.ConfigFlags
    84  	config := ctrl.GetConfigOrDie()
    85  	kubeConfig = genericclioptions.NewConfigFlags(false)
    86  	kubeConfig.APIServer = &config.Host
    87  	kubeConfig.BearerToken = &config.BearerToken
    88  	kubeConfig.CAFile = &config.CAFile
    89  	kubeConfig.Namespace = &chart.Namespace
    90  
    91  	actionConfig := new(helmaction.Configuration)
    92  	if err := actionConfig.Init(kubeConfig, chart.Namespace, "secret", klog.Infof); err != nil {
    93  		klog.Error(err, "Unable to initialize Helm")
    94  		return nil, err
    95  	}
    96  
    97  	return actionConfig, nil
    98  }
    99  
   100  // Load the chart, and assign it to the chart field
   101  func (chart *HelmChart) Load(ctx context.Context) error {
   102  	logger := log.FromContext(ctx)
   103  	config, err := chart.GetConfig()
   104  	if err != nil {
   105  		logger.Error(err, "Failed to get config")
   106  		return err
   107  	}
   108  
   109  	client := helmaction.NewInstall(config)
   110  	client.Namespace = chart.Namespace
   111  	client.ReleaseName = chart.ReleaseName
   112  	name := "assets/charts/" + chart.Name
   113  
   114  	result, err := helmloader.Load(name)
   115  	if err != nil {
   116  		logger.Error(err, "Failed to load Helm Chart")
   117  		return errors.Wrapf(err, "Failed to load resources from %s", name)
   118  	}
   119  	chart.chart = result
   120  	return nil
   121  }
   122  
   123  func (chart *HelmChart) Version() string {
   124  	chartPackageSplit := chart.parsePackageName()
   125  	chartVersion := chartPackageSplit[1]
   126  	if chartPackageSplit[2] != "" {
   127  		chartVersion = fmt.Sprintf("%s-%s", chartVersion, chartPackageSplit[2])
   128  	}
   129  	return chartVersion
   130  }
   131  
   132  func (chart *HelmChart) parsePackageName() []string {
   133  	packageNameRegexp := regexp.MustCompile(`([a-z\-]+)-([0-9\.]*[0-9]+)(-([0-9]+))?`)
   134  	packageSubstringSubmatch := packageNameRegexp.FindStringSubmatch(chart.Name)
   135  	parsedOutput := []string{"", "", ""}
   136  	if len(packageSubstringSubmatch) > 2 {
   137  		parsedOutput[0] = packageSubstringSubmatch[1]
   138  		parsedOutput[1] = packageSubstringSubmatch[2]
   139  	}
   140  	if len(packageSubstringSubmatch) > 4 {
   141  		parsedOutput[2] = packageSubstringSubmatch[4]
   142  	}
   143  
   144  	return parsedOutput
   145  }
   146  
   147  func (chart *HelmChart) CanInstall(ctx context.Context) (bool, error) {
   148  	err := chart.Load(ctx)
   149  	if err != nil {
   150  		return false, err
   151  	}
   152  	switch chart.chart.Metadata.Type {
   153  	case "", "application":
   154  		return true, err
   155  	}
   156  	return false, err
   157  }
   158  
   159  func (chart *HelmChart) Get(ctx context.Context) (*helmrelease.Release, error) {
   160  	logger := log.FromContext(ctx)
   161  
   162  	config, err := chart.GetConfig()
   163  	if err != nil {
   164  		logger.Error(err, "failed to get config")
   165  		return nil, err
   166  	}
   167  	// Check if the Release Exists
   168  	aList := helmaction.NewList(config) // NewGet provides bad error message if release doesn't exist
   169  	aList.All = true
   170  	charts, err := aList.Run()
   171  	if err != nil {
   172  		logger.Error(err, "failed to get config")
   173  		return nil, errors.Wrap(err, "failed to run get")
   174  	}
   175  	for _, release := range charts {
   176  		if release.Name == chart.ReleaseName && release.Namespace == chart.Namespace {
   177  			return release, nil
   178  		}
   179  	}
   180  	return nil, errors.Errorf("unable to find release '%s' in namespace '%s'", chart.ReleaseName, chart.Namespace)
   181  }
   182  
   183  // check if the chart is already installed
   184  func (chart *HelmChart) IsInstalled(ctx context.Context) (bool, error) {
   185  	logger := log.FromContext(ctx)
   186  	err := chart.Load(ctx)
   187  	if err != nil {
   188  		logger.Error(err, "failed to load chart")
   189  		return false, errors.Wrapf(err, "failed to load chart")
   190  	}
   191  	existingRelease, _ := chart.Get(ctx)
   192  	/*if err != nil {
   193  		logger.Error(err, "failed to get chart")
   194  		return false, err
   195  	}*/
   196  	if existingRelease != nil {
   197  		return true, nil
   198  	}
   199  	return false, nil
   200  }
   201  
   202  func (chart *HelmChart) GetStatus(ctx context.Context) (helmrelease.Status, error) {
   203  	err := chart.Load(ctx)
   204  	if err != nil {
   205  		return helmrelease.StatusUnknown, errors.Wrapf(err, "failed to load chart")
   206  	}
   207  	existingRelease, err := chart.Get(ctx)
   208  	if err != nil {
   209  		return helmrelease.StatusUnknown, errors.Wrapf(err, "chart does not exist")
   210  	}
   211  
   212  	return existingRelease.Info.Status, nil
   213  
   214  }
   215  
   216  func (chart *HelmChart) Install(ctx context.Context) error {
   217  	logger := log.FromContext(ctx)
   218  	logger.Info("Installing Helm Chart", "release", chart.ReleaseName, "namespace", chart.Namespace, "name", chart.Name)
   219  	err := chart.Load(ctx)
   220  	if err != nil {
   221  		logger.Error(err, "Failed to load chart")
   222  		return errors.Wrapf(err, "Failed to load chart")
   223  	}
   224  	// Check if resource already exists
   225  	existingRelease, _ := chart.Get(ctx)
   226  	if existingRelease != nil {
   227  		logger.Error(err, fmt.Sprintf("Release \"%s\" already exists in namespace \"%s\"", existingRelease.Name, existingRelease.Namespace))
   228  		return errors.Wrapf(err, "Release '%s' already exists in namespace '%s'", existingRelease.Name, existingRelease.Namespace)
   229  	}
   230  
   231  	can, err := chart.CanInstall(ctx)
   232  	if err != nil {
   233  		logger.Error(err, "Failed to check if Helm Chart is installed", "namespace", existingRelease.Namespace)
   234  		return errors.Wrapf(err, "Failed to check if Helm Chart is installed (namespace=%s)", existingRelease.Namespace)
   235  	}
   236  	if !can {
   237  		return errors.Wrapf(err, "release at '%s' is not installable", chart.Name)
   238  	}
   239  
   240  	config, err := chart.GetConfig()
   241  	if err != nil {
   242  		logger.Error(err, "failed to get config")
   243  		return errors.Wrap(err, "failed to get config")
   244  	}
   245  
   246  	inst := helmaction.NewInstall(config)
   247  	if inst.Version == "" && inst.Devel {
   248  		inst.Version = ">0.0.0-0"
   249  	}
   250  	inst.ReleaseName = chart.ReleaseName
   251  	inst.Namespace = chart.Namespace
   252  	inst.DryRun = chart.DryRun
   253  	inst.CreateNamespace = chart.CreateNamespace
   254  	inst.Version = chart.ChartVersion
   255  	inst.PostRenderer = LabelPostRenderer{map[string]string{"app.kubernetes.io/created-by": "modela-operator"}}
   256  	inst.Replace = true
   257  	inst.ClientOnly = false
   258  
   259  	_, err = inst.Run(chart.chart, chart.Values)
   260  	if err != nil {
   261  		logger.Error(err, "failed to install")
   262  		return fmt.Errorf("failed to run install due to %s", err)
   263  	}
   264  	return nil
   265  
   266  }
   267  
   268  func (chart *HelmChart) Upgrade(ctx context.Context) error {
   269  	logger := log.FromContext(ctx)
   270  
   271  	logger.Info("Enter upgrade")
   272  
   273  	err := chart.Load(ctx)
   274  	if err != nil {
   275  		logger.Error(err, "failed to load chart")
   276  		return errors.Wrapf(err, "failed to load chart")
   277  	}
   278  	// Check if resource already exists
   279  	existingRelease, err := chart.Get(ctx)
   280  	if existingRelease != nil {
   281  		return errors.Wrapf(err, "release '%s' already exists in namespace '%s'", existingRelease.Name, existingRelease.Namespace)
   282  	}
   283  
   284  	can, err := chart.CanInstall(ctx)
   285  	if err != nil {
   286  		return errors.Wrapf(err, "failed to check if chart is installed '%s'", existingRelease.Namespace)
   287  	}
   288  	if !can {
   289  		return errors.Wrapf(err, "release at '%s' is not installable", chart.Name)
   290  	}
   291  
   292  	config, err := chart.GetConfig()
   293  	if err != nil {
   294  		return errors.Wrap(err, "failed to get config")
   295  	}
   296  
   297  	isInstalled, err := chart.IsInstalled(ctx)
   298  	if err != nil {
   299  		return fmt.Errorf("failed to get installed state %s", err)
   300  	}
   301  
   302  	if !isInstalled {
   303  		inst := helmaction.NewInstall(config)
   304  		if inst.Version == "" && inst.Devel {
   305  			inst.Version = ">0.0.0-0"
   306  		}
   307  		inst.ReleaseName = chart.ReleaseName
   308  		inst.Namespace = chart.Namespace
   309  		inst.DryRun = chart.DryRun
   310  		inst.CreateNamespace = chart.CreateNamespace
   311  		inst.Version = chart.ChartVersion
   312  
   313  		_, err = inst.Run(chart.chart, chart.Values)
   314  		if err != nil {
   315  			return fmt.Errorf("failed to run install due to %s", err)
   316  		}
   317  		return nil
   318  	} else {
   319  		inst := helmaction.NewUpgrade(config)
   320  		if inst.Version == "" && inst.Devel {
   321  			inst.Version = ">0.0.0-0"
   322  		}
   323  		inst.DryRun = chart.DryRun
   324  		inst.Version = chart.ChartVersion
   325  
   326  		_, err = inst.Run(chart.ReleaseName, chart.chart, chart.Values)
   327  		if err != nil {
   328  			return fmt.Errorf("failed to run install due to %s", err)
   329  		}
   330  		return nil
   331  	}
   332  
   333  }
   334  
   335  func (chart *HelmChart) Uninstall(ctx context.Context) error {
   336  	err := chart.Load(ctx)
   337  	if err != nil {
   338  		return errors.Wrapf(err, "failed to load chart")
   339  	}
   340  	// Check if resource already exists
   341  	existingRelease, _ := chart.Get(ctx)
   342  	if existingRelease == nil {
   343  		return nil
   344  	}
   345  
   346  	config, err := chart.GetConfig()
   347  	if err != nil {
   348  		return errors.Wrap(err, "failed to get config")
   349  	}
   350  
   351  	inst := helmaction.NewUninstall(config)
   352  	inst.DryRun = chart.DryRun
   353  
   354  	_, err = inst.Run(chart.ReleaseName)
   355  	if err != nil {
   356  		return fmt.Errorf("failed to run uninstall due to %s", err)
   357  	}
   358  	return nil
   359  }
   360  
   361  func InstallChart(ctx context.Context, name, ns, releaseName string, values map[string]interface{}) error {
   362  	chart := NewHelmChart(name, ns, releaseName, false)
   363  	chart.ReleaseName = releaseName
   364  	chart.Namespace = ns
   365  	chart.Values = values
   366  
   367  	canInstall, err := chart.CanInstall(ctx)
   368  	if err != nil {
   369  		return errors.Errorf("Failed to check if chart is installed ,err: %s", err)
   370  	}
   371  	if canInstall {
   372  		err = chart.Install(ctx)
   373  		if err != nil {
   374  			return errors.Wrapf(err, "Error installing chart %s", name)
   375  		}
   376  	}
   377  	return nil
   378  }
   379  
   380  func UninstallChart(ctx context.Context, name, ns, releaseName string, values map[string]interface{}) error {
   381  	chart := NewHelmChart(name, ns, releaseName, false)
   382  	chart.ReleaseName = releaseName
   383  	chart.Namespace = ns
   384  	chart.Values = values
   385  
   386  	if installed, err := chart.IsInstalled(ctx); err != nil {
   387  		return err
   388  	} else if !installed {
   389  		return nil
   390  	}
   391  
   392  	if err := chart.Uninstall(ctx); err != nil {
   393  		return errors.Wrapf(err, "Error uninstalling chart %s", name)
   394  	}
   395  
   396  	return nil
   397  }
   398  
   399  func IsChartInstalled(ctx context.Context, name, ns, releaseName string) (bool, error) {
   400  	// TODO(liam): Refactor these methods into the helm chart struct
   401  	chart := NewHelmChart(name, ns, releaseName, false)
   402  
   403  	chartStatus, _ := chart.GetStatus(ctx)
   404  	if chartStatus == helmrelease.StatusUnknown {
   405  		return false, nil
   406  	}
   407  	if chartStatus != helmrelease.StatusDeployed {
   408  		return false, nil
   409  	}
   410  	return true, nil
   411  }