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

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