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