github.com/danielqsj/helm@v2.0.0-alpha.4.0.20160908204436-976e0ba5199b+incompatible/cmd/helm/fetch.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/url"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  
    31  	"github.com/spf13/cobra"
    32  	"k8s.io/helm/pkg/chartutil"
    33  	"k8s.io/helm/pkg/provenance"
    34  	"k8s.io/helm/pkg/repo"
    35  )
    36  
    37  const fetchDesc = `
    38  Retrieve a package from a package repository, and download it locally.
    39  
    40  This is useful for fetching packages to inspect, modify, or repackage. It can
    41  also be used to perform cryptographic verification of a chart without installing
    42  the chart.
    43  
    44  There are options for unpacking the chart after download. This will create a
    45  directory for the chart and uncomparess into that directory.
    46  
    47  If the --verify flag is specified, the requested chart MUST have a provenance
    48  file, and MUST pass the verification process. Failure in any part of this will
    49  result in an error, and the chart will not be saved locally.
    50  `
    51  
    52  type fetchCmd struct {
    53  	untar    bool
    54  	untardir string
    55  	chartRef string
    56  
    57  	verify  bool
    58  	keyring string
    59  
    60  	out io.Writer
    61  }
    62  
    63  func newFetchCmd(out io.Writer) *cobra.Command {
    64  	fch := &fetchCmd{out: out}
    65  
    66  	cmd := &cobra.Command{
    67  		Use:   "fetch [flags] [chart URL | repo/chartname] [...]",
    68  		Short: "download a chart from a repository and (optionally) unpack it in local directory",
    69  		Long:  fetchDesc,
    70  		RunE: func(cmd *cobra.Command, args []string) error {
    71  			if len(args) == 0 {
    72  				return fmt.Errorf("This command needs at least one argument, url or repo/name of the chart.")
    73  			}
    74  			for i := 0; i < len(args); i++ {
    75  				fch.chartRef = args[i]
    76  				if err := fch.run(); err != nil {
    77  					return err
    78  				}
    79  			}
    80  			return nil
    81  		},
    82  	}
    83  
    84  	f := cmd.Flags()
    85  	f.BoolVar(&fch.untar, "untar", false, "If set to true, will untar the chart after downloading it.")
    86  	f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies where to untar the chart.")
    87  	f.BoolVar(&fch.verify, "verify", false, "Verify the package against its signature.")
    88  	f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.")
    89  
    90  	return cmd
    91  }
    92  
    93  func (f *fetchCmd) run() error {
    94  	pname := f.chartRef
    95  	if filepath.Ext(pname) != ".tgz" {
    96  		pname += ".tgz"
    97  	}
    98  
    99  	return downloadChart(pname, f.untar, f.untardir, f.verify, f.keyring)
   100  }
   101  
   102  // downloadChart fetches a chart over HTTP, and then (if verify is true) verifies it.
   103  //
   104  // If untar is true, it also unpacks the file into untardir.
   105  func downloadChart(pname string, untar bool, untardir string, verify bool, keyring string) error {
   106  	r, err := repo.LoadRepositoriesFile(repositoriesFile())
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	// get download url
   112  	u, err := mapRepoArg(pname, r.Repositories)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	href := u.String()
   118  	buf, err := fetchChart(href)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	if verify {
   124  		basename := filepath.Base(pname)
   125  		sigref := href + ".prov"
   126  		sig, err := fetchChart(sigref)
   127  		if err != nil {
   128  			return fmt.Errorf("provenance data not downloaded from %s: %s", sigref, err)
   129  		}
   130  		if err := ioutil.WriteFile(basename+".prov", sig.Bytes(), 0755); err != nil {
   131  			return fmt.Errorf("provenance data not saved: %s", err)
   132  		}
   133  		if err := verifyChart(basename, keyring); err != nil {
   134  			return err
   135  		}
   136  	}
   137  
   138  	return saveChart(pname, buf, untar, untardir)
   139  }
   140  
   141  // verifyChart takes a path to a chart archive and a keyring, and verifies the chart.
   142  //
   143  // It assumes that a chart archive file is accompanied by a provenance file whose
   144  // name is the archive file name plus the ".prov" extension.
   145  func verifyChart(path string, keyring string) error {
   146  	// For now, error out if it's not a tar file.
   147  	if fi, err := os.Stat(path); err != nil {
   148  		return err
   149  	} else if fi.IsDir() {
   150  		return errors.New("unpacked charts cannot be verified")
   151  	} else if !isTar(path) {
   152  		return errors.New("chart must be a tgz file")
   153  	}
   154  
   155  	provfile := path + ".prov"
   156  	if _, err := os.Stat(provfile); err != nil {
   157  		return fmt.Errorf("could not load provenance file %s: %s", provfile, err)
   158  	}
   159  
   160  	sig, err := provenance.NewFromKeyring(keyring, "")
   161  	if err != nil {
   162  		return fmt.Errorf("failed to load keyring: %s", err)
   163  	}
   164  	ver, err := sig.Verify(path, provfile)
   165  	if flagDebug {
   166  		for name := range ver.SignedBy.Identities {
   167  			fmt.Printf("Signed by %q\n", name)
   168  		}
   169  	}
   170  	return err
   171  }
   172  
   173  // defaultKeyring returns the expanded path to the default keyring.
   174  func defaultKeyring() string {
   175  	return os.ExpandEnv("$HOME/.gnupg/pubring.gpg")
   176  }
   177  
   178  // isTar tests whether the given file is a tar file.
   179  //
   180  // Currently, this simply checks extension, since a subsequent function will
   181  // untar the file and validate its binary format.
   182  func isTar(filename string) bool {
   183  	return strings.ToLower(filepath.Ext(filename)) == ".tgz"
   184  }
   185  
   186  // saveChart saves a chart locally.
   187  func saveChart(name string, buf *bytes.Buffer, untar bool, untardir string) error {
   188  	if untar {
   189  		return chartutil.Expand(untardir, buf)
   190  	}
   191  
   192  	p := strings.Split(name, "/")
   193  	return saveChartFile(p[len(p)-1], buf)
   194  }
   195  
   196  // fetchChart retrieves a chart over HTTP.
   197  func fetchChart(href string) (*bytes.Buffer, error) {
   198  	buf := bytes.NewBuffer(nil)
   199  
   200  	resp, err := http.Get(href)
   201  	if err != nil {
   202  		return buf, err
   203  	}
   204  	if resp.StatusCode != 200 {
   205  		return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status)
   206  	}
   207  
   208  	_, err = io.Copy(buf, resp.Body)
   209  	resp.Body.Close()
   210  	return buf, err
   211  }
   212  
   213  // mapRepoArg figures out which format the argument is given, and creates a fetchable
   214  // url from it.
   215  func mapRepoArg(arg string, r map[string]string) (*url.URL, error) {
   216  	// See if it's already a full URL.
   217  	u, err := url.ParseRequestURI(arg)
   218  	if err == nil {
   219  		// If it has a scheme and host and path, it's a full URL
   220  		if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 {
   221  			return u, nil
   222  		}
   223  		return nil, fmt.Errorf("Invalid chart url format: %s", arg)
   224  	}
   225  	// See if it's of the form: repo/path_to_chart
   226  	p := strings.Split(arg, "/")
   227  	if len(p) > 1 {
   228  		if baseURL, ok := r[p[0]]; ok {
   229  			if !strings.HasSuffix(baseURL, "/") {
   230  				baseURL = baseURL + "/"
   231  			}
   232  			return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/"))
   233  		}
   234  		return nil, fmt.Errorf("No such repo: %s", p[0])
   235  	}
   236  	return nil, fmt.Errorf("Invalid chart url format: %s", arg)
   237  }
   238  
   239  func saveChartFile(c string, r io.Reader) error {
   240  	// Grab the chart name that we'll use for the name of the file to download to.
   241  	out, err := os.Create(c)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	defer out.Close()
   246  
   247  	_, err = io.Copy(out, r)
   248  	return err
   249  }