github.com/x-helm/helm@v3.0.0-beta.3+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/internal/resolver" 34 "helm.sh/helm/internal/urlutil" 35 "helm.sh/helm/pkg/chart" 36 "helm.sh/helm/pkg/chart/loader" 37 "helm.sh/helm/pkg/chartutil" 38 "helm.sh/helm/pkg/getter" 39 "helm.sh/helm/pkg/helmpath" 40 "helm.sh/helm/pkg/repo" 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 // Verification indicates whether the chart should be verified. 50 Verify VerificationStrategy 51 // Debug is the global "--debug" flag 52 Debug bool 53 // Keyring is the key ring file. 54 Keyring string 55 // SkipUpdate indicates that the repository should not be updated first. 56 SkipUpdate bool 57 // Getter collection for the operation 58 Getters []getter.Provider 59 RepositoryConfig string 60 RepositoryCache string 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 return m.downloadAll(lock.Dependencies) 100 } 101 102 // Update updates a local charts directory. 103 // 104 // It first reads the Chart.yaml file, and then attempts to 105 // negotiate versions based on that. It will download the versions 106 // from remote chart repositories unless SkipUpdate is true. 107 func (m *Manager) Update() error { 108 c, err := m.loadChartDir() 109 if err != nil { 110 return err 111 } 112 113 // If no dependencies are found, we consider this a successful 114 // completion. 115 req := c.Metadata.Dependencies 116 if req == nil { 117 return nil 118 } 119 120 // Check that all of the repos we're dependent on actually exist and 121 // the repo index names. 122 repoNames, err := m.getRepoNames(req) 123 if err != nil { 124 return err 125 } 126 127 // For each repo in the file, update the cached copy of that repo 128 if !m.SkipUpdate { 129 if err := m.UpdateRepositories(); err != nil { 130 return err 131 } 132 } 133 134 // Now we need to find out which version of a chart best satisfies the 135 // dependencies in the Chart.yaml 136 lock, err := m.resolve(req, repoNames) 137 if err != nil { 138 return err 139 } 140 141 // Now we need to fetch every package here into charts/ 142 if err := m.downloadAll(lock.Dependencies); err != nil { 143 return err 144 } 145 146 // If the lock file hasn't changed, don't write a new one. 147 oldLock := c.Lock 148 if oldLock != nil && oldLock.Digest == lock.Digest { 149 return nil 150 } 151 152 // Finally, we need to write the lockfile. 153 return writeLock(m.ChartPath, lock) 154 } 155 156 func (m *Manager) loadChartDir() (*chart.Chart, error) { 157 if fi, err := os.Stat(m.ChartPath); err != nil { 158 return nil, errors.Wrapf(err, "could not find %s", m.ChartPath) 159 } else if !fi.IsDir() { 160 return nil, errors.New("only unpacked charts can be updated") 161 } 162 return loader.LoadDir(m.ChartPath) 163 } 164 165 // resolve takes a list of dependencies and translates them into an exact version to download. 166 // 167 // This returns a lock file, which has all of the dependencies normalized to a specific version. 168 func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) { 169 res := resolver.New(m.ChartPath, m.RepositoryCache) 170 return res.Resolve(req, repoNames) 171 } 172 173 // downloadAll takes a list of dependencies and downloads them into charts/ 174 // 175 // It will delete versions of the chart that exist on disk and might cause 176 // a conflict. 177 func (m *Manager) downloadAll(deps []*chart.Dependency) error { 178 repos, err := m.loadChartRepositories() 179 if err != nil { 180 return err 181 } 182 183 destPath := filepath.Join(m.ChartPath, "charts") 184 tmpPath := filepath.Join(m.ChartPath, "tmpcharts") 185 186 // Create 'charts' directory if it doesn't already exist. 187 if fi, err := os.Stat(destPath); err != nil { 188 if err := os.MkdirAll(destPath, 0755); err != nil { 189 return err 190 } 191 } else if !fi.IsDir() { 192 return errors.Errorf("%q is not a directory", destPath) 193 } 194 195 if err := os.Rename(destPath, tmpPath); err != nil { 196 return errors.Wrap(err, "unable to move current charts to tmp dir") 197 } 198 199 if err := os.MkdirAll(destPath, 0755); err != nil { 200 return err 201 } 202 203 fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) 204 var saveError error 205 for _, dep := range deps { 206 if strings.HasPrefix(dep.Repository, "file://") { 207 if m.Debug { 208 fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) 209 } 210 ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version) 211 if err != nil { 212 saveError = err 213 break 214 } 215 dep.Version = ver 216 continue 217 } 218 219 fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) 220 221 // Any failure to resolve/download a chart should fail: 222 // https://github.com/helm/helm/issues/1439 223 churl, username, password, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos) 224 if err != nil { 225 saveError = errors.Wrapf(err, "could not find %s", churl) 226 break 227 } 228 229 dl := ChartDownloader{ 230 Out: m.Out, 231 Verify: m.Verify, 232 Keyring: m.Keyring, 233 RepositoryConfig: m.RepositoryConfig, 234 RepositoryCache: m.RepositoryCache, 235 Getters: m.Getters, 236 Options: []getter.Option{ 237 getter.WithBasicAuth(username, password), 238 }, 239 } 240 241 if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil { 242 saveError = errors.Wrapf(err, "could not download %s", churl) 243 break 244 } 245 } 246 247 if saveError == nil { 248 fmt.Fprintln(m.Out, "Deleting outdated charts") 249 for _, dep := range deps { 250 if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil { 251 return err 252 } 253 } 254 if err := move(tmpPath, destPath); err != nil { 255 return err 256 } 257 if err := os.RemoveAll(tmpPath); err != nil { 258 return errors.Wrapf(err, "failed to remove %v", tmpPath) 259 } 260 } else { 261 fmt.Fprintln(m.Out, "Save error occurred: ", saveError) 262 fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state") 263 for _, dep := range deps { 264 if err := m.safeDeleteDep(dep.Name, destPath); err != nil { 265 return err 266 } 267 } 268 if err := os.RemoveAll(destPath); err != nil { 269 return errors.Wrapf(err, "failed to remove %v", destPath) 270 } 271 if err := os.Rename(tmpPath, destPath); err != nil { 272 return errors.Wrap(err, "unable to move current charts to tmp dir") 273 } 274 return saveError 275 } 276 return nil 277 } 278 279 // safeDeleteDep deletes any versions of the given dependency in the given directory. 280 // 281 // It does this by first matching the file name to an expected pattern, then loading 282 // the file to verify that it is a chart with the same name as the given name. 283 // 284 // Because it requires tar file introspection, it is more intensive than a basic delete. 285 // 286 // This will only return errors that should stop processing entirely. Other errors 287 // will emit log messages or be ignored. 288 func (m *Manager) safeDeleteDep(name, dir string) error { 289 files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz")) 290 if err != nil { 291 // Only for ErrBadPattern 292 return err 293 } 294 for _, fname := range files { 295 ch, err := loader.LoadFile(fname) 296 if err != nil { 297 fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) 298 continue 299 } 300 if ch.Name() != name { 301 // This is not the file you are looking for. 302 continue 303 } 304 if err := os.Remove(fname); err != nil { 305 fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) 306 continue 307 } 308 } 309 return nil 310 } 311 312 // hasAllRepos ensures that all of the referenced deps are in the local repo cache. 313 func (m *Manager) hasAllRepos(deps []*chart.Dependency) error { 314 rf, err := loadRepoConfig(m.RepositoryConfig) 315 if err != nil { 316 return err 317 } 318 repos := rf.Repositories 319 320 // Verify that all repositories referenced in the deps are actually known 321 // by Helm. 322 missing := []string{} 323 Loop: 324 for _, dd := range deps { 325 // If repo is from local path, continue 326 if strings.HasPrefix(dd.Repository, "file://") { 327 continue 328 } 329 330 if dd.Repository == "" { 331 continue 332 } 333 for _, repo := range repos { 334 if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { 335 continue Loop 336 } 337 } 338 missing = append(missing, dd.Repository) 339 } 340 if len(missing) > 0 { 341 return errors.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", ")) 342 } 343 return nil 344 } 345 346 // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file. 347 func (m *Manager) getRepoNames(deps []*chart.Dependency) (map[string]string, error) { 348 rf, err := loadRepoConfig(m.RepositoryConfig) 349 if err != nil { 350 if os.IsNotExist(err) { 351 return make(map[string]string), nil 352 } 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 "example" 408 repository, use "https://charts.example.com/" or "@example" instead of 409 "example". 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 := loadRepoConfig(m.RepositoryConfig) 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 fmt.Fprintln(m.Out, "Hang tight while we grab the latest from your chart repositories...") 434 var wg sync.WaitGroup 435 for _, c := range repos { 436 r, err := repo.NewChartRepository(c, m.Getters) 437 if err != nil { 438 return err 439 } 440 wg.Add(1) 441 go func(r *repo.ChartRepository) { 442 if _, err := r.DownloadIndexFile(); err != nil { 443 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) 444 } else { 445 fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) 446 } 447 wg.Done() 448 }(r) 449 } 450 wg.Wait() 451 fmt.Fprintln(m.Out, "Update Complete. ⎈Happy Helming!⎈") 452 return nil 453 } 454 455 // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. 456 // 457 // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the 458 // newest version will be returned. 459 // 460 // repoURL is the repository to search 461 // 462 // If it finds a URL that is "relative", it will prepend the repoURL. 463 func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) { 464 for _, cr := range repos { 465 if urlutil.Equal(repoURL, cr.Config.URL) { 466 var entry repo.ChartVersions 467 entry, err = findEntryByName(name, cr) 468 if err != nil { 469 return 470 } 471 var ve *repo.ChartVersion 472 ve, err = findVersionedEntry(version, entry) 473 if err != nil { 474 return 475 } 476 url, err = normalizeURL(repoURL, ve.URLs[0]) 477 if err != nil { 478 return 479 } 480 username = cr.Config.Username 481 password = cr.Config.Password 482 return 483 } 484 } 485 err = errors.Errorf("chart %s not found in %s", name, repoURL) 486 return 487 } 488 489 // findEntryByName finds an entry in the chart repository whose name matches the given name. 490 // 491 // It returns the ChartVersions for that entry. 492 func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) { 493 for ename, entry := range cr.IndexFile.Entries { 494 if ename == name { 495 return entry, nil 496 } 497 } 498 return nil, errors.New("entry not found") 499 } 500 501 // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints. 502 // 503 // If version is empty, the first chart found is returned. 504 func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) { 505 for _, verEntry := range vers { 506 if len(verEntry.URLs) == 0 { 507 // Not a legit entry. 508 continue 509 } 510 511 if version == "" || versionEquals(version, verEntry.Version) { 512 return verEntry, nil 513 } 514 } 515 return nil, errors.New("no matching version") 516 } 517 518 func versionEquals(v1, v2 string) bool { 519 sv1, err := semver.NewVersion(v1) 520 if err != nil { 521 // Fallback to string comparison. 522 return v1 == v2 523 } 524 sv2, err := semver.NewVersion(v2) 525 if err != nil { 526 return false 527 } 528 return sv1.Equal(sv2) 529 } 530 531 func normalizeURL(baseURL, urlOrPath string) (string, error) { 532 u, err := url.Parse(urlOrPath) 533 if err != nil { 534 return urlOrPath, err 535 } 536 if u.IsAbs() { 537 return u.String(), nil 538 } 539 u2, err := url.Parse(baseURL) 540 if err != nil { 541 return urlOrPath, errors.Wrap(err, "base URL failed to parse") 542 } 543 544 u2.Path = path.Join(u2.Path, urlOrPath) 545 return u2.String(), nil 546 } 547 548 // loadChartRepositories reads the repositories.yaml, and then builds a map of 549 // ChartRepositories. 550 // 551 // The key is the local name (which is only present in the repositories.yaml). 552 func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { 553 indices := map[string]*repo.ChartRepository{} 554 555 // Load repositories.yaml file 556 rf, err := loadRepoConfig(m.RepositoryConfig) 557 if err != nil { 558 return indices, errors.Wrapf(err, "failed to load %s", m.RepositoryConfig) 559 } 560 561 for _, re := range rf.Repositories { 562 lname := re.Name 563 idxFile := filepath.Join(m.RepositoryCache, helmpath.CacheIndexFile(lname)) 564 index, err := repo.LoadIndexFile(idxFile) 565 if err != nil { 566 return indices, err 567 } 568 569 // TODO: use constructor 570 cr := &repo.ChartRepository{ 571 Config: re, 572 IndexFile: index, 573 } 574 indices[lname] = cr 575 } 576 return indices, nil 577 } 578 579 // writeLock writes a lockfile to disk 580 func writeLock(chartpath string, lock *chart.Lock) error { 581 data, err := yaml.Marshal(lock) 582 if err != nil { 583 return err 584 } 585 dest := filepath.Join(chartpath, "Chart.lock") 586 return ioutil.WriteFile(dest, data, 0644) 587 } 588 589 // archive a dep chart from local directory and save it into charts/ 590 func tarFromLocalDir(chartpath, name, repo, version string) (string, error) { 591 destPath := filepath.Join(chartpath, "charts") 592 593 if !strings.HasPrefix(repo, "file://") { 594 return "", errors.Errorf("wrong format: chart %s repository %s", name, repo) 595 } 596 597 origPath, err := resolver.GetLocalPath(repo, chartpath) 598 if err != nil { 599 return "", err 600 } 601 602 ch, err := loader.LoadDir(origPath) 603 if err != nil { 604 return "", err 605 } 606 607 constraint, err := semver.NewConstraint(version) 608 if err != nil { 609 return "", errors.Wrapf(err, "dependency %s has an invalid version/constraint format", name) 610 } 611 612 v, err := semver.NewVersion(ch.Metadata.Version) 613 if err != nil { 614 return "", err 615 } 616 617 if constraint.Check(v) { 618 _, err = chartutil.Save(ch, destPath) 619 return ch.Metadata.Version, err 620 } 621 622 return "", errors.Errorf("can't get a valid version for dependency %s", name) 623 } 624 625 // move files from tmppath to destpath 626 func move(tmpPath, destPath string) error { 627 files, _ := ioutil.ReadDir(tmpPath) 628 for _, file := range files { 629 filename := file.Name() 630 tmpfile := filepath.Join(tmpPath, filename) 631 destfile := filepath.Join(destPath, filename) 632 if err := os.Rename(tmpfile, destfile); err != nil { 633 return errors.Wrap(err, "unable to move local charts to charts dir") 634 } 635 } 636 return nil 637 }