github.com/cloudposse/helm@v2.2.3+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  	"bytes"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"net/http"
    25  	"net/url"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"k8s.io/helm/cmd/helm/helmpath"
    31  	"k8s.io/helm/pkg/provenance"
    32  	"k8s.io/helm/pkg/repo"
    33  	"k8s.io/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  }
    70  
    71  // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file.
    72  //
    73  // If Verify is set to VerifyNever, the verification will be nil.
    74  // If Verify is set to VerifyIfPossible, this will return a verification (or nil on failure), and print a warning on failure.
    75  // If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails.
    76  // If Verify is set to VerifyLater, this will download the prov file (if it exists), but not verify it.
    77  //
    78  // For VerifyNever and VerifyIfPossible, the Verification may be empty.
    79  //
    80  // Returns a string path to the location where the file was downloaded and a verification
    81  // (if provenance was verified), or an error if something bad happened.
    82  func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) {
    83  	var r repo.Getter
    84  	u, r, err := c.ResolveChartVersion(ref, version)
    85  	if err != nil {
    86  		return "", nil, err
    87  	}
    88  
    89  	data, err := download(u.String(), r)
    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 := download(u.String()+".prov", r)
   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, repo.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  				return u, http.DefaultClient, nil
   165  			}
   166  			return u, nil, err
   167  		}
   168  		r, err := repo.NewChartRepository(rc)
   169  		// If we get here, we don't need to go through the next phase of looking
   170  		// up the URL. We have it already. So we just return.
   171  		return u, r, err
   172  	}
   173  
   174  	// See if it's of the form: repo/path_to_chart
   175  	p := strings.SplitN(u.Path, "/", 2)
   176  	if len(p) < 2 {
   177  		return u, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u)
   178  	}
   179  
   180  	repoName := p[0]
   181  	chartName := p[1]
   182  	rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories)
   183  	if err != nil {
   184  		return u, nil, err
   185  	}
   186  
   187  	r, err := repo.NewChartRepository(rc)
   188  	if err != nil {
   189  		return u, nil, err
   190  	}
   191  
   192  	// Next, we need to load the index, and actually look up the chart.
   193  	i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
   194  	if err != nil {
   195  		return u, r, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
   196  	}
   197  
   198  	cv, err := i.Get(chartName, version)
   199  	if err != nil {
   200  		return u, r, fmt.Errorf("chart %q not found in %s index. (try 'helm repo update'). %s", chartName, r.Config.Name, err)
   201  	}
   202  
   203  	if len(cv.URLs) == 0 {
   204  		return u, r, fmt.Errorf("chart %q has no downloadable URLs", ref)
   205  	}
   206  
   207  	// TODO: Seems that picking first URL is not fully correct
   208  	u, err = url.Parse(cv.URLs[0])
   209  	if err != nil {
   210  		return u, r, fmt.Errorf("invalid chart URL format: %s", ref)
   211  	}
   212  
   213  	return u, r, nil
   214  }
   215  
   216  // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.
   217  //
   218  // It assumes that a chart archive file is accompanied by a provenance file whose
   219  // name is the archive file name plus the ".prov" extension.
   220  func VerifyChart(path string, keyring string) (*provenance.Verification, error) {
   221  	// For now, error out if it's not a tar file.
   222  	if fi, err := os.Stat(path); err != nil {
   223  		return nil, err
   224  	} else if fi.IsDir() {
   225  		return nil, errors.New("unpacked charts cannot be verified")
   226  	} else if !isTar(path) {
   227  		return nil, errors.New("chart must be a tgz file")
   228  	}
   229  
   230  	provfile := path + ".prov"
   231  	if _, err := os.Stat(provfile); err != nil {
   232  		return nil, fmt.Errorf("could not load provenance file %s: %s", provfile, err)
   233  	}
   234  
   235  	sig, err := provenance.NewFromKeyring(keyring, "")
   236  	if err != nil {
   237  		return nil, fmt.Errorf("failed to load keyring: %s", err)
   238  	}
   239  	return sig.Verify(path, provfile)
   240  }
   241  
   242  // download performs a Get from repo.Getter and returns the body.
   243  func download(href string, r repo.Getter) (*bytes.Buffer, error) {
   244  	buf := bytes.NewBuffer(nil)
   245  
   246  	resp, err := r.Get(href)
   247  	if err != nil {
   248  		return buf, err
   249  	}
   250  	if resp.StatusCode != 200 {
   251  		return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status)
   252  	}
   253  
   254  	_, err = io.Copy(buf, resp.Body)
   255  	resp.Body.Close()
   256  	return buf, err
   257  }
   258  
   259  // isTar tests whether the given file is a tar file.
   260  //
   261  // Currently, this simply checks extension, since a subsequent function will
   262  // untar the file and validate its binary format.
   263  func isTar(filename string) bool {
   264  	return strings.ToLower(filepath.Ext(filename)) == ".tgz"
   265  }
   266  
   267  func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) {
   268  	for _, rc := range cfgs {
   269  		if rc.Name == name {
   270  			if rc.URL == "" {
   271  				return nil, fmt.Errorf("no URL found for repository %s", name)
   272  			}
   273  			return rc, nil
   274  		}
   275  	}
   276  	return nil, fmt.Errorf("repo %s not found", name)
   277  }
   278  
   279  // scanReposForURL scans all repos to find which repo contains the given URL.
   280  //
   281  // This will attempt to find the given URL in all of the known repositories files.
   282  //
   283  // If the URL is found, this will return the repo entry that contained that URL.
   284  //
   285  // If all of the repos are checked, but the URL is not found, an ErrNoOwnerRepo
   286  // error is returned.
   287  //
   288  // Other errors may be returned when repositories cannot be loaded or searched.
   289  //
   290  // Technically, the fact that a URL is not found in a repo is not a failure indication.
   291  // Charts are not required to be included in an index before they are valid. So
   292  // be mindful of this case.
   293  //
   294  // The same URL can technically exist in two or more repositories. This algorithm
   295  // will return the first one it finds. Order is determined by the order of repositories
   296  // in the repositories.yaml file.
   297  func (c *ChartDownloader) scanReposForURL(u string, rf *repo.RepoFile) (*repo.Entry, error) {
   298  	// FIXME: This is far from optimal. Larger installations and index files will
   299  	// incur a performance hit for this type of scanning.
   300  	for _, rc := range rf.Repositories {
   301  		r, err := repo.NewChartRepository(rc)
   302  		if err != nil {
   303  			return nil, err
   304  		}
   305  
   306  		i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
   307  		if err != nil {
   308  			return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
   309  		}
   310  
   311  		for _, entry := range i.Entries {
   312  			for _, ver := range entry {
   313  				for _, dl := range ver.URLs {
   314  					if urlutil.Equal(u, dl) {
   315  						return rc, nil
   316  					}
   317  				}
   318  			}
   319  		}
   320  	}
   321  	// This means that there is no repo file for the given URL.
   322  	return nil, ErrNoOwnerRepo
   323  }