github.com/koderover/helm@v2.17.0+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 "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 // Getters collection for the operation 69 Getters getter.Providers 70 // Username chart repository username 71 Username string 72 // Password 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 g, err := getterConstructor(ref, "", "", "") 174 if t, ok := g.(*getter.HttpGetter); ok { 175 t.SetCredentials(c.Username, c.Password) 176 } 177 return u, g, err 178 } 179 return u, nil, err 180 } 181 r, err := repo.NewChartRepository(rc, c.Getters) 182 c.setCredentials(r) 183 // If we get here, we don't need to go through the next phase of looking 184 // up the URL. We have it already. So we just return. 185 return u, r.Client, err 186 } 187 188 // See if it's of the form: repo/path_to_chart 189 p := strings.SplitN(u.Path, "/", 2) 190 if len(p) < 2 { 191 return u, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u) 192 } 193 194 repoName := p[0] 195 chartName := p[1] 196 rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories) 197 198 if err != nil { 199 return u, nil, err 200 } 201 202 r, err := repo.NewChartRepository(rc, c.Getters) 203 if err != nil { 204 return u, nil, err 205 } 206 c.setCredentials(r) 207 208 // Skip if dependency not contain name 209 if len(r.Config.Name) == 0 { 210 return u, r.Client, nil 211 } 212 213 // Next, we need to load the index, and actually look up the chart. 214 i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) 215 if err != nil { 216 return u, r.Client, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) 217 } 218 219 cv, err := i.Get(chartName, version) 220 if err != nil { 221 return u, r.Client, fmt.Errorf("chart %q matching version %q not found in %s index. (try 'helm repo update'). %s", chartName, version, r.Config.Name, err) 222 } 223 224 if len(cv.URLs) == 0 { 225 return u, r.Client, fmt.Errorf("chart %q has no downloadable URLs", ref) 226 } 227 228 // TODO: Seems that picking first URL is not fully correct 229 u, err = url.Parse(cv.URLs[0]) 230 if err != nil { 231 return u, r.Client, fmt.Errorf("invalid chart URL format: %s", ref) 232 } 233 234 // If the URL is relative (no scheme), prepend the chart repo's base URL 235 if !u.IsAbs() { 236 repoURL, err := url.Parse(rc.URL) 237 if err != nil { 238 return repoURL, r.Client, err 239 } 240 q := repoURL.Query() 241 // We need a trailing slash for ResolveReference to work, but make sure there isn't already one 242 repoURL.Path = strings.TrimSuffix(repoURL.Path, "/") + "/" 243 u = repoURL.ResolveReference(u) 244 u.RawQuery = q.Encode() 245 return u, r.Client, err 246 } 247 248 return u, r.Client, nil 249 } 250 251 // setCredentials if HttpGetter is used, this method sets the configured repository credentials on the HttpGetter. 252 func (c *ChartDownloader) setCredentials(r *repo.ChartRepository) { 253 if t, ok := r.Client.(*getter.HttpGetter); ok { 254 t.SetCredentials(c.getRepoCredentials(r)) 255 } 256 } 257 258 // getRepoCredentials if this ChartDownloader is not configured to use credentials, and the chart repository sent as an argument is, 259 // then the repository's configured credentials are returned. 260 // Else, this ChartDownloader's credentials are returned. 261 func (c *ChartDownloader) getRepoCredentials(r *repo.ChartRepository) (username, password string) { 262 username = c.Username 263 password = c.Password 264 if r != nil && r.Config != nil { 265 if username == "" { 266 username = r.Config.Username 267 } 268 if password == "" { 269 password = r.Config.Password 270 } 271 } 272 return 273 } 274 275 // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. 276 // 277 // It assumes that a chart archive file is accompanied by a provenance file whose 278 // name is the archive file name plus the ".prov" extension. 279 func VerifyChart(path string, keyring string) (*provenance.Verification, error) { 280 // For now, error out if it's not a tar file. 281 if fi, err := os.Stat(path); err != nil { 282 return nil, err 283 } else if fi.IsDir() { 284 return nil, errors.New("unpacked charts cannot be verified") 285 } else if !isTar(path) { 286 return nil, errors.New("chart must be a tgz file") 287 } 288 289 provfile := path + ".prov" 290 if _, err := os.Stat(provfile); err != nil { 291 return nil, fmt.Errorf("could not load provenance file %s: %s", provfile, err) 292 } 293 294 sig, err := provenance.NewFromKeyring(keyring, "") 295 if err != nil { 296 return nil, fmt.Errorf("failed to load keyring: %s", err) 297 } 298 return sig.Verify(path, provfile) 299 } 300 301 // isTar tests whether the given file is a tar file. 302 // 303 // Currently, this simply checks extension, since a subsequent function will 304 // untar the file and validate its binary format. 305 func isTar(filename string) bool { 306 return strings.ToLower(filepath.Ext(filename)) == ".tgz" 307 } 308 309 func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) { 310 for _, rc := range cfgs { 311 if rc.Name == name { 312 if rc.URL == "" { 313 return nil, fmt.Errorf("no URL found for repository %s", name) 314 } 315 return rc, nil 316 } 317 } 318 return nil, fmt.Errorf("repo %s not found", name) 319 } 320 321 // scanReposForURL scans all repos to find which repo contains the given URL. 322 // 323 // This will attempt to find the given URL in all of the known repositories files. 324 // 325 // If the URL is found, this will return the repo entry that contained that URL. 326 // 327 // If all of the repos are checked, but the URL is not found, an ErrNoOwnerRepo 328 // error is returned. 329 // 330 // Other errors may be returned when repositories cannot be loaded or searched. 331 // 332 // Technically, the fact that a URL is not found in a repo is not a failure indication. 333 // Charts are not required to be included in an index before they are valid. So 334 // be mindful of this case. 335 // 336 // The same URL can technically exist in two or more repositories. This algorithm 337 // will return the first one it finds. Order is determined by the order of repositories 338 // in the repositories.yaml file. 339 func (c *ChartDownloader) scanReposForURL(u string, rf *repo.RepoFile) (*repo.Entry, error) { 340 // FIXME: This is far from optimal. Larger installations and index files will 341 // incur a performance hit for this type of scanning. 342 for _, rc := range rf.Repositories { 343 r, err := repo.NewChartRepository(rc, c.Getters) 344 if err != nil { 345 return nil, err 346 } 347 348 i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) 349 if err != nil { 350 return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) 351 } 352 353 for _, entry := range i.Entries { 354 for _, ver := range entry { 355 for _, dl := range ver.URLs { 356 if urlutil.Equal(u, dl) { 357 return rc, nil 358 } 359 } 360 } 361 } 362 } 363 // This means that there is no repo file for the given URL. 364 return nil, ErrNoOwnerRepo 365 }