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