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 }