github.com/valdemarpavesi/helm@v2.9.1+incompatible/pkg/downloader/chart_downloader.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/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  	// Getter collection for the operation
    69  	Getters getter.Providers
    70  	// Chart repository username
    71  	Username string
    72  	// 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  				getter, err := getterConstructor(ref, "", "", "")
   174  				return u, getter, err
   175  			}
   176  			return u, nil, err
   177  		}
   178  		r, err := repo.NewChartRepository(rc, c.Getters)
   179  		c.setCredentials(r)
   180  		// If we get here, we don't need to go through the next phase of looking
   181  		// up the URL. We have it already. So we just return.
   182  		return u, r.Client, err
   183  	}
   184  
   185  	// See if it's of the form: repo/path_to_chart
   186  	p := strings.SplitN(u.Path, "/", 2)
   187  	if len(p) < 2 {
   188  		return u, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u)
   189  	}
   190  
   191  	repoName := p[0]
   192  	chartName := p[1]
   193  	rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories)
   194  
   195  	if err != nil {
   196  		return u, nil, err
   197  	}
   198  
   199  	r, err := repo.NewChartRepository(rc, c.Getters)
   200  	if err != nil {
   201  		return u, nil, err
   202  	}
   203  	c.setCredentials(r)
   204  
   205  	// Next, we need to load the index, and actually look up the chart.
   206  	i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
   207  	if err != nil {
   208  		return u, r.Client, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
   209  	}
   210  
   211  	cv, err := i.Get(chartName, version)
   212  	if err != nil {
   213  		return u, r.Client, fmt.Errorf("chart %q matching %s not found in %s index. (try 'helm repo update'). %s", chartName, version, r.Config.Name, err)
   214  	}
   215  
   216  	if len(cv.URLs) == 0 {
   217  		return u, r.Client, fmt.Errorf("chart %q has no downloadable URLs", ref)
   218  	}
   219  
   220  	// TODO: Seems that picking first URL is not fully correct
   221  	u, err = url.Parse(cv.URLs[0])
   222  	if err != nil {
   223  		return u, r.Client, fmt.Errorf("invalid chart URL format: %s", ref)
   224  	}
   225  
   226  	// If the URL is relative (no scheme), prepend the chart repo's base URL
   227  	if !u.IsAbs() {
   228  		repoURL, err := url.Parse(rc.URL)
   229  		if err != nil {
   230  			return repoURL, r.Client, err
   231  		}
   232  		q := repoURL.Query()
   233  		// We need a trailing slash for ResolveReference to work, but make sure there isn't already one
   234  		repoURL.Path = strings.TrimSuffix(repoURL.Path, "/") + "/"
   235  		u = repoURL.ResolveReference(u)
   236  		u.RawQuery = q.Encode()
   237  		return u, r.Client, err
   238  	}
   239  
   240  	return u, r.Client, nil
   241  }
   242  
   243  // If HttpGetter is used, this method sets the configured repository credentials on the HttpGetter.
   244  func (c *ChartDownloader) setCredentials(r *repo.ChartRepository) {
   245  	if t, ok := r.Client.(*getter.HttpGetter); ok {
   246  		t.SetCredentials(c.getRepoCredentials(r))
   247  	}
   248  }
   249  
   250  // If this ChartDownloader is not configured to use credentials, and the chart repository sent as an argument is,
   251  // then the repository's configured credentials are returned.
   252  // Else, this ChartDownloader's credentials are returned.
   253  func (c *ChartDownloader) getRepoCredentials(r *repo.ChartRepository) (username, password string) {
   254  	username = c.Username
   255  	password = c.Password
   256  	if r != nil && r.Config != nil {
   257  		if username == "" {
   258  			username = r.Config.Username
   259  		}
   260  		if password == "" {
   261  			password = r.Config.Password
   262  		}
   263  	}
   264  	return
   265  }
   266  
   267  // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.
   268  //
   269  // It assumes that a chart archive file is accompanied by a provenance file whose
   270  // name is the archive file name plus the ".prov" extension.
   271  func VerifyChart(path string, keyring string) (*provenance.Verification, error) {
   272  	// For now, error out if it's not a tar file.
   273  	if fi, err := os.Stat(path); err != nil {
   274  		return nil, err
   275  	} else if fi.IsDir() {
   276  		return nil, errors.New("unpacked charts cannot be verified")
   277  	} else if !isTar(path) {
   278  		return nil, errors.New("chart must be a tgz file")
   279  	}
   280  
   281  	provfile := path + ".prov"
   282  	if _, err := os.Stat(provfile); err != nil {
   283  		return nil, fmt.Errorf("could not load provenance file %s: %s", provfile, err)
   284  	}
   285  
   286  	sig, err := provenance.NewFromKeyring(keyring, "")
   287  	if err != nil {
   288  		return nil, fmt.Errorf("failed to load keyring: %s", err)
   289  	}
   290  	return sig.Verify(path, provfile)
   291  }
   292  
   293  // isTar tests whether the given file is a tar file.
   294  //
   295  // Currently, this simply checks extension, since a subsequent function will
   296  // untar the file and validate its binary format.
   297  func isTar(filename string) bool {
   298  	return strings.ToLower(filepath.Ext(filename)) == ".tgz"
   299  }
   300  
   301  func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) {
   302  	for _, rc := range cfgs {
   303  		if rc.Name == name {
   304  			if rc.URL == "" {
   305  				return nil, fmt.Errorf("no URL found for repository %s", name)
   306  			}
   307  			return rc, nil
   308  		}
   309  	}
   310  	return nil, fmt.Errorf("repo %s not found", name)
   311  }
   312  
   313  // scanReposForURL scans all repos to find which repo contains the given URL.
   314  //
   315  // This will attempt to find the given URL in all of the known repositories files.
   316  //
   317  // If the URL is found, this will return the repo entry that contained that URL.
   318  //
   319  // If all of the repos are checked, but the URL is not found, an ErrNoOwnerRepo
   320  // error is returned.
   321  //
   322  // Other errors may be returned when repositories cannot be loaded or searched.
   323  //
   324  // Technically, the fact that a URL is not found in a repo is not a failure indication.
   325  // Charts are not required to be included in an index before they are valid. So
   326  // be mindful of this case.
   327  //
   328  // The same URL can technically exist in two or more repositories. This algorithm
   329  // will return the first one it finds. Order is determined by the order of repositories
   330  // in the repositories.yaml file.
   331  func (c *ChartDownloader) scanReposForURL(u string, rf *repo.RepoFile) (*repo.Entry, error) {
   332  	// FIXME: This is far from optimal. Larger installations and index files will
   333  	// incur a performance hit for this type of scanning.
   334  	for _, rc := range rf.Repositories {
   335  		r, err := repo.NewChartRepository(rc, c.Getters)
   336  		if err != nil {
   337  			return nil, err
   338  		}
   339  
   340  		i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
   341  		if err != nil {
   342  			return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
   343  		}
   344  
   345  		for _, entry := range i.Entries {
   346  			for _, ver := range entry {
   347  				for _, dl := range ver.URLs {
   348  					if urlutil.Equal(u, dl) {
   349  						return rc, nil
   350  					}
   351  				}
   352  			}
   353  		}
   354  	}
   355  	// This means that there is no repo file for the given URL.
   356  	return nil, ErrNoOwnerRepo
   357  }