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