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