github.com/x-helm/helm@v3.0.0-beta.3+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/internal/urlutil"
    30  	"helm.sh/helm/pkg/getter"
    31  	"helm.sh/helm/pkg/helmpath"
    32  	"helm.sh/helm/pkg/provenance"
    33  	"helm.sh/helm/pkg/repo"
    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  	// Getter collection for the operation
    68  	Getters getter.Providers
    69  	// Options provide parameters to be passed along to the Getter being initialized.
    70  	Options          []getter.Option
    71  	RepositoryConfig string
    72  	RepositoryCache  string
    73  }
    74  
    75  // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file.
    76  //
    77  // If Verify is set to VerifyNever, the verification will be nil.
    78  // If Verify is set to VerifyIfPossible, this will return a verification (or nil on failure), and print a warning on failure.
    79  // If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails.
    80  // If Verify is set to VerifyLater, this will download the prov file (if it exists), but not verify it.
    81  //
    82  // For VerifyNever and VerifyIfPossible, the Verification may be empty.
    83  //
    84  // Returns a string path to the location where the file was downloaded and a verification
    85  // (if provenance was verified), or an error if something bad happened.
    86  func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) {
    87  	u, err := c.ResolveChartVersion(ref, version)
    88  	if err != nil {
    89  		return "", nil, err
    90  	}
    91  
    92  	g, err := c.Getters.ByScheme(u.Scheme)
    93  	if err != nil {
    94  		return "", nil, err
    95  	}
    96  
    97  	data, err := g.Get(u.String(), c.Options...)
    98  	if err != nil {
    99  		return "", nil, err
   100  	}
   101  
   102  	name := filepath.Base(u.Path)
   103  	destfile := filepath.Join(dest, name)
   104  	if err := ioutil.WriteFile(destfile, data.Bytes(), 0644); err != nil {
   105  		return destfile, nil, err
   106  	}
   107  
   108  	// If provenance is requested, verify it.
   109  	ver := &provenance.Verification{}
   110  	if c.Verify > VerifyNever {
   111  		body, err := g.Get(u.String() + ".prov")
   112  		if err != nil {
   113  			if c.Verify == VerifyAlways {
   114  				return destfile, ver, errors.Errorf("failed to fetch provenance %q", u.String()+".prov")
   115  			}
   116  			fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err)
   117  			return destfile, ver, nil
   118  		}
   119  		provfile := destfile + ".prov"
   120  		if err := ioutil.WriteFile(provfile, body.Bytes(), 0644); err != nil {
   121  			return destfile, nil, err
   122  		}
   123  
   124  		if c.Verify != VerifyLater {
   125  			ver, err = VerifyChart(destfile, c.Keyring)
   126  			if err != nil {
   127  				// Fail always in this case, since it means the verification step
   128  				// failed.
   129  				return destfile, ver, err
   130  			}
   131  		}
   132  	}
   133  	return destfile, ver, nil
   134  }
   135  
   136  // ResolveChartVersion resolves a chart reference to a URL.
   137  //
   138  // It returns the URL and sets the ChartDownloader's Options that can fetch
   139  // the URL using the appropriate Getter.
   140  //
   141  // A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path.
   142  //
   143  // A version is a SemVer string (1.2.3-beta.1+f334a6789).
   144  //
   145  //	- For fully qualified URLs, the version will be ignored (since URLs aren't versioned)
   146  //	- For a chart reference
   147  //		* If version is non-empty, this will return the URL for that version
   148  //		* If version is empty, this will return the URL for the latest version
   149  //		* If no version can be found, an error is returned
   150  func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) {
   151  	u, err := url.Parse(ref)
   152  	if err != nil {
   153  		return nil, errors.Errorf("invalid chart URL format: %s", ref)
   154  	}
   155  	c.Options = append(c.Options, getter.WithURL(ref))
   156  
   157  	rf, err := loadRepoConfig(c.RepositoryConfig)
   158  	if err != nil {
   159  		return u, err
   160  	}
   161  
   162  	if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 {
   163  		// In this case, we have to find the parent repo that contains this chart
   164  		// URL. And this is an unfortunate problem, as it requires actually going
   165  		// through each repo cache file and finding a matching URL. But basically
   166  		// we want to find the repo in case we have special SSL cert config
   167  		// for that repo.
   168  
   169  		rc, err := c.scanReposForURL(ref, rf)
   170  		if err != nil {
   171  			// If there is no special config, return the default HTTP client and
   172  			// swallow the error.
   173  			if err == ErrNoOwnerRepo {
   174  				return u, nil
   175  			}
   176  			return u, err
   177  		}
   178  
   179  		// If we get here, we don't need to go through the next phase of looking
   180  		// up the URL. We have it already. So we just set the parameters and return.
   181  		c.Options = append(
   182  			c.Options,
   183  			getter.WithURL(rc.URL),
   184  			getter.WithTLSClientConfig(rc.CertFile, rc.KeyFile, rc.CAFile),
   185  		)
   186  		if rc.Username != "" && rc.Password != "" {
   187  			c.Options = append(
   188  				c.Options,
   189  				getter.WithBasicAuth(rc.Username, rc.Password),
   190  			)
   191  		}
   192  		return u, nil
   193  	}
   194  
   195  	// See if it's of the form: repo/path_to_chart
   196  	p := strings.SplitN(u.Path, "/", 2)
   197  	if len(p) < 2 {
   198  		return u, errors.Errorf("non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u)
   199  	}
   200  
   201  	repoName := p[0]
   202  	chartName := p[1]
   203  	rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories)
   204  
   205  	if err != nil {
   206  		return u, err
   207  	}
   208  
   209  	r, err := repo.NewChartRepository(rc, c.Getters)
   210  	if err != nil {
   211  		return u, err
   212  	}
   213  	if r != nil && r.Config != nil && r.Config.Username != "" && r.Config.Password != "" {
   214  		c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password))
   215  	}
   216  
   217  	// Next, we need to load the index, and actually look up the chart.
   218  	idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name))
   219  	i, err := repo.LoadIndexFile(idxFile)
   220  	if err != nil {
   221  		return u, 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, 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, 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, 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, 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  		if _, err := getter.NewHTTPGetter(getter.WithURL(rc.URL)); err != nil {
   252  			return repoURL, err
   253  		}
   254  		if r != nil && r.Config != nil && r.Config.Username != "" && r.Config.Password != "" {
   255  			c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password))
   256  		}
   257  		return u, err
   258  	}
   259  
   260  	// TODO add user-agent
   261  	return u, nil
   262  }
   263  
   264  // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.
   265  //
   266  // It assumes that a chart archive file is accompanied by a provenance file whose
   267  // name is the archive file name plus the ".prov" extension.
   268  func VerifyChart(path, keyring string) (*provenance.Verification, error) {
   269  	// For now, error out if it's not a tar file.
   270  	switch fi, err := os.Stat(path); {
   271  	case err != nil:
   272  		return nil, err
   273  	case fi.IsDir():
   274  		return nil, errors.New("unpacked charts cannot be verified")
   275  	case !isTar(path):
   276  		return nil, errors.New("chart must be a tgz file")
   277  	}
   278  
   279  	provfile := path + ".prov"
   280  	if _, err := os.Stat(provfile); err != nil {
   281  		return nil, errors.Wrapf(err, "could not load provenance file %s", provfile)
   282  	}
   283  
   284  	sig, err := provenance.NewFromKeyring(keyring, "")
   285  	if err != nil {
   286  		return nil, errors.Wrap(err, "failed to load keyring")
   287  	}
   288  	return sig.Verify(path, provfile)
   289  }
   290  
   291  // isTar tests whether the given file is a tar file.
   292  //
   293  // Currently, this simply checks extension, since a subsequent function will
   294  // untar the file and validate its binary format.
   295  func isTar(filename string) bool {
   296  	return strings.EqualFold(filepath.Ext(filename), ".tgz")
   297  }
   298  
   299  func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) {
   300  	for _, rc := range cfgs {
   301  		if rc.Name == name {
   302  			if rc.URL == "" {
   303  				return nil, errors.Errorf("no URL found for repository %s", name)
   304  			}
   305  			return rc, nil
   306  		}
   307  	}
   308  	return nil, errors.Errorf("repo %s not found", name)
   309  }
   310  
   311  // scanReposForURL scans all repos to find which repo contains the given URL.
   312  //
   313  // This will attempt to find the given URL in all of the known repositories files.
   314  //
   315  // If the URL is found, this will return the repo entry that contained that URL.
   316  //
   317  // If all of the repos are checked, but the URL is not found, an ErrNoOwnerRepo
   318  // error is returned.
   319  //
   320  // Other errors may be returned when repositories cannot be loaded or searched.
   321  //
   322  // Technically, the fact that a URL is not found in a repo is not a failure indication.
   323  // Charts are not required to be included in an index before they are valid. So
   324  // be mindful of this case.
   325  //
   326  // The same URL can technically exist in two or more repositories. This algorithm
   327  // will return the first one it finds. Order is determined by the order of repositories
   328  // in the repositories.yaml file.
   329  func (c *ChartDownloader) scanReposForURL(u string, rf *repo.File) (*repo.Entry, error) {
   330  	// FIXME: This is far from optimal. Larger installations and index files will
   331  	// incur a performance hit for this type of scanning.
   332  	for _, rc := range rf.Repositories {
   333  		r, err := repo.NewChartRepository(rc, c.Getters)
   334  		if err != nil {
   335  			return nil, err
   336  		}
   337  
   338  		idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name))
   339  		i, err := repo.LoadIndexFile(idxFile)
   340  		if err != nil {
   341  			return nil, errors.Wrap(err, "no cached repo found. (try 'helm repo update')")
   342  		}
   343  
   344  		for _, entry := range i.Entries {
   345  			for _, ver := range entry {
   346  				for _, dl := range ver.URLs {
   347  					if urlutil.Equal(u, dl) {
   348  						return rc, nil
   349  					}
   350  				}
   351  			}
   352  		}
   353  	}
   354  	// This means that there is no repo file for the given URL.
   355  	return nil, ErrNoOwnerRepo
   356  }
   357  
   358  func loadRepoConfig(file string) (*repo.File, error) {
   359  	r, err := repo.LoadFile(file)
   360  	if err != nil && !os.IsNotExist(errors.Cause(err)) {
   361  		return nil, err
   362  	}
   363  	return r, nil
   364  }