github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/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  	"crypto"
    20  	"encoding/hex"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"log"
    25  	"net/url"
    26  	"os"
    27  	"path"
    28  	"path/filepath"
    29  	"regexp"
    30  	"strings"
    31  	"sync"
    32  
    33  	"github.com/Masterminds/semver/v3"
    34  	"github.com/pkg/errors"
    35  	"sigs.k8s.io/yaml"
    36  
    37  	"github.com/stefanmcshane/helm/internal/resolver"
    38  	"github.com/stefanmcshane/helm/internal/third_party/dep/fs"
    39  	"github.com/stefanmcshane/helm/internal/urlutil"
    40  	"github.com/stefanmcshane/helm/pkg/chart"
    41  	"github.com/stefanmcshane/helm/pkg/chart/loader"
    42  	"github.com/stefanmcshane/helm/pkg/chartutil"
    43  	"github.com/stefanmcshane/helm/pkg/getter"
    44  	"github.com/stefanmcshane/helm/pkg/helmpath"
    45  	"github.com/stefanmcshane/helm/pkg/registry"
    46  	"github.com/stefanmcshane/helm/pkg/repo"
    47  )
    48  
    49  // ErrRepoNotFound indicates that chart repositories can't be found in local repo cache.
    50  // The value of Repos is missing repos.
    51  type ErrRepoNotFound struct {
    52  	Repos []string
    53  }
    54  
    55  // Error implements the error interface.
    56  func (e ErrRepoNotFound) Error() string {
    57  	return fmt.Sprintf("no repository definition for %s", strings.Join(e.Repos, ", "))
    58  }
    59  
    60  // Manager handles the lifecycle of fetching, resolving, and storing dependencies.
    61  type Manager struct {
    62  	// Out is used to print warnings and notifications.
    63  	Out io.Writer
    64  	// ChartPath is the path to the unpacked base chart upon which this operates.
    65  	ChartPath string
    66  	// Verification indicates whether the chart should be verified.
    67  	Verify VerificationStrategy
    68  	// Debug is the global "--debug" flag
    69  	Debug bool
    70  	// Keyring is the key ring file.
    71  	Keyring string
    72  	// SkipUpdate indicates that the repository should not be updated first.
    73  	SkipUpdate bool
    74  	// Getter collection for the operation
    75  	Getters          []getter.Provider
    76  	RegistryClient   *registry.Client
    77  	RepositoryConfig string
    78  	RepositoryCache  string
    79  }
    80  
    81  // Build rebuilds a local charts directory from a lockfile.
    82  //
    83  // If the lockfile is not present, this will run a Manager.Update()
    84  //
    85  // If SkipUpdate is set, this will not update the repository.
    86  func (m *Manager) Build() error {
    87  	c, err := m.loadChartDir()
    88  	if err != nil {
    89  		return err
    90  	}
    91  
    92  	// If a lock file is found, run a build from that. Otherwise, just do
    93  	// an update.
    94  	lock := c.Lock
    95  	if lock == nil {
    96  		return m.Update()
    97  	}
    98  
    99  	// Check that all of the repos we're dependent on actually exist.
   100  	req := c.Metadata.Dependencies
   101  
   102  	// If using apiVersion v1, calculate the hash before resolve repo names
   103  	// because resolveRepoNames will change req if req uses repo alias
   104  	// and Helm 2 calculate the digest from the original req
   105  	// Fix for: https://github.com/helm/helm/issues/7619
   106  	var v2Sum string
   107  	if c.Metadata.APIVersion == chart.APIVersionV1 {
   108  		v2Sum, err = resolver.HashV2Req(req)
   109  		if err != nil {
   110  			return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies")
   111  		}
   112  	}
   113  
   114  	if _, err := m.resolveRepoNames(req); err != nil {
   115  		return err
   116  	}
   117  
   118  	if sum, err := resolver.HashReq(req, lock.Dependencies); err != nil || sum != lock.Digest {
   119  		// If lock digest differs and chart is apiVersion v1, it maybe because the lock was built
   120  		// with Helm 2 and therefore should be checked with Helm v2 hash
   121  		// Fix for: https://github.com/helm/helm/issues/7233
   122  		if c.Metadata.APIVersion == chart.APIVersionV1 {
   123  			log.Println("warning: a valid Helm v3 hash was not found. Checking against Helm v2 hash...")
   124  			if v2Sum != lock.Digest {
   125  				return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies")
   126  			}
   127  		} else {
   128  			return errors.New("the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies")
   129  		}
   130  	}
   131  
   132  	// Check that all of the repos we're dependent on actually exist.
   133  	if err := m.hasAllRepos(lock.Dependencies); err != nil {
   134  		return err
   135  	}
   136  
   137  	if !m.SkipUpdate {
   138  		// For each repo in the file, update the cached copy of that repo
   139  		if err := m.UpdateRepositories(); err != nil {
   140  			return err
   141  		}
   142  	}
   143  
   144  	// Now we need to fetch every package here into charts/
   145  	return m.downloadAll(lock.Dependencies)
   146  }
   147  
   148  // Update updates a local charts directory.
   149  //
   150  // It first reads the Chart.yaml file, and then attempts to
   151  // negotiate versions based on that. It will download the versions
   152  // from remote chart repositories unless SkipUpdate is true.
   153  func (m *Manager) Update() error {
   154  	c, err := m.loadChartDir()
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	// If no dependencies are found, we consider this a successful
   160  	// completion.
   161  	req := c.Metadata.Dependencies
   162  	if req == nil {
   163  		return nil
   164  	}
   165  
   166  	// Get the names of the repositories the dependencies need that Helm is
   167  	// configured to know about.
   168  	repoNames, err := m.resolveRepoNames(req)
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	// For the repositories Helm is not configured to know about, ensure Helm
   174  	// has some information about them and, when possible, the index files
   175  	// locally.
   176  	// TODO(mattfarina): Repositories should be explicitly added by end users
   177  	// rather than automattic. In Helm v4 require users to add repositories. They
   178  	// should have to add them in order to make sure they are aware of the
   179  	// repositories and opt-in to any locations, for security.
   180  	repoNames, err = m.ensureMissingRepos(repoNames, req)
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	// For each of the repositories Helm is configured to know about, update
   186  	// the index information locally.
   187  	if !m.SkipUpdate {
   188  		if err := m.UpdateRepositories(); err != nil {
   189  			return err
   190  		}
   191  	}
   192  
   193  	// Now we need to find out which version of a chart best satisfies the
   194  	// dependencies in the Chart.yaml
   195  	lock, err := m.resolve(req, repoNames)
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	// Now we need to fetch every package here into charts/
   201  	if err := m.downloadAll(lock.Dependencies); err != nil {
   202  		return err
   203  	}
   204  
   205  	// downloadAll might overwrite dependency version, recalculate lock digest
   206  	newDigest, err := resolver.HashReq(req, lock.Dependencies)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	lock.Digest = newDigest
   211  
   212  	// If the lock file hasn't changed, don't write a new one.
   213  	oldLock := c.Lock
   214  	if oldLock != nil && oldLock.Digest == lock.Digest {
   215  		return nil
   216  	}
   217  
   218  	// Finally, we need to write the lockfile.
   219  	return writeLock(m.ChartPath, lock, c.Metadata.APIVersion == chart.APIVersionV1)
   220  }
   221  
   222  func (m *Manager) loadChartDir() (*chart.Chart, error) {
   223  	if fi, err := os.Stat(m.ChartPath); err != nil {
   224  		return nil, errors.Wrapf(err, "could not find %s", m.ChartPath)
   225  	} else if !fi.IsDir() {
   226  		return nil, errors.New("only unpacked charts can be updated")
   227  	}
   228  	return loader.LoadDir(m.ChartPath)
   229  }
   230  
   231  // resolve takes a list of dependencies and translates them into an exact version to download.
   232  //
   233  // This returns a lock file, which has all of the dependencies normalized to a specific version.
   234  func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) {
   235  	res := resolver.New(m.ChartPath, m.RepositoryCache, m.RegistryClient)
   236  	return res.Resolve(req, repoNames)
   237  }
   238  
   239  // downloadAll takes a list of dependencies and downloads them into charts/
   240  //
   241  // It will delete versions of the chart that exist on disk and might cause
   242  // a conflict.
   243  func (m *Manager) downloadAll(deps []*chart.Dependency) error {
   244  	repos, err := m.loadChartRepositories()
   245  	if err != nil {
   246  		return err
   247  	}
   248  
   249  	destPath := filepath.Join(m.ChartPath, "charts")
   250  	tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
   251  
   252  	// Check if 'charts' directory is not actally a directory. If it does not exist, create it.
   253  	if fi, err := os.Stat(destPath); err == nil {
   254  		if !fi.IsDir() {
   255  			return errors.Errorf("%q is not a directory", destPath)
   256  		}
   257  	} else if os.IsNotExist(err) {
   258  		if err := os.MkdirAll(destPath, 0755); err != nil {
   259  			return err
   260  		}
   261  	} else {
   262  		return fmt.Errorf("unable to retrieve file info for '%s': %v", destPath, err)
   263  	}
   264  
   265  	// Prepare tmpPath
   266  	if err := os.MkdirAll(tmpPath, 0755); err != nil {
   267  		return err
   268  	}
   269  	defer os.RemoveAll(tmpPath)
   270  
   271  	fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
   272  	var saveError error
   273  	churls := make(map[string]struct{})
   274  	for _, dep := range deps {
   275  		// No repository means the chart is in charts directory
   276  		if dep.Repository == "" {
   277  			fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name)
   278  			// NOTE: we are only validating the local dependency conforms to the constraints. No copying to tmpPath is necessary.
   279  			chartPath := filepath.Join(destPath, dep.Name)
   280  			ch, err := loader.LoadDir(chartPath)
   281  			if err != nil {
   282  				return fmt.Errorf("unable to load chart '%s': %v", chartPath, err)
   283  			}
   284  
   285  			constraint, err := semver.NewConstraint(dep.Version)
   286  			if err != nil {
   287  				return fmt.Errorf("dependency %s has an invalid version/constraint format: %s", dep.Name, err)
   288  			}
   289  
   290  			v, err := semver.NewVersion(ch.Metadata.Version)
   291  			if err != nil {
   292  				return fmt.Errorf("invalid version %s for dependency %s: %s", dep.Version, dep.Name, err)
   293  			}
   294  
   295  			if !constraint.Check(v) {
   296  				saveError = fmt.Errorf("dependency %s at version %s does not satisfy the constraint %s", dep.Name, ch.Metadata.Version, dep.Version)
   297  				break
   298  			}
   299  			continue
   300  		}
   301  		if strings.HasPrefix(dep.Repository, "file://") {
   302  			if m.Debug {
   303  				fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
   304  			}
   305  			ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version, tmpPath)
   306  			if err != nil {
   307  				saveError = err
   308  				break
   309  			}
   310  			dep.Version = ver
   311  			continue
   312  		}
   313  
   314  		// Any failure to resolve/download a chart should fail:
   315  		// https://github.com/helm/helm/issues/1439
   316  		churl, username, password, insecureskiptlsverify, passcredentialsall, caFile, certFile, keyFile, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos)
   317  		if err != nil {
   318  			saveError = errors.Wrapf(err, "could not find %s", churl)
   319  			break
   320  		}
   321  
   322  		if _, ok := churls[churl]; ok {
   323  			fmt.Fprintf(m.Out, "Already downloaded %s from repo %s\n", dep.Name, dep.Repository)
   324  			continue
   325  		}
   326  
   327  		fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
   328  
   329  		dl := ChartDownloader{
   330  			Out:              m.Out,
   331  			Verify:           m.Verify,
   332  			Keyring:          m.Keyring,
   333  			RepositoryConfig: m.RepositoryConfig,
   334  			RepositoryCache:  m.RepositoryCache,
   335  			RegistryClient:   m.RegistryClient,
   336  			Getters:          m.Getters,
   337  			Options: []getter.Option{
   338  				getter.WithBasicAuth(username, password),
   339  				getter.WithPassCredentialsAll(passcredentialsall),
   340  				getter.WithInsecureSkipVerifyTLS(insecureskiptlsverify),
   341  				getter.WithTLSClientConfig(certFile, keyFile, caFile),
   342  			},
   343  		}
   344  
   345  		version := ""
   346  		if registry.IsOCI(churl) {
   347  			churl, version, err = parseOCIRef(churl)
   348  			if err != nil {
   349  				return errors.Wrapf(err, "could not parse OCI reference")
   350  			}
   351  			dl.Options = append(dl.Options,
   352  				getter.WithRegistryClient(m.RegistryClient),
   353  				getter.WithTagName(version))
   354  		}
   355  
   356  		if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil {
   357  			saveError = errors.Wrapf(err, "could not download %s", churl)
   358  			break
   359  		}
   360  
   361  		churls[churl] = struct{}{}
   362  	}
   363  
   364  	// TODO: this should probably be refactored to be a []error, so we can capture and provide more information rather than "last error wins".
   365  	if saveError == nil {
   366  		// now we can move all downloaded charts to destPath and delete outdated dependencies
   367  		if err := m.safeMoveDeps(deps, tmpPath, destPath); err != nil {
   368  			return err
   369  		}
   370  	} else {
   371  		fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
   372  		return saveError
   373  	}
   374  	return nil
   375  }
   376  
   377  func parseOCIRef(chartRef string) (string, string, error) {
   378  	refTagRegexp := regexp.MustCompile(`^(oci://[^:]+(:[0-9]{1,5})?[^:]+):(.*)$`)
   379  	caps := refTagRegexp.FindStringSubmatch(chartRef)
   380  	if len(caps) != 4 {
   381  		return "", "", errors.Errorf("improperly formatted oci chart reference: %s", chartRef)
   382  	}
   383  	chartRef = caps[1]
   384  	tag := caps[3]
   385  
   386  	return chartRef, tag, nil
   387  }
   388  
   389  // safeMoveDep moves all dependencies in the source and moves them into dest.
   390  //
   391  // It does this by first matching the file name to an expected pattern, then loading
   392  // the file to verify that it is a chart.
   393  //
   394  // Any charts in dest that do not exist in source are removed (barring local dependencies)
   395  //
   396  // Because it requires tar file introspection, it is more intensive than a basic move.
   397  //
   398  // This will only return errors that should stop processing entirely. Other errors
   399  // will emit log messages or be ignored.
   400  func (m *Manager) safeMoveDeps(deps []*chart.Dependency, source, dest string) error {
   401  	existsInSourceDirectory := map[string]bool{}
   402  	isLocalDependency := map[string]bool{}
   403  	sourceFiles, err := os.ReadDir(source)
   404  	if err != nil {
   405  		return err
   406  	}
   407  	// attempt to read destFiles; fail fast if we can't
   408  	destFiles, err := os.ReadDir(dest)
   409  	if err != nil {
   410  		return err
   411  	}
   412  
   413  	for _, dep := range deps {
   414  		if dep.Repository == "" {
   415  			isLocalDependency[dep.Name] = true
   416  		}
   417  	}
   418  
   419  	for _, file := range sourceFiles {
   420  		if file.IsDir() {
   421  			continue
   422  		}
   423  		filename := file.Name()
   424  		sourcefile := filepath.Join(source, filename)
   425  		destfile := filepath.Join(dest, filename)
   426  		existsInSourceDirectory[filename] = true
   427  		if _, err := loader.LoadFile(sourcefile); err != nil {
   428  			fmt.Fprintf(m.Out, "Could not verify %s for moving: %s (Skipping)", sourcefile, err)
   429  			continue
   430  		}
   431  		// NOTE: no need to delete the dest; os.Rename replaces it.
   432  		if err := fs.RenameWithFallback(sourcefile, destfile); err != nil {
   433  			fmt.Fprintf(m.Out, "Unable to move %s to charts dir %s (Skipping)", sourcefile, err)
   434  			continue
   435  		}
   436  	}
   437  
   438  	fmt.Fprintln(m.Out, "Deleting outdated charts")
   439  	// find all files that exist in dest that do not exist in source; delete them (outdated dependencies)
   440  	for _, file := range destFiles {
   441  		if !file.IsDir() && !existsInSourceDirectory[file.Name()] {
   442  			fname := filepath.Join(dest, file.Name())
   443  			ch, err := loader.LoadFile(fname)
   444  			if err != nil {
   445  				fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)\n", fname, err)
   446  				continue
   447  			}
   448  			// local dependency - skip
   449  			if isLocalDependency[ch.Name()] {
   450  				continue
   451  			}
   452  			if err := os.Remove(fname); err != nil {
   453  				fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
   454  				continue
   455  			}
   456  		}
   457  	}
   458  
   459  	return nil
   460  }
   461  
   462  // hasAllRepos ensures that all of the referenced deps are in the local repo cache.
   463  func (m *Manager) hasAllRepos(deps []*chart.Dependency) error {
   464  	rf, err := loadRepoConfig(m.RepositoryConfig)
   465  	if err != nil {
   466  		return err
   467  	}
   468  	repos := rf.Repositories
   469  
   470  	// Verify that all repositories referenced in the deps are actually known
   471  	// by Helm.
   472  	missing := []string{}
   473  Loop:
   474  	for _, dd := range deps {
   475  		// If repo is from local path or OCI, continue
   476  		if strings.HasPrefix(dd.Repository, "file://") || registry.IsOCI(dd.Repository) {
   477  			continue
   478  		}
   479  
   480  		if dd.Repository == "" {
   481  			continue
   482  		}
   483  		for _, repo := range repos {
   484  			if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
   485  				continue Loop
   486  			}
   487  		}
   488  		missing = append(missing, dd.Repository)
   489  	}
   490  	if len(missing) > 0 {
   491  		return ErrRepoNotFound{missing}
   492  	}
   493  	return nil
   494  }
   495  
   496  // ensureMissingRepos attempts to ensure the repository information for repos
   497  // not managed by Helm is present. This takes in the repoNames Helm is configured
   498  // to work with along with the chart dependencies. It will find the deps not
   499  // in a known repo and attempt to ensure the data is present for steps like
   500  // version resolution.
   501  func (m *Manager) ensureMissingRepos(repoNames map[string]string, deps []*chart.Dependency) (map[string]string, error) {
   502  
   503  	var ru []*repo.Entry
   504  
   505  	for _, dd := range deps {
   506  
   507  		// If the chart is in the local charts directory no repository needs
   508  		// to be specified.
   509  		if dd.Repository == "" {
   510  			continue
   511  		}
   512  
   513  		// When the repoName for a dependency is known we can skip ensuring
   514  		if _, ok := repoNames[dd.Name]; ok {
   515  			continue
   516  		}
   517  
   518  		// The generated repository name, which will result in an index being
   519  		// locally cached, has a name pattern of "helm-manager-" followed by a
   520  		// sha256 of the repo name. This assumes end users will never create
   521  		// repositories with these names pointing to other repositories. Using
   522  		// this method of naming allows the existing repository pulling and
   523  		// resolution code to do most of the work.
   524  		rn, err := key(dd.Repository)
   525  		if err != nil {
   526  			return repoNames, err
   527  		}
   528  		rn = managerKeyPrefix + rn
   529  
   530  		repoNames[dd.Name] = rn
   531  
   532  		// Assuming the repository is generally available. For Helm managed
   533  		// access controls the repository needs to be added through the user
   534  		// managed system. This path will work for public charts, like those
   535  		// supplied by Bitnami, but not for protected charts, like corp ones
   536  		// behind a username and pass.
   537  		ri := &repo.Entry{
   538  			Name: rn,
   539  			URL:  dd.Repository,
   540  		}
   541  		ru = append(ru, ri)
   542  	}
   543  
   544  	// Calls to UpdateRepositories (a public function) will only update
   545  	// repositories configured by the user. Here we update repos found in
   546  	// the dependencies that are not known to the user if update skipping
   547  	// is not configured.
   548  	if !m.SkipUpdate && len(ru) > 0 {
   549  		fmt.Fprintln(m.Out, "Getting updates for unmanaged Helm repositories...")
   550  		if err := m.parallelRepoUpdate(ru); err != nil {
   551  			return repoNames, err
   552  		}
   553  	}
   554  
   555  	return repoNames, nil
   556  }
   557  
   558  // resolveRepoNames returns the repo names of the referenced deps which can be used to fetch the cached index file
   559  // and replaces aliased repository URLs into resolved URLs in dependencies.
   560  func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string, error) {
   561  	rf, err := loadRepoConfig(m.RepositoryConfig)
   562  	if err != nil {
   563  		if os.IsNotExist(err) {
   564  			return make(map[string]string), nil
   565  		}
   566  		return nil, err
   567  	}
   568  	repos := rf.Repositories
   569  
   570  	reposMap := make(map[string]string)
   571  
   572  	// Verify that all repositories referenced in the deps are actually known
   573  	// by Helm.
   574  	missing := []string{}
   575  	for _, dd := range deps {
   576  		// Don't map the repository, we don't need to download chart from charts directory
   577  		if dd.Repository == "" {
   578  			continue
   579  		}
   580  		// if dep chart is from local path, verify the path is valid
   581  		if strings.HasPrefix(dd.Repository, "file://") {
   582  			if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil {
   583  				return nil, err
   584  			}
   585  
   586  			if m.Debug {
   587  				fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository)
   588  			}
   589  			reposMap[dd.Name] = dd.Repository
   590  			continue
   591  		}
   592  
   593  		if registry.IsOCI(dd.Repository) {
   594  			reposMap[dd.Name] = dd.Repository
   595  			continue
   596  		}
   597  
   598  		found := false
   599  
   600  		for _, repo := range repos {
   601  			if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) ||
   602  				(strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) {
   603  				found = true
   604  				dd.Repository = repo.URL
   605  				reposMap[dd.Name] = repo.Name
   606  				break
   607  			} else if urlutil.Equal(repo.URL, dd.Repository) {
   608  				found = true
   609  				reposMap[dd.Name] = repo.Name
   610  				break
   611  			}
   612  		}
   613  		if !found {
   614  			repository := dd.Repository
   615  			// Add if URL
   616  			_, err := url.ParseRequestURI(repository)
   617  			if err == nil {
   618  				reposMap[repository] = repository
   619  				continue
   620  			}
   621  			missing = append(missing, repository)
   622  		}
   623  	}
   624  	if len(missing) > 0 {
   625  		errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", "))
   626  		// It is common for people to try to enter "stable" as a repository instead of the actual URL.
   627  		// For this case, let's give them a suggestion.
   628  		containsNonURL := false
   629  		for _, repo := range missing {
   630  			if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") {
   631  				containsNonURL = true
   632  			}
   633  		}
   634  		if containsNonURL {
   635  			errorMessage += `
   636  Note that repositories must be URLs or aliases. For example, to refer to the "example"
   637  repository, use "https://charts.example.com/" or "@example" instead of
   638  "example". Don't forget to add the repo, too ('helm repo add').`
   639  		}
   640  		return nil, errors.New(errorMessage)
   641  	}
   642  	return reposMap, nil
   643  }
   644  
   645  // UpdateRepositories updates all of the local repos to the latest.
   646  func (m *Manager) UpdateRepositories() error {
   647  	rf, err := loadRepoConfig(m.RepositoryConfig)
   648  	if err != nil {
   649  		return err
   650  	}
   651  	repos := rf.Repositories
   652  	if len(repos) > 0 {
   653  		fmt.Fprintln(m.Out, "Hang tight while we grab the latest from your chart repositories...")
   654  		// This prints warnings straight to out.
   655  		if err := m.parallelRepoUpdate(repos); err != nil {
   656  			return err
   657  		}
   658  		fmt.Fprintln(m.Out, "Update Complete. ⎈Happy Helming!⎈")
   659  	}
   660  	return nil
   661  }
   662  
   663  func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
   664  
   665  	var wg sync.WaitGroup
   666  	for _, c := range repos {
   667  		r, err := repo.NewChartRepository(c, m.Getters)
   668  		if err != nil {
   669  			return err
   670  		}
   671  		wg.Add(1)
   672  		go func(r *repo.ChartRepository) {
   673  			if _, err := r.DownloadIndexFile(); err != nil {
   674  				// For those dependencies that are not known to helm and using a
   675  				// generated key name we display the repo url.
   676  				if strings.HasPrefix(r.Config.Name, managerKeyPrefix) {
   677  					fmt.Fprintf(m.Out, "...Unable to get an update from the %q chart repository:\n\t%s\n", r.Config.URL, err)
   678  				} else {
   679  					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)
   680  				}
   681  			} else {
   682  				// For those dependencies that are not known to helm and using a
   683  				// generated key name we display the repo url.
   684  				if strings.HasPrefix(r.Config.Name, managerKeyPrefix) {
   685  					fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.URL)
   686  				} else {
   687  					fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
   688  				}
   689  			}
   690  			wg.Done()
   691  		}(r)
   692  	}
   693  	wg.Wait()
   694  
   695  	return nil
   696  }
   697  
   698  // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified.
   699  //
   700  // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the
   701  // newest version will be returned.
   702  //
   703  // repoURL is the repository to search
   704  //
   705  // If it finds a URL that is "relative", it will prepend the repoURL.
   706  func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureskiptlsverify, passcredentialsall bool, caFile, certFile, keyFile string, err error) {
   707  	if registry.IsOCI(repoURL) {
   708  		return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, "", "", "", nil
   709  	}
   710  
   711  	for _, cr := range repos {
   712  
   713  		if urlutil.Equal(repoURL, cr.Config.URL) {
   714  			var entry repo.ChartVersions
   715  			entry, err = findEntryByName(name, cr)
   716  			if err != nil {
   717  				return
   718  			}
   719  			var ve *repo.ChartVersion
   720  			ve, err = findVersionedEntry(version, entry)
   721  			if err != nil {
   722  				return
   723  			}
   724  			url, err = normalizeURL(repoURL, ve.URLs[0])
   725  			if err != nil {
   726  				return
   727  			}
   728  			username = cr.Config.Username
   729  			password = cr.Config.Password
   730  			passcredentialsall = cr.Config.PassCredentialsAll
   731  			insecureskiptlsverify = cr.Config.InsecureSkipTLSverify
   732  			caFile = cr.Config.CAFile
   733  			certFile = cr.Config.CertFile
   734  			keyFile = cr.Config.KeyFile
   735  			return
   736  		}
   737  	}
   738  	url, err = repo.FindChartInRepoURL(repoURL, name, version, certFile, keyFile, caFile, m.Getters)
   739  	if err == nil {
   740  		return url, username, password, false, false, "", "", "", err
   741  	}
   742  	err = errors.Errorf("chart %s not found in %s: %s", name, repoURL, err)
   743  	return url, username, password, false, false, "", "", "", err
   744  }
   745  
   746  // findEntryByName finds an entry in the chart repository whose name matches the given name.
   747  //
   748  // It returns the ChartVersions for that entry.
   749  func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) {
   750  	for ename, entry := range cr.IndexFile.Entries {
   751  		if ename == name {
   752  			return entry, nil
   753  		}
   754  	}
   755  	return nil, errors.New("entry not found")
   756  }
   757  
   758  // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints.
   759  //
   760  // If version is empty, the first chart found is returned.
   761  func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) {
   762  	for _, verEntry := range vers {
   763  		if len(verEntry.URLs) == 0 {
   764  			// Not a legit entry.
   765  			continue
   766  		}
   767  
   768  		if version == "" || versionEquals(version, verEntry.Version) {
   769  			return verEntry, nil
   770  		}
   771  	}
   772  	return nil, errors.New("no matching version")
   773  }
   774  
   775  func versionEquals(v1, v2 string) bool {
   776  	sv1, err := semver.NewVersion(v1)
   777  	if err != nil {
   778  		// Fallback to string comparison.
   779  		return v1 == v2
   780  	}
   781  	sv2, err := semver.NewVersion(v2)
   782  	if err != nil {
   783  		return false
   784  	}
   785  	return sv1.Equal(sv2)
   786  }
   787  
   788  func normalizeURL(baseURL, urlOrPath string) (string, error) {
   789  	u, err := url.Parse(urlOrPath)
   790  	if err != nil {
   791  		return urlOrPath, err
   792  	}
   793  	if u.IsAbs() {
   794  		return u.String(), nil
   795  	}
   796  	u2, err := url.Parse(baseURL)
   797  	if err != nil {
   798  		return urlOrPath, errors.Wrap(err, "base URL failed to parse")
   799  	}
   800  
   801  	u2.RawPath = path.Join(u2.RawPath, urlOrPath)
   802  	u2.Path = path.Join(u2.Path, urlOrPath)
   803  	return u2.String(), nil
   804  }
   805  
   806  // loadChartRepositories reads the repositories.yaml, and then builds a map of
   807  // ChartRepositories.
   808  //
   809  // The key is the local name (which is only present in the repositories.yaml).
   810  func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) {
   811  	indices := map[string]*repo.ChartRepository{}
   812  
   813  	// Load repositories.yaml file
   814  	rf, err := loadRepoConfig(m.RepositoryConfig)
   815  	if err != nil {
   816  		return indices, errors.Wrapf(err, "failed to load %s", m.RepositoryConfig)
   817  	}
   818  
   819  	for _, re := range rf.Repositories {
   820  		lname := re.Name
   821  		idxFile := filepath.Join(m.RepositoryCache, helmpath.CacheIndexFile(lname))
   822  		index, err := repo.LoadIndexFile(idxFile)
   823  		if err != nil {
   824  			return indices, err
   825  		}
   826  
   827  		// TODO: use constructor
   828  		cr := &repo.ChartRepository{
   829  			Config:    re,
   830  			IndexFile: index,
   831  		}
   832  		indices[lname] = cr
   833  	}
   834  	return indices, nil
   835  }
   836  
   837  // writeLock writes a lockfile to disk
   838  func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error {
   839  	data, err := yaml.Marshal(lock)
   840  	if err != nil {
   841  		return err
   842  	}
   843  	lockfileName := "Chart.lock"
   844  	if legacyLockfile {
   845  		lockfileName = "requirements.lock"
   846  	}
   847  	dest := filepath.Join(chartpath, lockfileName)
   848  	return ioutil.WriteFile(dest, data, 0644)
   849  }
   850  
   851  // archive a dep chart from local directory and save it into destPath
   852  func tarFromLocalDir(chartpath, name, repo, version, destPath string) (string, error) {
   853  	if !strings.HasPrefix(repo, "file://") {
   854  		return "", errors.Errorf("wrong format: chart %s repository %s", name, repo)
   855  	}
   856  
   857  	origPath, err := resolver.GetLocalPath(repo, chartpath)
   858  	if err != nil {
   859  		return "", err
   860  	}
   861  
   862  	ch, err := loader.LoadDir(origPath)
   863  	if err != nil {
   864  		return "", err
   865  	}
   866  
   867  	constraint, err := semver.NewConstraint(version)
   868  	if err != nil {
   869  		return "", errors.Wrapf(err, "dependency %s has an invalid version/constraint format", name)
   870  	}
   871  
   872  	v, err := semver.NewVersion(ch.Metadata.Version)
   873  	if err != nil {
   874  		return "", err
   875  	}
   876  
   877  	if constraint.Check(v) {
   878  		_, err = chartutil.Save(ch, destPath)
   879  		return ch.Metadata.Version, err
   880  	}
   881  
   882  	return "", errors.Errorf("can't get a valid version for dependency %s", name)
   883  }
   884  
   885  // The prefix to use for cache keys created by the manager for repo names
   886  const managerKeyPrefix = "helm-manager-"
   887  
   888  // key is used to turn a name, such as a repository url, into a filesystem
   889  // safe name that is unique for querying. To accomplish this a unique hash of
   890  // the string is used.
   891  func key(name string) (string, error) {
   892  	in := strings.NewReader(name)
   893  	hash := crypto.SHA256.New()
   894  	if _, err := io.Copy(hash, in); err != nil {
   895  		return "", nil
   896  	}
   897  	return hex.EncodeToString(hash.Sum(nil)), nil
   898  }