github.com/umeshredd/helm@v3.0.0-alpha.1+incompatible/pkg/downloader/chart_downloader.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/filepath"
    25  	"strings"
    26  
    27  	"github.com/pkg/errors"
    28  
    29  	"helm.sh/helm/pkg/getter"
    30  	"helm.sh/helm/pkg/helmpath"
    31  	"helm.sh/helm/pkg/provenance"
    32  	"helm.sh/helm/pkg/repo"
    33  	"helm.sh/helm/pkg/urlutil"
    34  )
    35  
    36  // VerificationStrategy describes a strategy for determining whether to verify a chart.
    37  type VerificationStrategy int
    38  
    39  const (
    40  	// VerifyNever will skip all verification of a chart.
    41  	VerifyNever VerificationStrategy = iota
    42  	// VerifyIfPossible will attempt a verification, it will not error if verification
    43  	// data is missing. But it will not stop processing if verification fails.
    44  	VerifyIfPossible
    45  	// VerifyAlways will always attempt a verification, and will fail if the
    46  	// verification fails.
    47  	VerifyAlways
    48  	// VerifyLater will fetch verification data, but not do any verification.
    49  	// This is to accommodate the case where another step of the process will
    50  	// perform verification.
    51  	VerifyLater
    52  )
    53  
    54  // ErrNoOwnerRepo indicates that a given chart URL can't be found in any repos.
    55  var ErrNoOwnerRepo = errors.New("could not find a repo containing the given URL")
    56  
    57  // ChartDownloader handles downloading a chart.
    58  //
    59  // It is capable of performing verifications on charts as well.
    60  type ChartDownloader struct {
    61  	// Out is the location to write warning and info messages.
    62  	Out io.Writer
    63  	// Verify indicates what verification strategy to use.
    64  	Verify VerificationStrategy
    65  	// Keyring is the keyring file used for verification.
    66  	Keyring string
    67  	// HelmHome is the $HELM_HOME.
    68  	HelmHome helmpath.Home
    69  	// Getter collection for the operation
    70  	Getters getter.Providers
    71  	// Chart repository username
    72  	Username string
    73  	// Chart repository password
    74  	Password string
    75  }
    76  
    77  // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file.
    78  //
    79  // If Verify is set to VerifyNever, the verification will be nil.
    80  // If Verify is set to VerifyIfPossible, this will return a verification (or nil on failure), and print a warning on failure.
    81  // If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails.
    82  // If Verify is set to VerifyLater, this will download the prov file (if it exists), but not verify it.
    83  //
    84  // For VerifyNever and VerifyIfPossible, the Verification may be empty.
    85  //
    86  // Returns a string path to the location where the file was downloaded and a verification
    87  // (if provenance was verified), or an error if something bad happened.
    88  func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) {
    89  	u, r, g, err := c.ResolveChartVersionAndGetRepo(ref, version)
    90  	if err != nil {
    91  		return "", nil, err
    92  	}
    93  
    94  	data, err := g.Get(u.String())
    95  	if err != nil {
    96  		return "", nil, err
    97  	}
    98  
    99  	name := filepath.Base(u.Path)
   100  	destfile := filepath.Join(dest, name)
   101  	if err := ioutil.WriteFile(destfile, data.Bytes(), 0644); err != nil {
   102  		return destfile, nil, err
   103  	}
   104  
   105  	// If provenance is requested, verify it.
   106  	ver := &provenance.Verification{}
   107  	if c.Verify > VerifyNever {
   108  		body, err := r.Client.Get(u.String() + ".prov")
   109  		if err != nil {
   110  			if c.Verify == VerifyAlways {
   111  				return destfile, ver, errors.Errorf("failed to fetch provenance %q", u.String()+".prov")
   112  			}
   113  			fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err)
   114  			return destfile, ver, nil
   115  		}
   116  		provfile := destfile + ".prov"
   117  		if err := ioutil.WriteFile(provfile, body.Bytes(), 0644); err != nil {
   118  			return destfile, nil, err
   119  		}
   120  
   121  		if c.Verify != VerifyLater {
   122  			ver, err = VerifyChart(destfile, c.Keyring)
   123  			if err != nil {
   124  				// Fail always in this case, since it means the verification step
   125  				// failed.
   126  				return destfile, ver, err
   127  			}
   128  		}
   129  	}
   130  	return destfile, ver, nil
   131  }
   132  
   133  // ResolveChartVersion resolves a chart reference to a URL.
   134  //
   135  // It returns the URL as well as a preconfigured repo.Getter that can fetch
   136  // the URL.
   137  //
   138  // A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path.
   139  //
   140  // A version is a SemVer string (1.2.3-beta.1+f334a6789).
   141  //
   142  //	- For fully qualified URLs, the version will be ignored (since URLs aren't versioned)
   143  //	- For a chart reference
   144  //		* If version is non-empty, this will return the URL for that version
   145  //		* If version is empty, this will return the URL for the latest version
   146  //		* If no version can be found, an error is returned
   147  func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, getter.Getter, error) {
   148  	u, r, _, err := c.ResolveChartVersionAndGetRepo(ref, version)
   149  	if r != nil {
   150  		return u, r.Client, err
   151  	}
   152  	return u, nil, err
   153  }
   154  
   155  // ResolveChartVersionAndGetRepo is the same as the ResolveChartVersion method, but returns the chart repositoryy.
   156  func (c *ChartDownloader) ResolveChartVersionAndGetRepo(ref, version string) (*url.URL, *repo.ChartRepository, *getter.HTTPGetter, error) {
   157  	u, err := url.Parse(ref)
   158  	if err != nil {
   159  		return nil, nil, nil, errors.Errorf("invalid chart URL format: %s", ref)
   160  	}
   161  
   162  	rf, err := repo.LoadFile(c.HelmHome.RepositoryFile())
   163  	if err != nil {
   164  		return u, nil, nil, err
   165  	}
   166  
   167  	// TODO add user-agent
   168  	g, err := getter.NewHTTPGetter(ref, "", "", "")
   169  	if err != nil {
   170  		return u, nil, nil, err
   171  	}
   172  
   173  	if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 {
   174  		// In this case, we have to find the parent repo that contains this chart
   175  		// URL. And this is an unfortunate problem, as it requires actually going
   176  		// through each repo cache file and finding a matching URL. But basically
   177  		// we want to find the repo in case we have special SSL cert config
   178  		// for that repo.
   179  
   180  		rc, err := c.scanReposForURL(ref, rf)
   181  		if err != nil {
   182  			// If there is no special config, return the default HTTP client and
   183  			// swallow the error.
   184  			if err == ErrNoOwnerRepo {
   185  				r := &repo.ChartRepository{}
   186  				r.Client = g
   187  				g.SetCredentials(c.getRepoCredentials(r))
   188  				return u, r, g, err
   189  			}
   190  			return u, nil, nil, err
   191  		}
   192  		r, err := repo.NewChartRepository(rc, c.Getters)
   193  		// If we get here, we don't need to go through the next phase of looking
   194  		// up the URL. We have it already. So we just return.
   195  		return u, r, g, err
   196  	}
   197  
   198  	// See if it's of the form: repo/path_to_chart
   199  	p := strings.SplitN(u.Path, "/", 2)
   200  	if len(p) < 2 {
   201  		return u, nil, nil, errors.Errorf("non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u)
   202  	}
   203  
   204  	repoName := p[0]
   205  	chartName := p[1]
   206  	rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories)
   207  
   208  	if err != nil {
   209  		return u, nil, nil, err
   210  	}
   211  
   212  	r, err := repo.NewChartRepository(rc, c.Getters)
   213  	if err != nil {
   214  		return u, nil, nil, err
   215  	}
   216  	g.SetCredentials(c.getRepoCredentials(r))
   217  
   218  	// Next, we need to load the index, and actually look up the chart.
   219  	i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
   220  	if err != nil {
   221  		return u, r, g, errors.Wrap(err, "no cached repo found. (try 'helm repo update')")
   222  	}
   223  
   224  	cv, err := i.Get(chartName, version)
   225  	if err != nil {
   226  		return u, r, g, errors.Wrapf(err, "chart %q matching %s not found in %s index. (try 'helm repo update')", chartName, version, r.Config.Name)
   227  	}
   228  
   229  	if len(cv.URLs) == 0 {
   230  		return u, r, g, errors.Errorf("chart %q has no downloadable URLs", ref)
   231  	}
   232  
   233  	// TODO: Seems that picking first URL is not fully correct
   234  	u, err = url.Parse(cv.URLs[0])
   235  	if err != nil {
   236  		return u, r, g, errors.Errorf("invalid chart URL format: %s", ref)
   237  	}
   238  
   239  	// If the URL is relative (no scheme), prepend the chart repo's base URL
   240  	if !u.IsAbs() {
   241  		repoURL, err := url.Parse(rc.URL)
   242  		if err != nil {
   243  			return repoURL, r, nil, err
   244  		}
   245  		q := repoURL.Query()
   246  		// We need a trailing slash for ResolveReference to work, but make sure there isn't already one
   247  		repoURL.Path = strings.TrimSuffix(repoURL.Path, "/") + "/"
   248  		u = repoURL.ResolveReference(u)
   249  		u.RawQuery = q.Encode()
   250  		// TODO add user-agent
   251  		g, err := getter.NewHTTPGetter(rc.URL, "", "", "")
   252  		if err != nil {
   253  			return repoURL, r, nil, err
   254  		}
   255  		g.SetCredentials(c.getRepoCredentials(r))
   256  		return u, r, g, err
   257  	}
   258  
   259  	return u, r, g, nil
   260  }
   261  
   262  // If this ChartDownloader is not configured to use credentials, and the chart repository sent as an argument is,
   263  // then the repository's configured credentials are returned.
   264  // Else, this ChartDownloader's credentials are returned.
   265  func (c *ChartDownloader) getRepoCredentials(r *repo.ChartRepository) (username, password string) {
   266  	username = c.Username
   267  	password = c.Password
   268  	if r != nil && r.Config != nil {
   269  		if username == "" {
   270  			username = r.Config.Username
   271  		}
   272  		if password == "" {
   273  			password = r.Config.Password
   274  		}
   275  	}
   276  	return
   277  }
   278  
   279  // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.
   280  //
   281  // It assumes that a chart archive file is accompanied by a provenance file whose
   282  // name is the archive file name plus the ".prov" extension.
   283  func VerifyChart(path, keyring string) (*provenance.Verification, error) {
   284  	// For now, error out if it's not a tar file.
   285  	switch fi, err := os.Stat(path); {
   286  	case err != nil:
   287  		return nil, err
   288  	case fi.IsDir():
   289  		return nil, errors.New("unpacked charts cannot be verified")
   290  	case !isTar(path):
   291  		return nil, errors.New("chart must be a tgz file")
   292  	}
   293  
   294  	provfile := path + ".prov"
   295  	if _, err := os.Stat(provfile); err != nil {
   296  		return nil, errors.Wrapf(err, "could not load provenance file %s", provfile)
   297  	}
   298  
   299  	sig, err := provenance.NewFromKeyring(keyring, "")
   300  	if err != nil {
   301  		return nil, errors.Wrap(err, "failed to load keyring")
   302  	}
   303  	return sig.Verify(path, provfile)
   304  }
   305  
   306  // isTar tests whether the given file is a tar file.
   307  //
   308  // Currently, this simply checks extension, since a subsequent function will
   309  // untar the file and validate its binary format.
   310  func isTar(filename string) bool {
   311  	return strings.EqualFold(filepath.Ext(filename), ".tgz")
   312  }
   313  
   314  func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) {
   315  	for _, rc := range cfgs {
   316  		if rc.Name == name {
   317  			if rc.URL == "" {
   318  				return nil, errors.Errorf("no URL found for repository %s", name)
   319  			}
   320  			return rc, nil
   321  		}
   322  	}
   323  	return nil, errors.Errorf("repo %s not found", name)
   324  }
   325  
   326  // scanReposForURL scans all repos to find which repo contains the given URL.
   327  //
   328  // This will attempt to find the given URL in all of the known repositories files.
   329  //
   330  // If the URL is found, this will return the repo entry that contained that URL.
   331  //
   332  // If all of the repos are checked, but the URL is not found, an ErrNoOwnerRepo
   333  // error is returned.
   334  //
   335  // Other errors may be returned when repositories cannot be loaded or searched.
   336  //
   337  // Technically, the fact that a URL is not found in a repo is not a failure indication.
   338  // Charts are not required to be included in an index before they are valid. So
   339  // be mindful of this case.
   340  //
   341  // The same URL can technically exist in two or more repositories. This algorithm
   342  // will return the first one it finds. Order is determined by the order of repositories
   343  // in the repositories.yaml file.
   344  func (c *ChartDownloader) scanReposForURL(u string, rf *repo.File) (*repo.Entry, error) {
   345  	// FIXME: This is far from optimal. Larger installations and index files will
   346  	// incur a performance hit for this type of scanning.
   347  	for _, rc := range rf.Repositories {
   348  		r, err := repo.NewChartRepository(rc, c.Getters)
   349  		if err != nil {
   350  			return nil, err
   351  		}
   352  
   353  		i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
   354  		if err != nil {
   355  			return nil, errors.Wrap(err, "no cached repo found. (try 'helm repo update')")
   356  		}
   357  
   358  		for _, entry := range i.Entries {
   359  			for _, ver := range entry {
   360  				for _, dl := range ver.URLs {
   361  					if urlutil.Equal(u, dl) {
   362  						return rc, nil
   363  					}
   364  				}
   365  			}
   366  		}
   367  	}
   368  	// This means that there is no repo file for the given URL.
   369  	return nil, ErrNoOwnerRepo
   370  }