github.com/zoumo/helm@v2.5.0+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(), 0655); 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(), 0655); 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  		u, err = url.Parse(rc.URL + "/" + u.Path)
   221  		return u, r.Client, err
   222  	}
   223  
   224  	return u, r.Client, nil
   225  }
   226  
   227  // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.
   228  //
   229  // It assumes that a chart archive file is accompanied by a provenance file whose
   230  // name is the archive file name plus the ".prov" extension.
   231  func VerifyChart(path string, keyring string) (*provenance.Verification, error) {
   232  	// For now, error out if it's not a tar file.
   233  	if fi, err := os.Stat(path); err != nil {
   234  		return nil, err
   235  	} else if fi.IsDir() {
   236  		return nil, errors.New("unpacked charts cannot be verified")
   237  	} else if !isTar(path) {
   238  		return nil, errors.New("chart must be a tgz file")
   239  	}
   240  
   241  	provfile := path + ".prov"
   242  	if _, err := os.Stat(provfile); err != nil {
   243  		return nil, fmt.Errorf("could not load provenance file %s: %s", provfile, err)
   244  	}
   245  
   246  	sig, err := provenance.NewFromKeyring(keyring, "")
   247  	if err != nil {
   248  		return nil, fmt.Errorf("failed to load keyring: %s", err)
   249  	}
   250  	return sig.Verify(path, provfile)
   251  }
   252  
   253  // isTar tests whether the given file is a tar file.
   254  //
   255  // Currently, this simply checks extension, since a subsequent function will
   256  // untar the file and validate its binary format.
   257  func isTar(filename string) bool {
   258  	return strings.ToLower(filepath.Ext(filename)) == ".tgz"
   259  }
   260  
   261  func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) {
   262  	for _, rc := range cfgs {
   263  		if rc.Name == name {
   264  			if rc.URL == "" {
   265  				return nil, fmt.Errorf("no URL found for repository %s", name)
   266  			}
   267  			return rc, nil
   268  		}
   269  	}
   270  	return nil, fmt.Errorf("repo %s not found", name)
   271  }
   272  
   273  // scanReposForURL scans all repos to find which repo contains the given URL.
   274  //
   275  // This will attempt to find the given URL in all of the known repositories files.
   276  //
   277  // If the URL is found, this will return the repo entry that contained that URL.
   278  //
   279  // If all of the repos are checked, but the URL is not found, an ErrNoOwnerRepo
   280  // error is returned.
   281  //
   282  // Other errors may be returned when repositories cannot be loaded or searched.
   283  //
   284  // Technically, the fact that a URL is not found in a repo is not a failure indication.
   285  // Charts are not required to be included in an index before they are valid. So
   286  // be mindful of this case.
   287  //
   288  // The same URL can technically exist in two or more repositories. This algorithm
   289  // will return the first one it finds. Order is determined by the order of repositories
   290  // in the repositories.yaml file.
   291  func (c *ChartDownloader) scanReposForURL(u string, rf *repo.RepoFile) (*repo.Entry, error) {
   292  	// FIXME: This is far from optimal. Larger installations and index files will
   293  	// incur a performance hit for this type of scanning.
   294  	for _, rc := range rf.Repositories {
   295  		r, err := repo.NewChartRepository(rc, c.Getters)
   296  		if err != nil {
   297  			return nil, err
   298  		}
   299  
   300  		i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
   301  		if err != nil {
   302  			return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
   303  		}
   304  
   305  		for _, entry := range i.Entries {
   306  			for _, ver := range entry {
   307  				for _, dl := range ver.URLs {
   308  					if urlutil.Equal(u, dl) {
   309  						return rc, nil
   310  					}
   311  				}
   312  			}
   313  		}
   314  	}
   315  	// This means that there is no repo file for the given URL.
   316  	return nil, ErrNoOwnerRepo
   317  }