github.com/x-helm/helm@v3.0.0-beta.3+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  	"fmt"
    20  	"io"
    21  	"io/ioutil"
    22  	"net/url"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"strings"
    27  	"sync"
    28  
    29  	"github.com/Masterminds/semver"
    30  	"github.com/pkg/errors"
    31  	"sigs.k8s.io/yaml"
    32  
    33  	"helm.sh/helm/internal/resolver"
    34  	"helm.sh/helm/internal/urlutil"
    35  	"helm.sh/helm/pkg/chart"
    36  	"helm.sh/helm/pkg/chart/loader"
    37  	"helm.sh/helm/pkg/chartutil"
    38  	"helm.sh/helm/pkg/getter"
    39  	"helm.sh/helm/pkg/helmpath"
    40  	"helm.sh/helm/pkg/repo"
    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  	// 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  	// Getter collection for the operation
    58  	Getters          []getter.Provider
    59  	RepositoryConfig string
    60  	RepositoryCache  string
    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 := c.Lock
    77  	if lock == nil {
    78  		return m.Update()
    79  	}
    80  
    81  	req := c.Metadata.Dependencies
    82  	if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest {
    83  		return errors.New("Chart.lock is out of sync with Chart.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  	return m.downloadAll(lock.Dependencies)
   100  }
   101  
   102  // Update updates a local charts directory.
   103  //
   104  // It first reads the Chart.yaml file, and then attempts to
   105  // negotiate versions based on that. It will download the versions
   106  // from remote chart repositories unless SkipUpdate is true.
   107  func (m *Manager) Update() error {
   108  	c, err := m.loadChartDir()
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	// If no dependencies are found, we consider this a successful
   114  	// completion.
   115  	req := c.Metadata.Dependencies
   116  	if req == nil {
   117  		return nil
   118  	}
   119  
   120  	// Check that all of the repos we're dependent on actually exist and
   121  	// the repo index names.
   122  	repoNames, err := m.getRepoNames(req)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	// For each repo in the file, update the cached copy of that repo
   128  	if !m.SkipUpdate {
   129  		if err := m.UpdateRepositories(); err != nil {
   130  			return err
   131  		}
   132  	}
   133  
   134  	// Now we need to find out which version of a chart best satisfies the
   135  	// dependencies in the Chart.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 := c.Lock
   148  	if oldLock != 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, errors.Wrapf(err, "could not find %s", m.ChartPath)
   159  	} else if !fi.IsDir() {
   160  		return nil, errors.New("only unpacked charts can be updated")
   161  	}
   162  	return loader.LoadDir(m.ChartPath)
   163  }
   164  
   165  // resolve takes a list of dependencies and translates them into an exact version to download.
   166  //
   167  // This returns a lock file, which has all of the dependencies normalized to a specific version.
   168  func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) {
   169  	res := resolver.New(m.ChartPath, m.RepositoryCache)
   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 []*chart.Dependency) error {
   178  	repos, err := m.loadChartRepositories()
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	destPath := filepath.Join(m.ChartPath, "charts")
   184  	tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
   185  
   186  	// Create 'charts' directory if it doesn't already exist.
   187  	if fi, err := os.Stat(destPath); err != nil {
   188  		if err := os.MkdirAll(destPath, 0755); err != nil {
   189  			return err
   190  		}
   191  	} else if !fi.IsDir() {
   192  		return errors.Errorf("%q is not a directory", destPath)
   193  	}
   194  
   195  	if err := os.Rename(destPath, tmpPath); err != nil {
   196  		return errors.Wrap(err, "unable to move current charts to tmp dir")
   197  	}
   198  
   199  	if err := os.MkdirAll(destPath, 0755); err != nil {
   200  		return err
   201  	}
   202  
   203  	fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
   204  	var saveError error
   205  	for _, dep := range deps {
   206  		if strings.HasPrefix(dep.Repository, "file://") {
   207  			if m.Debug {
   208  				fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
   209  			}
   210  			ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version)
   211  			if err != nil {
   212  				saveError = err
   213  				break
   214  			}
   215  			dep.Version = ver
   216  			continue
   217  		}
   218  
   219  		fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
   220  
   221  		// Any failure to resolve/download a chart should fail:
   222  		// https://github.com/helm/helm/issues/1439
   223  		churl, username, password, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos)
   224  		if err != nil {
   225  			saveError = errors.Wrapf(err, "could not find %s", churl)
   226  			break
   227  		}
   228  
   229  		dl := ChartDownloader{
   230  			Out:              m.Out,
   231  			Verify:           m.Verify,
   232  			Keyring:          m.Keyring,
   233  			RepositoryConfig: m.RepositoryConfig,
   234  			RepositoryCache:  m.RepositoryCache,
   235  			Getters:          m.Getters,
   236  			Options: []getter.Option{
   237  				getter.WithBasicAuth(username, password),
   238  			},
   239  		}
   240  
   241  		if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil {
   242  			saveError = errors.Wrapf(err, "could not download %s", churl)
   243  			break
   244  		}
   245  	}
   246  
   247  	if saveError == nil {
   248  		fmt.Fprintln(m.Out, "Deleting outdated charts")
   249  		for _, dep := range deps {
   250  			if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil {
   251  				return err
   252  			}
   253  		}
   254  		if err := move(tmpPath, destPath); err != nil {
   255  			return err
   256  		}
   257  		if err := os.RemoveAll(tmpPath); err != nil {
   258  			return errors.Wrapf(err, "failed to remove %v", tmpPath)
   259  		}
   260  	} else {
   261  		fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
   262  		fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state")
   263  		for _, dep := range deps {
   264  			if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
   265  				return err
   266  			}
   267  		}
   268  		if err := os.RemoveAll(destPath); err != nil {
   269  			return errors.Wrapf(err, "failed to remove %v", destPath)
   270  		}
   271  		if err := os.Rename(tmpPath, destPath); err != nil {
   272  			return errors.Wrap(err, "unable to move current charts to tmp dir")
   273  		}
   274  		return saveError
   275  	}
   276  	return nil
   277  }
   278  
   279  // safeDeleteDep deletes any versions of the given dependency in the given directory.
   280  //
   281  // It does this by first matching the file name to an expected pattern, then loading
   282  // the file to verify that it is a chart with the same name as the given name.
   283  //
   284  // Because it requires tar file introspection, it is more intensive than a basic delete.
   285  //
   286  // This will only return errors that should stop processing entirely. Other errors
   287  // will emit log messages or be ignored.
   288  func (m *Manager) safeDeleteDep(name, dir string) error {
   289  	files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
   290  	if err != nil {
   291  		// Only for ErrBadPattern
   292  		return err
   293  	}
   294  	for _, fname := range files {
   295  		ch, err := loader.LoadFile(fname)
   296  		if err != nil {
   297  			fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
   298  			continue
   299  		}
   300  		if ch.Name() != name {
   301  			// This is not the file you are looking for.
   302  			continue
   303  		}
   304  		if err := os.Remove(fname); err != nil {
   305  			fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
   306  			continue
   307  		}
   308  	}
   309  	return nil
   310  }
   311  
   312  // hasAllRepos ensures that all of the referenced deps are in the local repo cache.
   313  func (m *Manager) hasAllRepos(deps []*chart.Dependency) error {
   314  	rf, err := loadRepoConfig(m.RepositoryConfig)
   315  	if err != nil {
   316  		return err
   317  	}
   318  	repos := rf.Repositories
   319  
   320  	// Verify that all repositories referenced in the deps are actually known
   321  	// by Helm.
   322  	missing := []string{}
   323  Loop:
   324  	for _, dd := range deps {
   325  		// If repo is from local path, continue
   326  		if strings.HasPrefix(dd.Repository, "file://") {
   327  			continue
   328  		}
   329  
   330  		if dd.Repository == "" {
   331  			continue
   332  		}
   333  		for _, repo := range repos {
   334  			if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
   335  				continue Loop
   336  			}
   337  		}
   338  		missing = append(missing, dd.Repository)
   339  	}
   340  	if len(missing) > 0 {
   341  		return errors.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", "))
   342  	}
   343  	return nil
   344  }
   345  
   346  // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file.
   347  func (m *Manager) getRepoNames(deps []*chart.Dependency) (map[string]string, error) {
   348  	rf, err := loadRepoConfig(m.RepositoryConfig)
   349  	if err != nil {
   350  		if os.IsNotExist(err) {
   351  			return make(map[string]string), nil
   352  		}
   353  		return nil, err
   354  	}
   355  	repos := rf.Repositories
   356  
   357  	reposMap := make(map[string]string)
   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 dep chart is from local path, verify the path is valid
   364  		if strings.HasPrefix(dd.Repository, "file://") {
   365  			if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil {
   366  				return nil, err
   367  			}
   368  
   369  			if m.Debug {
   370  				fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository)
   371  			}
   372  			reposMap[dd.Name] = dd.Repository
   373  			continue
   374  		}
   375  
   376  		found := false
   377  
   378  		for _, repo := range repos {
   379  			if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) ||
   380  				(strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) {
   381  				found = true
   382  				dd.Repository = repo.URL
   383  				reposMap[dd.Name] = repo.Name
   384  				break
   385  			} else if urlutil.Equal(repo.URL, dd.Repository) {
   386  				found = true
   387  				reposMap[dd.Name] = repo.Name
   388  				break
   389  			}
   390  		}
   391  		if !found {
   392  			missing = append(missing, dd.Repository)
   393  		}
   394  	}
   395  	if len(missing) > 0 {
   396  		errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", "))
   397  		// It is common for people to try to enter "stable" as a repository instead of the actual URL.
   398  		// For this case, let's give them a suggestion.
   399  		containsNonURL := false
   400  		for _, repo := range missing {
   401  			if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") {
   402  				containsNonURL = true
   403  			}
   404  		}
   405  		if containsNonURL {
   406  			errorMessage += `
   407  Note that repositories must be URLs or aliases. For example, to refer to the "example"
   408  repository, use "https://charts.example.com/" or "@example" instead of
   409  "example". Don't forget to add the repo, too ('helm repo add').`
   410  		}
   411  		return nil, errors.New(errorMessage)
   412  	}
   413  	return reposMap, nil
   414  }
   415  
   416  // UpdateRepositories updates all of the local repos to the latest.
   417  func (m *Manager) UpdateRepositories() error {
   418  	rf, err := loadRepoConfig(m.RepositoryConfig)
   419  	if err != nil {
   420  		return err
   421  	}
   422  	repos := rf.Repositories
   423  	if len(repos) > 0 {
   424  		// This prints warnings straight to out.
   425  		if err := m.parallelRepoUpdate(repos); err != nil {
   426  			return err
   427  		}
   428  	}
   429  	return nil
   430  }
   431  
   432  func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
   433  	fmt.Fprintln(m.Out, "Hang tight while we grab the latest from your chart repositories...")
   434  	var wg sync.WaitGroup
   435  	for _, c := range repos {
   436  		r, err := repo.NewChartRepository(c, m.Getters)
   437  		if err != nil {
   438  			return err
   439  		}
   440  		wg.Add(1)
   441  		go func(r *repo.ChartRepository) {
   442  			if _, err := r.DownloadIndexFile(); err != nil {
   443  				fmt.Fprintf(m.Out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err)
   444  			} else {
   445  				fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
   446  			}
   447  			wg.Done()
   448  		}(r)
   449  	}
   450  	wg.Wait()
   451  	fmt.Fprintln(m.Out, "Update Complete. ⎈Happy Helming!⎈")
   452  	return nil
   453  }
   454  
   455  // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified.
   456  //
   457  // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the
   458  // newest version will be returned.
   459  //
   460  // repoURL is the repository to search
   461  //
   462  // If it finds a URL that is "relative", it will prepend the repoURL.
   463  func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) {
   464  	for _, cr := range repos {
   465  		if urlutil.Equal(repoURL, cr.Config.URL) {
   466  			var entry repo.ChartVersions
   467  			entry, err = findEntryByName(name, cr)
   468  			if err != nil {
   469  				return
   470  			}
   471  			var ve *repo.ChartVersion
   472  			ve, err = findVersionedEntry(version, entry)
   473  			if err != nil {
   474  				return
   475  			}
   476  			url, err = normalizeURL(repoURL, ve.URLs[0])
   477  			if err != nil {
   478  				return
   479  			}
   480  			username = cr.Config.Username
   481  			password = cr.Config.Password
   482  			return
   483  		}
   484  	}
   485  	err = errors.Errorf("chart %s not found in %s", name, repoURL)
   486  	return
   487  }
   488  
   489  // findEntryByName finds an entry in the chart repository whose name matches the given name.
   490  //
   491  // It returns the ChartVersions for that entry.
   492  func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) {
   493  	for ename, entry := range cr.IndexFile.Entries {
   494  		if ename == name {
   495  			return entry, nil
   496  		}
   497  	}
   498  	return nil, errors.New("entry not found")
   499  }
   500  
   501  // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints.
   502  //
   503  // If version is empty, the first chart found is returned.
   504  func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) {
   505  	for _, verEntry := range vers {
   506  		if len(verEntry.URLs) == 0 {
   507  			// Not a legit entry.
   508  			continue
   509  		}
   510  
   511  		if version == "" || versionEquals(version, verEntry.Version) {
   512  			return verEntry, nil
   513  		}
   514  	}
   515  	return nil, errors.New("no matching version")
   516  }
   517  
   518  func versionEquals(v1, v2 string) bool {
   519  	sv1, err := semver.NewVersion(v1)
   520  	if err != nil {
   521  		// Fallback to string comparison.
   522  		return v1 == v2
   523  	}
   524  	sv2, err := semver.NewVersion(v2)
   525  	if err != nil {
   526  		return false
   527  	}
   528  	return sv1.Equal(sv2)
   529  }
   530  
   531  func normalizeURL(baseURL, urlOrPath string) (string, error) {
   532  	u, err := url.Parse(urlOrPath)
   533  	if err != nil {
   534  		return urlOrPath, err
   535  	}
   536  	if u.IsAbs() {
   537  		return u.String(), nil
   538  	}
   539  	u2, err := url.Parse(baseURL)
   540  	if err != nil {
   541  		return urlOrPath, errors.Wrap(err, "base URL failed to parse")
   542  	}
   543  
   544  	u2.Path = path.Join(u2.Path, urlOrPath)
   545  	return u2.String(), nil
   546  }
   547  
   548  // loadChartRepositories reads the repositories.yaml, and then builds a map of
   549  // ChartRepositories.
   550  //
   551  // The key is the local name (which is only present in the repositories.yaml).
   552  func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) {
   553  	indices := map[string]*repo.ChartRepository{}
   554  
   555  	// Load repositories.yaml file
   556  	rf, err := loadRepoConfig(m.RepositoryConfig)
   557  	if err != nil {
   558  		return indices, errors.Wrapf(err, "failed to load %s", m.RepositoryConfig)
   559  	}
   560  
   561  	for _, re := range rf.Repositories {
   562  		lname := re.Name
   563  		idxFile := filepath.Join(m.RepositoryCache, helmpath.CacheIndexFile(lname))
   564  		index, err := repo.LoadIndexFile(idxFile)
   565  		if err != nil {
   566  			return indices, err
   567  		}
   568  
   569  		// TODO: use constructor
   570  		cr := &repo.ChartRepository{
   571  			Config:    re,
   572  			IndexFile: index,
   573  		}
   574  		indices[lname] = cr
   575  	}
   576  	return indices, nil
   577  }
   578  
   579  // writeLock writes a lockfile to disk
   580  func writeLock(chartpath string, lock *chart.Lock) error {
   581  	data, err := yaml.Marshal(lock)
   582  	if err != nil {
   583  		return err
   584  	}
   585  	dest := filepath.Join(chartpath, "Chart.lock")
   586  	return ioutil.WriteFile(dest, data, 0644)
   587  }
   588  
   589  // archive a dep chart from local directory and save it into charts/
   590  func tarFromLocalDir(chartpath, name, repo, version string) (string, error) {
   591  	destPath := filepath.Join(chartpath, "charts")
   592  
   593  	if !strings.HasPrefix(repo, "file://") {
   594  		return "", errors.Errorf("wrong format: chart %s repository %s", name, repo)
   595  	}
   596  
   597  	origPath, err := resolver.GetLocalPath(repo, chartpath)
   598  	if err != nil {
   599  		return "", err
   600  	}
   601  
   602  	ch, err := loader.LoadDir(origPath)
   603  	if err != nil {
   604  		return "", err
   605  	}
   606  
   607  	constraint, err := semver.NewConstraint(version)
   608  	if err != nil {
   609  		return "", errors.Wrapf(err, "dependency %s has an invalid version/constraint format", name)
   610  	}
   611  
   612  	v, err := semver.NewVersion(ch.Metadata.Version)
   613  	if err != nil {
   614  		return "", err
   615  	}
   616  
   617  	if constraint.Check(v) {
   618  		_, err = chartutil.Save(ch, destPath)
   619  		return ch.Metadata.Version, err
   620  	}
   621  
   622  	return "", errors.Errorf("can't get a valid version for dependency %s", name)
   623  }
   624  
   625  // move files from tmppath to destpath
   626  func move(tmpPath, destPath string) error {
   627  	files, _ := ioutil.ReadDir(tmpPath)
   628  	for _, file := range files {
   629  		filename := file.Name()
   630  		tmpfile := filepath.Join(tmpPath, filename)
   631  		destfile := filepath.Join(destPath, filename)
   632  		if err := os.Rename(tmpfile, destfile); err != nil {
   633  			return errors.Wrap(err, "unable to move local charts to charts dir")
   634  		}
   635  	}
   636  	return nil
   637  }