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