github.com/sbward/docker@v1.4.2-0.20150114010528-c9dab702bed3/graph/pull.go (about)

     1  package graph
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net"
    10  	"net/url"
    11  	"os"
    12  	"strings"
    13  	"time"
    14  
    15  	log "github.com/Sirupsen/logrus"
    16  	"github.com/docker/docker/engine"
    17  	"github.com/docker/docker/image"
    18  	"github.com/docker/docker/registry"
    19  	"github.com/docker/docker/utils"
    20  	"github.com/docker/libtrust"
    21  )
    22  
    23  func (s *TagStore) verifyManifest(eng *engine.Engine, manifestBytes []byte) (*registry.ManifestData, bool, error) {
    24  	sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures")
    25  	if err != nil {
    26  		return nil, false, fmt.Errorf("error parsing payload: %s", err)
    27  	}
    28  	keys, err := sig.Verify()
    29  	if err != nil {
    30  		return nil, false, fmt.Errorf("error verifying payload: %s", err)
    31  	}
    32  
    33  	payload, err := sig.Payload()
    34  	if err != nil {
    35  		return nil, false, fmt.Errorf("error retrieving payload: %s", err)
    36  	}
    37  
    38  	var manifest registry.ManifestData
    39  	if err := json.Unmarshal(payload, &manifest); err != nil {
    40  		return nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
    41  	}
    42  	if manifest.SchemaVersion != 1 {
    43  		return nil, false, fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
    44  	}
    45  
    46  	var verified bool
    47  	for _, key := range keys {
    48  		job := eng.Job("trust_key_check")
    49  		b, err := key.MarshalJSON()
    50  		if err != nil {
    51  			return nil, false, fmt.Errorf("error marshalling public key: %s", err)
    52  		}
    53  		namespace := manifest.Name
    54  		if namespace[0] != '/' {
    55  			namespace = "/" + namespace
    56  		}
    57  		stdoutBuffer := bytes.NewBuffer(nil)
    58  
    59  		job.Args = append(job.Args, namespace)
    60  		job.Setenv("PublicKey", string(b))
    61  		// Check key has read/write permission (0x03)
    62  		job.SetenvInt("Permission", 0x03)
    63  		job.Stdout.Add(stdoutBuffer)
    64  		if err = job.Run(); err != nil {
    65  			return nil, false, fmt.Errorf("error running key check: %s", err)
    66  		}
    67  		result := engine.Tail(stdoutBuffer, 1)
    68  		log.Debugf("Key check result: %q", result)
    69  		if result == "verified" {
    70  			verified = true
    71  		}
    72  	}
    73  
    74  	return &manifest, verified, nil
    75  }
    76  
    77  func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
    78  	if n := len(job.Args); n != 1 && n != 2 {
    79  		return job.Errorf("Usage: %s IMAGE [TAG]", job.Name)
    80  	}
    81  
    82  	var (
    83  		localName   = job.Args[0]
    84  		tag         string
    85  		sf          = utils.NewStreamFormatter(job.GetenvBool("json"))
    86  		authConfig  = &registry.AuthConfig{}
    87  		metaHeaders map[string][]string
    88  	)
    89  
    90  	// Resolve the Repository name from fqn to RepositoryInfo
    91  	repoInfo, err := registry.ResolveRepositoryInfo(job, localName)
    92  	if err != nil {
    93  		return job.Error(err)
    94  	}
    95  
    96  	if len(job.Args) > 1 {
    97  		tag = job.Args[1]
    98  	}
    99  
   100  	job.GetenvJson("authConfig", authConfig)
   101  	job.GetenvJson("metaHeaders", &metaHeaders)
   102  
   103  	c, err := s.poolAdd("pull", repoInfo.LocalName+":"+tag)
   104  	if err != nil {
   105  		if c != nil {
   106  			// Another pull of the same repository is already taking place; just wait for it to finish
   107  			job.Stdout.Write(sf.FormatStatus("", "Repository %s already being pulled by another client. Waiting.", repoInfo.LocalName))
   108  			<-c
   109  			return engine.StatusOK
   110  		}
   111  		return job.Error(err)
   112  	}
   113  	defer s.poolRemove("pull", repoInfo.LocalName+":"+tag)
   114  
   115  	endpoint, err := repoInfo.GetEndpoint()
   116  	if err != nil {
   117  		return job.Error(err)
   118  	}
   119  
   120  	r, err := registry.NewSession(authConfig, registry.HTTPRequestFactory(metaHeaders), endpoint, true)
   121  	if err != nil {
   122  		return job.Error(err)
   123  	}
   124  
   125  	logName := repoInfo.LocalName
   126  	if tag != "" {
   127  		logName += ":" + tag
   128  	}
   129  
   130  	if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Official || endpoint.Version == registry.APIVersion2) {
   131  		j := job.Eng.Job("trust_update_base")
   132  		if err = j.Run(); err != nil {
   133  			return job.Errorf("error updating trust base graph: %s", err)
   134  		}
   135  
   136  		if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err == nil {
   137  			if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil {
   138  				log.Errorf("Error logging event 'pull' for %s: %s", logName, err)
   139  			}
   140  			return engine.StatusOK
   141  		} else if err != registry.ErrDoesNotExist {
   142  			log.Errorf("Error from V2 registry: %s", err)
   143  		}
   144  	}
   145  
   146  	if err = s.pullRepository(r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err != nil {
   147  		return job.Error(err)
   148  	}
   149  
   150  	if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil {
   151  		log.Errorf("Error logging event 'pull' for %s: %s", logName, err)
   152  	}
   153  
   154  	return engine.StatusOK
   155  }
   156  
   157  func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, askedTag string, sf *utils.StreamFormatter, parallel bool) error {
   158  	out.Write(sf.FormatStatus("", "Pulling repository %s", repoInfo.CanonicalName))
   159  
   160  	repoData, err := r.GetRepositoryData(repoInfo.RemoteName)
   161  	if err != nil {
   162  		if strings.Contains(err.Error(), "HTTP code: 404") {
   163  			return fmt.Errorf("Error: image %s:%s not found", repoInfo.RemoteName, askedTag)
   164  		}
   165  		// Unexpected HTTP error
   166  		return err
   167  	}
   168  
   169  	log.Debugf("Retrieving the tag list")
   170  	tagsList, err := r.GetRemoteTags(repoData.Endpoints, repoInfo.RemoteName, repoData.Tokens)
   171  	if err != nil {
   172  		log.Errorf("%v", err)
   173  		return err
   174  	}
   175  
   176  	for tag, id := range tagsList {
   177  		repoData.ImgList[id] = &registry.ImgData{
   178  			ID:       id,
   179  			Tag:      tag,
   180  			Checksum: "",
   181  		}
   182  	}
   183  
   184  	log.Debugf("Registering tags")
   185  	// If no tag has been specified, pull them all
   186  	var imageId string
   187  	if askedTag == "" {
   188  		for tag, id := range tagsList {
   189  			repoData.ImgList[id].Tag = tag
   190  		}
   191  	} else {
   192  		// Otherwise, check that the tag exists and use only that one
   193  		id, exists := tagsList[askedTag]
   194  		if !exists {
   195  			return fmt.Errorf("Tag %s not found in repository %s", askedTag, repoInfo.CanonicalName)
   196  		}
   197  		imageId = id
   198  		repoData.ImgList[id].Tag = askedTag
   199  	}
   200  
   201  	errors := make(chan error)
   202  
   203  	layers_downloaded := false
   204  	for _, image := range repoData.ImgList {
   205  		downloadImage := func(img *registry.ImgData) {
   206  			if askedTag != "" && img.Tag != askedTag {
   207  				log.Debugf("(%s) does not match %s (id: %s), skipping", img.Tag, askedTag, img.ID)
   208  				if parallel {
   209  					errors <- nil
   210  				}
   211  				return
   212  			}
   213  
   214  			if img.Tag == "" {
   215  				log.Debugf("Image (id: %s) present in this repository but untagged, skipping", img.ID)
   216  				if parallel {
   217  					errors <- nil
   218  				}
   219  				return
   220  			}
   221  
   222  			// ensure no two downloads of the same image happen at the same time
   223  			if c, err := s.poolAdd("pull", "img:"+img.ID); err != nil {
   224  				if c != nil {
   225  					out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Layer already being pulled by another client. Waiting.", nil))
   226  					<-c
   227  					out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil))
   228  				} else {
   229  					log.Debugf("Image (id: %s) pull is already running, skipping: %v", img.ID, err)
   230  				}
   231  				if parallel {
   232  					errors <- nil
   233  				}
   234  				return
   235  			}
   236  			defer s.poolRemove("pull", "img:"+img.ID)
   237  
   238  			out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, repoInfo.CanonicalName), nil))
   239  			success := false
   240  			var lastErr, err error
   241  			var is_downloaded bool
   242  			for _, ep := range repoInfo.Index.Mirrors {
   243  				out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, repoInfo.CanonicalName, ep), nil))
   244  				if is_downloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil {
   245  					// Don't report errors when pulling from mirrors.
   246  					log.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, repoInfo.CanonicalName, ep, err)
   247  					continue
   248  				}
   249  				layers_downloaded = layers_downloaded || is_downloaded
   250  				success = true
   251  				break
   252  			}
   253  			if !success {
   254  				for _, ep := range repoData.Endpoints {
   255  					out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, repoInfo.CanonicalName, ep), nil))
   256  					if is_downloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil {
   257  						// It's not ideal that only the last error is returned, it would be better to concatenate the errors.
   258  						// As the error is also given to the output stream the user will see the error.
   259  						lastErr = err
   260  						out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, repoInfo.CanonicalName, ep, err), nil))
   261  						continue
   262  					}
   263  					layers_downloaded = layers_downloaded || is_downloaded
   264  					success = true
   265  					break
   266  				}
   267  			}
   268  			if !success {
   269  				err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, repoInfo.CanonicalName, lastErr)
   270  				out.Write(sf.FormatProgress(utils.TruncateID(img.ID), err.Error(), nil))
   271  				if parallel {
   272  					errors <- err
   273  					return
   274  				}
   275  			}
   276  			out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil))
   277  
   278  			if parallel {
   279  				errors <- nil
   280  			}
   281  		}
   282  
   283  		if parallel {
   284  			go downloadImage(image)
   285  		} else {
   286  			downloadImage(image)
   287  		}
   288  	}
   289  	if parallel {
   290  		var lastError error
   291  		for i := 0; i < len(repoData.ImgList); i++ {
   292  			if err := <-errors; err != nil {
   293  				lastError = err
   294  			}
   295  		}
   296  		if lastError != nil {
   297  			return lastError
   298  		}
   299  
   300  	}
   301  	for tag, id := range tagsList {
   302  		if askedTag != "" && id != imageId {
   303  			continue
   304  		}
   305  		if err := s.Set(repoInfo.LocalName, tag, id, true); err != nil {
   306  			return err
   307  		}
   308  	}
   309  
   310  	requestedTag := repoInfo.CanonicalName
   311  	if len(askedTag) > 0 {
   312  		requestedTag = repoInfo.CanonicalName + ":" + askedTag
   313  	}
   314  	WriteStatus(requestedTag, out, sf, layers_downloaded)
   315  	return nil
   316  }
   317  
   318  func (s *TagStore) pullImage(r *registry.Session, out io.Writer, imgID, endpoint string, token []string, sf *utils.StreamFormatter) (bool, error) {
   319  	history, err := r.GetRemoteHistory(imgID, endpoint, token)
   320  	if err != nil {
   321  		return false, err
   322  	}
   323  	out.Write(sf.FormatProgress(utils.TruncateID(imgID), "Pulling dependent layers", nil))
   324  	// FIXME: Try to stream the images?
   325  	// FIXME: Launch the getRemoteImage() in goroutines
   326  
   327  	layers_downloaded := false
   328  	for i := len(history) - 1; i >= 0; i-- {
   329  		id := history[i]
   330  
   331  		// ensure no two downloads of the same layer happen at the same time
   332  		if c, err := s.poolAdd("pull", "layer:"+id); err != nil {
   333  			log.Debugf("Image (id: %s) pull is already running, skipping: %v", id, err)
   334  			<-c
   335  		}
   336  		defer s.poolRemove("pull", "layer:"+id)
   337  
   338  		if !s.graph.Exists(id) {
   339  			out.Write(sf.FormatProgress(utils.TruncateID(id), "Pulling metadata", nil))
   340  			var (
   341  				imgJSON []byte
   342  				imgSize int
   343  				err     error
   344  				img     *image.Image
   345  			)
   346  			retries := 5
   347  			for j := 1; j <= retries; j++ {
   348  				imgJSON, imgSize, err = r.GetRemoteImageJSON(id, endpoint, token)
   349  				if err != nil && j == retries {
   350  					out.Write(sf.FormatProgress(utils.TruncateID(id), "Error pulling dependent layers", nil))
   351  					return layers_downloaded, err
   352  				} else if err != nil {
   353  					time.Sleep(time.Duration(j) * 500 * time.Millisecond)
   354  					continue
   355  				}
   356  				img, err = image.NewImgJSON(imgJSON)
   357  				layers_downloaded = true
   358  				if err != nil && j == retries {
   359  					out.Write(sf.FormatProgress(utils.TruncateID(id), "Error pulling dependent layers", nil))
   360  					return layers_downloaded, fmt.Errorf("Failed to parse json: %s", err)
   361  				} else if err != nil {
   362  					time.Sleep(time.Duration(j) * 500 * time.Millisecond)
   363  					continue
   364  				} else {
   365  					break
   366  				}
   367  			}
   368  
   369  			for j := 1; j <= retries; j++ {
   370  				// Get the layer
   371  				status := "Pulling fs layer"
   372  				if j > 1 {
   373  					status = fmt.Sprintf("Pulling fs layer [retries: %d]", j)
   374  				}
   375  				out.Write(sf.FormatProgress(utils.TruncateID(id), status, nil))
   376  				layer, err := r.GetRemoteImageLayer(img.ID, endpoint, token, int64(imgSize))
   377  				if uerr, ok := err.(*url.Error); ok {
   378  					err = uerr.Err
   379  				}
   380  				if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
   381  					time.Sleep(time.Duration(j) * 500 * time.Millisecond)
   382  					continue
   383  				} else if err != nil {
   384  					out.Write(sf.FormatProgress(utils.TruncateID(id), "Error pulling dependent layers", nil))
   385  					return layers_downloaded, err
   386  				}
   387  				layers_downloaded = true
   388  				defer layer.Close()
   389  
   390  				err = s.graph.Register(img,
   391  					utils.ProgressReader(layer, imgSize, out, sf, false, utils.TruncateID(id), "Downloading"))
   392  				if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
   393  					time.Sleep(time.Duration(j) * 500 * time.Millisecond)
   394  					continue
   395  				} else if err != nil {
   396  					out.Write(sf.FormatProgress(utils.TruncateID(id), "Error downloading dependent layers", nil))
   397  					return layers_downloaded, err
   398  				} else {
   399  					break
   400  				}
   401  			}
   402  		}
   403  		out.Write(sf.FormatProgress(utils.TruncateID(id), "Download complete", nil))
   404  	}
   405  	return layers_downloaded, nil
   406  }
   407  
   408  func WriteStatus(requestedTag string, out io.Writer, sf *utils.StreamFormatter, layers_downloaded bool) {
   409  	if layers_downloaded {
   410  		out.Write(sf.FormatStatus("", "Status: Downloaded newer image for %s", requestedTag))
   411  	} else {
   412  		out.Write(sf.FormatStatus("", "Status: Image is up to date for %s", requestedTag))
   413  	}
   414  }
   415  
   416  // downloadInfo is used to pass information from download to extractor
   417  type downloadInfo struct {
   418  	imgJSON    []byte
   419  	img        *image.Image
   420  	tmpFile    *os.File
   421  	length     int64
   422  	downloaded bool
   423  	err        chan error
   424  }
   425  
   426  func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) error {
   427  	var layersDownloaded bool
   428  	if tag == "" {
   429  		log.Debugf("Pulling tag list from V2 registry for %s", repoInfo.CanonicalName)
   430  		tags, err := r.GetV2RemoteTags(repoInfo.RemoteName, nil)
   431  		if err != nil {
   432  			return err
   433  		}
   434  		for _, t := range tags {
   435  			if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, t, sf, parallel); err != nil {
   436  				return err
   437  			} else if downloaded {
   438  				layersDownloaded = true
   439  			}
   440  		}
   441  	} else {
   442  		if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, tag, sf, parallel); err != nil {
   443  			return err
   444  		} else if downloaded {
   445  			layersDownloaded = true
   446  		}
   447  	}
   448  
   449  	requestedTag := repoInfo.CanonicalName
   450  	if len(tag) > 0 {
   451  		requestedTag = repoInfo.CanonicalName + ":" + tag
   452  	}
   453  	WriteStatus(requestedTag, out, sf, layersDownloaded)
   454  	return nil
   455  }
   456  
   457  func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) (bool, error) {
   458  	log.Debugf("Pulling tag from V2 registry: %q", tag)
   459  	manifestBytes, err := r.GetV2ImageManifest(repoInfo.RemoteName, tag, nil)
   460  	if err != nil {
   461  		return false, err
   462  	}
   463  
   464  	manifest, verified, err := s.verifyManifest(eng, manifestBytes)
   465  	if err != nil {
   466  		return false, fmt.Errorf("error verifying manifest: %s", err)
   467  	}
   468  
   469  	if len(manifest.FSLayers) != len(manifest.History) {
   470  		return false, fmt.Errorf("length of history not equal to number of layers")
   471  	}
   472  
   473  	if verified {
   474  		out.Write(sf.FormatStatus(repoInfo.CanonicalName+":"+tag, "The image you are pulling has been verified"))
   475  	} else {
   476  		out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName))
   477  	}
   478  
   479  	if len(manifest.FSLayers) == 0 {
   480  		return false, fmt.Errorf("no blobSums in manifest")
   481  	}
   482  
   483  	downloads := make([]downloadInfo, len(manifest.FSLayers))
   484  
   485  	for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
   486  		var (
   487  			sumStr  = manifest.FSLayers[i].BlobSum
   488  			imgJSON = []byte(manifest.History[i].V1Compatibility)
   489  		)
   490  
   491  		img, err := image.NewImgJSON(imgJSON)
   492  		if err != nil {
   493  			return false, fmt.Errorf("failed to parse json: %s", err)
   494  		}
   495  		downloads[i].img = img
   496  
   497  		// Check if exists
   498  		if s.graph.Exists(img.ID) {
   499  			log.Debugf("Image already exists: %s", img.ID)
   500  			continue
   501  		}
   502  
   503  		chunks := strings.SplitN(sumStr, ":", 2)
   504  		if len(chunks) < 2 {
   505  			return false, fmt.Errorf("expected 2 parts in the sumStr, got %#v", chunks)
   506  		}
   507  		sumType, checksum := chunks[0], chunks[1]
   508  		out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Pulling fs layer", nil))
   509  
   510  		downloadFunc := func(di *downloadInfo) error {
   511  			log.Debugf("pulling blob %q to V1 img %s", sumStr, img.ID)
   512  
   513  			if c, err := s.poolAdd("pull", "img:"+img.ID); err != nil {
   514  				if c != nil {
   515  					out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Layer already being pulled by another client. Waiting.", nil))
   516  					<-c
   517  					out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil))
   518  				} else {
   519  					log.Debugf("Image (id: %s) pull is already running, skipping: %v", img.ID, err)
   520  				}
   521  			} else {
   522  				defer s.poolRemove("pull", "img:"+img.ID)
   523  				tmpFile, err := ioutil.TempFile("", "GetV2ImageBlob")
   524  				if err != nil {
   525  					return err
   526  				}
   527  
   528  				r, l, err := r.GetV2ImageBlobReader(repoInfo.RemoteName, sumType, checksum, nil)
   529  				if err != nil {
   530  					return err
   531  				}
   532  				defer r.Close()
   533  				io.Copy(tmpFile, utils.ProgressReader(r, int(l), out, sf, false, utils.TruncateID(img.ID), "Downloading"))
   534  
   535  				out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil))
   536  
   537  				log.Debugf("Downloaded %s to tempfile %s", img.ID, tmpFile.Name())
   538  				di.tmpFile = tmpFile
   539  				di.length = l
   540  				di.downloaded = true
   541  			}
   542  			di.imgJSON = imgJSON
   543  
   544  			return nil
   545  		}
   546  
   547  		if parallel {
   548  			downloads[i].err = make(chan error)
   549  			go func(di *downloadInfo) {
   550  				di.err <- downloadFunc(di)
   551  			}(&downloads[i])
   552  		} else {
   553  			err := downloadFunc(&downloads[i])
   554  			if err != nil {
   555  				return false, err
   556  			}
   557  		}
   558  	}
   559  
   560  	var layersDownloaded bool
   561  	for i := len(downloads) - 1; i >= 0; i-- {
   562  		d := &downloads[i]
   563  		if d.err != nil {
   564  			err := <-d.err
   565  			if err != nil {
   566  				return false, err
   567  			}
   568  		}
   569  		if d.downloaded {
   570  			// if tmpFile is empty assume download and extracted elsewhere
   571  			defer os.Remove(d.tmpFile.Name())
   572  			defer d.tmpFile.Close()
   573  			d.tmpFile.Seek(0, 0)
   574  			if d.tmpFile != nil {
   575  				err = s.graph.Register(d.img,
   576  					utils.ProgressReader(d.tmpFile, int(d.length), out, sf, false, utils.TruncateID(d.img.ID), "Extracting"))
   577  				if err != nil {
   578  					return false, err
   579  				}
   580  
   581  				// FIXME: Pool release here for parallel tag pull (ensures any downloads block until fully extracted)
   582  			}
   583  			out.Write(sf.FormatProgress(utils.TruncateID(d.img.ID), "Pull complete", nil))
   584  			layersDownloaded = true
   585  		} else {
   586  			out.Write(sf.FormatProgress(utils.TruncateID(d.img.ID), "Already exists", nil))
   587  		}
   588  
   589  	}
   590  
   591  	if err = s.Set(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil {
   592  		return false, err
   593  	}
   594  
   595  	return layersDownloaded, nil
   596  }