github.com/valdemarpavesi/helm@v2.9.1+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 // Chart repository username 71 Username string 72 // Chart repository password 73 Password string 74 } 75 76 // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. 77 // 78 // If Verify is set to VerifyNever, the verification will be nil. 79 // If Verify is set to VerifyIfPossible, this will return a verification (or nil on failure), and print a warning on failure. 80 // If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails. 81 // If Verify is set to VerifyLater, this will download the prov file (if it exists), but not verify it. 82 // 83 // For VerifyNever and VerifyIfPossible, the Verification may be empty. 84 // 85 // Returns a string path to the location where the file was downloaded and a verification 86 // (if provenance was verified), or an error if something bad happened. 87 func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) { 88 u, g, err := c.ResolveChartVersion(ref, version) 89 if err != nil { 90 return "", nil, err 91 } 92 93 data, err := g.Get(u.String()) 94 if err != nil { 95 return "", nil, err 96 } 97 98 name := filepath.Base(u.Path) 99 destfile := filepath.Join(dest, name) 100 if err := ioutil.WriteFile(destfile, data.Bytes(), 0644); err != nil { 101 return destfile, nil, err 102 } 103 104 // If provenance is requested, verify it. 105 ver := &provenance.Verification{} 106 if c.Verify > VerifyNever { 107 body, err := g.Get(u.String() + ".prov") 108 if err != nil { 109 if c.Verify == VerifyAlways { 110 return destfile, ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov") 111 } 112 fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err) 113 return destfile, ver, nil 114 } 115 provfile := destfile + ".prov" 116 if err := ioutil.WriteFile(provfile, body.Bytes(), 0644); err != nil { 117 return destfile, nil, err 118 } 119 120 if c.Verify != VerifyLater { 121 ver, err = VerifyChart(destfile, c.Keyring) 122 if err != nil { 123 // Fail always in this case, since it means the verification step 124 // failed. 125 return destfile, ver, err 126 } 127 } 128 } 129 return destfile, ver, nil 130 } 131 132 // ResolveChartVersion resolves a chart reference to a URL. 133 // 134 // It returns the URL as well as a preconfigured repo.Getter that can fetch 135 // the URL. 136 // 137 // A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path. 138 // 139 // A version is a SemVer string (1.2.3-beta.1+f334a6789). 140 // 141 // - For fully qualified URLs, the version will be ignored (since URLs aren't versioned) 142 // - For a chart reference 143 // * If version is non-empty, this will return the URL for that version 144 // * If version is empty, this will return the URL for the latest version 145 // * If no version can be found, an error is returned 146 func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, getter.Getter, error) { 147 u, err := url.Parse(ref) 148 if err != nil { 149 return nil, nil, fmt.Errorf("invalid chart URL format: %s", ref) 150 } 151 152 rf, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile()) 153 if err != nil { 154 return u, nil, err 155 } 156 157 if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 { 158 // In this case, we have to find the parent repo that contains this chart 159 // URL. And this is an unfortunate problem, as it requires actually going 160 // through each repo cache file and finding a matching URL. But basically 161 // we want to find the repo in case we have special SSL cert config 162 // for that repo. 163 164 rc, err := c.scanReposForURL(ref, rf) 165 if err != nil { 166 // If there is no special config, return the default HTTP client and 167 // swallow the error. 168 if err == ErrNoOwnerRepo { 169 getterConstructor, err := c.Getters.ByScheme(u.Scheme) 170 if err != nil { 171 return u, nil, err 172 } 173 getter, err := getterConstructor(ref, "", "", "") 174 return u, getter, err 175 } 176 return u, nil, err 177 } 178 r, err := repo.NewChartRepository(rc, c.Getters) 179 c.setCredentials(r) 180 // If we get here, we don't need to go through the next phase of looking 181 // up the URL. We have it already. So we just return. 182 return u, r.Client, err 183 } 184 185 // See if it's of the form: repo/path_to_chart 186 p := strings.SplitN(u.Path, "/", 2) 187 if len(p) < 2 { 188 return u, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u) 189 } 190 191 repoName := p[0] 192 chartName := p[1] 193 rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories) 194 195 if err != nil { 196 return u, nil, err 197 } 198 199 r, err := repo.NewChartRepository(rc, c.Getters) 200 if err != nil { 201 return u, nil, err 202 } 203 c.setCredentials(r) 204 205 // Next, we need to load the index, and actually look up the chart. 206 i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) 207 if err != nil { 208 return u, r.Client, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) 209 } 210 211 cv, err := i.Get(chartName, version) 212 if err != nil { 213 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) 214 } 215 216 if len(cv.URLs) == 0 { 217 return u, r.Client, fmt.Errorf("chart %q has no downloadable URLs", ref) 218 } 219 220 // TODO: Seems that picking first URL is not fully correct 221 u, err = url.Parse(cv.URLs[0]) 222 if err != nil { 223 return u, r.Client, fmt.Errorf("invalid chart URL format: %s", ref) 224 } 225 226 // If the URL is relative (no scheme), prepend the chart repo's base URL 227 if !u.IsAbs() { 228 repoURL, err := url.Parse(rc.URL) 229 if err != nil { 230 return repoURL, r.Client, err 231 } 232 q := repoURL.Query() 233 // We need a trailing slash for ResolveReference to work, but make sure there isn't already one 234 repoURL.Path = strings.TrimSuffix(repoURL.Path, "/") + "/" 235 u = repoURL.ResolveReference(u) 236 u.RawQuery = q.Encode() 237 return u, r.Client, err 238 } 239 240 return u, r.Client, nil 241 } 242 243 // If HttpGetter is used, this method sets the configured repository credentials on the HttpGetter. 244 func (c *ChartDownloader) setCredentials(r *repo.ChartRepository) { 245 if t, ok := r.Client.(*getter.HttpGetter); ok { 246 t.SetCredentials(c.getRepoCredentials(r)) 247 } 248 } 249 250 // If this ChartDownloader is not configured to use credentials, and the chart repository sent as an argument is, 251 // then the repository's configured credentials are returned. 252 // Else, this ChartDownloader's credentials are returned. 253 func (c *ChartDownloader) getRepoCredentials(r *repo.ChartRepository) (username, password string) { 254 username = c.Username 255 password = c.Password 256 if r != nil && r.Config != nil { 257 if username == "" { 258 username = r.Config.Username 259 } 260 if password == "" { 261 password = r.Config.Password 262 } 263 } 264 return 265 } 266 267 // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. 268 // 269 // It assumes that a chart archive file is accompanied by a provenance file whose 270 // name is the archive file name plus the ".prov" extension. 271 func VerifyChart(path string, keyring string) (*provenance.Verification, error) { 272 // For now, error out if it's not a tar file. 273 if fi, err := os.Stat(path); err != nil { 274 return nil, err 275 } else if fi.IsDir() { 276 return nil, errors.New("unpacked charts cannot be verified") 277 } else if !isTar(path) { 278 return nil, errors.New("chart must be a tgz file") 279 } 280 281 provfile := path + ".prov" 282 if _, err := os.Stat(provfile); err != nil { 283 return nil, fmt.Errorf("could not load provenance file %s: %s", provfile, err) 284 } 285 286 sig, err := provenance.NewFromKeyring(keyring, "") 287 if err != nil { 288 return nil, fmt.Errorf("failed to load keyring: %s", err) 289 } 290 return sig.Verify(path, provfile) 291 } 292 293 // isTar tests whether the given file is a tar file. 294 // 295 // Currently, this simply checks extension, since a subsequent function will 296 // untar the file and validate its binary format. 297 func isTar(filename string) bool { 298 return strings.ToLower(filepath.Ext(filename)) == ".tgz" 299 } 300 301 func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) { 302 for _, rc := range cfgs { 303 if rc.Name == name { 304 if rc.URL == "" { 305 return nil, fmt.Errorf("no URL found for repository %s", name) 306 } 307 return rc, nil 308 } 309 } 310 return nil, fmt.Errorf("repo %s not found", name) 311 } 312 313 // scanReposForURL scans all repos to find which repo contains the given URL. 314 // 315 // This will attempt to find the given URL in all of the known repositories files. 316 // 317 // If the URL is found, this will return the repo entry that contained that URL. 318 // 319 // If all of the repos are checked, but the URL is not found, an ErrNoOwnerRepo 320 // error is returned. 321 // 322 // Other errors may be returned when repositories cannot be loaded or searched. 323 // 324 // Technically, the fact that a URL is not found in a repo is not a failure indication. 325 // Charts are not required to be included in an index before they are valid. So 326 // be mindful of this case. 327 // 328 // The same URL can technically exist in two or more repositories. This algorithm 329 // will return the first one it finds. Order is determined by the order of repositories 330 // in the repositories.yaml file. 331 func (c *ChartDownloader) scanReposForURL(u string, rf *repo.RepoFile) (*repo.Entry, error) { 332 // FIXME: This is far from optimal. Larger installations and index files will 333 // incur a performance hit for this type of scanning. 334 for _, rc := range rf.Repositories { 335 r, err := repo.NewChartRepository(rc, c.Getters) 336 if err != nil { 337 return nil, err 338 } 339 340 i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) 341 if err != nil { 342 return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) 343 } 344 345 for _, entry := range i.Entries { 346 for _, ver := range entry { 347 for _, dl := range ver.URLs { 348 if urlutil.Equal(u, dl) { 349 return rc, nil 350 } 351 } 352 } 353 } 354 } 355 // This means that there is no repo file for the given URL. 356 return nil, ErrNoOwnerRepo 357 }