github.com/valdemarpavesi/helm@v2.9.1+incompatible/pkg/downloader/manager.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package downloader
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"net/url"
    24  	"os"
    25  	"path"
    26  	"path/filepath"
    27  	"strings"
    28  	"sync"
    29  
    30  	"github.com/Masterminds/semver"
    31  	"github.com/ghodss/yaml"
    32  
    33  	"k8s.io/helm/pkg/chartutil"
    34  	"k8s.io/helm/pkg/getter"
    35  	"k8s.io/helm/pkg/helm/helmpath"
    36  	"k8s.io/helm/pkg/proto/hapi/chart"
    37  	"k8s.io/helm/pkg/repo"
    38  	"k8s.io/helm/pkg/resolver"
    39  	"k8s.io/helm/pkg/urlutil"
    40  )
    41  
    42  // Manager handles the lifecycle of fetching, resolving, and storing dependencies.
    43  type Manager struct {
    44  	// Out is used to print warnings and notifications.
    45  	Out io.Writer
    46  	// ChartPath is the path to the unpacked base chart upon which this operates.
    47  	ChartPath string
    48  	// HelmHome is the $HELM_HOME directory
    49  	HelmHome helmpath.Home
    50  	// Verification indicates whether the chart should be verified.
    51  	Verify VerificationStrategy
    52  	// Debug is the global "--debug" flag
    53  	Debug bool
    54  	// Keyring is the key ring file.
    55  	Keyring string
    56  	// SkipUpdate indicates that the repository should not be updated first.
    57  	SkipUpdate bool
    58  	// Getter collection for the operation
    59  	Getters []getter.Provider
    60  }
    61  
    62  // Build rebuilds a local charts directory from a lockfile.
    63  //
    64  // If the lockfile is not present, this will run a Manager.Update()
    65  //
    66  // If SkipUpdate is set, this will not update the repository.
    67  func (m *Manager) Build() error {
    68  	c, err := m.loadChartDir()
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	// If a lock file is found, run a build from that. Otherwise, just do
    74  	// an update.
    75  	lock, err := chartutil.LoadRequirementsLock(c)
    76  	if err != nil {
    77  		return m.Update()
    78  	}
    79  
    80  	// A lock must accompany a requirements.yaml file.
    81  	req, err := chartutil.LoadRequirements(c)
    82  	if err != nil {
    83  		return fmt.Errorf("requirements.yaml cannot be opened: %s", err)
    84  	}
    85  	if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest {
    86  		return fmt.Errorf("requirements.lock is out of sync with requirements.yaml")
    87  	}
    88  
    89  	// Check that all of the repos we're dependent on actually exist.
    90  	if err := m.hasAllRepos(lock.Dependencies); err != nil {
    91  		return err
    92  	}
    93  
    94  	if !m.SkipUpdate {
    95  		// For each repo in the file, update the cached copy of that repo
    96  		if err := m.UpdateRepositories(); err != nil {
    97  			return err
    98  		}
    99  	}
   100  
   101  	// Now we need to fetch every package here into charts/
   102  	if err := m.downloadAll(lock.Dependencies); err != nil {
   103  		return err
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  // Update updates a local charts directory.
   110  //
   111  // It first reads the requirements.yaml file, and then attempts to
   112  // negotiate versions based on that. It will download the versions
   113  // from remote chart repositories unless SkipUpdate is true.
   114  func (m *Manager) Update() error {
   115  	c, err := m.loadChartDir()
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	// If no requirements file is found, we consider this a successful
   121  	// completion.
   122  	req, err := chartutil.LoadRequirements(c)
   123  	if err != nil {
   124  		if err == chartutil.ErrRequirementsNotFound {
   125  			fmt.Fprintf(m.Out, "No requirements found in %s/charts.\n", m.ChartPath)
   126  			return nil
   127  		}
   128  		return err
   129  	}
   130  
   131  	// Hash requirements.yaml
   132  	hash, err := resolver.HashReq(req)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	// Check that all of the repos we're dependent on actually exist and
   138  	// the repo index names.
   139  	repoNames, err := m.getRepoNames(req.Dependencies)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	// For each repo in the file, update the cached copy of that repo
   145  	if !m.SkipUpdate {
   146  		if err := m.UpdateRepositories(); err != nil {
   147  			return err
   148  		}
   149  	}
   150  
   151  	// Now we need to find out which version of a chart best satisfies the
   152  	// requirements the requirements.yaml
   153  	lock, err := m.resolve(req, repoNames, hash)
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	// Now we need to fetch every package here into charts/
   159  	if err := m.downloadAll(lock.Dependencies); err != nil {
   160  		return err
   161  	}
   162  
   163  	// If the lock file hasn't changed, don't write a new one.
   164  	oldLock, err := chartutil.LoadRequirementsLock(c)
   165  	if err == nil && oldLock.Digest == lock.Digest {
   166  		return nil
   167  	}
   168  
   169  	// Finally, we need to write the lockfile.
   170  	return writeLock(m.ChartPath, lock)
   171  }
   172  
   173  func (m *Manager) loadChartDir() (*chart.Chart, error) {
   174  	if fi, err := os.Stat(m.ChartPath); err != nil {
   175  		return nil, fmt.Errorf("could not find %s: %s", m.ChartPath, err)
   176  	} else if !fi.IsDir() {
   177  		return nil, errors.New("only unpacked charts can be updated")
   178  	}
   179  	return chartutil.LoadDir(m.ChartPath)
   180  }
   181  
   182  // resolve takes a list of requirements and translates them into an exact version to download.
   183  //
   184  // This returns a lock file, which has all of the requirements normalized to a specific version.
   185  func (m *Manager) resolve(req *chartutil.Requirements, repoNames map[string]string, hash string) (*chartutil.RequirementsLock, error) {
   186  	res := resolver.New(m.ChartPath, m.HelmHome)
   187  	return res.Resolve(req, repoNames, hash)
   188  }
   189  
   190  // downloadAll takes a list of dependencies and downloads them into charts/
   191  //
   192  // It will delete versions of the chart that exist on disk and might cause
   193  // a conflict.
   194  func (m *Manager) downloadAll(deps []*chartutil.Dependency) error {
   195  	repos, err := m.loadChartRepositories()
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	destPath := filepath.Join(m.ChartPath, "charts")
   201  	tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
   202  
   203  	// Create 'charts' directory if it doesn't already exist.
   204  	if fi, err := os.Stat(destPath); err != nil {
   205  		if err := os.MkdirAll(destPath, 0755); err != nil {
   206  			return err
   207  		}
   208  	} else if !fi.IsDir() {
   209  		return fmt.Errorf("%q is not a directory", destPath)
   210  	}
   211  
   212  	if err := os.Rename(destPath, tmpPath); err != nil {
   213  		return fmt.Errorf("Unable to move current charts to tmp dir: %v", err)
   214  	}
   215  
   216  	if err := os.MkdirAll(destPath, 0755); err != nil {
   217  		return err
   218  	}
   219  
   220  	fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
   221  	var saveError error
   222  	for _, dep := range deps {
   223  		if strings.HasPrefix(dep.Repository, "file://") {
   224  			if m.Debug {
   225  				fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
   226  			}
   227  			ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version)
   228  			if err != nil {
   229  				saveError = err
   230  				break
   231  			}
   232  			dep.Version = ver
   233  			continue
   234  		}
   235  
   236  		fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
   237  
   238  		// Any failure to resolve/download a chart should fail:
   239  		// https://github.com/kubernetes/helm/issues/1439
   240  		churl, username, password, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos)
   241  		if err != nil {
   242  			saveError = fmt.Errorf("could not find %s: %s", churl, err)
   243  			break
   244  		}
   245  
   246  		dl := ChartDownloader{
   247  			Out:      m.Out,
   248  			Verify:   m.Verify,
   249  			Keyring:  m.Keyring,
   250  			HelmHome: m.HelmHome,
   251  			Getters:  m.Getters,
   252  			Username: username,
   253  			Password: password,
   254  		}
   255  
   256  		if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil {
   257  			saveError = fmt.Errorf("could not download %s: %s", churl, err)
   258  			break
   259  		}
   260  	}
   261  
   262  	if saveError == nil {
   263  		fmt.Fprintln(m.Out, "Deleting outdated charts")
   264  		for _, dep := range deps {
   265  			if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil {
   266  				return err
   267  			}
   268  		}
   269  		if err := move(tmpPath, destPath); err != nil {
   270  			return err
   271  		}
   272  		if err := os.RemoveAll(tmpPath); err != nil {
   273  			return fmt.Errorf("Failed to remove %v: %v", tmpPath, err)
   274  		}
   275  	} else {
   276  		fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
   277  		fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state")
   278  		for _, dep := range deps {
   279  			if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
   280  				return err
   281  			}
   282  		}
   283  		if err := os.RemoveAll(destPath); err != nil {
   284  			return fmt.Errorf("Failed to remove %v: %v", destPath, err)
   285  		}
   286  		if err := os.Rename(tmpPath, destPath); err != nil {
   287  			return fmt.Errorf("Unable to move current charts to tmp dir: %v", err)
   288  		}
   289  		return saveError
   290  	}
   291  	return nil
   292  }
   293  
   294  // safeDeleteDep deletes any versions of the given dependency in the given directory.
   295  //
   296  // It does this by first matching the file name to an expected pattern, then loading
   297  // the file to verify that it is a chart with the same name as the given name.
   298  //
   299  // Because it requires tar file introspection, it is more intensive than a basic delete.
   300  //
   301  // This will only return errors that should stop processing entirely. Other errors
   302  // will emit log messages or be ignored.
   303  func (m *Manager) safeDeleteDep(name, dir string) error {
   304  	files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
   305  	if err != nil {
   306  		// Only for ErrBadPattern
   307  		return err
   308  	}
   309  	for _, fname := range files {
   310  		ch, err := chartutil.LoadFile(fname)
   311  		if err != nil {
   312  			fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
   313  			continue
   314  		}
   315  		if ch.Metadata.Name != name {
   316  			// This is not the file you are looking for.
   317  			continue
   318  		}
   319  		if err := os.Remove(fname); err != nil {
   320  			fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
   321  			continue
   322  		}
   323  	}
   324  	return nil
   325  }
   326  
   327  // hasAllRepos ensures that all of the referenced deps are in the local repo cache.
   328  func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
   329  	rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
   330  	if err != nil {
   331  		return err
   332  	}
   333  	repos := rf.Repositories
   334  
   335  	// Verify that all repositories referenced in the deps are actually known
   336  	// by Helm.
   337  	missing := []string{}
   338  	for _, dd := range deps {
   339  		// If repo is from local path, continue
   340  		if strings.HasPrefix(dd.Repository, "file://") {
   341  			continue
   342  		}
   343  
   344  		found := false
   345  		if dd.Repository == "" {
   346  			found = true
   347  		} else {
   348  			for _, repo := range repos {
   349  				if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
   350  					found = true
   351  				}
   352  			}
   353  		}
   354  		if !found {
   355  			missing = append(missing, dd.Repository)
   356  		}
   357  	}
   358  	if len(missing) > 0 {
   359  		return fmt.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", "))
   360  	}
   361  	return nil
   362  }
   363  
   364  // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file.
   365  func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, error) {
   366  	rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	repos := rf.Repositories
   371  
   372  	reposMap := make(map[string]string)
   373  
   374  	// Verify that all repositories referenced in the deps are actually known
   375  	// by Helm.
   376  	missing := []string{}
   377  	for _, dd := range deps {
   378  		// if dep chart is from local path, verify the path is valid
   379  		if strings.HasPrefix(dd.Repository, "file://") {
   380  			if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil {
   381  				return nil, err
   382  			}
   383  
   384  			if m.Debug {
   385  				fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository)
   386  			}
   387  			reposMap[dd.Name] = dd.Repository
   388  			continue
   389  		}
   390  
   391  		found := false
   392  
   393  		for _, repo := range repos {
   394  			if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) ||
   395  				(strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) {
   396  				found = true
   397  				dd.Repository = repo.URL
   398  				reposMap[dd.Name] = repo.Name
   399  				break
   400  			} else if urlutil.Equal(repo.URL, dd.Repository) {
   401  				found = true
   402  				reposMap[dd.Name] = repo.Name
   403  				break
   404  			}
   405  		}
   406  		if !found {
   407  			missing = append(missing, dd.Repository)
   408  		}
   409  	}
   410  	if len(missing) > 0 {
   411  		if len(missing) > 0 {
   412  			errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", "))
   413  			// It is common for people to try to enter "stable" as a repository instead of the actual URL.
   414  			// For this case, let's give them a suggestion.
   415  			containsNonURL := false
   416  			for _, repo := range missing {
   417  				if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") {
   418  					containsNonURL = true
   419  				}
   420  			}
   421  			if containsNonURL {
   422  				errorMessage += `
   423  Note that repositories must be URLs or aliases. For example, to refer to the stable
   424  repository, use "https://kubernetes-charts.storage.googleapis.com/" or "@stable" instead of
   425  "stable". Don't forget to add the repo, too ('helm repo add').`
   426  			}
   427  			return nil, errors.New(errorMessage)
   428  		}
   429  	}
   430  	return reposMap, nil
   431  }
   432  
   433  // UpdateRepositories updates all of the local repos to the latest.
   434  func (m *Manager) UpdateRepositories() error {
   435  	rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
   436  	if err != nil {
   437  		return err
   438  	}
   439  	repos := rf.Repositories
   440  	if len(repos) > 0 {
   441  		// This prints warnings straight to out.
   442  		if err := m.parallelRepoUpdate(repos); err != nil {
   443  			return err
   444  		}
   445  	}
   446  	return nil
   447  }
   448  
   449  func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
   450  	out := m.Out
   451  	fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
   452  	var wg sync.WaitGroup
   453  	for _, c := range repos {
   454  		r, err := repo.NewChartRepository(c, m.Getters)
   455  		if err != nil {
   456  			return err
   457  		}
   458  		wg.Add(1)
   459  		go func(r *repo.ChartRepository) {
   460  			if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil {
   461  				fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err)
   462  			} else {
   463  				fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
   464  			}
   465  			wg.Done()
   466  		}(r)
   467  	}
   468  	wg.Wait()
   469  	fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
   470  	return nil
   471  }
   472  
   473  // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified.
   474  //
   475  // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the
   476  // newest version will be returned.
   477  //
   478  // repoURL is the repository to search
   479  //
   480  // If it finds a URL that is "relative", it will prepend the repoURL.
   481  func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) {
   482  	for _, cr := range repos {
   483  		if urlutil.Equal(repoURL, cr.Config.URL) {
   484  			var entry repo.ChartVersions
   485  			entry, err = findEntryByName(name, cr)
   486  			if err != nil {
   487  				return
   488  			}
   489  			var ve *repo.ChartVersion
   490  			ve, err = findVersionedEntry(version, entry)
   491  			if err != nil {
   492  				return
   493  			}
   494  			url, err = normalizeURL(repoURL, ve.URLs[0])
   495  			if err != nil {
   496  				return
   497  			}
   498  			username = cr.Config.Username
   499  			password = cr.Config.Password
   500  			return
   501  		}
   502  	}
   503  	err = fmt.Errorf("chart %s not found in %s", name, repoURL)
   504  	return
   505  }
   506  
   507  // findEntryByName finds an entry in the chart repository whose name matches the given name.
   508  //
   509  // It returns the ChartVersions for that entry.
   510  func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) {
   511  	for ename, entry := range cr.IndexFile.Entries {
   512  		if ename == name {
   513  			return entry, nil
   514  		}
   515  	}
   516  	return nil, errors.New("entry not found")
   517  }
   518  
   519  // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints.
   520  //
   521  // If version is empty, the first chart found is returned.
   522  func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) {
   523  	for _, verEntry := range vers {
   524  		if len(verEntry.URLs) == 0 {
   525  			// Not a legit entry.
   526  			continue
   527  		}
   528  
   529  		if version == "" || versionEquals(version, verEntry.Version) {
   530  			return verEntry, nil
   531  		}
   532  	}
   533  	return nil, errors.New("no matching version")
   534  }
   535  
   536  func versionEquals(v1, v2 string) bool {
   537  	sv1, err := semver.NewVersion(v1)
   538  	if err != nil {
   539  		// Fallback to string comparison.
   540  		return v1 == v2
   541  	}
   542  	sv2, err := semver.NewVersion(v2)
   543  	if err != nil {
   544  		return false
   545  	}
   546  	return sv1.Equal(sv2)
   547  }
   548  
   549  func normalizeURL(baseURL, urlOrPath string) (string, error) {
   550  	u, err := url.Parse(urlOrPath)
   551  	if err != nil {
   552  		return urlOrPath, err
   553  	}
   554  	if u.IsAbs() {
   555  		return u.String(), nil
   556  	}
   557  	u2, err := url.Parse(baseURL)
   558  	if err != nil {
   559  		return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err)
   560  	}
   561  
   562  	u2.Path = path.Join(u2.Path, urlOrPath)
   563  	return u2.String(), nil
   564  }
   565  
   566  // loadChartRepositories reads the repositories.yaml, and then builds a map of
   567  // ChartRepositories.
   568  //
   569  // The key is the local name (which is only present in the repositories.yaml).
   570  func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) {
   571  	indices := map[string]*repo.ChartRepository{}
   572  	repoyaml := m.HelmHome.RepositoryFile()
   573  
   574  	// Load repositories.yaml file
   575  	rf, err := repo.LoadRepositoriesFile(repoyaml)
   576  	if err != nil {
   577  		return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err)
   578  	}
   579  
   580  	for _, re := range rf.Repositories {
   581  		lname := re.Name
   582  		cacheindex := m.HelmHome.CacheIndex(lname)
   583  		index, err := repo.LoadIndexFile(cacheindex)
   584  		if err != nil {
   585  			return indices, err
   586  		}
   587  
   588  		// TODO: use constructor
   589  		cr := &repo.ChartRepository{
   590  			Config:    re,
   591  			IndexFile: index,
   592  		}
   593  		indices[lname] = cr
   594  	}
   595  	return indices, nil
   596  }
   597  
   598  // writeLock writes a lockfile to disk
   599  func writeLock(chartpath string, lock *chartutil.RequirementsLock) error {
   600  	data, err := yaml.Marshal(lock)
   601  	if err != nil {
   602  		return err
   603  	}
   604  	dest := filepath.Join(chartpath, "requirements.lock")
   605  	return ioutil.WriteFile(dest, data, 0644)
   606  }
   607  
   608  // archive a dep chart from local directory and save it into charts/
   609  func tarFromLocalDir(chartpath string, name string, repo string, version string) (string, error) {
   610  	destPath := filepath.Join(chartpath, "charts")
   611  
   612  	if !strings.HasPrefix(repo, "file://") {
   613  		return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo)
   614  	}
   615  
   616  	origPath, err := resolver.GetLocalPath(repo, chartpath)
   617  	if err != nil {
   618  		return "", err
   619  	}
   620  
   621  	ch, err := chartutil.LoadDir(origPath)
   622  	if err != nil {
   623  		return "", err
   624  	}
   625  
   626  	constraint, err := semver.NewConstraint(version)
   627  	if err != nil {
   628  		return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %s", name, err)
   629  	}
   630  
   631  	v, err := semver.NewVersion(ch.Metadata.Version)
   632  	if err != nil {
   633  		return "", err
   634  	}
   635  
   636  	if constraint.Check(v) {
   637  		_, err = chartutil.Save(ch, destPath)
   638  		return ch.Metadata.Version, err
   639  	}
   640  
   641  	return "", fmt.Errorf("can't get a valid version for dependency %s", name)
   642  }
   643  
   644  // move files from tmppath to destpath
   645  func move(tmpPath, destPath string) error {
   646  	files, _ := ioutil.ReadDir(tmpPath)
   647  	for _, file := range files {
   648  		filename := file.Name()
   649  		tmpfile := filepath.Join(tmpPath, filename)
   650  		destfile := filepath.Join(destPath, filename)
   651  		if err := os.Rename(tmpfile, destfile); err != nil {
   652  			return fmt.Errorf("Unable to move local charts to charts dir: %v", err)
   653  		}
   654  	}
   655  	return nil
   656  }