github.com/argoproj/argo-cd/v3@v3.2.1/util/helm/cmd.go (about)

     1  package helm
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"path"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  
    13  	log "github.com/sirupsen/logrus"
    14  
    15  	"github.com/argoproj/argo-cd/v3/common"
    16  	executil "github.com/argoproj/argo-cd/v3/util/exec"
    17  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    18  	pathutil "github.com/argoproj/argo-cd/v3/util/io/path"
    19  	"github.com/argoproj/argo-cd/v3/util/proxy"
    20  )
    21  
    22  // A thin wrapper around the "helm" command, adding logging and error translation.
    23  type Cmd struct {
    24  	helmHome  string
    25  	WorkDir   string
    26  	IsLocal   bool
    27  	IsHelmOci bool
    28  	proxy     string
    29  	noProxy   string
    30  }
    31  
    32  func NewCmd(workDir string, version string, proxy string, noProxy string) (*Cmd, error) {
    33  	switch version {
    34  	// If v3 is specified (or by default, if no value is specified) then use v3
    35  	case "", "v3":
    36  		return NewCmdWithVersion(workDir, false, proxy, noProxy)
    37  	}
    38  	return nil, fmt.Errorf("helm chart version '%s' is not supported", version)
    39  }
    40  
    41  func NewCmdWithVersion(workDir string, isHelmOci bool, proxy string, noProxy string) (*Cmd, error) {
    42  	tmpDir, err := os.MkdirTemp("", "helm")
    43  	if err != nil {
    44  		return nil, fmt.Errorf("failed to create temporary directory for helm: %w", err)
    45  	}
    46  	return &Cmd{WorkDir: workDir, helmHome: tmpDir, IsHelmOci: isHelmOci, proxy: proxy, noProxy: noProxy}, err
    47  }
    48  
    49  var redactor = func(text string) string {
    50  	return regexp.MustCompile("(--username|--password) [^ ]*").ReplaceAllString(text, "$1 ******")
    51  }
    52  
    53  func (c Cmd) run(args ...string) (string, string, error) {
    54  	cmd := exec.Command("helm", args...)
    55  	cmd.Dir = c.WorkDir
    56  	cmd.Env = os.Environ()
    57  	if !c.IsLocal {
    58  		cmd.Env = append(cmd.Env,
    59  			fmt.Sprintf("XDG_CACHE_HOME=%s/cache", c.helmHome),
    60  			fmt.Sprintf("XDG_CONFIG_HOME=%s/config", c.helmHome),
    61  			fmt.Sprintf("XDG_DATA_HOME=%s/data", c.helmHome),
    62  			fmt.Sprintf("HELM_CONFIG_HOME=%s/config", c.helmHome))
    63  	}
    64  
    65  	if c.IsHelmOci {
    66  		cmd.Env = append(cmd.Env, "HELM_EXPERIMENTAL_OCI=1")
    67  	}
    68  
    69  	cmd.Env = proxy.UpsertEnv(cmd, c.proxy, c.noProxy)
    70  
    71  	out, err := executil.RunWithRedactor(cmd, redactor)
    72  	fullCommand := executil.GetCommandArgsToLog(cmd)
    73  	if err != nil {
    74  		return out, fullCommand, fmt.Errorf("failed to get command args to log: %w", err)
    75  	}
    76  	return out, fullCommand, nil
    77  }
    78  
    79  func (c *Cmd) RegistryLogin(repo string, creds Creds) (string, error) {
    80  	args := []string{"registry", "login"}
    81  	args = append(args, repo)
    82  
    83  	if creds.GetUsername() != "" {
    84  		args = append(args, "--username", creds.GetUsername())
    85  	}
    86  
    87  	helmPassword, err := creds.GetPassword()
    88  	if err != nil {
    89  		return "", fmt.Errorf("failed to get password for helm registry: %w", err)
    90  	}
    91  	if helmPassword != "" {
    92  		args = append(args, "--password", helmPassword)
    93  	}
    94  
    95  	if creds.GetCAPath() != "" {
    96  		args = append(args, "--ca-file", creds.GetCAPath())
    97  	}
    98  
    99  	if len(creds.GetCertData()) > 0 {
   100  		filePath, closer, err := writeToTmp(creds.GetCertData())
   101  		if err != nil {
   102  			return "", fmt.Errorf("failed to write certificate data to temporary file: %w", err)
   103  		}
   104  		defer utilio.Close(closer)
   105  		args = append(args, "--cert-file", filePath)
   106  	}
   107  
   108  	if len(creds.GetKeyData()) > 0 {
   109  		filePath, closer, err := writeToTmp(creds.GetKeyData())
   110  		if err != nil {
   111  			return "", fmt.Errorf("failed to write key data to temporary file: %w", err)
   112  		}
   113  		defer utilio.Close(closer)
   114  		args = append(args, "--key-file", filePath)
   115  	}
   116  
   117  	if creds.GetInsecureSkipVerify() {
   118  		args = append(args, "--insecure")
   119  	}
   120  	out, _, err := c.run(args...)
   121  	if err != nil {
   122  		return "", fmt.Errorf("failed to login to registry: %w", err)
   123  	}
   124  	return out, nil
   125  }
   126  
   127  func (c *Cmd) RegistryLogout(repo string, _ Creds) (string, error) {
   128  	args := []string{"registry", "logout"}
   129  	args = append(args, repo)
   130  	out, _, err := c.run(args...)
   131  	if err != nil {
   132  		return "", fmt.Errorf("failed to logout from registry: %w", err)
   133  	}
   134  	return out, nil
   135  }
   136  
   137  func (c *Cmd) RepoAdd(name string, url string, opts Creds, passCredentials bool) (string, error) {
   138  	tmp, err := os.MkdirTemp("", "helm")
   139  	if err != nil {
   140  		return "", fmt.Errorf("failed to create temporary directory for repo: %w", err)
   141  	}
   142  	defer func() { _ = os.RemoveAll(tmp) }()
   143  
   144  	args := []string{"repo", "add"}
   145  
   146  	if opts.GetUsername() != "" {
   147  		args = append(args, "--username", opts.GetUsername())
   148  	}
   149  
   150  	helmPassword, err := opts.GetPassword()
   151  	if err != nil {
   152  		return "", fmt.Errorf("failed to get password for helm registry: %w", err)
   153  	}
   154  	if helmPassword != "" {
   155  		args = append(args, "--password", helmPassword)
   156  	}
   157  
   158  	if opts.GetCAPath() != "" {
   159  		args = append(args, "--ca-file", opts.GetCAPath())
   160  	}
   161  
   162  	if opts.GetInsecureSkipVerify() {
   163  		args = append(args, "--insecure-skip-tls-verify")
   164  	}
   165  
   166  	if len(opts.GetCertData()) > 0 {
   167  		certFile, err := os.CreateTemp("", "helm")
   168  		if err != nil {
   169  			return "", fmt.Errorf("failed to create temporary certificate file: %w", err)
   170  		}
   171  		_, err = certFile.Write(opts.GetCertData())
   172  		if err != nil {
   173  			return "", fmt.Errorf("failed to write certificate data: %w", err)
   174  		}
   175  		defer certFile.Close()
   176  		args = append(args, "--cert-file", certFile.Name())
   177  	}
   178  
   179  	if len(opts.GetKeyData()) > 0 {
   180  		keyFile, err := os.CreateTemp("", "helm")
   181  		if err != nil {
   182  			return "", fmt.Errorf("failed to create temporary key file: %w", err)
   183  		}
   184  		_, err = keyFile.Write(opts.GetKeyData())
   185  		if err != nil {
   186  			return "", fmt.Errorf("failed to write key data: %w", err)
   187  		}
   188  		defer keyFile.Close()
   189  		args = append(args, "--key-file", keyFile.Name())
   190  	}
   191  
   192  	if passCredentials {
   193  		args = append(args, "--pass-credentials")
   194  	}
   195  
   196  	args = append(args, name, url)
   197  
   198  	out, _, err := c.run(args...)
   199  	if err != nil {
   200  		return "", fmt.Errorf("failed to add repository: %w", err)
   201  	}
   202  	return out, err
   203  }
   204  
   205  func writeToTmp(data []byte) (string, utilio.Closer, error) {
   206  	file, err := os.CreateTemp("", "")
   207  	if err != nil {
   208  		return "", nil, fmt.Errorf("failed to create temporary file: %w", err)
   209  	}
   210  	err = os.WriteFile(file.Name(), data, 0o644)
   211  	if err != nil {
   212  		_ = os.RemoveAll(file.Name())
   213  		return "", nil, fmt.Errorf("failed to write data to temporary file: %w", err)
   214  	}
   215  	defer func() {
   216  		if err = file.Close(); err != nil {
   217  			log.WithFields(log.Fields{
   218  				common.SecurityField:    common.SecurityMedium,
   219  				common.SecurityCWEField: common.SecurityCWEMissingReleaseOfFileDescriptor,
   220  			}).Errorf("error closing file %q: %v", file.Name(), err)
   221  		}
   222  	}()
   223  	return file.Name(), utilio.NewCloser(func() error {
   224  		return os.RemoveAll(file.Name())
   225  	}), nil
   226  }
   227  
   228  func (c *Cmd) Fetch(repo, chartName, version, destination string, creds Creds, passCredentials bool) (string, error) {
   229  	args := []string{"pull", "--destination", destination}
   230  	if version != "" {
   231  		args = append(args, "--version", version)
   232  	}
   233  	if creds.GetUsername() != "" {
   234  		args = append(args, "--username", creds.GetUsername())
   235  	}
   236  
   237  	helmPassword, err := creds.GetPassword()
   238  	if err != nil {
   239  		return "", fmt.Errorf("failed to get password for helm registry: %w", err)
   240  	}
   241  	if helmPassword != "" {
   242  		args = append(args, "--password", helmPassword)
   243  	}
   244  	if creds.GetInsecureSkipVerify() {
   245  		args = append(args, "--insecure-skip-tls-verify")
   246  	}
   247  
   248  	args = append(args, "--repo", repo, chartName)
   249  
   250  	if creds.GetCAPath() != "" {
   251  		args = append(args, "--ca-file", creds.GetCAPath())
   252  	}
   253  	if len(creds.GetCertData()) > 0 {
   254  		filePath, closer, err := writeToTmp(creds.GetCertData())
   255  		if err != nil {
   256  			return "", fmt.Errorf("failed to write certificate data to temporary file: %w", err)
   257  		}
   258  		defer utilio.Close(closer)
   259  		args = append(args, "--cert-file", filePath)
   260  	}
   261  	if len(creds.GetKeyData()) > 0 {
   262  		filePath, closer, err := writeToTmp(creds.GetKeyData())
   263  		if err != nil {
   264  			return "", fmt.Errorf("failed to write key data to temporary file: %w", err)
   265  		}
   266  		defer utilio.Close(closer)
   267  		args = append(args, "--key-file", filePath)
   268  	}
   269  	if passCredentials {
   270  		args = append(args, "--pass-credentials")
   271  	}
   272  
   273  	out, _, err := c.run(args...)
   274  	if err != nil {
   275  		return "", fmt.Errorf("failed to fetch chart: %w", err)
   276  	}
   277  	return out, nil
   278  }
   279  
   280  func (c *Cmd) PullOCI(repo string, chart string, version string, destination string, creds Creds) (string, error) {
   281  	args := []string{
   282  		"pull", fmt.Sprintf("oci://%s/%s", repo, chart), "--version",
   283  		version,
   284  		"--destination",
   285  		destination,
   286  	}
   287  	if creds.GetCAPath() != "" {
   288  		args = append(args, "--ca-file", creds.GetCAPath())
   289  	}
   290  
   291  	if len(creds.GetCertData()) > 0 {
   292  		filePath, closer, err := writeToTmp(creds.GetCertData())
   293  		if err != nil {
   294  			return "", fmt.Errorf("failed to write certificate data to temporary file: %w", err)
   295  		}
   296  		defer utilio.Close(closer)
   297  		args = append(args, "--cert-file", filePath)
   298  	}
   299  
   300  	if len(creds.GetKeyData()) > 0 {
   301  		filePath, closer, err := writeToTmp(creds.GetKeyData())
   302  		if err != nil {
   303  			return "", fmt.Errorf("failed to write key data to temporary file: %w", err)
   304  		}
   305  		defer utilio.Close(closer)
   306  		args = append(args, "--key-file", filePath)
   307  	}
   308  
   309  	if creds.GetInsecureSkipVerify() {
   310  		args = append(args, "--insecure-skip-tls-verify")
   311  	}
   312  	out, _, err := c.run(args...)
   313  	if err != nil {
   314  		return "", fmt.Errorf("failed to pull OCI chart: %w", err)
   315  	}
   316  	return out, nil
   317  }
   318  
   319  func (c *Cmd) dependencyBuild() (string, error) {
   320  	out, _, err := c.run("dependency", "build")
   321  	if err != nil {
   322  		return "", fmt.Errorf("failed to build dependencies: %w", err)
   323  	}
   324  	return out, nil
   325  }
   326  
   327  func (c *Cmd) inspectValues(values string) (string, error) {
   328  	out, _, err := c.run("show", "values", values)
   329  	if err != nil {
   330  		return "", fmt.Errorf("failed to inspect values: %w", err)
   331  	}
   332  	return out, nil
   333  }
   334  
   335  func (c *Cmd) InspectChart() (string, error) {
   336  	out, _, err := c.run("show", "chart", ".")
   337  	if err != nil {
   338  		return "", fmt.Errorf("failed to inspect chart: %w", err)
   339  	}
   340  	return out, nil
   341  }
   342  
   343  type TemplateOpts struct {
   344  	Name        string
   345  	Namespace   string
   346  	KubeVersion string
   347  	APIVersions []string
   348  	Set         map[string]string
   349  	SetString   map[string]string
   350  	SetFile     map[string]pathutil.ResolvedFilePath
   351  	Values      []pathutil.ResolvedFilePath
   352  	// ExtraValues is the randomly-generated path to the temporary values file holding the contents of
   353  	// spec.source.helm.values/valuesObject.
   354  	ExtraValues          pathutil.ResolvedFilePath
   355  	SkipCrds             bool
   356  	SkipSchemaValidation bool
   357  	SkipTests            bool
   358  }
   359  
   360  func cleanSetParameters(val string) string {
   361  	// `{}` equal helm list parameters format, so don't escape `,`.
   362  	if strings.HasPrefix(val, `{`) && strings.HasSuffix(val, `}`) {
   363  		return val
   364  	}
   365  
   366  	val = replaceAllWithLookbehind(val, ',', `\,`, '\\')
   367  	return val
   368  }
   369  
   370  func replaceAllWithLookbehind(val string, old rune, newV string, lookbehind rune) string {
   371  	var result strings.Builder
   372  	var prevR rune
   373  	for _, r := range val {
   374  		if r == old {
   375  			if prevR != lookbehind {
   376  				result.WriteString(newV)
   377  			} else {
   378  				result.WriteRune(old)
   379  			}
   380  		} else {
   381  			result.WriteRune(r)
   382  		}
   383  		prevR = r
   384  	}
   385  	return result.String()
   386  }
   387  
   388  var apiVersionsRemover = regexp.MustCompile(`(--api-versions [^ ]+ )+`)
   389  
   390  func (c *Cmd) template(chartPath string, opts *TemplateOpts) (string, string, error) {
   391  	if callback, err := cleanupChartLockFile(filepath.Clean(path.Join(c.WorkDir, chartPath))); err == nil {
   392  		defer callback()
   393  	} else {
   394  		return "", "", fmt.Errorf("failed to clean up chart lock file: %w", err)
   395  	}
   396  
   397  	args := []string{"template", chartPath, "--name-template", opts.Name}
   398  
   399  	if opts.Namespace != "" {
   400  		args = append(args, "--namespace", opts.Namespace)
   401  	}
   402  	if opts.KubeVersion != "" {
   403  		args = append(args, "--kube-version", opts.KubeVersion)
   404  	}
   405  	for key, val := range opts.Set {
   406  		args = append(args, "--set", key+"="+cleanSetParameters(val))
   407  	}
   408  	for key, val := range opts.SetString {
   409  		args = append(args, "--set-string", key+"="+cleanSetParameters(val))
   410  	}
   411  	for key, val := range opts.SetFile {
   412  		args = append(args, "--set-file", key+"="+cleanSetParameters(string(val)))
   413  	}
   414  	for _, val := range opts.Values {
   415  		args = append(args, "--values", string(val))
   416  	}
   417  	if opts.ExtraValues != "" {
   418  		args = append(args, "--values", string(opts.ExtraValues))
   419  	}
   420  	for _, v := range opts.APIVersions {
   421  		args = append(args, "--api-versions", v)
   422  	}
   423  	if !opts.SkipCrds {
   424  		args = append(args, "--include-crds")
   425  	}
   426  	if opts.SkipSchemaValidation {
   427  		args = append(args, "--skip-schema-validation")
   428  	}
   429  	if opts.SkipTests {
   430  		args = append(args, "--skip-tests")
   431  	}
   432  
   433  	out, command, err := c.run(args...)
   434  	if err != nil {
   435  		msg := err.Error()
   436  		if strings.Contains(msg, "--api-versions") {
   437  			log.Debug(msg)
   438  			msg = apiVersionsRemover.ReplaceAllString(msg, "<api versions removed> ")
   439  		}
   440  		return "", command, errors.New(msg)
   441  	}
   442  	return out, command, nil
   443  }
   444  
   445  // Workaround for Helm3 behavior (see https://github.com/helm/helm/issues/6870).
   446  // The `helm template` command generates Chart.lock after which `helm dependency build` does not work
   447  // As workaround removing lock file unless it exists before running helm template
   448  func cleanupChartLockFile(chartPath string) (func(), error) {
   449  	exists := true
   450  	lockPath := path.Join(chartPath, "Chart.lock")
   451  	if _, err := os.Stat(lockPath); err != nil {
   452  		if !os.IsNotExist(err) {
   453  			return nil, fmt.Errorf("failed to check lock file status: %w", err)
   454  		}
   455  		exists = false
   456  	}
   457  	return func() {
   458  		if !exists {
   459  			_ = os.Remove(lockPath)
   460  		}
   461  	}, nil
   462  }
   463  
   464  func (c *Cmd) Freestyle(args ...string) (string, error) {
   465  	out, _, err := c.run(args...)
   466  	if err != nil {
   467  		return "", fmt.Errorf("failed to execute freestyle helm command: %w", err)
   468  	}
   469  	return out, nil
   470  }
   471  
   472  func (c *Cmd) Close() {
   473  	_ = os.RemoveAll(c.helmHome)
   474  }