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