1/*
2Copyright The Helm Authors.
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6
7http://www.apache.org/licenses/LICENSE-2.0
8
9Unless required by applicable law or agreed to in writing, software
10distributed under the License is distributed on an "AS IS" BASIS,
11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12See the License for the specific language governing permissions and
13limitations under the License.
14*/
15
16package downloader
17
18import (
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	"helm.sh/helm/v3/internal/experimental/registry"
38	"helm.sh/helm/v3/internal/resolver"
39	"helm.sh/helm/v3/internal/third_party/dep/fs"
40	"helm.sh/helm/v3/internal/urlutil"
41	"helm.sh/helm/v3/pkg/chart"
42	"helm.sh/helm/v3/pkg/chart/loader"
43	"helm.sh/helm/v3/pkg/chartutil"
44	"helm.sh/helm/v3/pkg/getter"
45	"helm.sh/helm/v3/pkg/helmpath"
46	"helm.sh/helm/v3/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.
51type ErrRepoNotFound struct {
52	Repos []string
53}
54
55// Error implements the error interface.
56func (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.
61type 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.
86func (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.
153func (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	// respoitories 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
222func (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.
234func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) {
235	res := resolver.New(m.ChartPath, m.RepositoryCache)
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.
243func (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	// Create 'charts' directory if it doesn't already exist.
253	if fi, err := os.Stat(destPath); err != nil {
254		if err := os.MkdirAll(destPath, 0755); err != nil {
255			return err
256		}
257	} else if !fi.IsDir() {
258		return errors.Errorf("%q is not a directory", destPath)
259	}
260
261	if err := fs.RenameWithFallback(destPath, tmpPath); err != nil {
262		return errors.Wrap(err, "unable to move current charts to tmp dir")
263	}
264
265	if err := os.MkdirAll(destPath, 0755); err != nil {
266		return err
267	}
268
269	fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
270	var saveError error
271	churls := make(map[string]struct{})
272	for _, dep := range deps {
273		// No repository means the chart is in charts directory
274		if dep.Repository == "" {
275			fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name)
276			chartPath := filepath.Join(tmpPath, dep.Name)
277			ch, err := loader.LoadDir(chartPath)
278			if err != nil {
279				return fmt.Errorf("Unable to load chart: %v", err)
280			}
281
282			constraint, err := semver.NewConstraint(dep.Version)
283			if err != nil {
284				return fmt.Errorf("Dependency %s has an invalid version/constraint format: %s", dep.Name, err)
285			}
286
287			v, err := semver.NewVersion(ch.Metadata.Version)
288			if err != nil {
289				return fmt.Errorf("Invalid version %s for dependency %s: %s", dep.Version, dep.Name, err)
290			}
291
292			if !constraint.Check(v) {
293				saveError = fmt.Errorf("Dependency %s at version %s does not satisfy the constraint %s", dep.Name, ch.Metadata.Version, dep.Version)
294				break
295			}
296			continue
297		}
298		if strings.HasPrefix(dep.Repository, "file://") {
299			if m.Debug {
300				fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
301			}
302			ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version)
303			if err != nil {
304				saveError = err
305				break
306			}
307			dep.Version = ver
308			continue
309		}
310
311		// Any failure to resolve/download a chart should fail:
312		// https://github.com/helm/helm/issues/1439
313		churl, username, password, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos)
314		if err != nil {
315			saveError = errors.Wrapf(err, "could not find %s", churl)
316			break
317		}
318
319		if _, ok := churls[churl]; ok {
320			fmt.Fprintf(m.Out, "Already downloaded %s from repo %s\n", dep.Name, dep.Repository)
321			continue
322		}
323
324		fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
325
326		dl := ChartDownloader{
327			Out:              m.Out,
328			Verify:           m.Verify,
329			Keyring:          m.Keyring,
330			RepositoryConfig: m.RepositoryConfig,
331			RepositoryCache:  m.RepositoryCache,
332			Getters:          m.Getters,
333			Options: []getter.Option{
334				getter.WithBasicAuth(username, password),
335			},
336		}
337
338		version := ""
339		if strings.HasPrefix(churl, "oci://") {
340			if !resolver.FeatureGateOCI.IsEnabled() {
341				return errors.Wrapf(resolver.FeatureGateOCI.Error(),
342					"the repository %s is an OCI registry", churl)
343			}
344
345			churl, version, err = parseOCIRef(churl)
346			if err != nil {
347				return errors.Wrapf(err, "could not parse OCI reference")
348			}
349			dl.Options = append(dl.Options,
350				getter.WithRegistryClient(m.RegistryClient),
351				getter.WithTagName(version))
352		}
353
354		_, _, err = dl.DownloadTo(churl, version, destPath)
355		if err != nil {
356			saveError = errors.Wrapf(err, "could not download %s", churl)
357			break
358		}
359
360		churls[churl] = struct{}{}
361	}
362
363	if saveError == nil {
364		fmt.Fprintln(m.Out, "Deleting outdated charts")
365		for _, dep := range deps {
366			// Chart from local charts directory stays in place
367			if dep.Repository != "" {
368				if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil {
369					return err
370				}
371			}
372		}
373		if err := move(tmpPath, destPath); err != nil {
374			return err
375		}
376		if err := os.RemoveAll(tmpPath); err != nil {
377			return errors.Wrapf(err, "failed to remove %v", tmpPath)
378		}
379	} else {
380		fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
381		fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state")
382		for _, dep := range deps {
383			if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
384				return err
385			}
386		}
387		if err := os.RemoveAll(destPath); err != nil {
388			return errors.Wrapf(err, "failed to remove %v", destPath)
389		}
390		if err := fs.RenameWithFallback(tmpPath, destPath); err != nil {
391			return errors.Wrap(err, "unable to move current charts to tmp dir")
392		}
393		return saveError
394	}
395	return nil
396}
397
398func parseOCIRef(chartRef string) (string, string, error) {
399	refTagRegexp := regexp.MustCompile(`^(oci://[^:]+(:[0-9]{1,5})?[^:]+):(.*)$`)
400	caps := refTagRegexp.FindStringSubmatch(chartRef)
401	if len(caps) != 4 {
402		return "", "", errors.Errorf("improperly formatted oci chart reference: %s", chartRef)
403	}
404	chartRef = caps[1]
405	tag := caps[3]
406
407	return chartRef, tag, nil
408}
409
410// safeDeleteDep deletes any versions of the given dependency in the given directory.
411//
412// It does this by first matching the file name to an expected pattern, then loading
413// the file to verify that it is a chart with the same name as the given name.
414//
415// Because it requires tar file introspection, it is more intensive than a basic delete.
416//
417// This will only return errors that should stop processing entirely. Other errors
418// will emit log messages or be ignored.
419func (m *Manager) safeDeleteDep(name, dir string) error {
420	files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
421	if err != nil {
422		// Only for ErrBadPattern
423		return err
424	}
425	for _, fname := range files {
426		ch, err := loader.LoadFile(fname)
427		if err != nil {
428			fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
429			continue
430		}
431		if ch.Name() != name {
432			// This is not the file you are looking for.
433			continue
434		}
435		if err := os.Remove(fname); err != nil {
436			fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
437			continue
438		}
439	}
440	return nil
441}
442
443// hasAllRepos ensures that all of the referenced deps are in the local repo cache.
444func (m *Manager) hasAllRepos(deps []*chart.Dependency) error {
445	rf, err := loadRepoConfig(m.RepositoryConfig)
446	if err != nil {
447		return err
448	}
449	repos := rf.Repositories
450
451	// Verify that all repositories referenced in the deps are actually known
452	// by Helm.
453	missing := []string{}
454Loop:
455	for _, dd := range deps {
456		// If repo is from local path or OCI, continue
457		if strings.HasPrefix(dd.Repository, "file://") || strings.HasPrefix(dd.Repository, "oci://") {
458			continue
459		}
460
461		if dd.Repository == "" {
462			continue
463		}
464		for _, repo := range repos {
465			if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
466				continue Loop
467			}
468		}
469		missing = append(missing, dd.Repository)
470	}
471	if len(missing) > 0 {
472		return ErrRepoNotFound{missing}
473	}
474	return nil
475}
476
477// ensureMissingRepos attempts to ensure the repository information for repos
478// not managed by Helm is present. This takes in the repoNames Helm is configured
479// to work with along with the chart dependencies. It will find the deps not
480// in a known repo and attempt to ensure the data is present for steps like
481// version resolution.
482func (m *Manager) ensureMissingRepos(repoNames map[string]string, deps []*chart.Dependency) (map[string]string, error) {
483
484	var ru []*repo.Entry
485
486	for _, dd := range deps {
487
488		// If the chart is in the local charts directory no repository needs
489		// to be specified.
490		if dd.Repository == "" {
491			continue
492		}
493
494		// When the repoName for a dependency is known we can skip ensuring
495		if _, ok := repoNames[dd.Name]; ok {
496			continue
497		}
498
499		// The generated repository name, which will result in an index being
500		// locally cached, has a name pattern of "helm-manager-" followed by a
501		// sha256 of the repo name. This assumes end users will never create
502		// repositories with these names pointing to other repositories. Using
503		// this method of naming allows the existing repository pulling and
504		// resolution code to do most of the work.
505		rn, err := key(dd.Repository)
506		if err != nil {
507			return repoNames, err
508		}
509		rn = managerKeyPrefix + rn
510
511		repoNames[dd.Name] = rn
512
513		// Assuming the repository is generally available. For Helm managed
514		// access controls the repository needs to be added through the user
515		// managed system. This path will work for public charts, like those
516		// supplied by Bitnami, but not for protected charts, like corp ones
517		// behind a username and pass.
518		ri := &repo.Entry{
519			Name: rn,
520			URL:  dd.Repository,
521		}
522		ru = append(ru, ri)
523	}
524
525	// Calls to UpdateRepositories (a public function) will only update
526	// repositories configured by the user. Here we update repos found in
527	// the dependencies that are not known to the user if update skipping
528	// is not configured.
529	if !m.SkipUpdate && len(ru) > 0 {
530		fmt.Fprintln(m.Out, "Getting updates for unmanaged Helm repositories...")
531		if err := m.parallelRepoUpdate(ru); err != nil {
532			return repoNames, err
533		}
534	}
535
536	return repoNames, nil
537}
538
539// resolveRepoNames returns the repo names of the referenced deps which can be used to fetch the cached index file
540// and replaces aliased repository URLs into resolved URLs in dependencies.
541func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string, error) {
542	rf, err := loadRepoConfig(m.RepositoryConfig)
543	if err != nil {
544		if os.IsNotExist(err) {
545			return make(map[string]string), nil
546		}
547		return nil, err
548	}
549	repos := rf.Repositories
550
551	reposMap := make(map[string]string)
552
553	// Verify that all repositories referenced in the deps are actually known
554	// by Helm.
555	missing := []string{}
556	for _, dd := range deps {
557		// Don't map the repository, we don't need to download chart from charts directory
558		// When OCI is used there is no Helm repository
559		if dd.Repository == "" || strings.HasPrefix(dd.Repository, "oci://") {
560			continue
561		}
562		// if dep chart is from local path, verify the path is valid
563		if strings.HasPrefix(dd.Repository, "file://") {
564			if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil {
565				return nil, err
566			}
567
568			if m.Debug {
569				fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository)
570			}
571			reposMap[dd.Name] = dd.Repository
572			continue
573		}
574
575		if strings.HasPrefix(dd.Repository, "oci://") {
576			reposMap[dd.Name] = dd.Repository
577			continue
578		}
579
580		found := false
581
582		for _, repo := range repos {
583			if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) ||
584				(strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) {
585				found = true
586				dd.Repository = repo.URL
587				reposMap[dd.Name] = repo.Name
588				break
589			} else if urlutil.Equal(repo.URL, dd.Repository) {
590				found = true
591				reposMap[dd.Name] = repo.Name
592				break
593			}
594		}
595		if !found {
596			repository := dd.Repository
597			// Add if URL
598			_, err := url.ParseRequestURI(repository)
599			if err == nil {
600				reposMap[repository] = repository
601				continue
602			}
603			missing = append(missing, repository)
604		}
605	}
606	if len(missing) > 0 {
607		errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", "))
608		// It is common for people to try to enter "stable" as a repository instead of the actual URL.
609		// For this case, let's give them a suggestion.
610		containsNonURL := false
611		for _, repo := range missing {
612			if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") {
613				containsNonURL = true
614			}
615		}
616		if containsNonURL {
617			errorMessage += `
618Note that repositories must be URLs or aliases. For example, to refer to the "example"
619repository, use "https://charts.example.com/" or "@example" instead of
620"example". Don't forget to add the repo, too ('helm repo add').`
621		}
622		return nil, errors.New(errorMessage)
623	}
624	return reposMap, nil
625}
626
627// UpdateRepositories updates all of the local repos to the latest.
628func (m *Manager) UpdateRepositories() error {
629	rf, err := loadRepoConfig(m.RepositoryConfig)
630	if err != nil {
631		return err
632	}
633	repos := rf.Repositories
634	if len(repos) > 0 {
635		fmt.Fprintln(m.Out, "Hang tight while we grab the latest from your chart repositories...")
636		// This prints warnings straight to out.
637		if err := m.parallelRepoUpdate(repos); err != nil {
638			return err
639		}
640		fmt.Fprintln(m.Out, "Update Complete. ⎈Happy Helming!⎈")
641	}
642	return nil
643}
644
645func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
646
647	var wg sync.WaitGroup
648	for _, c := range repos {
649		r, err := repo.NewChartRepository(c, m.Getters)
650		if err != nil {
651			return err
652		}
653		wg.Add(1)
654		go func(r *repo.ChartRepository) {
655			if _, err := r.DownloadIndexFile(); err != nil {
656				// For those dependencies that are not known to helm and using a
657				// generated key name we display the repo url.
658				if strings.HasPrefix(r.Config.Name, managerKeyPrefix) {
659					fmt.Fprintf(m.Out, "...Unable to get an update from the %q chart repository:\n\t%s\n", r.Config.URL, err)
660				} else {
661					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)
662				}
663			} else {
664				// For those dependencies that are not known to helm and using a
665				// generated key name we display the repo url.
666				if strings.HasPrefix(r.Config.Name, managerKeyPrefix) {
667					fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.URL)
668				} else {
669					fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
670				}
671			}
672			wg.Done()
673		}(r)
674	}
675	wg.Wait()
676
677	return nil
678}
679
680// findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified.
681//
682// 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the
683// newest version will be returned.
684//
685// repoURL is the repository to search
686//
687// If it finds a URL that is "relative", it will prepend the repoURL.
688func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) {
689	if strings.HasPrefix(repoURL, "oci://") {
690		return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", nil
691	}
692
693	for _, cr := range repos {
694
695		if urlutil.Equal(repoURL, cr.Config.URL) {
696			var entry repo.ChartVersions
697			entry, err = findEntryByName(name, cr)
698			if err != nil {
699				return
700			}
701			var ve *repo.ChartVersion
702			ve, err = findVersionedEntry(version, entry)
703			if err != nil {
704				return
705			}
706			url, err = normalizeURL(repoURL, ve.URLs[0])
707			if err != nil {
708				return
709			}
710			username = cr.Config.Username
711			password = cr.Config.Password
712			return
713		}
714	}
715	url, err = repo.FindChartInRepoURL(repoURL, name, version, "", "", "", m.Getters)
716	if err == nil {
717		return url, username, password, err
718	}
719	err = errors.Errorf("chart %s not found in %s: %s", name, repoURL, err)
720	return url, username, password, err
721}
722
723// findEntryByName finds an entry in the chart repository whose name matches the given name.
724//
725// It returns the ChartVersions for that entry.
726func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) {
727	for ename, entry := range cr.IndexFile.Entries {
728		if ename == name {
729			return entry, nil
730		}
731	}
732	return nil, errors.New("entry not found")
733}
734
735// findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints.
736//
737// If version is empty, the first chart found is returned.
738func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) {
739	for _, verEntry := range vers {
740		if len(verEntry.URLs) == 0 {
741			// Not a legit entry.
742			continue
743		}
744
745		if version == "" || versionEquals(version, verEntry.Version) {
746			return verEntry, nil
747		}
748	}
749	return nil, errors.New("no matching version")
750}
751
752func versionEquals(v1, v2 string) bool {
753	sv1, err := semver.NewVersion(v1)
754	if err != nil {
755		// Fallback to string comparison.
756		return v1 == v2
757	}
758	sv2, err := semver.NewVersion(v2)
759	if err != nil {
760		return false
761	}
762	return sv1.Equal(sv2)
763}
764
765func normalizeURL(baseURL, urlOrPath string) (string, error) {
766	u, err := url.Parse(urlOrPath)
767	if err != nil {
768		return urlOrPath, err
769	}
770	if u.IsAbs() {
771		return u.String(), nil
772	}
773	u2, err := url.Parse(baseURL)
774	if err != nil {
775		return urlOrPath, errors.Wrap(err, "base URL failed to parse")
776	}
777
778	u2.Path = path.Join(u2.Path, urlOrPath)
779	return u2.String(), nil
780}
781
782// loadChartRepositories reads the repositories.yaml, and then builds a map of
783// ChartRepositories.
784//
785// The key is the local name (which is only present in the repositories.yaml).
786func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) {
787	indices := map[string]*repo.ChartRepository{}
788
789	// Load repositories.yaml file
790	rf, err := loadRepoConfig(m.RepositoryConfig)
791	if err != nil {
792		return indices, errors.Wrapf(err, "failed to load %s", m.RepositoryConfig)
793	}
794
795	for _, re := range rf.Repositories {
796		lname := re.Name
797		idxFile := filepath.Join(m.RepositoryCache, helmpath.CacheIndexFile(lname))
798		index, err := repo.LoadIndexFile(idxFile)
799		if err != nil {
800			return indices, err
801		}
802
803		// TODO: use constructor
804		cr := &repo.ChartRepository{
805			Config:    re,
806			IndexFile: index,
807		}
808		indices[lname] = cr
809	}
810	return indices, nil
811}
812
813// writeLock writes a lockfile to disk
814func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error {
815	data, err := yaml.Marshal(lock)
816	if err != nil {
817		return err
818	}
819	lockfileName := "Chart.lock"
820	if legacyLockfile {
821		lockfileName = "requirements.lock"
822	}
823	dest := filepath.Join(chartpath, lockfileName)
824	return ioutil.WriteFile(dest, data, 0644)
825}
826
827// archive a dep chart from local directory and save it into charts/
828func tarFromLocalDir(chartpath, name, repo, version string) (string, error) {
829	destPath := filepath.Join(chartpath, "charts")
830
831	if !strings.HasPrefix(repo, "file://") {
832		return "", errors.Errorf("wrong format: chart %s repository %s", name, repo)
833	}
834
835	origPath, err := resolver.GetLocalPath(repo, chartpath)
836	if err != nil {
837		return "", err
838	}
839
840	ch, err := loader.LoadDir(origPath)
841	if err != nil {
842		return "", err
843	}
844
845	constraint, err := semver.NewConstraint(version)
846	if err != nil {
847		return "", errors.Wrapf(err, "dependency %s has an invalid version/constraint format", name)
848	}
849
850	v, err := semver.NewVersion(ch.Metadata.Version)
851	if err != nil {
852		return "", err
853	}
854
855	if constraint.Check(v) {
856		_, err = chartutil.Save(ch, destPath)
857		return ch.Metadata.Version, err
858	}
859
860	return "", errors.Errorf("can't get a valid version for dependency %s", name)
861}
862
863// move files from tmppath to destpath
864func move(tmpPath, destPath string) error {
865	files, _ := ioutil.ReadDir(tmpPath)
866	for _, file := range files {
867		filename := file.Name()
868		tmpfile := filepath.Join(tmpPath, filename)
869		destfile := filepath.Join(destPath, filename)
870		if err := fs.RenameWithFallback(tmpfile, destfile); err != nil {
871			return errors.Wrap(err, "unable to move local charts to charts dir")
872		}
873	}
874	return nil
875}
876
877// The prefix to use for cache keys created by the manager for repo names
878const managerKeyPrefix = "helm-manager-"
879
880// key is used to turn a name, such as a repository url, into a filesystem
881// safe name that is unique for querying. To accomplish this a unique hash of
882// the string is used.
883func key(name string) (string, error) {
884	in := strings.NewReader(name)
885	hash := crypto.SHA256.New()
886	if _, err := io.Copy(hash, in); err != nil {
887		return "", nil
888	}
889	return hex.EncodeToString(hash.Sum(nil)), nil
890}
891