github.com/verrazzano/verrazzano@v1.7.0/pkg/helm/helmcli.go (about)

     1  // Copyright (c) 2020, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package helm
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/url"
    10  	"os"
    11  	"regexp"
    12  	"strings"
    13  
    14  	yaml2 "github.com/verrazzano/verrazzano/pkg/yaml"
    15  	"helm.sh/helm/v3/pkg/action"
    16  	"helm.sh/helm/v3/pkg/chart"
    17  	"helm.sh/helm/v3/pkg/chart/loader"
    18  	"helm.sh/helm/v3/pkg/cli"
    19  	"helm.sh/helm/v3/pkg/getter"
    20  	"helm.sh/helm/v3/pkg/release"
    21  	"helm.sh/helm/v3/pkg/strvals"
    22  	"sigs.k8s.io/yaml"
    23  
    24  	"github.com/verrazzano/verrazzano/pkg/log/vzlog"
    25  	"go.uber.org/zap"
    26  )
    27  
    28  // Debug is set from a platform-operator arg and sets the helm --debug flag
    29  var Debug bool
    30  
    31  // Helm chart status values: unknown, deployed, uninstalled, superseded, failed, uninstalling, pending-install, pending-upgrade or pending-rollback
    32  const ChartNotFound = "NotFound"
    33  const ChartStatusDeployed = "deployed"
    34  const ChartStatusPendingInstall = "pending-install"
    35  const ChartStatusFailed = "failed"
    36  
    37  // ChartStatusFnType - Package-level var and functions to allow overriding GetChartStatus for unit test purposes
    38  type ChartStatusFnType func(releaseName string, namespace string) (string, error)
    39  
    40  // HelmOverrides contains all of the overrides that gets passed to the helm cli runner
    41  type HelmOverrides struct {
    42  	SetOverrides       string // for --set
    43  	SetStringOverrides string // for --set-string
    44  	SetFileOverrides   string // for --set-file
    45  	FileOverride       string // for -f
    46  }
    47  
    48  type ActionConfigFnType func(log vzlog.VerrazzanoLogger, settings *cli.EnvSettings, namespace string) (*action.Configuration, error)
    49  
    50  var actionConfigFn ActionConfigFnType = getActionConfig
    51  
    52  func SetActionConfigFunction(f ActionConfigFnType) {
    53  	actionConfigFn = f
    54  }
    55  
    56  // SetDefaultActionConfigFunction Resets the action config function
    57  func SetDefaultActionConfigFunction() {
    58  	actionConfigFn = getActionConfig
    59  }
    60  
    61  type LoadChartFnType func(chartDir string) (*chart.Chart, error)
    62  
    63  var loadChartFn LoadChartFnType = loadChart
    64  
    65  func SetLoadChartFunction(f LoadChartFnType) {
    66  	loadChartFn = f
    67  }
    68  
    69  func SetDefaultLoadChartFunction() {
    70  	loadChartFn = loadChart
    71  }
    72  
    73  // GetValuesMap will run 'helm get values' command and return the output from the command.
    74  func GetValuesMap(log vzlog.VerrazzanoLogger, releaseName string, namespace string) (map[string]interface{}, error) {
    75  	settings := cli.New()
    76  	settings.SetNamespace(namespace)
    77  	actionConfig, err := actionConfigFn(log, settings, namespace)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	client := action.NewGetValues(actionConfig)
    83  	vals, err := client.Run(releaseName)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	return vals, nil
    89  }
    90  
    91  // GetValues will run 'helm get values' command and return the output from the command.
    92  func GetValues(log vzlog.VerrazzanoLogger, releaseName string, namespace string) ([]byte, error) {
    93  	vals, err := GetValuesMap(log, releaseName, namespace)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  
    98  	yamlValues, err := yaml.Marshal(vals)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	return yamlValues, nil
   103  }
   104  
   105  // Upgrade will upgrade a Helm helmRelease with the specified charts.  The override files array
   106  // are in order with the first files in the array have lower precedence than latter files.
   107  func Upgrade(log vzlog.VerrazzanoLogger, releaseName string, namespace string, chartDir string, wait bool, dryRun bool, overrides []HelmOverrides) (*release.Release, error) {
   108  	settings := cli.New()
   109  	settings.SetNamespace(namespace)
   110  	actionConfig, err := actionConfigFn(log, settings, namespace)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	p := getter.All(settings)
   116  	vals, err := mergeValues(overrides, p)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	// load chart from the path
   121  	chart, err := loadChartFn(chartDir)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	installed, err := IsReleaseInstalled(releaseName, namespace)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	var rel *release.Release
   131  	if installed {
   132  		// upgrade it
   133  		log.Progressf("Starting Helm upgrade of release %s in namespace %s with overrides: %v", releaseName, namespace, overrides)
   134  		client := action.NewUpgrade(actionConfig)
   135  		client.Namespace = namespace
   136  		client.DryRun = dryRun
   137  		client.Wait = wait
   138  		client.MaxHistory = 1
   139  
   140  		rel, err = client.Run(releaseName, chart, vals)
   141  		if err != nil {
   142  			return nil, log.ErrorfThrottledNewErr("Failed running Helm command for release %s, error: %s",
   143  				releaseName, err.Error())
   144  		}
   145  	} else {
   146  		log.Progressf("Starting Helm installation of release %s in namespace %s with overrides: %v", releaseName, namespace, overrides)
   147  		client := action.NewInstall(actionConfig)
   148  		client.Namespace = namespace
   149  		client.ReleaseName = releaseName
   150  		client.DryRun = dryRun
   151  		client.Replace = true
   152  		client.Wait = wait
   153  
   154  		rel, err = client.Run(chart, vals)
   155  		if err != nil {
   156  			log.ErrorfThrottled("Failed running Helm command for release %s: %v",
   157  				releaseName, err.Error())
   158  			return nil, err
   159  		}
   160  	}
   161  
   162  	log.Progressf("Helm upgraded/installed %s in namespace %s", rel.Name, rel.Namespace)
   163  
   164  	return rel, nil
   165  }
   166  
   167  // Uninstall will uninstall the helmRelease in the specified namespace  using helm uninstall
   168  func Uninstall(log vzlog.VerrazzanoLogger, releaseName string, namespace string, dryRun bool) (err error) {
   169  	settings := cli.New()
   170  	settings.SetNamespace(namespace)
   171  	actionConfig, err := actionConfigFn(log, settings, namespace)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	client := action.NewUninstall(actionConfig)
   177  	client.DryRun = dryRun
   178  
   179  	_, err = client.Run(releaseName)
   180  	if err != nil {
   181  		log.Errorf("Error uninstalling release %s: %s", releaseName, err.Error())
   182  		return err
   183  	}
   184  
   185  	return nil
   186  }
   187  
   188  // maskSensitiveData replaces sensitive data in a string with mask characters.
   189  func maskSensitiveData(str string) string {
   190  	const maskString = "*****"
   191  	re := regexp.MustCompile(`[Pp]assword=(.+?)(?:,|\z)`)
   192  
   193  	matches := re.FindAllStringSubmatch(str, -1)
   194  	for _, match := range matches {
   195  		if len(match) == 2 {
   196  			str = strings.Replace(str, match[1], maskString, 1)
   197  		}
   198  	}
   199  
   200  	return str
   201  }
   202  
   203  // IsReleaseFailed Returns true if the chart helmRelease state is marked 'failed'
   204  func IsReleaseFailed(releaseName string, namespace string) (bool, error) {
   205  	log := zap.S()
   206  	releaseStatus, err := getReleaseState(releaseName, namespace)
   207  	if err != nil {
   208  		log.Errorf("Getting status for chart %s/%s failed", namespace, releaseName)
   209  		return false, err
   210  	}
   211  	return releaseStatus == ChartStatusFailed, nil
   212  }
   213  
   214  // IsReleaseDeployed returns true if the helmRelease is deployed
   215  func IsReleaseDeployed(releaseName string, namespace string) (found bool, err error) {
   216  	log := zap.S()
   217  	releaseStatus, err := getChartStatus(releaseName, namespace)
   218  	if err != nil {
   219  		log.Errorf("Getting status for chart %s/%s failed with error: %v\n", namespace, releaseName, err)
   220  		return false, err
   221  	}
   222  	switch releaseStatus {
   223  	case ChartNotFound:
   224  		log.Debugf("releasename=%s/%s; status= %s", namespace, releaseName, releaseStatus)
   225  	case ChartStatusDeployed:
   226  		return true, nil
   227  	}
   228  	return false, nil
   229  }
   230  
   231  // GetReleaseStatus returns the helmRelease status
   232  func GetReleaseStatus(log vzlog.VerrazzanoLogger, releaseName string, namespace string) (status string, err error) {
   233  	releaseStatus, err := getChartStatus(releaseName, namespace)
   234  	if err != nil {
   235  		log.ErrorfNewErr("Failed getting status for chart %s/%s with stderr: %v\n", namespace, releaseName, err)
   236  		return "", err
   237  	}
   238  	if releaseStatus == ChartNotFound {
   239  		log.Debugf("Chart %s/%s not found", namespace, releaseName)
   240  	}
   241  	return releaseStatus, nil
   242  }
   243  
   244  // IsReleaseInstalled returns true if the helmRelease is installed
   245  func IsReleaseInstalled(releaseName string, namespace string) (found bool, err error) {
   246  	settings := cli.New()
   247  	settings.SetNamespace(namespace)
   248  	actionConfig, err := actionConfigFn(vzlog.DefaultLogger(), settings, namespace)
   249  	if err != nil {
   250  		return false, err
   251  	}
   252  
   253  	client := action.NewStatus(actionConfig)
   254  	helmRelease, err := client.Run(releaseName)
   255  	if err != nil {
   256  		if strings.Contains(err.Error(), "not found") {
   257  			return false, nil
   258  		}
   259  		return false, err
   260  	}
   261  	return release.StatusDeployed == helmRelease.Info.Status || release.StatusFailed == helmRelease.Info.Status, nil
   262  }
   263  
   264  // ReleaseExists returns true if the helm Release exists in the cluster in any state
   265  func ReleaseExists(releaseName string, namespace string) (found bool, err error) {
   266  	settings := cli.New()
   267  	settings.SetNamespace(namespace)
   268  	actionConfig, err := actionConfigFn(vzlog.DefaultLogger(), settings, namespace)
   269  	if err != nil {
   270  		return false, err
   271  	}
   272  
   273  	client := action.NewStatus(actionConfig)
   274  	helmRelease, err := client.Run(releaseName)
   275  	if err != nil {
   276  		if strings.Contains(err.Error(), "not found") {
   277  			return false, nil
   278  		}
   279  		return false, err
   280  	}
   281  	return len(helmRelease.Info.Status) > 0, nil
   282  }
   283  
   284  // getChartStatus extracts the Helm deployment status of the specified chart from the JSON output as a string
   285  func getChartStatus(releaseName string, namespace string) (string, error) {
   286  	settings := cli.New()
   287  	settings.SetNamespace(namespace)
   288  	actionConfig, err := actionConfigFn(vzlog.DefaultLogger(), settings, namespace)
   289  	if err != nil {
   290  		return "", err
   291  	}
   292  
   293  	client := action.NewStatus(actionConfig)
   294  	helmRelease, err := client.Run(releaseName)
   295  	if err != nil {
   296  		if strings.Contains(err.Error(), "not found") {
   297  			return ChartNotFound, nil
   298  		}
   299  		return "", err
   300  	}
   301  
   302  	return helmRelease.Info.Status.String(), nil
   303  }
   304  
   305  // getReleaseState extracts the helmRelease state from an "ls -o json" command for a specific helmRelease/namespace
   306  func getReleaseState(releaseName string, namespace string) (string, error) {
   307  	releases, err := getReleases(namespace)
   308  	if err != nil {
   309  		if strings.Contains(err.Error(), "not found") {
   310  			return ChartNotFound, nil
   311  		}
   312  		return "", err
   313  	}
   314  
   315  	status := ""
   316  	for _, info := range releases {
   317  		release := info.Name
   318  		if release == releaseName {
   319  			status = info.Info.Status.String()
   320  			break
   321  		}
   322  	}
   323  	return strings.TrimSpace(status), nil
   324  }
   325  
   326  // GetReleaseAppVersion - public function to execute releaseAppVersionFn
   327  func GetReleaseAppVersion(releaseName string, namespace string) (string, error) {
   328  	return getReleaseAppVersion(releaseName, namespace)
   329  }
   330  
   331  // GetReleaseStringValues - Returns a subset of Helm helmRelease values as a map of strings
   332  func GetReleaseStringValues(log vzlog.VerrazzanoLogger, valueKeys []string, releaseName string, namespace string) (map[string]string, error) {
   333  	values, err := GetReleaseValues(log, valueKeys, releaseName, namespace)
   334  	if err != nil {
   335  		return map[string]string{}, err
   336  	}
   337  	returnVals := map[string]string{}
   338  	for key, val := range values {
   339  		returnVals[key] = fmt.Sprintf("%v", val)
   340  	}
   341  	return returnVals, err
   342  }
   343  
   344  // GetReleaseValues - Returns a subset of Helm helmRelease values as a map of objects
   345  func GetReleaseValues(log vzlog.VerrazzanoLogger, valueKeys []string, releaseName string, namespace string) (map[string]interface{}, error) {
   346  	isDeployed, err := IsReleaseDeployed(releaseName, namespace)
   347  	if err != nil {
   348  		return map[string]interface{}{}, err
   349  	}
   350  	var values = map[string]interface{}{}
   351  	if isDeployed {
   352  		valuesMap, err := GetValuesMap(log, releaseName, namespace)
   353  		if err != nil {
   354  			return map[string]interface{}{}, err
   355  		}
   356  		for _, valueKey := range valueKeys {
   357  			if mapVal, ok := valuesMap[valueKey]; ok {
   358  				log.Debugf("Found value for %s: %v", valueKey, mapVal)
   359  				values[valueKey] = mapVal
   360  			}
   361  		}
   362  	}
   363  	return values, nil
   364  }
   365  
   366  // getReleaseAppVersion extracts the helmRelease app_version from a "ls -o json" command for a specific helmRelease/namespace
   367  func getReleaseAppVersion(releaseName string, namespace string) (string, error) {
   368  	releases, err := getReleases(namespace)
   369  	if err != nil {
   370  		if err.Error() == ChartNotFound {
   371  			return ChartNotFound, nil
   372  		}
   373  		return "", err
   374  	}
   375  
   376  	var status string
   377  	for _, info := range releases {
   378  		release := info.Name
   379  		if release == releaseName {
   380  			status = info.Chart.AppVersion()
   381  			break
   382  		}
   383  	}
   384  	return strings.TrimSpace(status), nil
   385  }
   386  
   387  func getReleases(namespace string) ([]*release.Release, error) {
   388  	settings := cli.New()
   389  	settings.SetNamespace(namespace)
   390  	actionConfig, err := actionConfigFn(vzlog.DefaultLogger(), settings, namespace)
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  
   395  	client := action.NewList(actionConfig)
   396  	client.AllNamespaces = false
   397  	client.All = true
   398  	client.StateMask = action.ListAll
   399  
   400  	releases, err := client.Run()
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  
   405  	return releases, nil
   406  }
   407  
   408  func getActionConfig(log vzlog.VerrazzanoLogger, settings *cli.EnvSettings, namespace string) (*action.Configuration, error) {
   409  	actionConfig := new(action.Configuration)
   410  	if err := actionConfig.Init(settings.RESTClientGetter(), namespace, os.Getenv("HELM_DRIVER"), log.Debugf); err != nil {
   411  		return nil, err
   412  	}
   413  	actionConfig.Releases.MaxHistory = 1
   414  	return actionConfig, nil
   415  }
   416  
   417  func loadChart(chartDir string) (*chart.Chart, error) {
   418  	return loader.Load(chartDir)
   419  }
   420  
   421  // readFile load a file using a URI scheme provider
   422  func readFile(filePath string, p getter.Providers) ([]byte, error) {
   423  	if strings.TrimSpace(filePath) == "-" {
   424  		return io.ReadAll(os.Stdin)
   425  	}
   426  	u, err := url.Parse(filePath)
   427  	if err != nil {
   428  		return nil, err
   429  	}
   430  
   431  	g, err := p.ByScheme(u.Scheme)
   432  	if err != nil {
   433  		return os.ReadFile(filePath)
   434  	}
   435  	data, err := g.Get(filePath, getter.WithURL(filePath))
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  	return data.Bytes(), err
   440  }
   441  
   442  // mergeValues merges values from the specified overrides
   443  func mergeValues(overrides []HelmOverrides, p getter.Providers) (map[string]interface{}, error) {
   444  	base := map[string]interface{}{}
   445  
   446  	// User specified a values files via -f/--values
   447  	for _, override := range overrides {
   448  		if len(override.FileOverride) > 0 {
   449  			currentMap := map[string]interface{}{}
   450  
   451  			bytes, err := readFile(override.FileOverride, p)
   452  			if err != nil {
   453  				return nil, err
   454  			}
   455  
   456  			if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
   457  				return nil, err
   458  			}
   459  			// Merge with the previous map
   460  			yaml2.MergeMaps(base, currentMap)
   461  		}
   462  
   463  		// User specified a value via --set
   464  		if len(override.SetOverrides) > 0 {
   465  			if err := strvals.ParseInto(override.SetOverrides, base); err != nil {
   466  				return nil, err
   467  			}
   468  		}
   469  
   470  		// User specified a value via --set-string
   471  		if len(override.SetStringOverrides) > 0 {
   472  			if err := strvals.ParseIntoString(override.SetStringOverrides, base); err != nil {
   473  				return nil, err
   474  			}
   475  		}
   476  
   477  		// User specified a value via --set-file
   478  		if len(override.SetFileOverrides) > 0 {
   479  			reader := func(rs []rune) (interface{}, error) {
   480  				bytes, err := readFile(string(rs), p)
   481  				if err != nil {
   482  					return nil, err
   483  				}
   484  				return string(bytes), err
   485  			}
   486  			if err := strvals.ParseIntoFile(override.SetFileOverrides, base, reader); err != nil {
   487  				return nil, err
   488  			}
   489  		}
   490  	}
   491  
   492  	return base, nil
   493  }