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