github.com/sgoings/helm@v2.0.0-alpha.2.0.20170406211108-734e92851ac3+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/helm/helmpath" 35 "k8s.io/helm/pkg/proto/hapi/chart" 36 "k8s.io/helm/pkg/repo" 37 "k8s.io/helm/pkg/resolver" 38 "k8s.io/helm/pkg/urlutil" 39 ) 40 41 // Manager handles the lifecycle of fetching, resolving, and storing dependencies. 42 type Manager struct { 43 // Out is used to print warnings and notifications. 44 Out io.Writer 45 // ChartPath is the path to the unpacked base chart upon which this operates. 46 ChartPath string 47 // HelmHome is the $HELM_HOME directory 48 HelmHome helmpath.Home 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 } 58 59 // Build rebuilds a local charts directory from a lockfile. 60 // 61 // If the lockfile is not present, this will run a Manager.Update() 62 // 63 // If SkipUpdate is set, this will not update the repository. 64 func (m *Manager) Build() error { 65 c, err := m.loadChartDir() 66 if err != nil { 67 return err 68 } 69 70 // If a lock file is found, run a build from that. Otherwise, just do 71 // an update. 72 lock, err := chartutil.LoadRequirementsLock(c) 73 if err != nil { 74 return m.Update() 75 } 76 77 // A lock must accompany a requirements.yaml file. 78 req, err := chartutil.LoadRequirements(c) 79 if err != nil { 80 return fmt.Errorf("requirements.yaml cannot be opened: %s", err) 81 } 82 if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest { 83 return fmt.Errorf("requirements.lock is out of sync with requirements.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 requirements.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 requirements file is found, we consider this a successful 118 // completion. 119 req, err := chartutil.LoadRequirements(c) 120 if err != nil { 121 if err == chartutil.ErrRequirementsNotFound { 122 fmt.Fprintf(m.Out, "No requirements found in %s/charts.\n", m.ChartPath) 123 return nil 124 } 125 return err 126 } 127 128 // Hash requirements.yaml 129 hash, err := resolver.HashReq(req) 130 if err != nil { 131 return err 132 } 133 134 // Check that all of the repos we're dependent on actually exist and 135 // the repo index names. 136 repoNames, err := m.getRepoNames(req.Dependencies) 137 if err != nil { 138 return err 139 } 140 141 // For each repo in the file, update the cached copy of that repo 142 if !m.SkipUpdate { 143 if err := m.UpdateRepositories(); err != nil { 144 return err 145 } 146 } 147 148 // Now we need to find out which version of a chart best satisfies the 149 // requirements the requirements.yaml 150 lock, err := m.resolve(req, repoNames, hash) 151 if err != nil { 152 return err 153 } 154 155 // Now we need to fetch every package here into charts/ 156 if err := m.downloadAll(lock.Dependencies); err != nil { 157 return err 158 } 159 160 // If the lock file hasn't changed, don't write a new one. 161 oldLock, err := chartutil.LoadRequirementsLock(c) 162 if err == nil && oldLock.Digest == lock.Digest { 163 return nil 164 } 165 166 // Finally, we need to write the lockfile. 167 return writeLock(m.ChartPath, lock) 168 } 169 170 func (m *Manager) loadChartDir() (*chart.Chart, error) { 171 if fi, err := os.Stat(m.ChartPath); err != nil { 172 return nil, fmt.Errorf("could not find %s: %s", m.ChartPath, err) 173 } else if !fi.IsDir() { 174 return nil, errors.New("only unpacked charts can be updated") 175 } 176 return chartutil.LoadDir(m.ChartPath) 177 } 178 179 // resolve takes a list of requirements and translates them into an exact version to download. 180 // 181 // This returns a lock file, which has all of the requirements normalized to a specific version. 182 func (m *Manager) resolve(req *chartutil.Requirements, repoNames map[string]string, hash string) (*chartutil.RequirementsLock, error) { 183 res := resolver.New(m.ChartPath, m.HelmHome) 184 return res.Resolve(req, repoNames, hash) 185 } 186 187 // downloadAll takes a list of dependencies and downloads them into charts/ 188 // 189 // It will delete versions of the chart that exist on disk and might cause 190 // a conflict. 191 func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { 192 repos, err := m.loadChartRepositories() 193 if err != nil { 194 return err 195 } 196 197 dl := ChartDownloader{ 198 Out: m.Out, 199 Verify: m.Verify, 200 Keyring: m.Keyring, 201 HelmHome: m.HelmHome, 202 } 203 204 destPath := filepath.Join(m.ChartPath, "charts") 205 206 // Create 'charts' directory if it doesn't already exist. 207 if fi, err := os.Stat(destPath); err != nil { 208 if err := os.MkdirAll(destPath, 0755); err != nil { 209 return err 210 } 211 } else if !fi.IsDir() { 212 return fmt.Errorf("%q is not a directory", destPath) 213 } 214 215 fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) 216 for _, dep := range deps { 217 if err := m.safeDeleteDep(dep.Name, destPath); err != nil { 218 return err 219 } 220 221 if strings.HasPrefix(dep.Repository, "file://") { 222 if m.Debug { 223 fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) 224 } 225 ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version) 226 if err != nil { 227 return err 228 } 229 dep.Version = ver 230 continue 231 } 232 233 fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) 234 235 // Any failure to resolve/download a chart should fail: 236 // https://github.com/kubernetes/helm/issues/1439 237 churl, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos) 238 if err != nil { 239 return fmt.Errorf("could not find %s: %s", churl, err) 240 } 241 242 if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil { 243 return fmt.Errorf("could not download %s: %s", churl, err) 244 } 245 } 246 return nil 247 } 248 249 // safeDeleteDep deletes any versions of the given dependency in the given directory. 250 // 251 // It does this by first matching the file name to an expected pattern, then loading 252 // the file to verify that it is a chart with the same name as the given name. 253 // 254 // Because it requires tar file introspection, it is more intensive than a basic delete. 255 // 256 // This will only return errors that should stop processing entirely. Other errors 257 // will emit log messages or be ignored. 258 func (m *Manager) safeDeleteDep(name, dir string) error { 259 files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz")) 260 if err != nil { 261 // Only for ErrBadPattern 262 return err 263 } 264 for _, fname := range files { 265 ch, err := chartutil.LoadFile(fname) 266 if err != nil { 267 fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) 268 continue 269 } 270 if ch.Metadata.Name != name { 271 // This is not the file you are looking for. 272 continue 273 } 274 if err := os.Remove(fname); err != nil { 275 fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) 276 continue 277 } 278 } 279 return nil 280 } 281 282 // hasAllRepos ensures that all of the referenced deps are in the local repo cache. 283 func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { 284 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 285 if err != nil { 286 return err 287 } 288 repos := rf.Repositories 289 290 // Verify that all repositories referenced in the deps are actually known 291 // by Helm. 292 missing := []string{} 293 for _, dd := range deps { 294 // If repo is from local path, continue 295 if strings.HasPrefix(dd.Repository, "file://") { 296 continue 297 } 298 299 found := false 300 if dd.Repository == "" { 301 found = true 302 } else { 303 for _, repo := range repos { 304 if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { 305 found = true 306 } 307 } 308 } 309 if !found { 310 missing = append(missing, dd.Repository) 311 } 312 } 313 if len(missing) > 0 { 314 return fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", ")) 315 } 316 return nil 317 } 318 319 // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file. 320 func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, error) { 321 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 322 if err != nil { 323 return nil, err 324 } 325 repos := rf.Repositories 326 327 reposMap := make(map[string]string) 328 329 // Verify that all repositories referenced in the deps are actually known 330 // by Helm. 331 missing := []string{} 332 for _, dd := range deps { 333 // if dep chart is from local path, verify the path is valid 334 if strings.HasPrefix(dd.Repository, "file://") { 335 if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil { 336 return nil, err 337 } 338 339 if m.Debug { 340 fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository) 341 } 342 reposMap[dd.Name] = dd.Repository 343 continue 344 } 345 346 found := false 347 348 for _, repo := range repos { 349 if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) || 350 (strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) { 351 found = true 352 dd.Repository = repo.URL 353 reposMap[dd.Name] = repo.Name 354 break 355 } else if urlutil.Equal(repo.URL, dd.Repository) { 356 found = true 357 reposMap[dd.Name] = repo.Name 358 break 359 } 360 } 361 if !found { 362 missing = append(missing, dd.Repository) 363 } 364 } 365 if len(missing) > 0 { 366 return nil, fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", ")) 367 } 368 return reposMap, nil 369 } 370 371 // UpdateRepositories updates all of the local repos to the latest. 372 func (m *Manager) UpdateRepositories() error { 373 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 374 if err != nil { 375 return err 376 } 377 repos := rf.Repositories 378 if len(repos) > 0 { 379 // This prints warnings straight to out. 380 if err := m.parallelRepoUpdate(repos); err != nil { 381 return err 382 } 383 } 384 return nil 385 } 386 387 func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { 388 out := m.Out 389 fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") 390 var wg sync.WaitGroup 391 for _, c := range repos { 392 r, err := repo.NewChartRepository(c) 393 if err != nil { 394 return err 395 } 396 wg.Add(1) 397 go func(r *repo.ChartRepository) { 398 if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil { 399 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) 400 } else { 401 fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) 402 } 403 wg.Done() 404 }(r) 405 } 406 wg.Wait() 407 fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") 408 return nil 409 } 410 411 // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. 412 // 413 // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the 414 // newest version will be returned. 415 // 416 // repoURL is the repository to search 417 // 418 // If it finds a URL that is "relative", it will prepend the repoURL. 419 func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) { 420 for _, cr := range repos { 421 if urlutil.Equal(repoURL, cr.Config.URL) { 422 entry, err := findEntryByName(name, cr) 423 if err != nil { 424 return "", err 425 } 426 ve, err := findVersionedEntry(version, entry) 427 if err != nil { 428 return "", err 429 } 430 431 return normalizeURL(repoURL, ve.URLs[0]) 432 } 433 } 434 return "", fmt.Errorf("chart %s not found in %s", name, repoURL) 435 } 436 437 // findEntryByName finds an entry in the chart repository whose name matches the given name. 438 // 439 // It returns the ChartVersions for that entry. 440 func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) { 441 for ename, entry := range cr.IndexFile.Entries { 442 if ename == name { 443 return entry, nil 444 } 445 } 446 return nil, errors.New("entry not found") 447 } 448 449 // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints. 450 // 451 // If version is empty, the first chart found is returned. 452 func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) { 453 for _, verEntry := range vers { 454 if len(verEntry.URLs) == 0 { 455 // Not a legit entry. 456 continue 457 } 458 459 if version == "" || versionEquals(version, verEntry.Version) { 460 return verEntry, nil 461 } 462 } 463 return nil, errors.New("no matching version") 464 } 465 466 func versionEquals(v1, v2 string) bool { 467 sv1, err := semver.NewVersion(v1) 468 if err != nil { 469 // Fallback to string comparison. 470 return v1 == v2 471 } 472 sv2, err := semver.NewVersion(v2) 473 if err != nil { 474 return false 475 } 476 return sv1.Equal(sv2) 477 } 478 479 func normalizeURL(baseURL, urlOrPath string) (string, error) { 480 u, err := url.Parse(urlOrPath) 481 if err != nil { 482 return urlOrPath, err 483 } 484 if u.IsAbs() { 485 return u.String(), nil 486 } 487 u2, err := url.Parse(baseURL) 488 if err != nil { 489 return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err) 490 } 491 492 u2.Path = path.Join(u2.Path, urlOrPath) 493 return u2.String(), nil 494 } 495 496 // loadChartRepositories reads the repositories.yaml, and then builds a map of 497 // ChartRepositories. 498 // 499 // The key is the local name (which is only present in the repositories.yaml). 500 func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { 501 indices := map[string]*repo.ChartRepository{} 502 repoyaml := m.HelmHome.RepositoryFile() 503 504 // Load repositories.yaml file 505 rf, err := repo.LoadRepositoriesFile(repoyaml) 506 if err != nil { 507 return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) 508 } 509 510 for _, re := range rf.Repositories { 511 lname := re.Name 512 cacheindex := m.HelmHome.CacheIndex(lname) 513 index, err := repo.LoadIndexFile(cacheindex) 514 if err != nil { 515 return indices, err 516 } 517 518 // TODO: use constructor 519 cr := &repo.ChartRepository{ 520 Config: re, 521 IndexFile: index, 522 } 523 indices[lname] = cr 524 } 525 return indices, nil 526 } 527 528 // writeLock writes a lockfile to disk 529 func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { 530 data, err := yaml.Marshal(lock) 531 if err != nil { 532 return err 533 } 534 dest := filepath.Join(chartpath, "requirements.lock") 535 return ioutil.WriteFile(dest, data, 0644) 536 } 537 538 // archive a dep chart from local directory and save it into charts/ 539 func tarFromLocalDir(chartpath string, name string, repo string, version string) (string, error) { 540 destPath := filepath.Join(chartpath, "charts") 541 542 if !strings.HasPrefix(repo, "file://") { 543 return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo) 544 } 545 546 origPath, err := resolver.GetLocalPath(repo, chartpath) 547 if err != nil { 548 return "", err 549 } 550 551 ch, err := chartutil.LoadDir(origPath) 552 if err != nil { 553 return "", err 554 } 555 556 constraint, err := semver.NewConstraint(version) 557 if err != nil { 558 return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %s", name, err) 559 } 560 561 v, err := semver.NewVersion(ch.Metadata.Version) 562 if err != nil { 563 return "", err 564 } 565 566 if constraint.Check(v) { 567 _, err = chartutil.Save(ch, destPath) 568 return ch.Metadata.Version, err 569 } 570 571 return "", fmt.Errorf("Can't get a valid version for dependency %s.", name) 572 }