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