github.com/sdbaiguanghe/helm@v2.16.7+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 "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/internal/third_party/dep/fs" 34 "k8s.io/helm/pkg/chartutil" 35 "k8s.io/helm/pkg/getter" 36 "k8s.io/helm/pkg/helm/helmpath" 37 "k8s.io/helm/pkg/proto/hapi/chart" 38 "k8s.io/helm/pkg/repo" 39 "k8s.io/helm/pkg/resolver" 40 "k8s.io/helm/pkg/urlutil" 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 // HelmHome is the $HELM_HOME directory 50 HelmHome helmpath.Home 51 // Verification indicates whether the chart should be verified. 52 Verify VerificationStrategy 53 // Debug is the global "--debug" flag 54 Debug bool 55 // Keyring is the key ring file. 56 Keyring string 57 // SkipUpdate indicates that the repository should not be updated first. 58 SkipUpdate bool 59 // Getter collection for the operation 60 Getters []getter.Provider 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, err := chartutil.LoadRequirementsLock(c) 77 if err != nil { 78 return m.Update() 79 } 80 81 // A lock must accompany a requirements.yaml file. 82 req, err := chartutil.LoadRequirements(c) 83 if err != nil { 84 return fmt.Errorf("requirements.yaml cannot be opened: %s", err) 85 } 86 if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest { 87 return fmt.Errorf("requirements.lock is out of sync with requirements.yaml") 88 } 89 90 // Check that all of the repos we're dependent on actually exist. 91 if err := m.hasAllRepos(lock.Dependencies); err != nil { 92 return err 93 } 94 95 if !m.SkipUpdate { 96 // For each repo in the file, update the cached copy of that repo 97 if err := m.UpdateRepositories(); err != nil { 98 return err 99 } 100 } 101 102 // Now we need to fetch every package here into charts/ 103 return m.downloadAll(lock.Dependencies) 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 destPath := filepath.Join(m.ChartPath, "charts") 198 tmpPath := filepath.Join(m.ChartPath, "tmpcharts") 199 200 // Create 'charts' directory if it doesn't already exist. 201 if fi, err := os.Stat(destPath); err != nil { 202 if err := os.MkdirAll(destPath, 0755); err != nil { 203 return err 204 } 205 } else if !fi.IsDir() { 206 return fmt.Errorf("%q is not a directory", destPath) 207 } 208 209 if err := fs.RenameWithFallback(destPath, tmpPath); err != nil { 210 return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) 211 } 212 213 if err := os.MkdirAll(destPath, 0755); err != nil { 214 return err 215 } 216 217 fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) 218 var saveError error 219 for _, dep := range deps { 220 // No repository means the chart is in charts directory 221 if dep.Repository == "" { 222 fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name) 223 chartPath := filepath.Join(tmpPath, dep.Name) 224 ch, err := chartutil.LoadDir(chartPath) 225 if err != nil { 226 return fmt.Errorf("Unable to load chart: %v", err) 227 } 228 229 constraint, err := semver.NewConstraint(dep.Version) 230 if err != nil { 231 return fmt.Errorf("Dependency %s has an invalid version/constraint format: %s", dep.Name, err) 232 } 233 234 v, err := semver.NewVersion(ch.Metadata.Version) 235 if err != nil { 236 return fmt.Errorf("Invalid version %s for dependency %s: %s", dep.Version, dep.Name, err) 237 } 238 239 if !constraint.Check(v) { 240 saveError = fmt.Errorf("Dependency %s at version %s does not satisfy the constraint %s", dep.Name, ch.Metadata.Version, dep.Version) 241 break 242 } 243 continue 244 } 245 if strings.HasPrefix(dep.Repository, "file://") { 246 if m.Debug { 247 fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) 248 } 249 ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version) 250 if err != nil { 251 saveError = err 252 break 253 } 254 dep.Version = ver 255 continue 256 } 257 258 fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) 259 260 // Any failure to resolve/download a chart should fail: 261 // https://github.com/kubernetes/helm/issues/1439 262 churl, username, password, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) 263 if err != nil { 264 saveError = fmt.Errorf("could not find %s: %s", churl, err) 265 break 266 } 267 268 dl := ChartDownloader{ 269 Out: m.Out, 270 Verify: m.Verify, 271 Keyring: m.Keyring, 272 HelmHome: m.HelmHome, 273 Getters: m.Getters, 274 Username: username, 275 Password: password, 276 } 277 278 if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil { 279 saveError = fmt.Errorf("could not download %s: %s", churl, err) 280 break 281 } 282 } 283 284 if saveError == nil { 285 fmt.Fprintln(m.Out, "Deleting outdated charts") 286 for _, dep := range deps { 287 // Chart from local charts directory stays in place 288 if dep.Repository != "" { 289 if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil { 290 return err 291 } 292 } 293 } 294 if err := move(tmpPath, destPath); err != nil { 295 return err 296 } 297 if err := os.RemoveAll(tmpPath); err != nil { 298 return fmt.Errorf("Failed to remove %v: %v", tmpPath, err) 299 } 300 } else { 301 fmt.Fprintln(m.Out, "Save error occurred: ", saveError) 302 fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state") 303 for _, dep := range deps { 304 if err := m.safeDeleteDep(dep.Name, destPath); err != nil { 305 return err 306 } 307 } 308 if err := os.RemoveAll(destPath); err != nil { 309 return fmt.Errorf("Failed to remove %v: %v", destPath, err) 310 } 311 if err := fs.RenameWithFallback(tmpPath, destPath); err != nil { 312 return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) 313 } 314 return saveError 315 } 316 return nil 317 } 318 319 // safeDeleteDep deletes any versions of the given dependency in the given directory. 320 // 321 // It does this by first matching the file name to an expected pattern, then loading 322 // the file to verify that it is a chart with the same name as the given name. 323 // 324 // Because it requires tar file introspection, it is more intensive than a basic delete. 325 // 326 // This will only return errors that should stop processing entirely. Other errors 327 // will emit log messages or be ignored. 328 func (m *Manager) safeDeleteDep(name, dir string) error { 329 files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz")) 330 if err != nil { 331 // Only for ErrBadPattern 332 return err 333 } 334 for _, fname := range files { 335 ch, err := chartutil.LoadFile(fname) 336 if err != nil { 337 fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) 338 continue 339 } 340 if ch.Metadata.Name != name { 341 // This is not the file you are looking for. 342 continue 343 } 344 if err := os.Remove(fname); err != nil { 345 fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) 346 continue 347 } 348 } 349 return nil 350 } 351 352 // hasAllRepos ensures that all of the referenced deps are in the local repo cache. 353 func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { 354 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 355 if err != nil { 356 return err 357 } 358 repos := rf.Repositories 359 360 // Verify that all repositories referenced in the deps are actually known 361 // by Helm. 362 missing := []string{} 363 for _, dd := range deps { 364 // If repo is from local path, continue 365 if strings.HasPrefix(dd.Repository, "file://") { 366 continue 367 } 368 369 found := false 370 if dd.Repository == "" { 371 found = true 372 } else { 373 for _, repo := range repos { 374 if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { 375 found = true 376 } 377 } 378 } 379 if !found { 380 missing = append(missing, dd.Repository) 381 } 382 } 383 if len(missing) > 0 { 384 return fmt.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", ")) 385 } 386 return nil 387 } 388 389 // getRepoNames returns the repo names of the referenced deps which can be used to fetch the cached index file. 390 func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, error) { 391 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 392 if err != nil { 393 return nil, err 394 } 395 repos := rf.Repositories 396 397 reposMap := make(map[string]string) 398 399 // Verify that all repositories referenced in the deps are actually known 400 // by Helm. 401 missing := []string{} 402 for _, dd := range deps { 403 // Don't map the repository, we don't need to download chart from charts directory 404 if dd.Repository == "" { 405 continue 406 } 407 // if dep chart is from local path, verify the path is valid 408 if strings.HasPrefix(dd.Repository, "file://") { 409 if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil { 410 return nil, err 411 } 412 413 if m.Debug { 414 fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository) 415 } 416 reposMap[dd.Name] = dd.Repository 417 continue 418 } 419 420 found := false 421 422 for _, repo := range repos { 423 if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) || 424 (strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) { 425 found = true 426 dd.Repository = repo.URL 427 reposMap[dd.Name] = repo.Name 428 break 429 } else if urlutil.Equal(repo.URL, dd.Repository) { 430 found = true 431 reposMap[dd.Name] = repo.Name 432 break 433 } 434 } 435 if !found { 436 repository := dd.Repository 437 // Add if URL 438 _, err := url.ParseRequestURI(repository) 439 if err == nil { 440 reposMap[repository] = repository 441 continue 442 } 443 missing = append(missing, repository) 444 } 445 } 446 447 if len(missing) > 0 { 448 errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", ")) 449 // It is common for people to try to enter "stable" as a repository instead of the actual URL. 450 // For this case, let's give them a suggestion. 451 containsNonURL := false 452 for _, repo := range missing { 453 if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") { 454 containsNonURL = true 455 } 456 } 457 if containsNonURL { 458 errorMessage += ` 459 Note that repositories must be URLs or aliases. For example, to refer to the stable 460 repository, use "https://kubernetes-charts.storage.googleapis.com/" or "@stable" instead of 461 "stable". Don't forget to add the repo, too ('helm repo add').` 462 } 463 return nil, errors.New(errorMessage) 464 } 465 466 return reposMap, nil 467 } 468 469 // UpdateRepositories updates all of the local repos to the latest. 470 func (m *Manager) UpdateRepositories() error { 471 rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) 472 if err != nil { 473 return err 474 } 475 if repos := rf.Repositories; len(repos) > 0 { 476 // This prints warnings straight to out. 477 if err := m.parallelRepoUpdate(repos); err != nil { 478 return err 479 } 480 } 481 return nil 482 } 483 484 func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { 485 out := m.Out 486 fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") 487 var wg sync.WaitGroup 488 for _, c := range repos { 489 r, err := repo.NewChartRepository(c, m.Getters) 490 if err != nil { 491 return err 492 } 493 wg.Add(1) 494 go func(r *repo.ChartRepository) { 495 if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil { 496 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) 497 } else { 498 fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) 499 } 500 wg.Done() 501 }(r) 502 } 503 wg.Wait() 504 fmt.Fprintln(out, "Update Complete.") 505 return nil 506 } 507 508 // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. 509 // 510 // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the 511 // newest version will be returned. 512 // 513 // repoURL is the repository to search 514 // 515 // If it finds a URL that is "relative", it will prepend the repoURL. 516 func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) { 517 for _, cr := range repos { 518 if urlutil.Equal(repoURL, cr.Config.URL) { 519 var entry repo.ChartVersions 520 entry, err = findEntryByName(name, cr) 521 if err != nil { 522 return 523 } 524 var ve *repo.ChartVersion 525 ve, err = findVersionedEntry(version, entry) 526 if err != nil { 527 return 528 } 529 url, err = normalizeURL(repoURL, ve.URLs[0]) 530 if err != nil { 531 return 532 } 533 username = cr.Config.Username 534 password = cr.Config.Password 535 return 536 } 537 } 538 url, err = repo.FindChartInRepoURL(repoURL, name, version, "", "", "", m.Getters) 539 if err == nil { 540 return 541 } 542 err = fmt.Errorf("chart %s not found in %s", name, repoURL) 543 return 544 } 545 546 // findEntryByName finds an entry in the chart repository whose name matches the given name. 547 // 548 // It returns the ChartVersions for that entry. 549 func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) { 550 for ename, entry := range cr.IndexFile.Entries { 551 if ename == name { 552 return entry, nil 553 } 554 } 555 return nil, errors.New("entry not found") 556 } 557 558 // findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints. 559 // 560 // If version is empty, the first chart found is returned. 561 func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) { 562 for _, verEntry := range vers { 563 if len(verEntry.URLs) == 0 { 564 // Not a legit entry. 565 continue 566 } 567 568 if version == "" || versionEquals(version, verEntry.Version) { 569 return verEntry, nil 570 } 571 } 572 return nil, errors.New("no matching version") 573 } 574 575 func versionEquals(v1, v2 string) bool { 576 sv1, err := semver.NewVersion(v1) 577 if err != nil { 578 // Fallback to string comparison. 579 return v1 == v2 580 } 581 sv2, err := semver.NewVersion(v2) 582 if err != nil { 583 return false 584 } 585 return sv1.Equal(sv2) 586 } 587 588 func normalizeURL(baseURL, urlOrPath string) (string, error) { 589 u, err := url.Parse(urlOrPath) 590 if err != nil { 591 return urlOrPath, err 592 } 593 if u.IsAbs() { 594 return u.String(), nil 595 } 596 u2, err := url.Parse(baseURL) 597 if err != nil { 598 return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err) 599 } 600 601 u2.Path = path.Join(u2.Path, urlOrPath) 602 return u2.String(), nil 603 } 604 605 // loadChartRepositories reads the repositories.yaml, and then builds a map of 606 // ChartRepositories. 607 // 608 // The key is the local name (which is only present in the repositories.yaml). 609 func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { 610 indices := map[string]*repo.ChartRepository{} 611 repoyaml := m.HelmHome.RepositoryFile() 612 613 // Load repositories.yaml file 614 rf, err := repo.LoadRepositoriesFile(repoyaml) 615 if err != nil { 616 return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) 617 } 618 619 for _, re := range rf.Repositories { 620 lname := re.Name 621 cacheindex := m.HelmHome.CacheIndex(lname) 622 index, err := repo.LoadIndexFile(cacheindex) 623 if err != nil { 624 return indices, err 625 } 626 627 // TODO: use constructor 628 cr := &repo.ChartRepository{ 629 Config: re, 630 IndexFile: index, 631 } 632 indices[lname] = cr 633 } 634 return indices, nil 635 } 636 637 // writeLock writes a lockfile to disk 638 func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { 639 data, err := yaml.Marshal(lock) 640 if err != nil { 641 return err 642 } 643 dest := filepath.Join(chartpath, "requirements.lock") 644 return ioutil.WriteFile(dest, data, 0644) 645 } 646 647 // tarFromLocalDir archive a dep chart from local directory and save it into charts/ 648 func tarFromLocalDir(chartpath, name, repo, version string) (string, error) { 649 destPath := filepath.Join(chartpath, "charts") 650 651 if !strings.HasPrefix(repo, "file://") { 652 return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo) 653 } 654 655 origPath, err := resolver.GetLocalPath(repo, chartpath) 656 if err != nil { 657 return "", err 658 } 659 660 ch, err := chartutil.LoadDir(origPath) 661 if err != nil { 662 return "", err 663 } 664 665 constraint, err := semver.NewConstraint(version) 666 if err != nil { 667 return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %s", name, err) 668 } 669 670 v, err := semver.NewVersion(ch.Metadata.Version) 671 if err != nil { 672 return "", err 673 } 674 675 if constraint.Check(v) { 676 _, err = chartutil.Save(ch, destPath) 677 return ch.Metadata.Version, err 678 } 679 680 return "", fmt.Errorf("can't get a valid version for dependency %s", name) 681 } 682 683 // move files from tmppath to destpath 684 func move(tmpPath, destPath string) error { 685 files, _ := ioutil.ReadDir(tmpPath) 686 for _, file := range files { 687 filename := file.Name() 688 tmpfile := filepath.Join(tmpPath, filename) 689 destfile := filepath.Join(destPath, filename) 690 if err := fs.RenameWithFallback(tmpfile, destfile); err != nil { 691 return fmt.Errorf("Unable to move local charts to charts dir: %v", err) 692 } 693 } 694 return nil 695 } 696 697 func copyFile(source string, destination string) (err error) { 698 sourceFile, err := os.Open(source) 699 if err != nil { 700 return err 701 } 702 defer sourceFile.Close() 703 destinationFile, err := os.Create(destination) 704 if err != nil { 705 return err 706 } 707 defer destinationFile.Close() 708 _, err = io.Copy(destinationFile, sourceFile) 709 if err == nil { 710 stats, err := os.Stat(source) 711 if err == nil { 712 return os.Chmod(destination, stats.Mode()) 713 } 714 } 715 return err 716 } 717 718 func copyDir(source string, destination string) (err error) { 719 fi, err := os.Stat(source) 720 if err != nil { 721 return err 722 } 723 if !fi.IsDir() { 724 return fmt.Errorf("Source is not a directory") 725 } 726 _, err = os.Open(destination) 727 if !os.IsNotExist(err) { 728 return fmt.Errorf("Destination already exists") 729 } 730 err = os.MkdirAll(destination, fi.Mode()) 731 if err != nil { 732 return err 733 } 734 735 entries, err := ioutil.ReadDir(source) 736 for _, entry := range entries { 737 sourceFile := source + "/" + entry.Name() 738 destinationFile := destination + "/" + entry.Name() 739 if entry.IsDir() { 740 err = copyDir(sourceFile, destinationFile) 741 if err != nil { 742 return err 743 } 744 } else { 745 err = copyFile(sourceFile, destinationFile) 746 if err != nil { 747 return err 748 } 749 } 750 } 751 return 752 }