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 }