github.com/azure-devops-engineer/helm@v3.0.0-alpha.2+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/pkg/chart"
    34  	"helm.sh/helm/pkg/chart/loader"
    35  	"helm.sh/helm/pkg/chartutil"
    36  	"helm.sh/helm/pkg/getter"
    37  	"helm.sh/helm/pkg/helmpath"
    38  	"helm.sh/helm/pkg/repo"
    39  	"helm.sh/helm/pkg/resolver"
    40  	"helm.sh/helm/pkg/urlutil"
    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  	// HelmHome is the $HELM_HOME directory
    50  	HelmHome helmpath.Home
    51  	// Verification indicates whether the chart should be verified.
    52  	Verify VerificationStrategy
    53  	// Debug is the global "--debug" flag
    54  	Debug bool
    55  	// Keyring is the key ring file.
    56  	Keyring string
    57  	// SkipUpdate indicates that the repository should not be updated first.
    58  	SkipUpdate bool
    59  	// Getter collection for the operation
    60  	Getters []getter.Provider
    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  	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 Chart.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 dependencies are found, we consider this a successful
   118  	// completion.
   119  	req := c.Metadata.Dependencies
   120  	if req == nil {
   121  		return nil
   122  	}
   123  
   124  	// Check that all of the repos we're dependent on actually exist and
   125  	// the repo index names.
   126  	repoNames, err := m.getRepoNames(req)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	// For each repo in the file, update the cached copy of that repo
   132  	if !m.SkipUpdate {
   133  		if err := m.UpdateRepositories(); err != nil {
   134  			return err
   135  		}
   136  	}
   137  
   138  	// Now we need to find out which version of a chart best satisfies the
   139  	// dependencies in the Chart.yaml
   140  	lock, err := m.resolve(req, repoNames)
   141  	if err != nil {
   142  		return err
   143  	}
   144  
   145  	// Now we need to fetch every package here into charts/
   146  	if err := m.downloadAll(lock.Dependencies); err != nil {
   147  		return err
   148  	}
   149  
   150  	// If the lock file hasn't changed, don't write a new one.
   151  	oldLock := c.Lock
   152  	if oldLock != nil && oldLock.Digest == lock.Digest {
   153  		return nil
   154  	}
   155  
   156  	// Finally, we need to write the lockfile.
   157  	return writeLock(m.ChartPath, lock)
   158  }
   159  
   160  func (m *Manager) loadChartDir() (*chart.Chart, error) {
   161  	if fi, err := os.Stat(m.ChartPath); err != nil {
   162  		return nil, errors.Wrapf(err, "could not find %s", m.ChartPath)
   163  	} else if !fi.IsDir() {
   164  		return nil, errors.New("only unpacked charts can be updated")
   165  	}
   166  	return loader.LoadDir(m.ChartPath)
   167  }
   168  
   169  // resolve takes a list of dependencies and translates them into an exact version to download.
   170  //
   171  // This returns a lock file, which has all of the dependencies normalized to a specific version.
   172  func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) {
   173  	res := resolver.New(m.ChartPath, m.HelmHome)
   174  	return res.Resolve(req, repoNames)
   175  }
   176  
   177  // downloadAll takes a list of dependencies and downloads them into charts/
   178  //
   179  // It will delete versions of the chart that exist on disk and might cause
   180  // a conflict.
   181  func (m *Manager) downloadAll(deps []*chart.Dependency) error {
   182  	repos, err := m.loadChartRepositories()
   183  	if err != nil {
   184  		return err
   185  	}
   186  
   187  	destPath := filepath.Join(m.ChartPath, "charts")
   188  	tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
   189  
   190  	// Create 'charts' directory if it doesn't already exist.
   191  	if fi, err := os.Stat(destPath); err != nil {
   192  		if err := os.MkdirAll(destPath, 0755); err != nil {
   193  			return err
   194  		}
   195  	} else if !fi.IsDir() {
   196  		return errors.Errorf("%q is not a directory", destPath)
   197  	}
   198  
   199  	if err := os.Rename(destPath, tmpPath); err != nil {
   200  		return errors.Wrap(err, "unable to move current charts to tmp dir")
   201  	}
   202  
   203  	if err := os.MkdirAll(destPath, 0755); err != nil {
   204  		return err
   205  	}
   206  
   207  	fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
   208  	var saveError error
   209  	for _, dep := range deps {
   210  		if strings.HasPrefix(dep.Repository, "file://") {
   211  			if m.Debug {
   212  				fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
   213  			}
   214  			ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version)
   215  			if err != nil {
   216  				saveError = err
   217  				break
   218  			}
   219  			dep.Version = ver
   220  			continue
   221  		}
   222  
   223  		fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
   224  
   225  		// Any failure to resolve/download a chart should fail:
   226  		// https://github.com/helm/helm/issues/1439
   227  		churl, username, password, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos)
   228  		if err != nil {
   229  			saveError = errors.Wrapf(err, "could not find %s", churl)
   230  			break
   231  		}
   232  
   233  		dl := ChartDownloader{
   234  			Out:      m.Out,
   235  			Verify:   m.Verify,
   236  			Keyring:  m.Keyring,
   237  			HelmHome: m.HelmHome,
   238  			Getters:  m.Getters,
   239  			Options: []getter.Option{
   240  				getter.WithBasicAuth(username, password),
   241  			},
   242  		}
   243  
   244  		if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil {
   245  			saveError = errors.Wrapf(err, "could not download %s", churl)
   246  			break
   247  		}
   248  	}
   249  
   250  	if saveError == nil {
   251  		fmt.Fprintln(m.Out, "Deleting outdated charts")
   252  		for _, dep := range deps {
   253  			if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil {
   254  				return err
   255  			}
   256  		}
   257  		if err := move(tmpPath, destPath); err != nil {
   258  			return err
   259  		}
   260  		if err := os.RemoveAll(tmpPath); err != nil {
   261  			return errors.Wrapf(err, "failed to remove %v", tmpPath)
   262  		}
   263  	} else {
   264  		fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
   265  		fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state")
   266  		for _, dep := range deps {
   267  			if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
   268  				return err
   269  			}
   270  		}
   271  		if err := os.RemoveAll(destPath); err != nil {
   272  			return errors.Wrapf(err, "failed to remove %v", destPath)
   273  		}
   274  		if err := os.Rename(tmpPath, destPath); err != nil {
   275  			return errors.Wrap(err, "unable to move current charts to tmp dir")
   276  		}
   277  		return saveError
   278  	}
   279  	return nil
   280  }
   281  
   282  // safeDeleteDep deletes any versions of the given dependency in the given directory.
   283  //
   284  // It does this by first matching the file name to an expected pattern, then loading
   285  // the file to verify that it is a chart with the same name as the given name.
   286  //
   287  // Because it requires tar file introspection, it is more intensive than a basic delete.
   288  //
   289  // This will only return errors that should stop processing entirely. Other errors
   290  // will emit log messages or be ignored.
   291  func (m *Manager) safeDeleteDep(name, dir string) error {
   292  	files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
   293  	if err != nil {
   294  		// Only for ErrBadPattern
   295  		return err
   296  	}
   297  	for _, fname := range files {
   298  		ch, err := loader.LoadFile(fname)
   299  		if err != nil {
   300  			fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
   301  			continue
   302  		}
   303  		if ch.Name() != name {
   304  			// This is not the file you are looking for.
   305  			continue
   306  		}
   307  		if err := os.Remove(fname); err != nil {
   308  			fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
   309  			continue
   310  		}
   311  	}
   312  	return nil
   313  }
   314  
   315  // hasAllRepos ensures that all of the referenced deps are in the local repo cache.
   316  func (m *Manager) hasAllRepos(deps []*chart.Dependency) error {
   317  	rf, err := repo.LoadFile(m.HelmHome.RepositoryFile())
   318  	if err != nil {
   319  		return err
   320  	}
   321  	repos := rf.Repositories
   322  
   323  	// Verify that all repositories referenced in the deps are actually known
   324  	// by Helm.
   325  	missing := []string{}
   326  Loop:
   327  	for _, dd := range deps {
   328  		// If repo is from local path, continue
   329  		if strings.HasPrefix(dd.Repository, "file://") {
   330  			continue
   331  		}
   332  
   333  		if dd.Repository == "" {
   334  			continue
   335  		}
   336  		for _, repo := range repos {
   337  			if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
   338  				continue Loop
   339  			}
   340  		}
   341  		missing = append(missing, dd.Repository)
   342  	}
   343  	if len(missing) > 0 {
   344  		return errors.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", "))
   345  	}
   346  	return nil
   347  }
   348  
   349  // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file.
   350  func (m *Manager) getRepoNames(deps []*chart.Dependency) (map[string]string, error) {
   351  	rf, err := repo.LoadFile(m.HelmHome.RepositoryFile())
   352  	if err != nil {
   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 stable
   408  repository, use "https://kubernetes-charts.storage.googleapis.com/" or "@stable" instead of
   409  "stable". 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 := repo.LoadFile(m.HelmHome.RepositoryFile())
   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  	out := m.Out
   434  	fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
   435  	var wg sync.WaitGroup
   436  	for _, c := range repos {
   437  		r, err := repo.NewChartRepository(c, m.Getters)
   438  		if err != nil {
   439  			return err
   440  		}
   441  		wg.Add(1)
   442  		go func(r *repo.ChartRepository) {
   443  			if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil {
   444  				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)
   445  			} else {
   446  				fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
   447  			}
   448  			wg.Done()
   449  		}(r)
   450  	}
   451  	wg.Wait()
   452  	fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
   453  	return nil
   454  }
   455  
   456  // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified.
   457  //
   458  // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the
   459  // newest version will be returned.
   460  //
   461  // repoURL is the repository to search
   462  //
   463  // If it finds a URL that is "relative", it will prepend the repoURL.
   464  func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) {
   465  	for _, cr := range repos {
   466  		if urlutil.Equal(repoURL, cr.Config.URL) {
   467  			var entry repo.ChartVersions
   468  			entry, err = findEntryByName(name, cr)
   469  			if err != nil {
   470  				return
   471  			}
   472  			var ve *repo.ChartVersion
   473  			ve, err = findVersionedEntry(version, entry)
   474  			if err != nil {
   475  				return
   476  			}
   477  			url, err = normalizeURL(repoURL, ve.URLs[0])
   478  			if err != nil {
   479  				return
   480  			}
   481  			username = cr.Config.Username
   482  			password = cr.Config.Password
   483  			return
   484  		}
   485  	}
   486  	err = errors.Errorf("chart %s not found in %s", name, repoURL)
   487  	return
   488  }
   489  
   490  // findEntryByName finds an entry in the chart repository whose name matches the given name.
   491  //
   492  // It returns the ChartVersions for that entry.
   493  func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) {
   494  	for ename, entry := range cr.IndexFile.Entries {
   495  		if ename == name {
   496  			return entry, nil
   497  		}
   498  	}
   499  	return nil, errors.New("entry not found")
   500  }
   501  
   502  // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints.
   503  //
   504  // If version is empty, the first chart found is returned.
   505  func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) {
   506  	for _, verEntry := range vers {
   507  		if len(verEntry.URLs) == 0 {
   508  			// Not a legit entry.
   509  			continue
   510  		}
   511  
   512  		if version == "" || versionEquals(version, verEntry.Version) {
   513  			return verEntry, nil
   514  		}
   515  	}
   516  	return nil, errors.New("no matching version")
   517  }
   518  
   519  func versionEquals(v1, v2 string) bool {
   520  	sv1, err := semver.NewVersion(v1)
   521  	if err != nil {
   522  		// Fallback to string comparison.
   523  		return v1 == v2
   524  	}
   525  	sv2, err := semver.NewVersion(v2)
   526  	if err != nil {
   527  		return false
   528  	}
   529  	return sv1.Equal(sv2)
   530  }
   531  
   532  func normalizeURL(baseURL, urlOrPath string) (string, error) {
   533  	u, err := url.Parse(urlOrPath)
   534  	if err != nil {
   535  		return urlOrPath, err
   536  	}
   537  	if u.IsAbs() {
   538  		return u.String(), nil
   539  	}
   540  	u2, err := url.Parse(baseURL)
   541  	if err != nil {
   542  		return urlOrPath, errors.Wrap(err, "base URL failed to parse")
   543  	}
   544  
   545  	u2.Path = path.Join(u2.Path, urlOrPath)
   546  	return u2.String(), nil
   547  }
   548  
   549  // loadChartRepositories reads the repositories.yaml, and then builds a map of
   550  // ChartRepositories.
   551  //
   552  // The key is the local name (which is only present in the repositories.yaml).
   553  func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) {
   554  	indices := map[string]*repo.ChartRepository{}
   555  	repoyaml := m.HelmHome.RepositoryFile()
   556  
   557  	// Load repositories.yaml file
   558  	rf, err := repo.LoadFile(repoyaml)
   559  	if err != nil {
   560  		return indices, errors.Wrapf(err, "failed to load %s", repoyaml)
   561  	}
   562  
   563  	for _, re := range rf.Repositories {
   564  		lname := re.Name
   565  		cacheindex := m.HelmHome.CacheIndex(lname)
   566  		index, err := repo.LoadIndexFile(cacheindex)
   567  		if err != nil {
   568  			return indices, err
   569  		}
   570  
   571  		// TODO: use constructor
   572  		cr := &repo.ChartRepository{
   573  			Config:    re,
   574  			IndexFile: index,
   575  		}
   576  		indices[lname] = cr
   577  	}
   578  	return indices, nil
   579  }
   580  
   581  // writeLock writes a lockfile to disk
   582  func writeLock(chartpath string, lock *chart.Lock) error {
   583  	data, err := yaml.Marshal(lock)
   584  	if err != nil {
   585  		return err
   586  	}
   587  	dest := filepath.Join(chartpath, "Chart.lock")
   588  	return ioutil.WriteFile(dest, data, 0644)
   589  }
   590  
   591  // archive a dep chart from local directory and save it into charts/
   592  func tarFromLocalDir(chartpath, name, repo, version string) (string, error) {
   593  	destPath := filepath.Join(chartpath, "charts")
   594  
   595  	if !strings.HasPrefix(repo, "file://") {
   596  		return "", errors.Errorf("wrong format: chart %s repository %s", name, repo)
   597  	}
   598  
   599  	origPath, err := resolver.GetLocalPath(repo, chartpath)
   600  	if err != nil {
   601  		return "", err
   602  	}
   603  
   604  	ch, err := loader.LoadDir(origPath)
   605  	if err != nil {
   606  		return "", err
   607  	}
   608  
   609  	constraint, err := semver.NewConstraint(version)
   610  	if err != nil {
   611  		return "", errors.Wrapf(err, "dependency %s has an invalid version/constraint format", name)
   612  	}
   613  
   614  	v, err := semver.NewVersion(ch.Metadata.Version)
   615  	if err != nil {
   616  		return "", err
   617  	}
   618  
   619  	if constraint.Check(v) {
   620  		_, err = chartutil.Save(ch, destPath)
   621  		return ch.Metadata.Version, err
   622  	}
   623  
   624  	return "", errors.Errorf("can't get a valid version for dependency %s", name)
   625  }
   626  
   627  // move files from tmppath to destpath
   628  func move(tmpPath, destPath string) error {
   629  	files, _ := ioutil.ReadDir(tmpPath)
   630  	for _, file := range files {
   631  		filename := file.Name()
   632  		tmpfile := filepath.Join(tmpPath, filename)
   633  		destfile := filepath.Join(destPath, filename)
   634  		if err := os.Rename(tmpfile, destfile); err != nil {
   635  			return errors.Wrap(err, "unable to move local charts to charts dir")
   636  		}
   637  	}
   638  	return nil
   639  }