github.com/darkowlzz/helm@v2.5.1-0.20171213183701-6707fe0468d4+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  	dl := ChartDownloader{
   201  		Out:      m.Out,
   202  		Verify:   m.Verify,
   203  		Keyring:  m.Keyring,
   204  		HelmHome: m.HelmHome,
   205  		Getters:  m.Getters,
   206  	}
   207  
   208  	destPath := filepath.Join(m.ChartPath, "charts")
   209  	tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
   210  
   211  	// Create 'charts' directory if it doesn't already exist.
   212  	if fi, err := os.Stat(destPath); err != nil {
   213  		if err := os.MkdirAll(destPath, 0755); err != nil {
   214  			return err
   215  		}
   216  	} else if !fi.IsDir() {
   217  		return fmt.Errorf("%q is not a directory", destPath)
   218  	}
   219  
   220  	if err := os.Rename(destPath, tmpPath); err != nil {
   221  		return fmt.Errorf("Unable to move current charts to tmp dir: %v", err)
   222  	}
   223  
   224  	if err := os.MkdirAll(destPath, 0755); err != nil {
   225  		return err
   226  	}
   227  
   228  	fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
   229  	var saveError error
   230  	for _, dep := range deps {
   231  		if strings.HasPrefix(dep.Repository, "file://") {
   232  			if m.Debug {
   233  				fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
   234  			}
   235  			ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version)
   236  			if err != nil {
   237  				saveError = err
   238  				break
   239  			}
   240  			dep.Version = ver
   241  			continue
   242  		}
   243  
   244  		fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
   245  
   246  		// Any failure to resolve/download a chart should fail:
   247  		// https://github.com/kubernetes/helm/issues/1439
   248  		churl, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos)
   249  		if err != nil {
   250  			saveError = fmt.Errorf("could not find %s: %s", churl, err)
   251  			break
   252  		}
   253  
   254  		if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil {
   255  			saveError = fmt.Errorf("could not download %s: %s", churl, err)
   256  			break
   257  		}
   258  	}
   259  
   260  	if saveError == nil {
   261  		fmt.Fprintln(m.Out, "Deleting outdated charts")
   262  		for _, dep := range deps {
   263  			if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil {
   264  				return err
   265  			}
   266  		}
   267  		if err := move(tmpPath, destPath); err != nil {
   268  			return err
   269  		}
   270  		if err := os.RemoveAll(tmpPath); err != nil {
   271  			return fmt.Errorf("Failed to remove %v: %v", tmpPath, err)
   272  		}
   273  	} else {
   274  		fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
   275  		fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state")
   276  		for _, dep := range deps {
   277  			if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
   278  				return err
   279  			}
   280  		}
   281  		if err := os.RemoveAll(destPath); err != nil {
   282  			return fmt.Errorf("Failed to remove %v: %v", destPath, err)
   283  		}
   284  		if err := os.Rename(tmpPath, destPath); err != nil {
   285  			return fmt.Errorf("Unable to move current charts to tmp dir: %v", err)
   286  		}
   287  		return saveError
   288  	}
   289  	return nil
   290  }
   291  
   292  // safeDeleteDep deletes any versions of the given dependency in the given directory.
   293  //
   294  // It does this by first matching the file name to an expected pattern, then loading
   295  // the file to verify that it is a chart with the same name as the given name.
   296  //
   297  // Because it requires tar file introspection, it is more intensive than a basic delete.
   298  //
   299  // This will only return errors that should stop processing entirely. Other errors
   300  // will emit log messages or be ignored.
   301  func (m *Manager) safeDeleteDep(name, dir string) error {
   302  	files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
   303  	if err != nil {
   304  		// Only for ErrBadPattern
   305  		return err
   306  	}
   307  	for _, fname := range files {
   308  		ch, err := chartutil.LoadFile(fname)
   309  		if err != nil {
   310  			fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
   311  			continue
   312  		}
   313  		if ch.Metadata.Name != name {
   314  			// This is not the file you are looking for.
   315  			continue
   316  		}
   317  		if err := os.Remove(fname); err != nil {
   318  			fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
   319  			continue
   320  		}
   321  	}
   322  	return nil
   323  }
   324  
   325  // hasAllRepos ensures that all of the referenced deps are in the local repo cache.
   326  func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
   327  	rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
   328  	if err != nil {
   329  		return err
   330  	}
   331  	repos := rf.Repositories
   332  
   333  	// Verify that all repositories referenced in the deps are actually known
   334  	// by Helm.
   335  	missing := []string{}
   336  	for _, dd := range deps {
   337  		// If repo is from local path, continue
   338  		if strings.HasPrefix(dd.Repository, "file://") {
   339  			continue
   340  		}
   341  
   342  		found := false
   343  		if dd.Repository == "" {
   344  			found = true
   345  		} else {
   346  			for _, repo := range repos {
   347  				if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
   348  					found = true
   349  				}
   350  			}
   351  		}
   352  		if !found {
   353  			missing = append(missing, dd.Repository)
   354  		}
   355  	}
   356  	if len(missing) > 0 {
   357  		return fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", "))
   358  	}
   359  	return nil
   360  }
   361  
   362  // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file.
   363  func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, error) {
   364  	rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
   365  	if err != nil {
   366  		return nil, err
   367  	}
   368  	repos := rf.Repositories
   369  
   370  	reposMap := make(map[string]string)
   371  
   372  	// Verify that all repositories referenced in the deps are actually known
   373  	// by Helm.
   374  	missing := []string{}
   375  	for _, dd := range deps {
   376  		// if dep chart is from local path, verify the path is valid
   377  		if strings.HasPrefix(dd.Repository, "file://") {
   378  			if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil {
   379  				return nil, err
   380  			}
   381  
   382  			if m.Debug {
   383  				fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository)
   384  			}
   385  			reposMap[dd.Name] = dd.Repository
   386  			continue
   387  		}
   388  
   389  		found := false
   390  
   391  		for _, repo := range repos {
   392  			if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) ||
   393  				(strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) {
   394  				found = true
   395  				dd.Repository = repo.URL
   396  				reposMap[dd.Name] = repo.Name
   397  				break
   398  			} else if urlutil.Equal(repo.URL, dd.Repository) {
   399  				found = true
   400  				reposMap[dd.Name] = repo.Name
   401  				break
   402  			}
   403  		}
   404  		if !found {
   405  			missing = append(missing, dd.Repository)
   406  		}
   407  	}
   408  	if len(missing) > 0 {
   409  		return nil, fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", "))
   410  	}
   411  	return reposMap, nil
   412  }
   413  
   414  // UpdateRepositories updates all of the local repos to the latest.
   415  func (m *Manager) UpdateRepositories() error {
   416  	rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
   417  	if err != nil {
   418  		return err
   419  	}
   420  	repos := rf.Repositories
   421  	if len(repos) > 0 {
   422  		// This prints warnings straight to out.
   423  		if err := m.parallelRepoUpdate(repos); err != nil {
   424  			return err
   425  		}
   426  	}
   427  	return nil
   428  }
   429  
   430  func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
   431  	out := m.Out
   432  	fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
   433  	var wg sync.WaitGroup
   434  	for _, c := range repos {
   435  		r, err := repo.NewChartRepository(c, m.Getters)
   436  		if err != nil {
   437  			return err
   438  		}
   439  		wg.Add(1)
   440  		go func(r *repo.ChartRepository) {
   441  			if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil {
   442  				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)
   443  			} else {
   444  				fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
   445  			}
   446  			wg.Done()
   447  		}(r)
   448  	}
   449  	wg.Wait()
   450  	fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
   451  	return nil
   452  }
   453  
   454  // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified.
   455  //
   456  // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the
   457  // newest version will be returned.
   458  //
   459  // repoURL is the repository to search
   460  //
   461  // If it finds a URL that is "relative", it will prepend the repoURL.
   462  func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) {
   463  	for _, cr := range repos {
   464  		if urlutil.Equal(repoURL, cr.Config.URL) {
   465  			entry, err := findEntryByName(name, cr)
   466  			if err != nil {
   467  				return "", err
   468  			}
   469  			ve, err := findVersionedEntry(version, entry)
   470  			if err != nil {
   471  				return "", err
   472  			}
   473  
   474  			return normalizeURL(repoURL, ve.URLs[0])
   475  		}
   476  	}
   477  	return "", fmt.Errorf("chart %s not found in %s", name, repoURL)
   478  }
   479  
   480  // findEntryByName finds an entry in the chart repository whose name matches the given name.
   481  //
   482  // It returns the ChartVersions for that entry.
   483  func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) {
   484  	for ename, entry := range cr.IndexFile.Entries {
   485  		if ename == name {
   486  			return entry, nil
   487  		}
   488  	}
   489  	return nil, errors.New("entry not found")
   490  }
   491  
   492  // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints.
   493  //
   494  // If version is empty, the first chart found is returned.
   495  func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) {
   496  	for _, verEntry := range vers {
   497  		if len(verEntry.URLs) == 0 {
   498  			// Not a legit entry.
   499  			continue
   500  		}
   501  
   502  		if version == "" || versionEquals(version, verEntry.Version) {
   503  			return verEntry, nil
   504  		}
   505  	}
   506  	return nil, errors.New("no matching version")
   507  }
   508  
   509  func versionEquals(v1, v2 string) bool {
   510  	sv1, err := semver.NewVersion(v1)
   511  	if err != nil {
   512  		// Fallback to string comparison.
   513  		return v1 == v2
   514  	}
   515  	sv2, err := semver.NewVersion(v2)
   516  	if err != nil {
   517  		return false
   518  	}
   519  	return sv1.Equal(sv2)
   520  }
   521  
   522  func normalizeURL(baseURL, urlOrPath string) (string, error) {
   523  	u, err := url.Parse(urlOrPath)
   524  	if err != nil {
   525  		return urlOrPath, err
   526  	}
   527  	if u.IsAbs() {
   528  		return u.String(), nil
   529  	}
   530  	u2, err := url.Parse(baseURL)
   531  	if err != nil {
   532  		return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err)
   533  	}
   534  
   535  	u2.Path = path.Join(u2.Path, urlOrPath)
   536  	return u2.String(), nil
   537  }
   538  
   539  // loadChartRepositories reads the repositories.yaml, and then builds a map of
   540  // ChartRepositories.
   541  //
   542  // The key is the local name (which is only present in the repositories.yaml).
   543  func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) {
   544  	indices := map[string]*repo.ChartRepository{}
   545  	repoyaml := m.HelmHome.RepositoryFile()
   546  
   547  	// Load repositories.yaml file
   548  	rf, err := repo.LoadRepositoriesFile(repoyaml)
   549  	if err != nil {
   550  		return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err)
   551  	}
   552  
   553  	for _, re := range rf.Repositories {
   554  		lname := re.Name
   555  		cacheindex := m.HelmHome.CacheIndex(lname)
   556  		index, err := repo.LoadIndexFile(cacheindex)
   557  		if err != nil {
   558  			return indices, err
   559  		}
   560  
   561  		// TODO: use constructor
   562  		cr := &repo.ChartRepository{
   563  			Config:    re,
   564  			IndexFile: index,
   565  		}
   566  		indices[lname] = cr
   567  	}
   568  	return indices, nil
   569  }
   570  
   571  // writeLock writes a lockfile to disk
   572  func writeLock(chartpath string, lock *chartutil.RequirementsLock) error {
   573  	data, err := yaml.Marshal(lock)
   574  	if err != nil {
   575  		return err
   576  	}
   577  	dest := filepath.Join(chartpath, "requirements.lock")
   578  	return ioutil.WriteFile(dest, data, 0644)
   579  }
   580  
   581  // archive a dep chart from local directory and save it into charts/
   582  func tarFromLocalDir(chartpath string, name string, repo string, version string) (string, error) {
   583  	destPath := filepath.Join(chartpath, "charts")
   584  
   585  	if !strings.HasPrefix(repo, "file://") {
   586  		return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo)
   587  	}
   588  
   589  	origPath, err := resolver.GetLocalPath(repo, chartpath)
   590  	if err != nil {
   591  		return "", err
   592  	}
   593  
   594  	ch, err := chartutil.LoadDir(origPath)
   595  	if err != nil {
   596  		return "", err
   597  	}
   598  
   599  	constraint, err := semver.NewConstraint(version)
   600  	if err != nil {
   601  		return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %s", name, err)
   602  	}
   603  
   604  	v, err := semver.NewVersion(ch.Metadata.Version)
   605  	if err != nil {
   606  		return "", err
   607  	}
   608  
   609  	if constraint.Check(v) {
   610  		_, err = chartutil.Save(ch, destPath)
   611  		return ch.Metadata.Version, err
   612  	}
   613  
   614  	return "", fmt.Errorf("can't get a valid version for dependency %s", name)
   615  }
   616  
   617  // move files from tmppath to destpath
   618  func move(tmpPath, destPath string) error {
   619  	files, _ := ioutil.ReadDir(tmpPath)
   620  	for _, file := range files {
   621  		filename := file.Name()
   622  		tmpfile := filepath.Join(tmpPath, filename)
   623  		destfile := filepath.Join(destPath, filename)
   624  		if err := os.Rename(tmpfile, destfile); err != nil {
   625  			return fmt.Errorf("Unable to move local charts to charts dir: %v", err)
   626  		}
   627  	}
   628  	return nil
   629  }