github.com/uhthomas/helm@v3.0.0-beta.3+incompatible/pkg/action/install.go (about)

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package action
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"strings"
    27  	"text/template"
    28  	"time"
    29  
    30  	"github.com/Masterminds/sprig"
    31  	"github.com/pkg/errors"
    32  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    33  	"k8s.io/cli-runtime/pkg/resource"
    34  
    35  	"helm.sh/helm/pkg/chart"
    36  	"helm.sh/helm/pkg/chartutil"
    37  	"helm.sh/helm/pkg/cli"
    38  	"helm.sh/helm/pkg/downloader"
    39  	"helm.sh/helm/pkg/engine"
    40  	"helm.sh/helm/pkg/getter"
    41  	kubefake "helm.sh/helm/pkg/kube/fake"
    42  	"helm.sh/helm/pkg/release"
    43  	"helm.sh/helm/pkg/releaseutil"
    44  	"helm.sh/helm/pkg/repo"
    45  	"helm.sh/helm/pkg/storage"
    46  	"helm.sh/helm/pkg/storage/driver"
    47  )
    48  
    49  // releaseNameMaxLen is the maximum length of a release name.
    50  //
    51  // As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for
    52  // charts to add data. Effectively, that gives us 53 chars.
    53  // See https://github.com/helm/helm/issues/1528
    54  const releaseNameMaxLen = 53
    55  
    56  // NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine
    57  // but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
    58  // wants to see this file after rendering in the status command. However, it must be a suffix
    59  // since there can be filepath in front of it.
    60  const notesFileSuffix = "NOTES.txt"
    61  
    62  const defaultDirectoryPermission = 0755
    63  
    64  // Install performs an installation operation.
    65  type Install struct {
    66  	cfg *Configuration
    67  
    68  	ChartPathOptions
    69  
    70  	ClientOnly       bool
    71  	DryRun           bool
    72  	DisableHooks     bool
    73  	Replace          bool
    74  	Wait             bool
    75  	Devel            bool
    76  	DependencyUpdate bool
    77  	Timeout          time.Duration
    78  	Namespace        string
    79  	ReleaseName      string
    80  	GenerateName     bool
    81  	NameTemplate     string
    82  	OutputDir        string
    83  	Atomic           bool
    84  	SkipCRDs         bool
    85  }
    86  
    87  // ChartPathOptions captures common options used for controlling chart paths
    88  type ChartPathOptions struct {
    89  	CaFile   string // --ca-file
    90  	CertFile string // --cert-file
    91  	KeyFile  string // --key-file
    92  	Keyring  string // --keyring
    93  	Password string // --password
    94  	RepoURL  string // --repo
    95  	Username string // --username
    96  	Verify   bool   // --verify
    97  	Version  string // --version
    98  }
    99  
   100  // NewInstall creates a new Install object with the given configuration.
   101  func NewInstall(cfg *Configuration) *Install {
   102  	return &Install{
   103  		cfg: cfg,
   104  	}
   105  }
   106  
   107  func (i *Install) installCRDs(crds []*chart.File) error {
   108  	// We do these one file at a time in the order they were read.
   109  	totalItems := []*resource.Info{}
   110  	for _, obj := range crds {
   111  		// Read in the resources
   112  		res, err := i.cfg.KubeClient.Build(bytes.NewBuffer(obj.Data))
   113  		if err != nil {
   114  			return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
   115  		}
   116  
   117  		// Send them to Kube
   118  		if _, err := i.cfg.KubeClient.Create(res); err != nil {
   119  			// If the error is CRD already exists, continue.
   120  			if apierrors.IsAlreadyExists(err) {
   121  				crdName := res[0].Name
   122  				i.cfg.Log("CRD %s is already present. Skipping.", crdName)
   123  				continue
   124  			}
   125  			return errors.Wrapf(err, "failed to instal CRD %s", obj.Name)
   126  		}
   127  		totalItems = append(totalItems, res...)
   128  	}
   129  	// Invalidate the local cache, since it will not have the new CRDs
   130  	// present.
   131  	discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient()
   132  	if err != nil {
   133  		return err
   134  	}
   135  	i.cfg.Log("Clearing discovery cache")
   136  	discoveryClient.Invalidate()
   137  	// Give time for the CRD to be recognized.
   138  	if err := i.cfg.KubeClient.Wait(totalItems, 60*time.Second); err != nil {
   139  		return err
   140  	}
   141  	// Make sure to force a rebuild of the cache.
   142  	discoveryClient.ServerGroups()
   143  	return nil
   144  }
   145  
   146  // Run executes the installation
   147  //
   148  // If DryRun is set to true, this will prepare the release, but not install it
   149  func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
   150  	if err := i.cfg.KubeClient.IsReachable(); err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	if err := i.availableName(); err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	// Pre-install anything in the crd/ directory. We do this before Helm
   159  	// contacts the upstream server and builds the capabilities object.
   160  	if crds := chrt.CRDs(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
   161  		// On dry run, bail here
   162  		if i.DryRun {
   163  			i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
   164  		} else if err := i.installCRDs(crds); err != nil {
   165  			return nil, err
   166  		}
   167  	}
   168  
   169  	if i.ClientOnly {
   170  		// Add mock objects in here so it doesn't use Kube API server
   171  		// NOTE(bacongobbler): used for `helm template`
   172  		i.cfg.Capabilities = chartutil.DefaultCapabilities
   173  		i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
   174  		i.cfg.Releases = storage.Init(driver.NewMemory())
   175  	}
   176  
   177  	if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	// Make sure if Atomic is set, that wait is set as well. This makes it so
   182  	// the user doesn't have to specify both
   183  	i.Wait = i.Wait || i.Atomic
   184  
   185  	caps, err := i.cfg.getCapabilities()
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	options := chartutil.ReleaseOptions{
   191  		Name:      i.ReleaseName,
   192  		Namespace: i.Namespace,
   193  		IsInstall: true,
   194  	}
   195  	valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  
   200  	rel := i.createRelease(chrt, vals)
   201  
   202  	var manifestDoc *bytes.Buffer
   203  	rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.OutputDir)
   204  	// Even for errors, attach this if available
   205  	if manifestDoc != nil {
   206  		rel.Manifest = manifestDoc.String()
   207  	}
   208  	// Check error from render
   209  	if err != nil {
   210  		rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
   211  		// Return a release with partial data so that the client can show debugging information.
   212  		return rel, err
   213  	}
   214  
   215  	// Mark this release as in-progress
   216  	rel.SetStatus(release.StatusPendingInstall, "Initial install underway")
   217  
   218  	resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest))
   219  	if err != nil {
   220  		return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
   221  	}
   222  
   223  	// Bail out here if it is a dry run
   224  	if i.DryRun {
   225  		rel.Info.Description = "Dry run complete"
   226  		return rel, nil
   227  	}
   228  
   229  	// If Replace is true, we need to supercede the last release.
   230  	if i.Replace {
   231  		if err := i.replaceRelease(rel); err != nil {
   232  			return nil, err
   233  		}
   234  	}
   235  
   236  	// Store the release in history before continuing (new in Helm 3). We always know
   237  	// that this is a create operation.
   238  	if err := i.cfg.Releases.Create(rel); err != nil {
   239  		// We could try to recover gracefully here, but since nothing has been installed
   240  		// yet, this is probably safer than trying to continue when we know storage is
   241  		// not working.
   242  		return rel, err
   243  	}
   244  
   245  	// pre-install hooks
   246  	if !i.DisableHooks {
   247  		if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
   248  			return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
   249  		}
   250  	}
   251  
   252  	// At this point, we can do the install. Note that before we were detecting whether to
   253  	// do an update, but it's not clear whether we WANT to do an update if the re-use is set
   254  	// to true, since that is basically an upgrade operation.
   255  	if _, err := i.cfg.KubeClient.Create(resources); err != nil {
   256  		return i.failRelease(rel, err)
   257  	}
   258  
   259  	if i.Wait {
   260  		if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil {
   261  			return i.failRelease(rel, err)
   262  		}
   263  
   264  	}
   265  
   266  	if !i.DisableHooks {
   267  		if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
   268  			return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
   269  		}
   270  	}
   271  
   272  	rel.SetStatus(release.StatusDeployed, "Install complete")
   273  
   274  	// This is a tricky case. The release has been created, but the result
   275  	// cannot be recorded. The truest thing to tell the user is that the
   276  	// release was created. However, the user will not be able to do anything
   277  	// further with this release.
   278  	//
   279  	// One possible strategy would be to do a timed retry to see if we can get
   280  	// this stored in the future.
   281  	i.recordRelease(rel)
   282  
   283  	return rel, nil
   284  }
   285  
   286  func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
   287  	rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
   288  	if i.Atomic {
   289  		i.cfg.Log("Install failed and atomic is set, uninstalling release")
   290  		uninstall := NewUninstall(i.cfg)
   291  		uninstall.DisableHooks = i.DisableHooks
   292  		uninstall.KeepHistory = false
   293  		uninstall.Timeout = i.Timeout
   294  		if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil {
   295  			return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err)
   296  		}
   297  		return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName)
   298  	}
   299  	i.recordRelease(rel) // Ignore the error, since we have another error to deal with.
   300  	return rel, err
   301  }
   302  
   303  // availableName tests whether a name is available
   304  //
   305  // Roughly, this will return an error if name is
   306  //
   307  //	- empty
   308  //	- too long
   309  //	- already in use, and not deleted
   310  //	- used by a deleted release, and i.Replace is false
   311  func (i *Install) availableName() error {
   312  	start := i.ReleaseName
   313  	if start == "" {
   314  		return errors.New("name is required")
   315  	}
   316  
   317  	if len(start) > releaseNameMaxLen {
   318  		return errors.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen)
   319  	}
   320  
   321  	if i.DryRun {
   322  		return nil
   323  	}
   324  
   325  	h, err := i.cfg.Releases.History(start)
   326  	if err != nil || len(h) < 1 {
   327  		return nil
   328  	}
   329  	releaseutil.Reverse(h, releaseutil.SortByRevision)
   330  	rel := h[0]
   331  
   332  	if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) {
   333  		return nil
   334  	}
   335  	return errors.New("cannot re-use a name that is still in use")
   336  }
   337  
   338  // createRelease creates a new release object
   339  func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release {
   340  	ts := i.cfg.Now()
   341  	return &release.Release{
   342  		Name:      i.ReleaseName,
   343  		Namespace: i.Namespace,
   344  		Chart:     chrt,
   345  		Config:    rawVals,
   346  		Info: &release.Info{
   347  			FirstDeployed: ts,
   348  			LastDeployed:  ts,
   349  			Status:        release.StatusUnknown,
   350  		},
   351  		Version: 1,
   352  	}
   353  }
   354  
   355  // recordRelease with an update operation in case reuse has been set.
   356  func (i *Install) recordRelease(r *release.Release) error {
   357  	// This is a legacy function which has been reduced to a oneliner. Could probably
   358  	// refactor it out.
   359  	return i.cfg.Releases.Update(r)
   360  }
   361  
   362  // replaceRelease replaces an older release with this one
   363  //
   364  // This allows us to re-use names by superseding an existing release with a new one
   365  func (i *Install) replaceRelease(rel *release.Release) error {
   366  	hist, err := i.cfg.Releases.History(rel.Name)
   367  	if err != nil || len(hist) == 0 {
   368  		// No releases exist for this name, so we can return early
   369  		return nil
   370  	}
   371  
   372  	releaseutil.Reverse(hist, releaseutil.SortByRevision)
   373  	last := hist[0]
   374  
   375  	// Update version to the next available
   376  	rel.Version = last.Version + 1
   377  
   378  	// Do not change the status of a failed release.
   379  	if last.Info.Status == release.StatusFailed {
   380  		return nil
   381  	}
   382  
   383  	// For any other status, mark it as superseded and store the old record
   384  	last.SetStatus(release.StatusSuperseded, "superseded by new release")
   385  	return i.recordRelease(last)
   386  }
   387  
   388  // renderResources renders the templates in a chart
   389  func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, outputDir string) ([]*release.Hook, *bytes.Buffer, string, error) {
   390  	hs := []*release.Hook{}
   391  	b := bytes.NewBuffer(nil)
   392  
   393  	caps, err := c.getCapabilities()
   394  	if err != nil {
   395  		return hs, b, "", err
   396  	}
   397  
   398  	if ch.Metadata.KubeVersion != "" {
   399  		if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
   400  			return hs, b, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
   401  		}
   402  	}
   403  
   404  	files, err := engine.Render(ch, values)
   405  	if err != nil {
   406  		return hs, b, "", err
   407  	}
   408  
   409  	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
   410  	// pull it out of here into a separate file so that we can actually use the output of the rendered
   411  	// text file. We have to spin through this map because the file contains path information, so we
   412  	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
   413  	// it in the sortHooks.
   414  	notes := ""
   415  	for k, v := range files {
   416  		if strings.HasSuffix(k, notesFileSuffix) {
   417  			// Only apply the notes if it belongs to the parent chart
   418  			// Note: Do not use filePath.Join since it creates a path with \ which is not expected
   419  			if k == path.Join(ch.Name(), "templates", notesFileSuffix) {
   420  				notes = v
   421  			}
   422  			delete(files, k)
   423  		}
   424  	}
   425  
   426  	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
   427  	// as partials are not used after renderer.Render. Empty manifests are also
   428  	// removed here.
   429  	hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder)
   430  	if err != nil {
   431  		// By catching parse errors here, we can prevent bogus releases from going
   432  		// to Kubernetes.
   433  		//
   434  		// We return the files as a big blob of data to help the user debug parser
   435  		// errors.
   436  		for name, content := range files {
   437  			if strings.TrimSpace(content) == "" {
   438  				continue
   439  			}
   440  			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
   441  		}
   442  		return hs, b, "", err
   443  	}
   444  
   445  	// Aggregate all valid manifests into one big doc.
   446  	fileWritten := make(map[string]bool)
   447  	for _, m := range manifests {
   448  		if outputDir == "" {
   449  			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
   450  		} else {
   451  			err = writeToFile(outputDir, m.Name, m.Content, fileWritten[m.Name])
   452  			if err != nil {
   453  				return hs, b, "", err
   454  			}
   455  			fileWritten[m.Name] = true
   456  		}
   457  	}
   458  
   459  	return hs, b, notes, nil
   460  }
   461  
   462  // write the <data> to <output-dir>/<name>. <append> controls if the file is created or content will be appended
   463  func writeToFile(outputDir string, name string, data string, append bool) error {
   464  	outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))
   465  
   466  	err := ensureDirectoryForFile(outfileName)
   467  	if err != nil {
   468  		return err
   469  	}
   470  
   471  	f, err := createOrOpenFile(outfileName, append)
   472  	if err != nil {
   473  		return err
   474  	}
   475  
   476  	defer f.Close()
   477  
   478  	_, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data))
   479  
   480  	if err != nil {
   481  		return err
   482  	}
   483  
   484  	fmt.Printf("wrote %s\n", outfileName)
   485  	return nil
   486  }
   487  
   488  func createOrOpenFile(filename string, append bool) (*os.File, error) {
   489  	if append {
   490  		return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
   491  	}
   492  	return os.Create(filename)
   493  }
   494  
   495  // check if the directory exists to create file. creates if don't exists
   496  func ensureDirectoryForFile(file string) error {
   497  	baseDir := path.Dir(file)
   498  	_, err := os.Stat(baseDir)
   499  	if err != nil && !os.IsNotExist(err) {
   500  		return err
   501  	}
   502  
   503  	return os.MkdirAll(baseDir, defaultDirectoryPermission)
   504  }
   505  
   506  // NameAndChart returns the name and chart that should be used.
   507  //
   508  // This will read the flags and handle name generation if necessary.
   509  func (i *Install) NameAndChart(args []string) (string, string, error) {
   510  	flagsNotSet := func() error {
   511  		if i.GenerateName {
   512  			return errors.New("cannot set --generate-name and also specify a name")
   513  		}
   514  		if i.NameTemplate != "" {
   515  			return errors.New("cannot set --name-template and also specify a name")
   516  		}
   517  		return nil
   518  	}
   519  
   520  	if len(args) == 2 {
   521  		return args[0], args[1], flagsNotSet()
   522  	}
   523  
   524  	if i.NameTemplate != "" {
   525  		name, err := TemplateName(i.NameTemplate)
   526  		return name, args[0], err
   527  	}
   528  
   529  	if i.ReleaseName != "" {
   530  		return i.ReleaseName, args[0], nil
   531  	}
   532  
   533  	if !i.GenerateName {
   534  		return "", args[0], errors.New("must either provide a name or specify --generate-name")
   535  	}
   536  
   537  	base := filepath.Base(args[0])
   538  	if base == "." || base == "" {
   539  		base = "chart"
   540  	}
   541  
   542  	return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil
   543  }
   544  
   545  // TemplateName renders a name template, returning the name or an error.
   546  func TemplateName(nameTemplate string) (string, error) {
   547  	if nameTemplate == "" {
   548  		return "", nil
   549  	}
   550  
   551  	t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
   552  	if err != nil {
   553  		return "", err
   554  	}
   555  	var b bytes.Buffer
   556  	if err := t.Execute(&b, nil); err != nil {
   557  		return "", err
   558  	}
   559  
   560  	return b.String(), nil
   561  }
   562  
   563  // CheckDependencies checks the dependencies for a chart.
   564  func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error {
   565  	var missing []string
   566  
   567  OUTER:
   568  	for _, r := range reqs {
   569  		for _, d := range ch.Dependencies() {
   570  			if d.Name() == r.Name {
   571  				continue OUTER
   572  			}
   573  		}
   574  		missing = append(missing, r.Name)
   575  	}
   576  
   577  	if len(missing) > 0 {
   578  		return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", "))
   579  	}
   580  	return nil
   581  }
   582  
   583  // LocateChart looks for a chart directory in known places, and returns either the full path or an error.
   584  //
   585  // This does not ensure that the chart is well-formed; only that the requested filename exists.
   586  //
   587  // Order of resolution:
   588  // - relative to current working directory
   589  // - if path is absolute or begins with '.', error out here
   590  // - URL
   591  //
   592  // If 'verify' was set on ChartPathOptions, this will attempt to also verify the chart.
   593  func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (string, error) {
   594  	name = strings.TrimSpace(name)
   595  	version := strings.TrimSpace(c.Version)
   596  
   597  	if _, err := os.Stat(name); err == nil {
   598  		abs, err := filepath.Abs(name)
   599  		if err != nil {
   600  			return abs, err
   601  		}
   602  		if c.Verify {
   603  			if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil {
   604  				return "", err
   605  			}
   606  		}
   607  		return abs, nil
   608  	}
   609  	if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
   610  		return name, errors.Errorf("path %q not found", name)
   611  	}
   612  
   613  	dl := downloader.ChartDownloader{
   614  		Out:     os.Stdout,
   615  		Keyring: c.Keyring,
   616  		Getters: getter.All(settings),
   617  		Options: []getter.Option{
   618  			getter.WithBasicAuth(c.Username, c.Password),
   619  		},
   620  		RepositoryConfig: settings.RepositoryConfig,
   621  		RepositoryCache:  settings.RepositoryCache,
   622  	}
   623  	if c.Verify {
   624  		dl.Verify = downloader.VerifyAlways
   625  	}
   626  	if c.RepoURL != "" {
   627  		chartURL, err := repo.FindChartInAuthRepoURL(c.RepoURL, c.Username, c.Password, name, version,
   628  			c.CertFile, c.KeyFile, c.CaFile, getter.All(settings))
   629  		if err != nil {
   630  			return "", err
   631  		}
   632  		name = chartURL
   633  	}
   634  
   635  	if err := os.MkdirAll(settings.RepositoryCache, 0755); err != nil {
   636  		return "", err
   637  	}
   638  
   639  	filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache)
   640  	if err == nil {
   641  		lname, err := filepath.Abs(filename)
   642  		if err != nil {
   643  			return filename, err
   644  		}
   645  		return lname, nil
   646  	} else if settings.Debug {
   647  		return filename, err
   648  	}
   649  
   650  	return filename, errors.Errorf("failed to download %q (hint: running `helm repo update` may help)", name)
   651  }