github.com/darkowlzz/helm@v2.5.1-0.20171213183701-6707fe0468d4+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. Try '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 return nil, fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", ")) 410 } 411 return reposMap, nil 412 } 413 414 // UpdateRepositories updates all of the local repos to the latest. 415 func (m *Manager) UpdateRepositories() error { 416 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 417 if err != nil { 418 return err 419 } 420 repos := rf.Repositories 421 if len(repos) > 0 { 422 // This prints warnings straight to out. 423 if err := m.parallelRepoUpdate(repos); err != nil { 424 return err 425 } 426 } 427 return nil 428 } 429 430 func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { 431 out := m.Out 432 fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") 433 var wg sync.WaitGroup 434 for _, c := range repos { 435 r, err := repo.NewChartRepository(c, m.Getters) 436 if err != nil { 437 return err 438 } 439 wg.Add(1) 440 go func(r *repo.ChartRepository) { 441 if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil { 442 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) 443 } else { 444 fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) 445 } 446 wg.Done() 447 }(r) 448 } 449 wg.Wait() 450 fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") 451 return nil 452 } 453 454 // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. 455 // 456 // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the 457 // newest version will be returned. 458 // 459 // repoURL is the repository to search 460 // 461 // If it finds a URL that is "relative", it will prepend the repoURL. 462 func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) { 463 for _, cr := range repos { 464 if urlutil.Equal(repoURL, cr.Config.URL) { 465 entry, err := findEntryByName(name, cr) 466 if err != nil { 467 return "", err 468 } 469 ve, err := findVersionedEntry(version, entry) 470 if err != nil { 471 return "", err 472 } 473 474 return normalizeURL(repoURL, ve.URLs[0]) 475 } 476 } 477 return "", fmt.Errorf("chart %s not found in %s", name, repoURL) 478 } 479 480 // findEntryByName finds an entry in the chart repository whose name matches the given name. 481 // 482 // It returns the ChartVersions for that entry. 483 func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) { 484 for ename, entry := range cr.IndexFile.Entries { 485 if ename == name { 486 return entry, nil 487 } 488 } 489 return nil, errors.New("entry not found") 490 } 491 492 // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints. 493 // 494 // If version is empty, the first chart found is returned. 495 func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) { 496 for _, verEntry := range vers { 497 if len(verEntry.URLs) == 0 { 498 // Not a legit entry. 499 continue 500 } 501 502 if version == "" || versionEquals(version, verEntry.Version) { 503 return verEntry, nil 504 } 505 } 506 return nil, errors.New("no matching version") 507 } 508 509 func versionEquals(v1, v2 string) bool { 510 sv1, err := semver.NewVersion(v1) 511 if err != nil { 512 // Fallback to string comparison. 513 return v1 == v2 514 } 515 sv2, err := semver.NewVersion(v2) 516 if err != nil { 517 return false 518 } 519 return sv1.Equal(sv2) 520 } 521 522 func normalizeURL(baseURL, urlOrPath string) (string, error) { 523 u, err := url.Parse(urlOrPath) 524 if err != nil { 525 return urlOrPath, err 526 } 527 if u.IsAbs() { 528 return u.String(), nil 529 } 530 u2, err := url.Parse(baseURL) 531 if err != nil { 532 return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err) 533 } 534 535 u2.Path = path.Join(u2.Path, urlOrPath) 536 return u2.String(), nil 537 } 538 539 // loadChartRepositories reads the repositories.yaml, and then builds a map of 540 // ChartRepositories. 541 // 542 // The key is the local name (which is only present in the repositories.yaml). 543 func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { 544 indices := map[string]*repo.ChartRepository{} 545 repoyaml := m.HelmHome.RepositoryFile() 546 547 // Load repositories.yaml file 548 rf, err := repo.LoadRepositoriesFile(repoyaml) 549 if err != nil { 550 return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) 551 } 552 553 for _, re := range rf.Repositories { 554 lname := re.Name 555 cacheindex := m.HelmHome.CacheIndex(lname) 556 index, err := repo.LoadIndexFile(cacheindex) 557 if err != nil { 558 return indices, err 559 } 560 561 // TODO: use constructor 562 cr := &repo.ChartRepository{ 563 Config: re, 564 IndexFile: index, 565 } 566 indices[lname] = cr 567 } 568 return indices, nil 569 } 570 571 // writeLock writes a lockfile to disk 572 func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { 573 data, err := yaml.Marshal(lock) 574 if err != nil { 575 return err 576 } 577 dest := filepath.Join(chartpath, "requirements.lock") 578 return ioutil.WriteFile(dest, data, 0644) 579 } 580 581 // archive a dep chart from local directory and save it into charts/ 582 func tarFromLocalDir(chartpath string, name string, repo string, version string) (string, error) { 583 destPath := filepath.Join(chartpath, "charts") 584 585 if !strings.HasPrefix(repo, "file://") { 586 return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo) 587 } 588 589 origPath, err := resolver.GetLocalPath(repo, chartpath) 590 if err != nil { 591 return "", err 592 } 593 594 ch, err := chartutil.LoadDir(origPath) 595 if err != nil { 596 return "", err 597 } 598 599 constraint, err := semver.NewConstraint(version) 600 if err != nil { 601 return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %s", name, err) 602 } 603 604 v, err := semver.NewVersion(ch.Metadata.Version) 605 if err != nil { 606 return "", err 607 } 608 609 if constraint.Check(v) { 610 _, err = chartutil.Save(ch, destPath) 611 return ch.Metadata.Version, err 612 } 613 614 return "", fmt.Errorf("can't get a valid version for dependency %s", name) 615 } 616 617 // move files from tmppath to destpath 618 func move(tmpPath, destPath string) error { 619 files, _ := ioutil.ReadDir(tmpPath) 620 for _, file := range files { 621 filename := file.Name() 622 tmpfile := filepath.Join(tmpPath, filename) 623 destfile := filepath.Join(destPath, filename) 624 if err := os.Rename(tmpfile, destfile); err != nil { 625 return fmt.Errorf("Unable to move local charts to charts dir: %v", err) 626 } 627 } 628 return nil 629 }