github.com/sgoings/helm@v2.0.0-alpha.2.0.20170406211108-734e92851ac3+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/helm/helmpath"
    35  	"k8s.io/helm/pkg/proto/hapi/chart"
    36  	"k8s.io/helm/pkg/repo"
    37  	"k8s.io/helm/pkg/resolver"
    38  	"k8s.io/helm/pkg/urlutil"
    39  )
    40  
    41  // Manager handles the lifecycle of fetching, resolving, and storing dependencies.
    42  type Manager struct {
    43  	// Out is used to print warnings and notifications.
    44  	Out io.Writer
    45  	// ChartPath is the path to the unpacked base chart upon which this operates.
    46  	ChartPath string
    47  	// HelmHome is the $HELM_HOME directory
    48  	HelmHome helmpath.Home
    49  	// Verification indicates whether the chart should be verified.
    50  	Verify VerificationStrategy
    51  	// Debug is the global "--debug" flag
    52  	Debug bool
    53  	// Keyring is the key ring file.
    54  	Keyring string
    55  	// SkipUpdate indicates that the repository should not be updated first.
    56  	SkipUpdate bool
    57  }
    58  
    59  // Build rebuilds a local charts directory from a lockfile.
    60  //
    61  // If the lockfile is not present, this will run a Manager.Update()
    62  //
    63  // If SkipUpdate is set, this will not update the repository.
    64  func (m *Manager) Build() error {
    65  	c, err := m.loadChartDir()
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	// If a lock file is found, run a build from that. Otherwise, just do
    71  	// an update.
    72  	lock, err := chartutil.LoadRequirementsLock(c)
    73  	if err != nil {
    74  		return m.Update()
    75  	}
    76  
    77  	// A lock must accompany a requirements.yaml file.
    78  	req, err := chartutil.LoadRequirements(c)
    79  	if err != nil {
    80  		return fmt.Errorf("requirements.yaml cannot be opened: %s", err)
    81  	}
    82  	if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest {
    83  		return fmt.Errorf("requirements.lock is out of sync with requirements.yaml")
    84  	}
    85  
    86  	// Check that all of the repos we're dependent on actually exist.
    87  	if err := m.hasAllRepos(lock.Dependencies); err != nil {
    88  		return err
    89  	}
    90  
    91  	if !m.SkipUpdate {
    92  		// For each repo in the file, update the cached copy of that repo
    93  		if err := m.UpdateRepositories(); err != nil {
    94  			return err
    95  		}
    96  	}
    97  
    98  	// Now we need to fetch every package here into charts/
    99  	if err := m.downloadAll(lock.Dependencies); err != nil {
   100  		return err
   101  	}
   102  
   103  	return nil
   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  	dl := ChartDownloader{
   198  		Out:      m.Out,
   199  		Verify:   m.Verify,
   200  		Keyring:  m.Keyring,
   201  		HelmHome: m.HelmHome,
   202  	}
   203  
   204  	destPath := filepath.Join(m.ChartPath, "charts")
   205  
   206  	// Create 'charts' directory if it doesn't already exist.
   207  	if fi, err := os.Stat(destPath); err != nil {
   208  		if err := os.MkdirAll(destPath, 0755); err != nil {
   209  			return err
   210  		}
   211  	} else if !fi.IsDir() {
   212  		return fmt.Errorf("%q is not a directory", destPath)
   213  	}
   214  
   215  	fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
   216  	for _, dep := range deps {
   217  		if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
   218  			return err
   219  		}
   220  
   221  		if strings.HasPrefix(dep.Repository, "file://") {
   222  			if m.Debug {
   223  				fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
   224  			}
   225  			ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version)
   226  			if err != nil {
   227  				return err
   228  			}
   229  			dep.Version = ver
   230  			continue
   231  		}
   232  
   233  		fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
   234  
   235  		// Any failure to resolve/download a chart should fail:
   236  		// https://github.com/kubernetes/helm/issues/1439
   237  		churl, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos)
   238  		if err != nil {
   239  			return fmt.Errorf("could not find %s: %s", churl, err)
   240  		}
   241  
   242  		if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil {
   243  			return fmt.Errorf("could not download %s: %s", churl, err)
   244  		}
   245  	}
   246  	return nil
   247  }
   248  
   249  // safeDeleteDep deletes any versions of the given dependency in the given directory.
   250  //
   251  // It does this by first matching the file name to an expected pattern, then loading
   252  // the file to verify that it is a chart with the same name as the given name.
   253  //
   254  // Because it requires tar file introspection, it is more intensive than a basic delete.
   255  //
   256  // This will only return errors that should stop processing entirely. Other errors
   257  // will emit log messages or be ignored.
   258  func (m *Manager) safeDeleteDep(name, dir string) error {
   259  	files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
   260  	if err != nil {
   261  		// Only for ErrBadPattern
   262  		return err
   263  	}
   264  	for _, fname := range files {
   265  		ch, err := chartutil.LoadFile(fname)
   266  		if err != nil {
   267  			fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
   268  			continue
   269  		}
   270  		if ch.Metadata.Name != name {
   271  			// This is not the file you are looking for.
   272  			continue
   273  		}
   274  		if err := os.Remove(fname); err != nil {
   275  			fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
   276  			continue
   277  		}
   278  	}
   279  	return nil
   280  }
   281  
   282  // hasAllRepos ensures that all of the referenced deps are in the local repo cache.
   283  func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
   284  	rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
   285  	if err != nil {
   286  		return err
   287  	}
   288  	repos := rf.Repositories
   289  
   290  	// Verify that all repositories referenced in the deps are actually known
   291  	// by Helm.
   292  	missing := []string{}
   293  	for _, dd := range deps {
   294  		// If repo is from local path, continue
   295  		if strings.HasPrefix(dd.Repository, "file://") {
   296  			continue
   297  		}
   298  
   299  		found := false
   300  		if dd.Repository == "" {
   301  			found = true
   302  		} else {
   303  			for _, repo := range repos {
   304  				if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
   305  					found = true
   306  				}
   307  			}
   308  		}
   309  		if !found {
   310  			missing = append(missing, dd.Repository)
   311  		}
   312  	}
   313  	if len(missing) > 0 {
   314  		return fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", "))
   315  	}
   316  	return nil
   317  }
   318  
   319  // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file.
   320  func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, error) {
   321  	rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  	repos := rf.Repositories
   326  
   327  	reposMap := make(map[string]string)
   328  
   329  	// Verify that all repositories referenced in the deps are actually known
   330  	// by Helm.
   331  	missing := []string{}
   332  	for _, dd := range deps {
   333  		// if dep chart is from local path, verify the path is valid
   334  		if strings.HasPrefix(dd.Repository, "file://") {
   335  			if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil {
   336  				return nil, err
   337  			}
   338  
   339  			if m.Debug {
   340  				fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository)
   341  			}
   342  			reposMap[dd.Name] = dd.Repository
   343  			continue
   344  		}
   345  
   346  		found := false
   347  
   348  		for _, repo := range repos {
   349  			if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) ||
   350  				(strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) {
   351  				found = true
   352  				dd.Repository = repo.URL
   353  				reposMap[dd.Name] = repo.Name
   354  				break
   355  			} else if urlutil.Equal(repo.URL, dd.Repository) {
   356  				found = true
   357  				reposMap[dd.Name] = repo.Name
   358  				break
   359  			}
   360  		}
   361  		if !found {
   362  			missing = append(missing, dd.Repository)
   363  		}
   364  	}
   365  	if len(missing) > 0 {
   366  		return nil, fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", "))
   367  	}
   368  	return reposMap, nil
   369  }
   370  
   371  // UpdateRepositories updates all of the local repos to the latest.
   372  func (m *Manager) UpdateRepositories() error {
   373  	rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
   374  	if err != nil {
   375  		return err
   376  	}
   377  	repos := rf.Repositories
   378  	if len(repos) > 0 {
   379  		// This prints warnings straight to out.
   380  		if err := m.parallelRepoUpdate(repos); err != nil {
   381  			return err
   382  		}
   383  	}
   384  	return nil
   385  }
   386  
   387  func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
   388  	out := m.Out
   389  	fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
   390  	var wg sync.WaitGroup
   391  	for _, c := range repos {
   392  		r, err := repo.NewChartRepository(c)
   393  		if err != nil {
   394  			return err
   395  		}
   396  		wg.Add(1)
   397  		go func(r *repo.ChartRepository) {
   398  			if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil {
   399  				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)
   400  			} else {
   401  				fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
   402  			}
   403  			wg.Done()
   404  		}(r)
   405  	}
   406  	wg.Wait()
   407  	fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
   408  	return nil
   409  }
   410  
   411  // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified.
   412  //
   413  // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the
   414  // newest version will be returned.
   415  //
   416  // repoURL is the repository to search
   417  //
   418  // If it finds a URL that is "relative", it will prepend the repoURL.
   419  func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) {
   420  	for _, cr := range repos {
   421  		if urlutil.Equal(repoURL, cr.Config.URL) {
   422  			entry, err := findEntryByName(name, cr)
   423  			if err != nil {
   424  				return "", err
   425  			}
   426  			ve, err := findVersionedEntry(version, entry)
   427  			if err != nil {
   428  				return "", err
   429  			}
   430  
   431  			return normalizeURL(repoURL, ve.URLs[0])
   432  		}
   433  	}
   434  	return "", fmt.Errorf("chart %s not found in %s", name, repoURL)
   435  }
   436  
   437  // findEntryByName finds an entry in the chart repository whose name matches the given name.
   438  //
   439  // It returns the ChartVersions for that entry.
   440  func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) {
   441  	for ename, entry := range cr.IndexFile.Entries {
   442  		if ename == name {
   443  			return entry, nil
   444  		}
   445  	}
   446  	return nil, errors.New("entry not found")
   447  }
   448  
   449  // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints.
   450  //
   451  // If version is empty, the first chart found is returned.
   452  func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) {
   453  	for _, verEntry := range vers {
   454  		if len(verEntry.URLs) == 0 {
   455  			// Not a legit entry.
   456  			continue
   457  		}
   458  
   459  		if version == "" || versionEquals(version, verEntry.Version) {
   460  			return verEntry, nil
   461  		}
   462  	}
   463  	return nil, errors.New("no matching version")
   464  }
   465  
   466  func versionEquals(v1, v2 string) bool {
   467  	sv1, err := semver.NewVersion(v1)
   468  	if err != nil {
   469  		// Fallback to string comparison.
   470  		return v1 == v2
   471  	}
   472  	sv2, err := semver.NewVersion(v2)
   473  	if err != nil {
   474  		return false
   475  	}
   476  	return sv1.Equal(sv2)
   477  }
   478  
   479  func normalizeURL(baseURL, urlOrPath string) (string, error) {
   480  	u, err := url.Parse(urlOrPath)
   481  	if err != nil {
   482  		return urlOrPath, err
   483  	}
   484  	if u.IsAbs() {
   485  		return u.String(), nil
   486  	}
   487  	u2, err := url.Parse(baseURL)
   488  	if err != nil {
   489  		return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err)
   490  	}
   491  
   492  	u2.Path = path.Join(u2.Path, urlOrPath)
   493  	return u2.String(), nil
   494  }
   495  
   496  // loadChartRepositories reads the repositories.yaml, and then builds a map of
   497  // ChartRepositories.
   498  //
   499  // The key is the local name (which is only present in the repositories.yaml).
   500  func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) {
   501  	indices := map[string]*repo.ChartRepository{}
   502  	repoyaml := m.HelmHome.RepositoryFile()
   503  
   504  	// Load repositories.yaml file
   505  	rf, err := repo.LoadRepositoriesFile(repoyaml)
   506  	if err != nil {
   507  		return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err)
   508  	}
   509  
   510  	for _, re := range rf.Repositories {
   511  		lname := re.Name
   512  		cacheindex := m.HelmHome.CacheIndex(lname)
   513  		index, err := repo.LoadIndexFile(cacheindex)
   514  		if err != nil {
   515  			return indices, err
   516  		}
   517  
   518  		// TODO: use constructor
   519  		cr := &repo.ChartRepository{
   520  			Config:    re,
   521  			IndexFile: index,
   522  		}
   523  		indices[lname] = cr
   524  	}
   525  	return indices, nil
   526  }
   527  
   528  // writeLock writes a lockfile to disk
   529  func writeLock(chartpath string, lock *chartutil.RequirementsLock) error {
   530  	data, err := yaml.Marshal(lock)
   531  	if err != nil {
   532  		return err
   533  	}
   534  	dest := filepath.Join(chartpath, "requirements.lock")
   535  	return ioutil.WriteFile(dest, data, 0644)
   536  }
   537  
   538  // archive a dep chart from local directory and save it into charts/
   539  func tarFromLocalDir(chartpath string, name string, repo string, version string) (string, error) {
   540  	destPath := filepath.Join(chartpath, "charts")
   541  
   542  	if !strings.HasPrefix(repo, "file://") {
   543  		return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo)
   544  	}
   545  
   546  	origPath, err := resolver.GetLocalPath(repo, chartpath)
   547  	if err != nil {
   548  		return "", err
   549  	}
   550  
   551  	ch, err := chartutil.LoadDir(origPath)
   552  	if err != nil {
   553  		return "", err
   554  	}
   555  
   556  	constraint, err := semver.NewConstraint(version)
   557  	if err != nil {
   558  		return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %s", name, err)
   559  	}
   560  
   561  	v, err := semver.NewVersion(ch.Metadata.Version)
   562  	if err != nil {
   563  		return "", err
   564  	}
   565  
   566  	if constraint.Check(v) {
   567  		_, err = chartutil.Save(ch, destPath)
   568  		return ch.Metadata.Version, err
   569  	}
   570  
   571  	return "", fmt.Errorf("Can't get a valid version for dependency %s.", name)
   572  }