github.com/azure-devops-engineer/helm@v3.0.0-alpha.2+incompatible/pkg/downloader/manager.go (about) 1 /* 2 Copyright The Helm Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package downloader 17 18 import ( 19 "fmt" 20 "io" 21 "io/ioutil" 22 "net/url" 23 "os" 24 "path" 25 "path/filepath" 26 "strings" 27 "sync" 28 29 "github.com/Masterminds/semver" 30 "github.com/pkg/errors" 31 "sigs.k8s.io/yaml" 32 33 "helm.sh/helm/pkg/chart" 34 "helm.sh/helm/pkg/chart/loader" 35 "helm.sh/helm/pkg/chartutil" 36 "helm.sh/helm/pkg/getter" 37 "helm.sh/helm/pkg/helmpath" 38 "helm.sh/helm/pkg/repo" 39 "helm.sh/helm/pkg/resolver" 40 "helm.sh/helm/pkg/urlutil" 41 ) 42 43 // Manager handles the lifecycle of fetching, resolving, and storing dependencies. 44 type Manager struct { 45 // Out is used to print warnings and notifications. 46 Out io.Writer 47 // ChartPath is the path to the unpacked base chart upon which this operates. 48 ChartPath string 49 // HelmHome is the $HELM_HOME directory 50 HelmHome helmpath.Home 51 // Verification indicates whether the chart should be verified. 52 Verify VerificationStrategy 53 // Debug is the global "--debug" flag 54 Debug bool 55 // Keyring is the key ring file. 56 Keyring string 57 // SkipUpdate indicates that the repository should not be updated first. 58 SkipUpdate bool 59 // Getter collection for the operation 60 Getters []getter.Provider 61 } 62 63 // Build rebuilds a local charts directory from a lockfile. 64 // 65 // If the lockfile is not present, this will run a Manager.Update() 66 // 67 // If SkipUpdate is set, this will not update the repository. 68 func (m *Manager) Build() error { 69 c, err := m.loadChartDir() 70 if err != nil { 71 return err 72 } 73 74 // If a lock file is found, run a build from that. Otherwise, just do 75 // an update. 76 lock := c.Lock 77 if lock == nil { 78 return m.Update() 79 } 80 81 req := c.Metadata.Dependencies 82 if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest { 83 return errors.New("Chart.lock is out of sync with Chart.yaml") 84 } 85 86 // Check that all of the repos we're dependent on actually exist. 87 if err := m.hasAllRepos(lock.Dependencies); err != nil { 88 return err 89 } 90 91 if !m.SkipUpdate { 92 // For each repo in the file, update the cached copy of that repo 93 if err := m.UpdateRepositories(); err != nil { 94 return err 95 } 96 } 97 98 // Now we need to fetch every package here into charts/ 99 if err := m.downloadAll(lock.Dependencies); err != nil { 100 return err 101 } 102 103 return nil 104 } 105 106 // Update updates a local charts directory. 107 // 108 // It first reads the Chart.yaml file, and then attempts to 109 // negotiate versions based on that. It will download the versions 110 // from remote chart repositories unless SkipUpdate is true. 111 func (m *Manager) Update() error { 112 c, err := m.loadChartDir() 113 if err != nil { 114 return err 115 } 116 117 // If no dependencies are found, we consider this a successful 118 // completion. 119 req := c.Metadata.Dependencies 120 if req == nil { 121 return nil 122 } 123 124 // Check that all of the repos we're dependent on actually exist and 125 // the repo index names. 126 repoNames, err := m.getRepoNames(req) 127 if err != nil { 128 return err 129 } 130 131 // For each repo in the file, update the cached copy of that repo 132 if !m.SkipUpdate { 133 if err := m.UpdateRepositories(); err != nil { 134 return err 135 } 136 } 137 138 // Now we need to find out which version of a chart best satisfies the 139 // dependencies in the Chart.yaml 140 lock, err := m.resolve(req, repoNames) 141 if err != nil { 142 return err 143 } 144 145 // Now we need to fetch every package here into charts/ 146 if err := m.downloadAll(lock.Dependencies); err != nil { 147 return err 148 } 149 150 // If the lock file hasn't changed, don't write a new one. 151 oldLock := c.Lock 152 if oldLock != nil && oldLock.Digest == lock.Digest { 153 return nil 154 } 155 156 // Finally, we need to write the lockfile. 157 return writeLock(m.ChartPath, lock) 158 } 159 160 func (m *Manager) loadChartDir() (*chart.Chart, error) { 161 if fi, err := os.Stat(m.ChartPath); err != nil { 162 return nil, errors.Wrapf(err, "could not find %s", m.ChartPath) 163 } else if !fi.IsDir() { 164 return nil, errors.New("only unpacked charts can be updated") 165 } 166 return loader.LoadDir(m.ChartPath) 167 } 168 169 // resolve takes a list of dependencies and translates them into an exact version to download. 170 // 171 // This returns a lock file, which has all of the dependencies normalized to a specific version. 172 func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) { 173 res := resolver.New(m.ChartPath, m.HelmHome) 174 return res.Resolve(req, repoNames) 175 } 176 177 // downloadAll takes a list of dependencies and downloads them into charts/ 178 // 179 // It will delete versions of the chart that exist on disk and might cause 180 // a conflict. 181 func (m *Manager) downloadAll(deps []*chart.Dependency) error { 182 repos, err := m.loadChartRepositories() 183 if err != nil { 184 return err 185 } 186 187 destPath := filepath.Join(m.ChartPath, "charts") 188 tmpPath := filepath.Join(m.ChartPath, "tmpcharts") 189 190 // Create 'charts' directory if it doesn't already exist. 191 if fi, err := os.Stat(destPath); err != nil { 192 if err := os.MkdirAll(destPath, 0755); err != nil { 193 return err 194 } 195 } else if !fi.IsDir() { 196 return errors.Errorf("%q is not a directory", destPath) 197 } 198 199 if err := os.Rename(destPath, tmpPath); err != nil { 200 return errors.Wrap(err, "unable to move current charts to tmp dir") 201 } 202 203 if err := os.MkdirAll(destPath, 0755); err != nil { 204 return err 205 } 206 207 fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) 208 var saveError error 209 for _, dep := range deps { 210 if strings.HasPrefix(dep.Repository, "file://") { 211 if m.Debug { 212 fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) 213 } 214 ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version) 215 if err != nil { 216 saveError = err 217 break 218 } 219 dep.Version = ver 220 continue 221 } 222 223 fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) 224 225 // Any failure to resolve/download a chart should fail: 226 // https://github.com/helm/helm/issues/1439 227 churl, username, password, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos) 228 if err != nil { 229 saveError = errors.Wrapf(err, "could not find %s", churl) 230 break 231 } 232 233 dl := ChartDownloader{ 234 Out: m.Out, 235 Verify: m.Verify, 236 Keyring: m.Keyring, 237 HelmHome: m.HelmHome, 238 Getters: m.Getters, 239 Options: []getter.Option{ 240 getter.WithBasicAuth(username, password), 241 }, 242 } 243 244 if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil { 245 saveError = errors.Wrapf(err, "could not download %s", churl) 246 break 247 } 248 } 249 250 if saveError == nil { 251 fmt.Fprintln(m.Out, "Deleting outdated charts") 252 for _, dep := range deps { 253 if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil { 254 return err 255 } 256 } 257 if err := move(tmpPath, destPath); err != nil { 258 return err 259 } 260 if err := os.RemoveAll(tmpPath); err != nil { 261 return errors.Wrapf(err, "failed to remove %v", tmpPath) 262 } 263 } else { 264 fmt.Fprintln(m.Out, "Save error occurred: ", saveError) 265 fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state") 266 for _, dep := range deps { 267 if err := m.safeDeleteDep(dep.Name, destPath); err != nil { 268 return err 269 } 270 } 271 if err := os.RemoveAll(destPath); err != nil { 272 return errors.Wrapf(err, "failed to remove %v", destPath) 273 } 274 if err := os.Rename(tmpPath, destPath); err != nil { 275 return errors.Wrap(err, "unable to move current charts to tmp dir") 276 } 277 return saveError 278 } 279 return nil 280 } 281 282 // safeDeleteDep deletes any versions of the given dependency in the given directory. 283 // 284 // It does this by first matching the file name to an expected pattern, then loading 285 // the file to verify that it is a chart with the same name as the given name. 286 // 287 // Because it requires tar file introspection, it is more intensive than a basic delete. 288 // 289 // This will only return errors that should stop processing entirely. Other errors 290 // will emit log messages or be ignored. 291 func (m *Manager) safeDeleteDep(name, dir string) error { 292 files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz")) 293 if err != nil { 294 // Only for ErrBadPattern 295 return err 296 } 297 for _, fname := range files { 298 ch, err := loader.LoadFile(fname) 299 if err != nil { 300 fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) 301 continue 302 } 303 if ch.Name() != name { 304 // This is not the file you are looking for. 305 continue 306 } 307 if err := os.Remove(fname); err != nil { 308 fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) 309 continue 310 } 311 } 312 return nil 313 } 314 315 // hasAllRepos ensures that all of the referenced deps are in the local repo cache. 316 func (m *Manager) hasAllRepos(deps []*chart.Dependency) error { 317 rf, err := repo.LoadFile(m.HelmHome.RepositoryFile()) 318 if err != nil { 319 return err 320 } 321 repos := rf.Repositories 322 323 // Verify that all repositories referenced in the deps are actually known 324 // by Helm. 325 missing := []string{} 326 Loop: 327 for _, dd := range deps { 328 // If repo is from local path, continue 329 if strings.HasPrefix(dd.Repository, "file://") { 330 continue 331 } 332 333 if dd.Repository == "" { 334 continue 335 } 336 for _, repo := range repos { 337 if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { 338 continue Loop 339 } 340 } 341 missing = append(missing, dd.Repository) 342 } 343 if len(missing) > 0 { 344 return errors.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", ")) 345 } 346 return nil 347 } 348 349 // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file. 350 func (m *Manager) getRepoNames(deps []*chart.Dependency) (map[string]string, error) { 351 rf, err := repo.LoadFile(m.HelmHome.RepositoryFile()) 352 if err != nil { 353 return nil, err 354 } 355 repos := rf.Repositories 356 357 reposMap := make(map[string]string) 358 359 // Verify that all repositories referenced in the deps are actually known 360 // by Helm. 361 missing := []string{} 362 for _, dd := range deps { 363 // if dep chart is from local path, verify the path is valid 364 if strings.HasPrefix(dd.Repository, "file://") { 365 if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil { 366 return nil, err 367 } 368 369 if m.Debug { 370 fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository) 371 } 372 reposMap[dd.Name] = dd.Repository 373 continue 374 } 375 376 found := false 377 378 for _, repo := range repos { 379 if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) || 380 (strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) { 381 found = true 382 dd.Repository = repo.URL 383 reposMap[dd.Name] = repo.Name 384 break 385 } else if urlutil.Equal(repo.URL, dd.Repository) { 386 found = true 387 reposMap[dd.Name] = repo.Name 388 break 389 } 390 } 391 if !found { 392 missing = append(missing, dd.Repository) 393 } 394 } 395 if len(missing) > 0 { 396 errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", ")) 397 // It is common for people to try to enter "stable" as a repository instead of the actual URL. 398 // For this case, let's give them a suggestion. 399 containsNonURL := false 400 for _, repo := range missing { 401 if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") { 402 containsNonURL = true 403 } 404 } 405 if containsNonURL { 406 errorMessage += ` 407 Note that repositories must be URLs or aliases. For example, to refer to the stable 408 repository, use "https://kubernetes-charts.storage.googleapis.com/" or "@stable" instead of 409 "stable". Don't forget to add the repo, too ('helm repo add').` 410 } 411 return nil, errors.New(errorMessage) 412 } 413 return reposMap, nil 414 } 415 416 // UpdateRepositories updates all of the local repos to the latest. 417 func (m *Manager) UpdateRepositories() error { 418 rf, err := repo.LoadFile(m.HelmHome.RepositoryFile()) 419 if err != nil { 420 return err 421 } 422 repos := rf.Repositories 423 if len(repos) > 0 { 424 // This prints warnings straight to out. 425 if err := m.parallelRepoUpdate(repos); err != nil { 426 return err 427 } 428 } 429 return nil 430 } 431 432 func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { 433 out := m.Out 434 fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") 435 var wg sync.WaitGroup 436 for _, c := range repos { 437 r, err := repo.NewChartRepository(c, m.Getters) 438 if err != nil { 439 return err 440 } 441 wg.Add(1) 442 go func(r *repo.ChartRepository) { 443 if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil { 444 fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err) 445 } else { 446 fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) 447 } 448 wg.Done() 449 }(r) 450 } 451 wg.Wait() 452 fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") 453 return nil 454 } 455 456 // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. 457 // 458 // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the 459 // newest version will be returned. 460 // 461 // repoURL is the repository to search 462 // 463 // If it finds a URL that is "relative", it will prepend the repoURL. 464 func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) { 465 for _, cr := range repos { 466 if urlutil.Equal(repoURL, cr.Config.URL) { 467 var entry repo.ChartVersions 468 entry, err = findEntryByName(name, cr) 469 if err != nil { 470 return 471 } 472 var ve *repo.ChartVersion 473 ve, err = findVersionedEntry(version, entry) 474 if err != nil { 475 return 476 } 477 url, err = normalizeURL(repoURL, ve.URLs[0]) 478 if err != nil { 479 return 480 } 481 username = cr.Config.Username 482 password = cr.Config.Password 483 return 484 } 485 } 486 err = errors.Errorf("chart %s not found in %s", name, repoURL) 487 return 488 } 489 490 // findEntryByName finds an entry in the chart repository whose name matches the given name. 491 // 492 // It returns the ChartVersions for that entry. 493 func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) { 494 for ename, entry := range cr.IndexFile.Entries { 495 if ename == name { 496 return entry, nil 497 } 498 } 499 return nil, errors.New("entry not found") 500 } 501 502 // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints. 503 // 504 // If version is empty, the first chart found is returned. 505 func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) { 506 for _, verEntry := range vers { 507 if len(verEntry.URLs) == 0 { 508 // Not a legit entry. 509 continue 510 } 511 512 if version == "" || versionEquals(version, verEntry.Version) { 513 return verEntry, nil 514 } 515 } 516 return nil, errors.New("no matching version") 517 } 518 519 func versionEquals(v1, v2 string) bool { 520 sv1, err := semver.NewVersion(v1) 521 if err != nil { 522 // Fallback to string comparison. 523 return v1 == v2 524 } 525 sv2, err := semver.NewVersion(v2) 526 if err != nil { 527 return false 528 } 529 return sv1.Equal(sv2) 530 } 531 532 func normalizeURL(baseURL, urlOrPath string) (string, error) { 533 u, err := url.Parse(urlOrPath) 534 if err != nil { 535 return urlOrPath, err 536 } 537 if u.IsAbs() { 538 return u.String(), nil 539 } 540 u2, err := url.Parse(baseURL) 541 if err != nil { 542 return urlOrPath, errors.Wrap(err, "base URL failed to parse") 543 } 544 545 u2.Path = path.Join(u2.Path, urlOrPath) 546 return u2.String(), nil 547 } 548 549 // loadChartRepositories reads the repositories.yaml, and then builds a map of 550 // ChartRepositories. 551 // 552 // The key is the local name (which is only present in the repositories.yaml). 553 func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { 554 indices := map[string]*repo.ChartRepository{} 555 repoyaml := m.HelmHome.RepositoryFile() 556 557 // Load repositories.yaml file 558 rf, err := repo.LoadFile(repoyaml) 559 if err != nil { 560 return indices, errors.Wrapf(err, "failed to load %s", repoyaml) 561 } 562 563 for _, re := range rf.Repositories { 564 lname := re.Name 565 cacheindex := m.HelmHome.CacheIndex(lname) 566 index, err := repo.LoadIndexFile(cacheindex) 567 if err != nil { 568 return indices, err 569 } 570 571 // TODO: use constructor 572 cr := &repo.ChartRepository{ 573 Config: re, 574 IndexFile: index, 575 } 576 indices[lname] = cr 577 } 578 return indices, nil 579 } 580 581 // writeLock writes a lockfile to disk 582 func writeLock(chartpath string, lock *chart.Lock) error { 583 data, err := yaml.Marshal(lock) 584 if err != nil { 585 return err 586 } 587 dest := filepath.Join(chartpath, "Chart.lock") 588 return ioutil.WriteFile(dest, data, 0644) 589 } 590 591 // archive a dep chart from local directory and save it into charts/ 592 func tarFromLocalDir(chartpath, name, repo, version string) (string, error) { 593 destPath := filepath.Join(chartpath, "charts") 594 595 if !strings.HasPrefix(repo, "file://") { 596 return "", errors.Errorf("wrong format: chart %s repository %s", name, repo) 597 } 598 599 origPath, err := resolver.GetLocalPath(repo, chartpath) 600 if err != nil { 601 return "", err 602 } 603 604 ch, err := loader.LoadDir(origPath) 605 if err != nil { 606 return "", err 607 } 608 609 constraint, err := semver.NewConstraint(version) 610 if err != nil { 611 return "", errors.Wrapf(err, "dependency %s has an invalid version/constraint format", name) 612 } 613 614 v, err := semver.NewVersion(ch.Metadata.Version) 615 if err != nil { 616 return "", err 617 } 618 619 if constraint.Check(v) { 620 _, err = chartutil.Save(ch, destPath) 621 return ch.Metadata.Version, err 622 } 623 624 return "", errors.Errorf("can't get a valid version for dependency %s", name) 625 } 626 627 // move files from tmppath to destpath 628 func move(tmpPath, destPath string) error { 629 files, _ := ioutil.ReadDir(tmpPath) 630 for _, file := range files { 631 filename := file.Name() 632 tmpfile := filepath.Join(tmpPath, filename) 633 destfile := filepath.Join(destPath, filename) 634 if err := os.Rename(tmpfile, destfile); err != nil { 635 return errors.Wrap(err, "unable to move local charts to charts dir") 636 } 637 } 638 return nil 639 }