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  }