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 }