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