github.com/y-taka-23/helm@v2.8.0+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 dl := ChartDownloader{ 201 Out: m.Out, 202 Verify: m.Verify, 203 Keyring: m.Keyring, 204 HelmHome: m.HelmHome, 205 Getters: m.Getters, 206 } 207 208 destPath := filepath.Join(m.ChartPath, "charts") 209 tmpPath := filepath.Join(m.ChartPath, "tmpcharts") 210 211 // Create 'charts' directory if it doesn't already exist. 212 if fi, err := os.Stat(destPath); err != nil { 213 if err := os.MkdirAll(destPath, 0755); err != nil { 214 return err 215 } 216 } else if !fi.IsDir() { 217 return fmt.Errorf("%q is not a directory", destPath) 218 } 219 220 if err := os.Rename(destPath, tmpPath); err != nil { 221 return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) 222 } 223 224 if err := os.MkdirAll(destPath, 0755); err != nil { 225 return err 226 } 227 228 fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) 229 var saveError error 230 for _, dep := range deps { 231 if strings.HasPrefix(dep.Repository, "file://") { 232 if m.Debug { 233 fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) 234 } 235 ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version) 236 if err != nil { 237 saveError = err 238 break 239 } 240 dep.Version = ver 241 continue 242 } 243 244 fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) 245 246 // Any failure to resolve/download a chart should fail: 247 // https://github.com/kubernetes/helm/issues/1439 248 churl, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos) 249 if err != nil { 250 saveError = fmt.Errorf("could not find %s: %s", churl, err) 251 break 252 } 253 254 if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil { 255 saveError = fmt.Errorf("could not download %s: %s", churl, err) 256 break 257 } 258 } 259 260 if saveError == nil { 261 fmt.Fprintln(m.Out, "Deleting outdated charts") 262 for _, dep := range deps { 263 if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil { 264 return err 265 } 266 } 267 if err := move(tmpPath, destPath); err != nil { 268 return err 269 } 270 if err := os.RemoveAll(tmpPath); err != nil { 271 return fmt.Errorf("Failed to remove %v: %v", tmpPath, err) 272 } 273 } else { 274 fmt.Fprintln(m.Out, "Save error occurred: ", saveError) 275 fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state") 276 for _, dep := range deps { 277 if err := m.safeDeleteDep(dep.Name, destPath); err != nil { 278 return err 279 } 280 } 281 if err := os.RemoveAll(destPath); err != nil { 282 return fmt.Errorf("Failed to remove %v: %v", destPath, err) 283 } 284 if err := os.Rename(tmpPath, destPath); err != nil { 285 return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) 286 } 287 return saveError 288 } 289 return nil 290 } 291 292 // safeDeleteDep deletes any versions of the given dependency in the given directory. 293 // 294 // It does this by first matching the file name to an expected pattern, then loading 295 // the file to verify that it is a chart with the same name as the given name. 296 // 297 // Because it requires tar file introspection, it is more intensive than a basic delete. 298 // 299 // This will only return errors that should stop processing entirely. Other errors 300 // will emit log messages or be ignored. 301 func (m *Manager) safeDeleteDep(name, dir string) error { 302 files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz")) 303 if err != nil { 304 // Only for ErrBadPattern 305 return err 306 } 307 for _, fname := range files { 308 ch, err := chartutil.LoadFile(fname) 309 if err != nil { 310 fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) 311 continue 312 } 313 if ch.Metadata.Name != name { 314 // This is not the file you are looking for. 315 continue 316 } 317 if err := os.Remove(fname); err != nil { 318 fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) 319 continue 320 } 321 } 322 return nil 323 } 324 325 // hasAllRepos ensures that all of the referenced deps are in the local repo cache. 326 func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { 327 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 328 if err != nil { 329 return err 330 } 331 repos := rf.Repositories 332 333 // Verify that all repositories referenced in the deps are actually known 334 // by Helm. 335 missing := []string{} 336 for _, dd := range deps { 337 // If repo is from local path, continue 338 if strings.HasPrefix(dd.Repository, "file://") { 339 continue 340 } 341 342 found := false 343 if dd.Repository == "" { 344 found = true 345 } else { 346 for _, repo := range repos { 347 if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { 348 found = true 349 } 350 } 351 } 352 if !found { 353 missing = append(missing, dd.Repository) 354 } 355 } 356 if len(missing) > 0 { 357 return fmt.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", ")) 358 } 359 return nil 360 } 361 362 // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file. 363 func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, error) { 364 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 365 if err != nil { 366 return nil, err 367 } 368 repos := rf.Repositories 369 370 reposMap := make(map[string]string) 371 372 // Verify that all repositories referenced in the deps are actually known 373 // by Helm. 374 missing := []string{} 375 for _, dd := range deps { 376 // if dep chart is from local path, verify the path is valid 377 if strings.HasPrefix(dd.Repository, "file://") { 378 if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil { 379 return nil, err 380 } 381 382 if m.Debug { 383 fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository) 384 } 385 reposMap[dd.Name] = dd.Repository 386 continue 387 } 388 389 found := false 390 391 for _, repo := range repos { 392 if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) || 393 (strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) { 394 found = true 395 dd.Repository = repo.URL 396 reposMap[dd.Name] = repo.Name 397 break 398 } else if urlutil.Equal(repo.URL, dd.Repository) { 399 found = true 400 reposMap[dd.Name] = repo.Name 401 break 402 } 403 } 404 if !found { 405 missing = append(missing, dd.Repository) 406 } 407 } 408 if len(missing) > 0 { 409 if len(missing) > 0 { 410 errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", ")) 411 // It is common for people to try to enter "stable" as a repository instead of the actual URL. 412 // For this case, let's give them a suggestion. 413 containsNonURL := false 414 for _, repo := range missing { 415 if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") { 416 containsNonURL = true 417 } 418 } 419 if containsNonURL { 420 errorMessage += ` 421 Note that repositories must be URLs or aliases. For example, to refer to the stable 422 repository, use "https://kubernetes-charts.storage.googleapis.com/" or "@stable" instead of 423 "stable". Don't forget to add the repo, too ('helm repo add').` 424 } 425 return nil, errors.New(errorMessage) 426 } 427 } 428 return reposMap, nil 429 } 430 431 // UpdateRepositories updates all of the local repos to the latest. 432 func (m *Manager) UpdateRepositories() error { 433 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 434 if err != nil { 435 return err 436 } 437 repos := rf.Repositories 438 if len(repos) > 0 { 439 // This prints warnings straight to out. 440 if err := m.parallelRepoUpdate(repos); err != nil { 441 return err 442 } 443 } 444 return nil 445 } 446 447 func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { 448 out := m.Out 449 fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") 450 var wg sync.WaitGroup 451 for _, c := range repos { 452 r, err := repo.NewChartRepository(c, m.Getters) 453 if err != nil { 454 return err 455 } 456 wg.Add(1) 457 go func(r *repo.ChartRepository) { 458 if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil { 459 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) 460 } else { 461 fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) 462 } 463 wg.Done() 464 }(r) 465 } 466 wg.Wait() 467 fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") 468 return nil 469 } 470 471 // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. 472 // 473 // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the 474 // newest version will be returned. 475 // 476 // repoURL is the repository to search 477 // 478 // If it finds a URL that is "relative", it will prepend the repoURL. 479 func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) { 480 for _, cr := range repos { 481 if urlutil.Equal(repoURL, cr.Config.URL) { 482 entry, err := findEntryByName(name, cr) 483 if err != nil { 484 return "", err 485 } 486 ve, err := findVersionedEntry(version, entry) 487 if err != nil { 488 return "", err 489 } 490 491 return normalizeURL(repoURL, ve.URLs[0]) 492 } 493 } 494 return "", fmt.Errorf("chart %s not found in %s", name, repoURL) 495 } 496 497 // findEntryByName finds an entry in the chart repository whose name matches the given name. 498 // 499 // It returns the ChartVersions for that entry. 500 func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) { 501 for ename, entry := range cr.IndexFile.Entries { 502 if ename == name { 503 return entry, nil 504 } 505 } 506 return nil, errors.New("entry not found") 507 } 508 509 // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints. 510 // 511 // If version is empty, the first chart found is returned. 512 func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) { 513 for _, verEntry := range vers { 514 if len(verEntry.URLs) == 0 { 515 // Not a legit entry. 516 continue 517 } 518 519 if version == "" || versionEquals(version, verEntry.Version) { 520 return verEntry, nil 521 } 522 } 523 return nil, errors.New("no matching version") 524 } 525 526 func versionEquals(v1, v2 string) bool { 527 sv1, err := semver.NewVersion(v1) 528 if err != nil { 529 // Fallback to string comparison. 530 return v1 == v2 531 } 532 sv2, err := semver.NewVersion(v2) 533 if err != nil { 534 return false 535 } 536 return sv1.Equal(sv2) 537 } 538 539 func normalizeURL(baseURL, urlOrPath string) (string, error) { 540 u, err := url.Parse(urlOrPath) 541 if err != nil { 542 return urlOrPath, err 543 } 544 if u.IsAbs() { 545 return u.String(), nil 546 } 547 u2, err := url.Parse(baseURL) 548 if err != nil { 549 return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err) 550 } 551 552 u2.Path = path.Join(u2.Path, urlOrPath) 553 return u2.String(), nil 554 } 555 556 // loadChartRepositories reads the repositories.yaml, and then builds a map of 557 // ChartRepositories. 558 // 559 // The key is the local name (which is only present in the repositories.yaml). 560 func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { 561 indices := map[string]*repo.ChartRepository{} 562 repoyaml := m.HelmHome.RepositoryFile() 563 564 // Load repositories.yaml file 565 rf, err := repo.LoadRepositoriesFile(repoyaml) 566 if err != nil { 567 return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) 568 } 569 570 for _, re := range rf.Repositories { 571 lname := re.Name 572 cacheindex := m.HelmHome.CacheIndex(lname) 573 index, err := repo.LoadIndexFile(cacheindex) 574 if err != nil { 575 return indices, err 576 } 577 578 // TODO: use constructor 579 cr := &repo.ChartRepository{ 580 Config: re, 581 IndexFile: index, 582 } 583 indices[lname] = cr 584 } 585 return indices, nil 586 } 587 588 // writeLock writes a lockfile to disk 589 func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { 590 data, err := yaml.Marshal(lock) 591 if err != nil { 592 return err 593 } 594 dest := filepath.Join(chartpath, "requirements.lock") 595 return ioutil.WriteFile(dest, data, 0644) 596 } 597 598 // archive a dep chart from local directory and save it into charts/ 599 func tarFromLocalDir(chartpath string, name string, repo string, version string) (string, error) { 600 destPath := filepath.Join(chartpath, "charts") 601 602 if !strings.HasPrefix(repo, "file://") { 603 return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo) 604 } 605 606 origPath, err := resolver.GetLocalPath(repo, chartpath) 607 if err != nil { 608 return "", err 609 } 610 611 ch, err := chartutil.LoadDir(origPath) 612 if err != nil { 613 return "", err 614 } 615 616 constraint, err := semver.NewConstraint(version) 617 if err != nil { 618 return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %s", name, err) 619 } 620 621 v, err := semver.NewVersion(ch.Metadata.Version) 622 if err != nil { 623 return "", err 624 } 625 626 if constraint.Check(v) { 627 _, err = chartutil.Save(ch, destPath) 628 return ch.Metadata.Version, err 629 } 630 631 return "", fmt.Errorf("can't get a valid version for dependency %s", name) 632 } 633 634 // move files from tmppath to destpath 635 func move(tmpPath, destPath string) error { 636 files, _ := ioutil.ReadDir(tmpPath) 637 for _, file := range files { 638 filename := file.Name() 639 tmpfile := filepath.Join(tmpPath, filename) 640 destfile := filepath.Join(destPath, filename) 641 if err := os.Rename(tmpfile, destfile); err != nil { 642 return fmt.Errorf("Unable to move local charts to charts dir: %v", err) 643 } 644 } 645 return nil 646 }