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