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