github.com/stackdocker/rkt@v0.10.1-0.20151109095037-1aa827478248/rkt/images.go (about) 1 // Copyright 2015 The rkt Authors 2 // 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 //+build linux 16 17 package main 18 19 import ( 20 "bytes" 21 "container/list" 22 "crypto/tls" 23 "errors" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "net/http" 28 "net/url" 29 "os" 30 "path" 31 "path/filepath" 32 "strconv" 33 "strings" 34 "time" 35 36 "github.com/coreos/rkt/common/apps" 37 "github.com/coreos/rkt/pkg/keystore" 38 "github.com/coreos/rkt/rkt/config" 39 "github.com/coreos/rkt/stage0" 40 "github.com/coreos/rkt/store" 41 "github.com/coreos/rkt/version" 42 43 docker2aci "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/docker2aci/lib" 44 "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/docker2aci/lib/common" 45 "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/aci" 46 "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/discovery" 47 "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema/types" 48 "github.com/coreos/rkt/Godeps/_workspace/src/github.com/coreos/ioprogress" 49 "github.com/coreos/rkt/Godeps/_workspace/src/golang.org/x/crypto/openpgp" 50 pgperrors "github.com/coreos/rkt/Godeps/_workspace/src/golang.org/x/crypto/openpgp/errors" 51 ) 52 53 type imageActionData struct { 54 s *store.Store 55 ks *keystore.Keystore 56 headers map[string]config.Headerer 57 dockerAuth map[string]config.BasicCredentials 58 insecureSkipVerify bool 59 debug bool 60 } 61 62 type finder struct { 63 imageActionData 64 storeOnly bool 65 noStore bool 66 withDeps bool 67 } 68 69 // findImages uses findImage to attain a list of image hashes 70 func (f *finder) findImages(al *apps.Apps) error { 71 return al.Walk(func(app *apps.App) error { 72 h, err := f.findImage(app.Image, app.Asc) 73 if err != nil { 74 return err 75 } 76 app.ImageID = *h 77 return nil 78 }) 79 } 80 81 // findImage will recognize a ACI hash and use that or will fetch using the 82 // provided img (image name string, local file, URL). 83 func (f *finder) findImage(img string, asc string) (*types.Hash, error) { 84 // check if it is a valid hash, if so let it pass through 85 h, err := types.NewHash(img) 86 if err == nil { 87 fullKey, err := f.s.ResolveKey(img) 88 if err != nil { 89 return nil, fmt.Errorf("could not resolve image ID: %v", err) 90 } 91 h, err = types.NewHash(fullKey) 92 if err != nil { 93 // should never happen 94 panic(err) 95 } 96 return h, nil 97 } 98 99 // try fetching the image, potentially remotely 100 ft := &fetcher{ 101 imageActionData: f.imageActionData, 102 storeOnly: f.storeOnly, 103 noStore: f.noStore, 104 withDeps: f.withDeps, 105 } 106 key, err := ft.fetchImage(img, asc) 107 if err != nil { 108 return nil, err 109 } 110 h, err = types.NewHash(key) 111 if err != nil { 112 // should never happen 113 panic(err) 114 } 115 116 return h, nil 117 } 118 119 var errStatusAccepted = errors.New("server is still processing the request") 120 121 type fetcher struct { 122 imageActionData 123 storeOnly bool 124 noStore bool 125 withDeps bool 126 } 127 128 // fetchImage will take an image as either a URL or a name string and import it 129 // into the store if found. If asc is not "", it must exist as a local file and 130 // will be used as the signature file for verification, unless verification is 131 // disabled. If f.withDeps is true also image dependencies are fetched. 132 func (f *fetcher) fetchImage(img string, asc string) (string, error) { 133 hash, err := f.fetchSingleImage(img, asc) 134 if err != nil { 135 return "", err 136 } 137 if f.withDeps { 138 err = f.fetchImageDeps(hash) 139 if err != nil { 140 return "", err 141 } 142 } 143 return hash, nil 144 } 145 146 func (f *fetcher) getImageDeps(hash string) (types.Dependencies, error) { 147 key, err := f.s.ResolveKey(hash) 148 if err != nil { 149 return nil, err 150 } 151 im, err := f.s.GetImageManifest(key) 152 if err != nil { 153 return nil, err 154 } 155 return im.Dependencies, nil 156 } 157 158 func (f *fetcher) addImageDeps(hash string, imgsl *list.List, seen map[string]struct{}) error { 159 dependencies, err := f.getImageDeps(hash) 160 if err != nil { 161 return err 162 } 163 for _, d := range dependencies { 164 app, err := discovery.NewApp(d.ImageName.String(), d.Labels.ToMap()) 165 if err != nil { 166 return err 167 } 168 imgsl.PushBack(app.String()) 169 if _, ok := seen[app.String()]; ok { 170 return fmt.Errorf("dependency %s specified multiple times in the dependency tree for image ID: %s", app.String(), hash) 171 } 172 seen[app.String()] = struct{}{} 173 } 174 return nil 175 } 176 177 // fetchImageDeps will recursively fetch all the image dependencies 178 func (f *fetcher) fetchImageDeps(hash string) error { 179 imgsl := list.New() 180 seen := map[string]struct{}{} 181 f.addImageDeps(hash, imgsl, seen) 182 for el := imgsl.Front(); el != nil; el = el.Next() { 183 img := el.Value.(string) 184 hash, err := f.fetchSingleImage(img, "") 185 if err != nil { 186 return err 187 } 188 f.addImageDeps(hash, imgsl, seen) 189 } 190 return nil 191 } 192 193 // fetchSingleImage will take an image as either a URL or a name string and 194 // import it into the store if found. If asc is not "", it must exist as a 195 // local file and will be used as the signature file for verification, unless 196 // verification is disabled. 197 func (f *fetcher) fetchSingleImage(img string, asc string) (string, error) { 198 var ( 199 ascFile *os.File 200 err error 201 ) 202 if asc != "" && f.ks != nil { 203 ascFile, err = os.Open(asc) 204 if err != nil { 205 return "", fmt.Errorf("unable to open signature file: %v", err) 206 } 207 defer ascFile.Close() 208 } 209 210 u, err := url.Parse(img) 211 if err != nil { 212 return "", fmt.Errorf("not a valid image reference (%s)", img) 213 } 214 215 // if img refers to a local file, ensure the scheme is file:// and make the url path absolute 216 _, err = os.Stat(u.Path) 217 if err == nil { 218 u.Path, err = filepath.Abs(u.Path) 219 if err != nil { 220 return "", fmt.Errorf("unable to get abs path: %v", err) 221 } 222 u.Scheme = "file" 223 } else if !os.IsNotExist(err) { 224 return "", fmt.Errorf("unable to access %q: %v", img, err) 225 } 226 227 switch u.Scheme { 228 case "": 229 // check if os and arch are valid early 230 if app := newDiscoveryApp(img); app != nil { 231 if err := types.IsValidOSArch(app.Labels, stage0.ValidOSArch); err != nil { 232 return "", err 233 } 234 } 235 return f.fetchImageByName(img, ascFile) 236 case "http", "https", "file", "docker": 237 return f.fetchImageByURL(u, ascFile) 238 } 239 240 return "", fmt.Errorf("rkt only supports fetching for image name and http, https, docker or file URLs (%s)", img) 241 } 242 243 // fetchImageByName will try to fetch an image using an image name string. 244 func (f *fetcher) fetchImageByName(img string, ascFile *os.File) (string, error) { 245 // check the store 246 if !f.noStore { 247 key, err := getStoreKeyFromApp(f.s, img) 248 if err == nil { 249 stderr("rkt: using image from local store for image name %s", img) 250 return key, nil 251 } 252 switch err.(type) { 253 // ignore the error if it's a store.ACINotFoundError 254 case store.ACINotFoundError: 255 default: 256 return "", err 257 } 258 } 259 260 // do remote fetching 261 if !f.storeOnly { 262 // Do image discovery 263 app := newDiscoveryApp(img) 264 if app == nil { 265 return "", fmt.Errorf("invalid image name for discovery: %s", img) 266 } 267 stderr("rkt: searching for app image %s", img) 268 ep, err := discoverApp(app, true) 269 if err != nil { 270 return "", fmt.Errorf("discovery failed for %q: %v", img, err) 271 } 272 latest := false 273 // No specified version label, mark it as latest 274 if _, ok := app.Labels["version"]; !ok { 275 latest = true 276 } 277 return f.fetchImageFromEndpoints(app.Name.String(), ep, ascFile, latest) 278 } 279 280 return "", fmt.Errorf("unable to fetch image for image name: %s", img) 281 } 282 283 // fetchImageByURL will try fetch an image using an URL. 284 func (f *fetcher) fetchImageByURL(u *url.URL, ascFile *os.File) (string, error) { 285 // Always fetch if it's a file 286 if u.Scheme == "file" { 287 stderr("rkt: using image from file %s", u.Path) 288 return f.fetchImageFromURL(u.String(), u.Scheme, ascFile, false) 289 } 290 291 // check the store 292 if !f.noStore { 293 rem, ok, err := f.s.GetRemote(u.String()) 294 if err != nil { 295 return "", err 296 } 297 if ok { 298 stderr("rkt: using image from local store for url %s", u.String()) 299 return rem.BlobKey, nil 300 } 301 } 302 303 // do remote fetching 304 if !f.storeOnly { 305 latest := false 306 if u.Scheme == "docker" { 307 dockerURL := common.ParseDockerURL(path.Join(u.Host, u.Path)) 308 if dockerURL.Tag == "latest" { 309 latest = true 310 } 311 } 312 stderr("rkt: remote fetching from url %s", u.String()) 313 return f.fetchImageFromURL(u.String(), u.Scheme, ascFile, latest) 314 } 315 316 return "", fmt.Errorf("unable to fetch image for url %s", u.String()) 317 } 318 319 func (f *fetcher) fetchImageFromEndpoints(appName string, ep *discovery.Endpoints, ascFile *os.File, latest bool) (string, error) { 320 stderr("rkt: remote fetching from url %s", ep.ACIEndpoints[0].ACI) 321 return f.fetchImageFrom(appName, ep.ACIEndpoints[0].ACI, ep.ACIEndpoints[0].ASC, "", ascFile, latest) 322 } 323 324 func (f *fetcher) fetchImageFromURL(imgurl string, scheme string, ascFile *os.File, latest bool) (string, error) { 325 return f.fetchImageFrom("", imgurl, ascURLFromImgURL(imgurl), scheme, ascFile, latest) 326 } 327 328 // fetchImageFrom fetches an image from the aciURL. 329 func (f *fetcher) fetchImageFrom(appName string, aciURL, ascURL, scheme string, ascFile *os.File, latest bool) (string, error) { 330 var rem *store.Remote 331 332 if f.insecureSkipVerify && f.ks != nil { 333 stderr("rkt: warning: TLS verification and signature verification has been disabled") 334 } 335 if !f.insecureSkipVerify && scheme == "docker" { 336 return "", fmt.Errorf("signature verification for docker images is not supported (try --insecure-skip-verify)") 337 } 338 339 if scheme != "file" { 340 var err error 341 ok := false 342 rem, ok, err = f.s.GetRemote(aciURL) 343 if err != nil { 344 return "", err 345 } 346 if ok && useCached(rem.DownloadTime, rem.CacheMaxAge) { 347 stderr("rkt: using cached image from local store") 348 return rem.BlobKey, nil 349 } 350 } 351 352 if scheme != "file" && f.debug { 353 stderr("rkt: fetching image from %s", aciURL) 354 } 355 356 var etag string 357 if rem != nil { 358 etag = rem.ETag 359 } 360 entity, aciFile, cd, err := f.fetch(appName, aciURL, ascURL, ascFile, etag) 361 if err != nil { 362 return "", err 363 } 364 if cd != nil && cd.useCached { 365 if rem != nil { 366 return rem.BlobKey, nil 367 } else { 368 // should never happen 369 panic("asked to use cached image but remote is nil") 370 } 371 } 372 if scheme != "file" { 373 defer os.Remove(aciFile.Name()) 374 } 375 376 if entity != nil && !f.insecureSkipVerify { 377 stderr("rkt: signature verified:") 378 for _, v := range entity.Identities { 379 stderr(" %s", v.Name) 380 } 381 } 382 key, err := f.s.WriteACI(aciFile, latest) 383 if err != nil { 384 return "", err 385 } 386 387 if scheme != "file" { 388 rem := store.NewRemote(aciURL, ascURL) 389 rem.BlobKey = key 390 rem.DownloadTime = time.Now() 391 if cd != nil { 392 rem.ETag = cd.etag 393 rem.CacheMaxAge = cd.maxAge 394 } 395 err = f.s.WriteRemote(rem) 396 if err != nil { 397 return "", err 398 } 399 } 400 401 return key, nil 402 } 403 404 // fetch opens/downloads and verifies the remote ACI. 405 // If appName is not "", it will be used to check that the manifest contain the correct appName 406 // If ascFile is not nil, it will be used as the signature file and ascURL will be ignored. 407 // If Keystore is nil signature verification will be skipped, regardless of ascFile. 408 // fetch returns the signer, an *os.File representing the ACI, and an error if any. 409 // err will be nil if the ACI fetches successfully and the ACI is verified. 410 func (f *fetcher) fetch(appName string, aciURL, ascURL string, ascFile *os.File, etag string) (*openpgp.Entity, *os.File, *cacheData, error) { 411 var ( 412 entity *openpgp.Entity 413 cd *cacheData 414 ) 415 416 u, err := url.Parse(aciURL) 417 if err != nil { 418 return nil, nil, nil, fmt.Errorf("error parsing ACI url: %v", err) 419 } 420 if u.Scheme == "docker" { 421 registryURL := strings.TrimPrefix(aciURL, "docker://") 422 423 storeTmpDir, err := f.s.TmpDir() 424 if err != nil { 425 return nil, nil, nil, fmt.Errorf("error creating temporary dir for docker to ACI conversion: %v", err) 426 } 427 tmpDir, err := ioutil.TempDir(storeTmpDir, "docker2aci-") 428 if err != nil { 429 return nil, nil, nil, err 430 } 431 defer os.RemoveAll(tmpDir) 432 433 indexName := docker2aci.GetIndexName(registryURL) 434 user := "" 435 password := "" 436 if creds, ok := f.dockerAuth[indexName]; ok { 437 user = creds.User 438 password = creds.Password 439 } 440 acis, err := docker2aci.Convert(registryURL, true, tmpDir, tmpDir, user, password) 441 if err != nil { 442 return nil, nil, nil, fmt.Errorf("error converting docker image to ACI: %v", err) 443 } 444 445 aciFile, err := os.Open(acis[0]) 446 if err != nil { 447 return nil, nil, nil, fmt.Errorf("error opening squashed ACI file: %v", err) 448 } 449 450 return nil, aciFile, nil, nil 451 } 452 453 // attempt to automatically fetch the public key in case it is available on a TLS connection. 454 if globalFlags.TrustKeysFromHttps && !globalFlags.InsecureSkipVerify && appName != "" { 455 pkls, err := getPubKeyLocations(appName, false, globalFlags.Debug) 456 if err != nil { 457 stderr("Error determining key location: %v", err) 458 } else { 459 // no http, don't ask user for accepting the key, no overriding 460 if err := addKeys(pkls, appName, false, true, false); err != nil { 461 stderr("Error adding keys: %v", err) 462 } 463 } 464 } 465 466 var retrySignature bool 467 if f.ks != nil && ascFile == nil { 468 u, err := url.Parse(ascURL) 469 if err != nil { 470 return nil, nil, nil, fmt.Errorf("error parsing ASC url: %v", err) 471 } 472 if u.Scheme == "file" { 473 ascFile, err = os.Open(u.Path) 474 if err != nil { 475 return nil, nil, nil, fmt.Errorf("error opening signature file: %v", err) 476 } 477 } else { 478 stderr("Downloading signature from %v\n", ascURL) 479 ascFile, err = f.s.TmpFile() 480 if err != nil { 481 return nil, nil, nil, fmt.Errorf("error setting up temporary file: %v", err) 482 } 483 defer os.Remove(ascFile.Name()) 484 485 err = f.downloadSignatureFile(ascURL, ascFile) 486 switch err { 487 case errStatusAccepted: 488 retrySignature = true 489 stderr("rkt: server requested deferring the signature download") 490 case nil: 491 break 492 default: 493 return nil, nil, nil, fmt.Errorf("error downloading the signature file: %v", err) 494 } 495 } 496 defer ascFile.Close() 497 } 498 499 // check if the identity used by the signature is in the store before a 500 // possibly expensive download. This is only an optimization and it's 501 // ok to skip the test if the signature will be downloaded later. 502 if !retrySignature && f.ks != nil && appName != "" { 503 if _, err := ascFile.Seek(0, 0); err != nil { 504 return nil, nil, nil, fmt.Errorf("error seeking signature file: %v", err) 505 } 506 if entity, err = f.ks.CheckSignature(appName, bytes.NewReader([]byte{}), ascFile); err != nil { 507 if _, ok := err.(pgperrors.SignatureError); !ok { 508 return nil, nil, nil, err 509 } 510 } 511 } 512 513 var aciFile *os.File 514 if u.Scheme == "file" { 515 aciFile, err = os.Open(u.Path) 516 if err != nil { 517 return nil, nil, nil, fmt.Errorf("error opening ACI file: %v", err) 518 } 519 } else { 520 aciFile, err = f.s.TmpFile() 521 if err != nil { 522 return nil, aciFile, nil, fmt.Errorf("error setting up temporary file: %v", err) 523 } 524 defer os.Remove(aciFile.Name()) 525 526 if cd, err = f.downloadACI(aciURL, aciFile, etag); err != nil { 527 return nil, nil, nil, fmt.Errorf("error downloading ACI: %v", err) 528 } 529 if cd.useCached { 530 return nil, nil, cd, nil 531 } 532 } 533 534 if retrySignature { 535 if err = f.downloadSignatureFile(ascURL, ascFile); err != nil { 536 return nil, aciFile, nil, fmt.Errorf("error downloading the signature file: %v", err) 537 } 538 } 539 540 manifest, err := aci.ManifestFromImage(aciFile) 541 if err != nil { 542 return nil, aciFile, nil, fmt.Errorf("invalid image manifest: %v", err) 543 } 544 // Check if the downloaded ACI has the correct app name. 545 // The check is only performed when the aci is downloaded through the 546 // discovery protocol, but not with local files or full URL. 547 if appName != "" && manifest.Name.String() != appName { 548 return nil, aciFile, nil, 549 fmt.Errorf("error when reading the app name: %q expected but %q found", 550 appName, manifest.Name.String()) 551 } 552 553 if f.ks != nil { 554 if _, err := aciFile.Seek(0, 0); err != nil { 555 return nil, aciFile, nil, fmt.Errorf("error seeking ACI file: %v", err) 556 } 557 if _, err := ascFile.Seek(0, 0); err != nil { 558 return nil, aciFile, nil, fmt.Errorf("error seeking signature file: %v", err) 559 } 560 if entity, err = f.ks.CheckSignature(manifest.Name.String(), aciFile, ascFile); err != nil { 561 return nil, aciFile, nil, err 562 } 563 } 564 565 if _, err := aciFile.Seek(0, 0); err != nil { 566 return nil, aciFile, nil, fmt.Errorf("error seeking ACI file: %v", err) 567 } 568 return entity, aciFile, cd, nil 569 } 570 571 type writeSyncer interface { 572 io.Writer 573 Sync() error 574 } 575 576 // downloadACI gets the aci specified at aciurl 577 func (f *fetcher) downloadACI(aciurl string, out writeSyncer, etag string) (*cacheData, error) { 578 return f.downloadHTTP(aciurl, "ACI", out, etag) 579 } 580 581 // downloadSignatureFile gets the signature specified at sigurl 582 func (f *fetcher) downloadSignatureFile(sigurl string, out writeSyncer) error { 583 _, err := f.downloadHTTP(sigurl, "signature", out, "") 584 return err 585 586 } 587 588 // downloadHTTP retrieves url, creating a temp file using getTempFile 589 // http:// and https:// urls supported 590 func (f *fetcher) downloadHTTP(url, label string, out writeSyncer, etag string) (*cacheData, error) { 591 req, err := http.NewRequest("GET", url, nil) 592 if err != nil { 593 return nil, err 594 } 595 transport := http.DefaultTransport 596 if f.insecureSkipVerify { 597 transport = &http.Transport{ 598 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 599 } 600 } 601 602 client := &http.Client{Transport: transport} 603 f.setHTTPHeaders(req, etag) 604 605 client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 606 if len(via) >= 10 { 607 return fmt.Errorf("too many redirects") 608 } 609 f.setHTTPHeaders(req, etag) 610 return nil 611 } 612 613 res, err := client.Do(req) 614 if err != nil { 615 return nil, err 616 } 617 defer res.Body.Close() 618 619 cd := &cacheData{} 620 // TODO(jonboulle): handle http more robustly (redirects?) 621 switch res.StatusCode { 622 case http.StatusAccepted: 623 // If the server returns Status Accepted (HTTP 202), we should retry 624 // downloading the signature later. 625 return nil, errStatusAccepted 626 case http.StatusOK: 627 fallthrough 628 case http.StatusNotModified: 629 cd.etag = res.Header.Get("ETag") 630 cd.maxAge = getMaxAge(res.Header.Get("Cache-Control")) 631 cd.useCached = (res.StatusCode == http.StatusNotModified) 632 if cd.useCached { 633 return cd, nil 634 } 635 default: 636 return nil, fmt.Errorf("bad HTTP status code: %d", res.StatusCode) 637 } 638 639 prefix := "Downloading " + label 640 fmtBytesSize := 18 641 barSize := int64(80 - len(prefix) - fmtBytesSize) 642 bar := ioprogress.DrawTextFormatBarForW(barSize, os.Stderr) 643 fmtfunc := func(progress, total int64) string { 644 // Content-Length is set to -1 when unknown. 645 if total == -1 { 646 return fmt.Sprintf( 647 "%s: %v of an unknown total size", 648 prefix, 649 ioprogress.ByteUnitStr(progress), 650 ) 651 } 652 return fmt.Sprintf( 653 "%s: %s %s", 654 prefix, 655 bar(progress, total), 656 ioprogress.DrawTextFormatBytes(progress, total), 657 ) 658 } 659 660 reader := &ioprogress.Reader{ 661 Reader: res.Body, 662 Size: res.ContentLength, 663 DrawFunc: ioprogress.DrawTerminalf(os.Stderr, fmtfunc), 664 DrawInterval: time.Second, 665 } 666 667 if _, err := io.Copy(out, reader); err != nil { 668 return nil, fmt.Errorf("error copying %s: %v", label, err) 669 } 670 671 if err := out.Sync(); err != nil { 672 return nil, fmt.Errorf("error writing %s: %v", label, err) 673 } 674 675 return cd, nil 676 } 677 678 func (f *fetcher) setHTTPHeaders(req *http.Request, etag string) { 679 options := make(http.Header) 680 // Send credentials only over secure channel 681 if req.URL.Scheme == "https" { 682 if hostOpts, ok := f.headers[req.URL.Host]; ok { 683 options = hostOpts.Header() 684 } 685 } 686 for k, v := range options { 687 for _, e := range v { 688 req.Header.Add(k, e) 689 } 690 } 691 if etag != "" { 692 req.Header.Add("If-None-Match", etag) 693 } 694 req.Header.Add("User-Agent", fmt.Sprintf("rkt/%s", version.Version)) 695 } 696 697 func ascURLFromImgURL(imgurl string) string { 698 s := strings.TrimSuffix(imgurl, ".aci") 699 return s + ".aci.asc" 700 } 701 702 // newDiscoveryApp creates a discovery app if the given img is an app name and 703 // has a URL-like structure, for example example.com/reduce-worker. 704 // Or it returns nil. 705 func newDiscoveryApp(img string) *discovery.App { 706 app, err := discovery.NewAppFromString(img) 707 if err != nil { 708 return nil 709 } 710 u, err := url.Parse(app.Name.String()) 711 if err != nil || u.Scheme != "" { 712 return nil 713 } 714 if _, ok := app.Labels["arch"]; !ok { 715 app.Labels["arch"] = defaultArch 716 } 717 if _, ok := app.Labels["os"]; !ok { 718 app.Labels["os"] = defaultOS 719 } 720 return app 721 } 722 723 func discoverApp(app *discovery.App, insecure bool) (*discovery.Endpoints, error) { 724 ep, attempts, err := discovery.DiscoverEndpoints(*app, insecure) 725 if globalFlags.Debug { 726 for _, a := range attempts { 727 stderr("meta tag 'ac-discovery' not found on %s: %v", a.Prefix, a.Error) 728 } 729 } 730 if err != nil { 731 return nil, err 732 } 733 if len(ep.ACIEndpoints) == 0 { 734 return nil, fmt.Errorf("no endpoints discovered") 735 } 736 return ep, nil 737 } 738 739 func getStoreKeyFromApp(s *store.Store, img string) (string, error) { 740 app, err := discovery.NewAppFromString(img) 741 if err != nil { 742 return "", fmt.Errorf("cannot parse the image name: %v", err) 743 } 744 labels, err := types.LabelsFromMap(app.Labels) 745 if err != nil { 746 return "", fmt.Errorf("invalid labels in the name: %v", err) 747 } 748 key, err := s.GetACI(app.Name, labels) 749 if err != nil { 750 switch err.(type) { 751 case store.ACINotFoundError: 752 return "", err 753 default: 754 return "", fmt.Errorf("cannot find image: %v", err) 755 } 756 } 757 return key, nil 758 } 759 760 func getStoreKeyFromAppOrHash(s *store.Store, input string) (string, error) { 761 var key string 762 if _, err := types.NewHash(input); err == nil { 763 key, err = s.ResolveKey(input) 764 if err != nil { 765 return "", fmt.Errorf("cannot resolve image ID: %v", err) 766 } 767 } else { 768 key, err = getStoreKeyFromApp(s, input) 769 if err != nil { 770 return "", fmt.Errorf("cannot find image: %v", err) 771 } 772 } 773 return key, nil 774 } 775 776 type cacheData struct { 777 useCached bool 778 etag string 779 maxAge int 780 } 781 782 func getMaxAge(headerValue string) int { 783 var MaxAge int = 0 784 785 if len(headerValue) > 0 { 786 parts := strings.Split(headerValue, " ") 787 for i := 0; i < len(parts); i++ { 788 attr, val := parts[i], "" 789 if j := strings.Index(attr, "="); j >= 0 { 790 attr, val = attr[:j], attr[j+1:] 791 } 792 lowerAttr := strings.ToLower(attr) 793 794 switch lowerAttr { 795 case "no-store": 796 MaxAge = 0 797 continue 798 case "no-cache": 799 MaxAge = 0 800 continue 801 case "max-age": 802 secs, err := strconv.Atoi(val) 803 if err != nil || secs != 0 && val[0] == '0' { 804 break 805 } 806 if secs <= 0 { 807 MaxAge = 0 808 } else { 809 MaxAge = secs 810 } 811 continue 812 } 813 } 814 } 815 return MaxAge 816 } 817 818 // useCached checks if downloadTime plus maxAge is before/after the current time. 819 // return true if the cached image should be used, false otherwise. 820 func useCached(downloadTime time.Time, maxAge int) bool { 821 freshnessLifetime := int(time.Now().Sub(downloadTime).Seconds()) 822 if maxAge > 0 && freshnessLifetime < maxAge { 823 return true 824 } 825 return false 826 }