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