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