github.com/felipejfc/helm@v2.1.2+incompatible/cmd/helm/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  )
    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  // ChartDownloader handles downloading a chart.
    54  //
    55  // It is capable of performing verifications on charts as well.
    56  type ChartDownloader struct {
    57  	// Out is the location to write warning and info messages.
    58  	Out io.Writer
    59  	// Verify indicates what verification strategy to use.
    60  	Verify VerificationStrategy
    61  	// Keyring is the keyring file used for verification.
    62  	Keyring string
    63  	// HelmHome is the $HELM_HOME.
    64  	HelmHome helmpath.Home
    65  }
    66  
    67  // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file.
    68  //
    69  // If Verify is set to VerifyNever, the verification will be nil.
    70  // If Verify is set to VerifyIfPossible, this will return a verification (or nil on failure), and print a warning on failure.
    71  // If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails.
    72  // If Verify is set to VerifyLater, this will download the prov file (if it exists), but not verify it.
    73  //
    74  // For VerifyNever and VerifyIfPossible, the Verification may be empty.
    75  //
    76  // Returns a string path to the location where the file was downloaded and a verification
    77  // (if provenance was verified), or an error if something bad happened.
    78  func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) {
    79  	// resolve URL
    80  	u, err := c.ResolveChartVersion(ref, version)
    81  	if err != nil {
    82  		return "", nil, err
    83  	}
    84  	data, err := download(u.String())
    85  	if err != nil {
    86  		return "", nil, err
    87  	}
    88  
    89  	name := filepath.Base(u.Path)
    90  	destfile := filepath.Join(dest, name)
    91  	if err := ioutil.WriteFile(destfile, data.Bytes(), 0655); err != nil {
    92  		return destfile, nil, err
    93  	}
    94  
    95  	// If provenance is requested, verify it.
    96  	ver := &provenance.Verification{}
    97  	if c.Verify > VerifyNever {
    98  
    99  		body, err := download(u.String() + ".prov")
   100  		if err != nil {
   101  			if c.Verify == VerifyAlways {
   102  				return destfile, ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov")
   103  			}
   104  			fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err)
   105  			return destfile, ver, nil
   106  		}
   107  		provfile := destfile + ".prov"
   108  		if err := ioutil.WriteFile(provfile, body.Bytes(), 0655); err != nil {
   109  			return destfile, nil, err
   110  		}
   111  
   112  		if c.Verify != VerifyLater {
   113  			ver, err = VerifyChart(destfile, c.Keyring)
   114  			if err != nil {
   115  				// Fail always in this case, since it means the verification step
   116  				// failed.
   117  				return destfile, ver, err
   118  			}
   119  		}
   120  	}
   121  	return destfile, ver, nil
   122  }
   123  
   124  // ResolveChartVersion resolves a chart reference to a URL.
   125  //
   126  // A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path.
   127  //
   128  // A version is a SemVer string (1.2.3-beta.1+f334a6789).
   129  //
   130  // 	- For fully qualified URLs, the version will be ignored (since URLs aren't versioned)
   131  //	- For a chart reference
   132  //		* If version is non-empty, this will return the URL for that version
   133  //		* If version is empty, this will return the URL for the latest version
   134  // 		* If no version can be found, an error is returned
   135  func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) {
   136  	// See if it's already a full URL.
   137  	// FIXME: Why do we use url.ParseRequestURI instead of url.Parse?
   138  	u, err := url.ParseRequestURI(ref)
   139  	if err == nil {
   140  		// If it has a scheme and host and path, it's a full URL
   141  		if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 {
   142  			return u, nil
   143  		}
   144  		return u, fmt.Errorf("invalid chart url format: %s", ref)
   145  	}
   146  
   147  	r, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile())
   148  	if err != nil {
   149  		return u, err
   150  	}
   151  
   152  	// See if it's of the form: repo/path_to_chart
   153  	p := strings.SplitN(ref, "/", 2)
   154  	if len(p) < 2 {
   155  		return u, fmt.Errorf("invalid chart url format: %s", ref)
   156  	}
   157  
   158  	repoName := p[0]
   159  	chartName := p[1]
   160  	rf, err := findRepoEntry(repoName, r.Repositories)
   161  	if err != nil {
   162  		return u, err
   163  	}
   164  	if rf.URL == "" {
   165  		return u, fmt.Errorf("no URL found for repository %q", repoName)
   166  	}
   167  
   168  	// Next, we need to load the index, and actually look up the chart.
   169  	i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(repoName))
   170  	if err != nil {
   171  		return u, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
   172  	}
   173  
   174  	cv, err := i.Get(chartName, version)
   175  	if err != nil {
   176  		return u, fmt.Errorf("chart %q not found in %s index. (try 'helm repo update'). %s", chartName, repoName, err)
   177  	}
   178  
   179  	if len(cv.URLs) == 0 {
   180  		return u, fmt.Errorf("chart %q has no downloadable URLs", ref)
   181  	}
   182  	return url.Parse(cv.URLs[0])
   183  }
   184  
   185  func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) {
   186  	for _, re := range repos {
   187  		if re.Name == name {
   188  			return re, nil
   189  		}
   190  	}
   191  	return nil, fmt.Errorf("no repo named %q", name)
   192  }
   193  
   194  // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.
   195  //
   196  // It assumes that a chart archive file is accompanied by a provenance file whose
   197  // name is the archive file name plus the ".prov" extension.
   198  func VerifyChart(path string, keyring string) (*provenance.Verification, error) {
   199  	// For now, error out if it's not a tar file.
   200  	if fi, err := os.Stat(path); err != nil {
   201  		return nil, err
   202  	} else if fi.IsDir() {
   203  		return nil, errors.New("unpacked charts cannot be verified")
   204  	} else if !isTar(path) {
   205  		return nil, errors.New("chart must be a tgz file")
   206  	}
   207  
   208  	provfile := path + ".prov"
   209  	if _, err := os.Stat(provfile); err != nil {
   210  		return nil, fmt.Errorf("could not load provenance file %s: %s", provfile, err)
   211  	}
   212  
   213  	sig, err := provenance.NewFromKeyring(keyring, "")
   214  	if err != nil {
   215  		return nil, fmt.Errorf("failed to load keyring: %s", err)
   216  	}
   217  	return sig.Verify(path, provfile)
   218  }
   219  
   220  // download performs a simple HTTP Get and returns the body.
   221  func download(href string) (*bytes.Buffer, error) {
   222  	buf := bytes.NewBuffer(nil)
   223  
   224  	resp, err := http.Get(href)
   225  	if err != nil {
   226  		return buf, err
   227  	}
   228  	if resp.StatusCode != 200 {
   229  		return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status)
   230  	}
   231  
   232  	_, err = io.Copy(buf, resp.Body)
   233  	resp.Body.Close()
   234  	return buf, err
   235  }
   236  
   237  // isTar tests whether the given file is a tar file.
   238  //
   239  // Currently, this simply checks extension, since a subsequent function will
   240  // untar the file and validate its binary format.
   241  func isTar(filename string) bool {
   242  	return strings.ToLower(filepath.Ext(filename)) == ".tgz"
   243  }