github.com/jwhonce/docker@v0.6.7-0.20190327063223-da823cf3a5a3/builder/builder-next/adapters/containerimage/pull.go (about)

     1  package containerimage
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"runtime"
    10  	"sync"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"github.com/containerd/containerd/content"
    15  	containerderrors "github.com/containerd/containerd/errdefs"
    16  	"github.com/containerd/containerd/images"
    17  	"github.com/containerd/containerd/platforms"
    18  	ctdreference "github.com/containerd/containerd/reference"
    19  	"github.com/containerd/containerd/remotes"
    20  	"github.com/containerd/containerd/remotes/docker"
    21  	"github.com/containerd/containerd/remotes/docker/schema1"
    22  	distreference "github.com/docker/distribution/reference"
    23  	"github.com/docker/docker/distribution"
    24  	"github.com/docker/docker/distribution/metadata"
    25  	"github.com/docker/docker/distribution/xfer"
    26  	"github.com/docker/docker/image"
    27  	"github.com/docker/docker/layer"
    28  	pkgprogress "github.com/docker/docker/pkg/progress"
    29  	"github.com/docker/docker/reference"
    30  	"github.com/moby/buildkit/cache"
    31  	gw "github.com/moby/buildkit/frontend/gateway/client"
    32  	"github.com/moby/buildkit/session"
    33  	"github.com/moby/buildkit/session/auth"
    34  	"github.com/moby/buildkit/source"
    35  	"github.com/moby/buildkit/util/flightcontrol"
    36  	"github.com/moby/buildkit/util/imageutil"
    37  	"github.com/moby/buildkit/util/progress"
    38  	"github.com/moby/buildkit/util/resolver"
    39  	"github.com/moby/buildkit/util/tracing"
    40  	"github.com/opencontainers/go-digest"
    41  	"github.com/opencontainers/image-spec/identity"
    42  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    43  	"github.com/pkg/errors"
    44  	"golang.org/x/time/rate"
    45  )
    46  
    47  // SourceOpt is options for creating the image source
    48  type SourceOpt struct {
    49  	ContentStore    content.Store
    50  	CacheAccessor   cache.Accessor
    51  	ReferenceStore  reference.Store
    52  	DownloadManager distribution.RootFSDownloadManager
    53  	MetadataStore   metadata.V2MetadataService
    54  	ImageStore      image.Store
    55  	ResolverOpt     resolver.ResolveOptionsFunc
    56  }
    57  
    58  type imageSource struct {
    59  	SourceOpt
    60  	g             flightcontrol.Group
    61  	resolverCache *resolverCache
    62  }
    63  
    64  // NewSource creates a new image source
    65  func NewSource(opt SourceOpt) (source.Source, error) {
    66  	is := &imageSource{
    67  		SourceOpt:     opt,
    68  		resolverCache: newResolverCache(),
    69  	}
    70  
    71  	return is, nil
    72  }
    73  
    74  func (is *imageSource) ID() string {
    75  	return source.DockerImageScheme
    76  }
    77  
    78  func (is *imageSource) getResolver(ctx context.Context, rfn resolver.ResolveOptionsFunc, ref string, sm *session.Manager) remotes.Resolver {
    79  	if res := is.resolverCache.Get(ctx, ref); res != nil {
    80  		return res
    81  	}
    82  
    83  	opt := docker.ResolverOptions{
    84  		Client: tracing.DefaultClient,
    85  	}
    86  	if rfn != nil {
    87  		opt = rfn(ref)
    88  	}
    89  	opt.Credentials = is.getCredentialsFromSession(ctx, sm)
    90  	r := docker.NewResolver(opt)
    91  	r = is.resolverCache.Add(ctx, ref, r)
    92  	return r
    93  }
    94  
    95  func (is *imageSource) getCredentialsFromSession(ctx context.Context, sm *session.Manager) func(string) (string, string, error) {
    96  	id := session.FromContext(ctx)
    97  	if id == "" {
    98  		// can be removed after containerd/containerd#2812
    99  		return func(string) (string, string, error) {
   100  			return "", "", nil
   101  		}
   102  	}
   103  	return func(host string) (string, string, error) {
   104  		timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   105  		defer cancel()
   106  
   107  		caller, err := sm.Get(timeoutCtx, id)
   108  		if err != nil {
   109  			return "", "", err
   110  		}
   111  
   112  		return auth.CredentialsFunc(tracing.ContextWithSpanFromContext(context.TODO(), ctx), caller)(host)
   113  	}
   114  }
   115  
   116  func (is *imageSource) resolveLocal(refStr string) ([]byte, error) {
   117  	ref, err := distreference.ParseNormalizedNamed(refStr)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	dgst, err := is.ReferenceStore.Get(ref)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	img, err := is.ImageStore.Get(image.ID(dgst))
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	return img.RawJSON(), nil
   130  }
   131  
   132  func (is *imageSource) resolveRemote(ctx context.Context, ref string, platform *ocispec.Platform, sm *session.Manager) (digest.Digest, []byte, error) {
   133  	type t struct {
   134  		dgst digest.Digest
   135  		dt   []byte
   136  	}
   137  	res, err := is.g.Do(ctx, ref, func(ctx context.Context) (interface{}, error) {
   138  		dgst, dt, err := imageutil.Config(ctx, ref, is.getResolver(ctx, is.ResolverOpt, ref, sm), is.ContentStore, platform)
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  		return &t{dgst: dgst, dt: dt}, nil
   143  	})
   144  	var typed *t
   145  	if err != nil {
   146  		return "", nil, err
   147  	}
   148  	typed = res.(*t)
   149  	return typed.dgst, typed.dt, nil
   150  }
   151  
   152  func (is *imageSource) ResolveImageConfig(ctx context.Context, ref string, opt gw.ResolveImageConfigOpt, sm *session.Manager) (digest.Digest, []byte, error) {
   153  	resolveMode, err := source.ParseImageResolveMode(opt.ResolveMode)
   154  	if err != nil {
   155  		return "", nil, err
   156  	}
   157  	switch resolveMode {
   158  	case source.ResolveModeForcePull:
   159  		dgst, dt, err := is.resolveRemote(ctx, ref, opt.Platform, sm)
   160  		// TODO: pull should fallback to local in case of failure to allow offline behavior
   161  		// the fallback doesn't work currently
   162  		return dgst, dt, err
   163  		/*
   164  			if err == nil {
   165  				return dgst, dt, err
   166  			}
   167  			// fallback to local
   168  			dt, err = is.resolveLocal(ref)
   169  			return "", dt, err
   170  		*/
   171  
   172  	case source.ResolveModeDefault:
   173  		// default == prefer local, but in the future could be smarter
   174  		fallthrough
   175  	case source.ResolveModePreferLocal:
   176  		dt, err := is.resolveLocal(ref)
   177  		if err == nil {
   178  			return "", dt, err
   179  		}
   180  		// fallback to remote
   181  		return is.resolveRemote(ctx, ref, opt.Platform, sm)
   182  	}
   183  	// should never happen
   184  	return "", nil, fmt.Errorf("builder cannot resolve image %s: invalid mode %q", ref, opt.ResolveMode)
   185  }
   186  
   187  func (is *imageSource) Resolve(ctx context.Context, id source.Identifier, sm *session.Manager) (source.SourceInstance, error) {
   188  	imageIdentifier, ok := id.(*source.ImageIdentifier)
   189  	if !ok {
   190  		return nil, errors.Errorf("invalid image identifier %v", id)
   191  	}
   192  
   193  	platform := platforms.DefaultSpec()
   194  	if imageIdentifier.Platform != nil {
   195  		platform = *imageIdentifier.Platform
   196  	}
   197  
   198  	p := &puller{
   199  		src:      imageIdentifier,
   200  		is:       is,
   201  		resolver: is.getResolver(ctx, is.ResolverOpt, imageIdentifier.Reference.String(), sm),
   202  		platform: platform,
   203  		sm:       sm,
   204  	}
   205  	return p, nil
   206  }
   207  
   208  type puller struct {
   209  	is               *imageSource
   210  	resolveOnce      sync.Once
   211  	resolveLocalOnce sync.Once
   212  	src              *source.ImageIdentifier
   213  	desc             ocispec.Descriptor
   214  	ref              string
   215  	resolveErr       error
   216  	resolver         remotes.Resolver
   217  	config           []byte
   218  	platform         ocispec.Platform
   219  	sm               *session.Manager
   220  }
   221  
   222  func (p *puller) mainManifestKey(dgst digest.Digest, platform ocispec.Platform) (digest.Digest, error) {
   223  	dt, err := json.Marshal(struct {
   224  		Digest  digest.Digest
   225  		OS      string
   226  		Arch    string
   227  		Variant string `json:",omitempty"`
   228  	}{
   229  		Digest:  p.desc.Digest,
   230  		OS:      platform.OS,
   231  		Arch:    platform.Architecture,
   232  		Variant: platform.Variant,
   233  	})
   234  	if err != nil {
   235  		return "", err
   236  	}
   237  	return digest.FromBytes(dt), nil
   238  }
   239  
   240  func (p *puller) resolveLocal() {
   241  	p.resolveLocalOnce.Do(func() {
   242  		dgst := p.src.Reference.Digest()
   243  		if dgst != "" {
   244  			info, err := p.is.ContentStore.Info(context.TODO(), dgst)
   245  			if err == nil {
   246  				p.ref = p.src.Reference.String()
   247  				desc := ocispec.Descriptor{
   248  					Size:   info.Size,
   249  					Digest: dgst,
   250  				}
   251  				ra, err := p.is.ContentStore.ReaderAt(context.TODO(), desc)
   252  				if err == nil {
   253  					mt, err := imageutil.DetectManifestMediaType(ra)
   254  					if err == nil {
   255  						desc.MediaType = mt
   256  						p.desc = desc
   257  					}
   258  				}
   259  			}
   260  		}
   261  
   262  		if p.src.ResolveMode == source.ResolveModeDefault || p.src.ResolveMode == source.ResolveModePreferLocal {
   263  			dt, err := p.is.resolveLocal(p.src.Reference.String())
   264  			if err == nil {
   265  				p.config = dt
   266  			}
   267  		}
   268  	})
   269  }
   270  
   271  func (p *puller) resolve(ctx context.Context) error {
   272  	p.resolveOnce.Do(func() {
   273  		resolveProgressDone := oneOffProgress(ctx, "resolve "+p.src.Reference.String())
   274  
   275  		ref, err := distreference.ParseNormalizedNamed(p.src.Reference.String())
   276  		if err != nil {
   277  			p.resolveErr = err
   278  			resolveProgressDone(err)
   279  			return
   280  		}
   281  
   282  		if p.desc.Digest == "" && p.config == nil {
   283  			origRef, desc, err := p.resolver.Resolve(ctx, ref.String())
   284  			if err != nil {
   285  				p.resolveErr = err
   286  				resolveProgressDone(err)
   287  				return
   288  			}
   289  
   290  			p.desc = desc
   291  			p.ref = origRef
   292  		}
   293  
   294  		// Schema 1 manifests cannot be resolved to an image config
   295  		// since the conversion must take place after all the content
   296  		// has been read.
   297  		// It may be possible to have a mapping between schema 1 manifests
   298  		// and the schema 2 manifests they are converted to.
   299  		if p.config == nil && p.desc.MediaType != images.MediaTypeDockerSchema1Manifest {
   300  			ref, err := distreference.WithDigest(ref, p.desc.Digest)
   301  			if err != nil {
   302  				p.resolveErr = err
   303  				resolveProgressDone(err)
   304  				return
   305  			}
   306  			_, dt, err := p.is.ResolveImageConfig(ctx, ref.String(), gw.ResolveImageConfigOpt{Platform: &p.platform, ResolveMode: resolveModeToString(p.src.ResolveMode)}, p.sm)
   307  			if err != nil {
   308  				p.resolveErr = err
   309  				resolveProgressDone(err)
   310  				return
   311  			}
   312  
   313  			p.config = dt
   314  		}
   315  		resolveProgressDone(nil)
   316  	})
   317  	return p.resolveErr
   318  }
   319  
   320  func (p *puller) CacheKey(ctx context.Context, index int) (string, bool, error) {
   321  	p.resolveLocal()
   322  
   323  	if p.desc.Digest != "" && index == 0 {
   324  		dgst, err := p.mainManifestKey(p.desc.Digest, p.platform)
   325  		if err != nil {
   326  			return "", false, err
   327  		}
   328  		return dgst.String(), false, nil
   329  	}
   330  
   331  	if p.config != nil {
   332  		k := cacheKeyFromConfig(p.config).String()
   333  		if k == "" {
   334  			return digest.FromBytes(p.config).String(), true, nil
   335  		}
   336  		return k, true, nil
   337  	}
   338  
   339  	if err := p.resolve(ctx); err != nil {
   340  		return "", false, err
   341  	}
   342  
   343  	if p.desc.Digest != "" && index == 0 {
   344  		dgst, err := p.mainManifestKey(p.desc.Digest, p.platform)
   345  		if err != nil {
   346  			return "", false, err
   347  		}
   348  		return dgst.String(), false, nil
   349  	}
   350  
   351  	k := cacheKeyFromConfig(p.config).String()
   352  	if k == "" {
   353  		dgst, err := p.mainManifestKey(p.desc.Digest, p.platform)
   354  		if err != nil {
   355  			return "", false, err
   356  		}
   357  		return dgst.String(), true, nil
   358  	}
   359  
   360  	return k, true, nil
   361  }
   362  
   363  func (p *puller) Snapshot(ctx context.Context) (cache.ImmutableRef, error) {
   364  	p.resolveLocal()
   365  	if err := p.resolve(ctx); err != nil {
   366  		return nil, err
   367  	}
   368  
   369  	if p.config != nil {
   370  		img, err := p.is.ImageStore.Get(image.ID(digest.FromBytes(p.config)))
   371  		if err == nil {
   372  			if len(img.RootFS.DiffIDs) == 0 {
   373  				return nil, nil
   374  			}
   375  			ref, err := p.is.CacheAccessor.GetFromSnapshotter(ctx, string(img.RootFS.ChainID()), cache.WithDescription(fmt.Sprintf("from local %s", p.ref)))
   376  			if err != nil {
   377  				return nil, err
   378  			}
   379  			return ref, nil
   380  		}
   381  	}
   382  
   383  	ongoing := newJobs(p.ref)
   384  
   385  	pctx, stopProgress := context.WithCancel(ctx)
   386  
   387  	pw, _, ctx := progress.FromContext(ctx)
   388  	defer pw.Close()
   389  
   390  	progressDone := make(chan struct{})
   391  	go func() {
   392  		showProgress(pctx, ongoing, p.is.ContentStore, pw)
   393  		close(progressDone)
   394  	}()
   395  	defer func() {
   396  		<-progressDone
   397  	}()
   398  
   399  	fetcher, err := p.resolver.Fetcher(ctx, p.ref)
   400  	if err != nil {
   401  		stopProgress()
   402  		return nil, err
   403  	}
   404  
   405  	platform := platforms.Only(p.platform)
   406  	// workaround for GCR bug that requires a request to manifest endpoint for authentication to work.
   407  	// if current resolver has not used manifests do a dummy request.
   408  	// in most cases resolver should be cached and extra request is not needed.
   409  	ensureManifestRequested(ctx, p.resolver, p.ref)
   410  
   411  	var (
   412  		schema1Converter *schema1.Converter
   413  		handlers         []images.Handler
   414  	)
   415  	if p.desc.MediaType == images.MediaTypeDockerSchema1Manifest {
   416  		schema1Converter = schema1.NewConverter(p.is.ContentStore, fetcher)
   417  		handlers = append(handlers, schema1Converter)
   418  
   419  		// TODO: Optimize to do dispatch and integrate pulling with download manager,
   420  		// leverage existing blob mapping and layer storage
   421  	} else {
   422  
   423  		// TODO: need a wrapper snapshot interface that combines content
   424  		// and snapshots as 1) buildkit shouldn't have a dependency on contentstore
   425  		// or 2) cachemanager should manage the contentstore
   426  		handlers = append(handlers, images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   427  			switch desc.MediaType {
   428  			case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest,
   429  				images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex,
   430  				images.MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
   431  			default:
   432  				return nil, images.ErrSkipDesc
   433  			}
   434  			ongoing.add(desc)
   435  			return nil, nil
   436  		}))
   437  
   438  		// Get all the children for a descriptor
   439  		childrenHandler := images.ChildrenHandler(p.is.ContentStore)
   440  		// Set any children labels for that content
   441  		childrenHandler = images.SetChildrenLabels(p.is.ContentStore, childrenHandler)
   442  		// Filter the children by the platform
   443  		childrenHandler = images.FilterPlatforms(childrenHandler, platform)
   444  		// Limit manifests pulled to the best match in an index
   445  		childrenHandler = images.LimitManifests(childrenHandler, platform, 1)
   446  
   447  		handlers = append(handlers,
   448  			remotes.FetchHandler(p.is.ContentStore, fetcher),
   449  			childrenHandler,
   450  		)
   451  	}
   452  
   453  	if err := images.Dispatch(ctx, images.Handlers(handlers...), nil, p.desc); err != nil {
   454  		stopProgress()
   455  		return nil, err
   456  	}
   457  	defer stopProgress()
   458  
   459  	if schema1Converter != nil {
   460  		p.desc, err = schema1Converter.Convert(ctx)
   461  		if err != nil {
   462  			return nil, err
   463  		}
   464  	}
   465  
   466  	mfst, err := images.Manifest(ctx, p.is.ContentStore, p.desc, platform)
   467  	if err != nil {
   468  		return nil, err
   469  	}
   470  
   471  	config, err := images.Config(ctx, p.is.ContentStore, p.desc, platform)
   472  	if err != nil {
   473  		return nil, err
   474  	}
   475  
   476  	dt, err := content.ReadBlob(ctx, p.is.ContentStore, config)
   477  	if err != nil {
   478  		return nil, err
   479  	}
   480  
   481  	var img ocispec.Image
   482  	if err := json.Unmarshal(dt, &img); err != nil {
   483  		return nil, err
   484  	}
   485  
   486  	if len(mfst.Layers) != len(img.RootFS.DiffIDs) {
   487  		return nil, errors.Errorf("invalid config for manifest")
   488  	}
   489  
   490  	pchan := make(chan pkgprogress.Progress, 10)
   491  	defer close(pchan)
   492  
   493  	go func() {
   494  		m := map[string]struct {
   495  			st      time.Time
   496  			limiter *rate.Limiter
   497  		}{}
   498  		for p := range pchan {
   499  			if p.Action == "Extracting" {
   500  				st, ok := m[p.ID]
   501  				if !ok {
   502  					st.st = time.Now()
   503  					st.limiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 1)
   504  					m[p.ID] = st
   505  				}
   506  				var end *time.Time
   507  				if p.LastUpdate || st.limiter.Allow() {
   508  					if p.LastUpdate {
   509  						tm := time.Now()
   510  						end = &tm
   511  					}
   512  					pw.Write("extracting "+p.ID, progress.Status{
   513  						Action:    "extract",
   514  						Started:   &st.st,
   515  						Completed: end,
   516  					})
   517  				}
   518  			}
   519  		}
   520  	}()
   521  
   522  	if len(mfst.Layers) == 0 {
   523  		return nil, nil
   524  	}
   525  
   526  	layers := make([]xfer.DownloadDescriptor, 0, len(mfst.Layers))
   527  
   528  	for i, desc := range mfst.Layers {
   529  		ongoing.add(desc)
   530  		layers = append(layers, &layerDescriptor{
   531  			desc:    desc,
   532  			diffID:  layer.DiffID(img.RootFS.DiffIDs[i]),
   533  			fetcher: fetcher,
   534  			ref:     p.src.Reference,
   535  			is:      p.is,
   536  		})
   537  	}
   538  
   539  	defer func() {
   540  		<-progressDone
   541  		for _, desc := range mfst.Layers {
   542  			p.is.ContentStore.Delete(context.TODO(), desc.Digest)
   543  		}
   544  	}()
   545  
   546  	r := image.NewRootFS()
   547  	rootFS, release, err := p.is.DownloadManager.Download(ctx, *r, runtime.GOOS, layers, pkgprogress.ChanOutput(pchan))
   548  	if err != nil {
   549  		return nil, err
   550  	}
   551  	stopProgress()
   552  
   553  	ref, err := p.is.CacheAccessor.GetFromSnapshotter(ctx, string(rootFS.ChainID()), cache.WithDescription(fmt.Sprintf("pulled from %s", p.ref)))
   554  	release()
   555  	if err != nil {
   556  		return nil, err
   557  	}
   558  
   559  	// TODO: handle windows layers for cross platform builds
   560  
   561  	if p.src.RecordType != "" && cache.GetRecordType(ref) == "" {
   562  		if err := cache.SetRecordType(ref, p.src.RecordType); err != nil {
   563  			ref.Release(context.TODO())
   564  			return nil, err
   565  		}
   566  	}
   567  
   568  	return ref, nil
   569  }
   570  
   571  // Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error)
   572  type layerDescriptor struct {
   573  	is      *imageSource
   574  	fetcher remotes.Fetcher
   575  	desc    ocispec.Descriptor
   576  	diffID  layer.DiffID
   577  	ref     ctdreference.Spec
   578  }
   579  
   580  func (ld *layerDescriptor) Key() string {
   581  	return "v2:" + ld.desc.Digest.String()
   582  }
   583  
   584  func (ld *layerDescriptor) ID() string {
   585  	return ld.desc.Digest.String()
   586  }
   587  
   588  func (ld *layerDescriptor) DiffID() (layer.DiffID, error) {
   589  	return ld.diffID, nil
   590  }
   591  
   592  func (ld *layerDescriptor) Download(ctx context.Context, progressOutput pkgprogress.Output) (io.ReadCloser, int64, error) {
   593  	rc, err := ld.fetcher.Fetch(ctx, ld.desc)
   594  	if err != nil {
   595  		return nil, 0, err
   596  	}
   597  	defer rc.Close()
   598  
   599  	refKey := remotes.MakeRefKey(ctx, ld.desc)
   600  
   601  	ld.is.ContentStore.Abort(ctx, refKey)
   602  
   603  	if err := content.WriteBlob(ctx, ld.is.ContentStore, refKey, rc, ld.desc); err != nil {
   604  		ld.is.ContentStore.Abort(ctx, refKey)
   605  		return nil, 0, err
   606  	}
   607  
   608  	ra, err := ld.is.ContentStore.ReaderAt(ctx, ld.desc)
   609  	if err != nil {
   610  		return nil, 0, err
   611  	}
   612  
   613  	return ioutil.NopCloser(content.NewReader(ra)), ld.desc.Size, nil
   614  }
   615  
   616  func (ld *layerDescriptor) Close() {
   617  	// ld.is.ContentStore.Delete(context.TODO(), ld.desc.Digest))
   618  }
   619  
   620  func (ld *layerDescriptor) Registered(diffID layer.DiffID) {
   621  	// Cache mapping from this layer's DiffID to the blobsum
   622  	ld.is.MetadataStore.Add(diffID, metadata.V2Metadata{Digest: ld.desc.Digest, SourceRepository: ld.ref.Locator})
   623  }
   624  
   625  func showProgress(ctx context.Context, ongoing *jobs, cs content.Store, pw progress.Writer) {
   626  	var (
   627  		ticker   = time.NewTicker(100 * time.Millisecond)
   628  		statuses = map[string]statusInfo{}
   629  		done     bool
   630  	)
   631  	defer ticker.Stop()
   632  
   633  	for {
   634  		select {
   635  		case <-ticker.C:
   636  		case <-ctx.Done():
   637  			done = true
   638  		}
   639  
   640  		resolved := "resolved"
   641  		if !ongoing.isResolved() {
   642  			resolved = "resolving"
   643  		}
   644  		statuses[ongoing.name] = statusInfo{
   645  			Ref:    ongoing.name,
   646  			Status: resolved,
   647  		}
   648  
   649  		actives := make(map[string]statusInfo)
   650  
   651  		if !done {
   652  			active, err := cs.ListStatuses(ctx)
   653  			if err != nil {
   654  				// log.G(ctx).WithError(err).Error("active check failed")
   655  				continue
   656  			}
   657  			// update status of active entries!
   658  			for _, active := range active {
   659  				actives[active.Ref] = statusInfo{
   660  					Ref:       active.Ref,
   661  					Status:    "downloading",
   662  					Offset:    active.Offset,
   663  					Total:     active.Total,
   664  					StartedAt: active.StartedAt,
   665  					UpdatedAt: active.UpdatedAt,
   666  				}
   667  			}
   668  		}
   669  
   670  		// now, update the items in jobs that are not in active
   671  		for _, j := range ongoing.jobs() {
   672  			refKey := remotes.MakeRefKey(ctx, j.Descriptor)
   673  			if a, ok := actives[refKey]; ok {
   674  				started := j.started
   675  				pw.Write(j.Digest.String(), progress.Status{
   676  					Action:  a.Status,
   677  					Total:   int(a.Total),
   678  					Current: int(a.Offset),
   679  					Started: &started,
   680  				})
   681  				continue
   682  			}
   683  
   684  			if !j.done {
   685  				info, err := cs.Info(context.TODO(), j.Digest)
   686  				if err != nil {
   687  					if containerderrors.IsNotFound(err) {
   688  						// pw.Write(j.Digest.String(), progress.Status{
   689  						// 	Action: "waiting",
   690  						// })
   691  						continue
   692  					}
   693  				} else {
   694  					j.done = true
   695  				}
   696  
   697  				if done || j.done {
   698  					started := j.started
   699  					createdAt := info.CreatedAt
   700  					pw.Write(j.Digest.String(), progress.Status{
   701  						Action:    "done",
   702  						Current:   int(info.Size),
   703  						Total:     int(info.Size),
   704  						Completed: &createdAt,
   705  						Started:   &started,
   706  					})
   707  				}
   708  			}
   709  		}
   710  		if done {
   711  			return
   712  		}
   713  	}
   714  }
   715  
   716  // jobs provides a way of identifying the download keys for a particular task
   717  // encountering during the pull walk.
   718  //
   719  // This is very minimal and will probably be replaced with something more
   720  // featured.
   721  type jobs struct {
   722  	name     string
   723  	added    map[digest.Digest]*job
   724  	mu       sync.Mutex
   725  	resolved bool
   726  }
   727  
   728  type job struct {
   729  	ocispec.Descriptor
   730  	done    bool
   731  	started time.Time
   732  }
   733  
   734  func newJobs(name string) *jobs {
   735  	return &jobs{
   736  		name:  name,
   737  		added: make(map[digest.Digest]*job),
   738  	}
   739  }
   740  
   741  func (j *jobs) add(desc ocispec.Descriptor) {
   742  	j.mu.Lock()
   743  	defer j.mu.Unlock()
   744  
   745  	if _, ok := j.added[desc.Digest]; ok {
   746  		return
   747  	}
   748  	j.added[desc.Digest] = &job{
   749  		Descriptor: desc,
   750  		started:    time.Now(),
   751  	}
   752  }
   753  
   754  func (j *jobs) jobs() []*job {
   755  	j.mu.Lock()
   756  	defer j.mu.Unlock()
   757  
   758  	descs := make([]*job, 0, len(j.added))
   759  	for _, j := range j.added {
   760  		descs = append(descs, j)
   761  	}
   762  	return descs
   763  }
   764  
   765  func (j *jobs) isResolved() bool {
   766  	j.mu.Lock()
   767  	defer j.mu.Unlock()
   768  	return j.resolved
   769  }
   770  
   771  type statusInfo struct {
   772  	Ref       string
   773  	Status    string
   774  	Offset    int64
   775  	Total     int64
   776  	StartedAt time.Time
   777  	UpdatedAt time.Time
   778  }
   779  
   780  func oneOffProgress(ctx context.Context, id string) func(err error) error {
   781  	pw, _, _ := progress.FromContext(ctx)
   782  	now := time.Now()
   783  	st := progress.Status{
   784  		Started: &now,
   785  	}
   786  	pw.Write(id, st)
   787  	return func(err error) error {
   788  		// TODO: set error on status
   789  		now := time.Now()
   790  		st.Completed = &now
   791  		pw.Write(id, st)
   792  		pw.Close()
   793  		return err
   794  	}
   795  }
   796  
   797  // cacheKeyFromConfig returns a stable digest from image config. If image config
   798  // is a known oci image we will use chainID of layers.
   799  func cacheKeyFromConfig(dt []byte) digest.Digest {
   800  	var img ocispec.Image
   801  	err := json.Unmarshal(dt, &img)
   802  	if err != nil {
   803  		return digest.FromBytes(dt)
   804  	}
   805  	if img.RootFS.Type != "layers" || len(img.RootFS.DiffIDs) == 0 {
   806  		return ""
   807  	}
   808  	return identity.ChainID(img.RootFS.DiffIDs)
   809  }
   810  
   811  // resolveModeToString is the equivalent of github.com/moby/buildkit/solver/llb.ResolveMode.String()
   812  // FIXME: add String method on source.ResolveMode
   813  func resolveModeToString(rm source.ResolveMode) string {
   814  	switch rm {
   815  	case source.ResolveModeDefault:
   816  		return "default"
   817  	case source.ResolveModeForcePull:
   818  		return "pull"
   819  	case source.ResolveModePreferLocal:
   820  		return "local"
   821  	}
   822  	return ""
   823  }
   824  
   825  type resolverCache struct {
   826  	mu sync.Mutex
   827  	m  map[string]cachedResolver
   828  }
   829  
   830  type cachedResolver struct {
   831  	timeout time.Time
   832  	remotes.Resolver
   833  	counter int64
   834  }
   835  
   836  func (cr *cachedResolver) Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error) {
   837  	atomic.AddInt64(&cr.counter, 1)
   838  	return cr.Resolver.Resolve(ctx, ref)
   839  }
   840  
   841  func (r *resolverCache) Add(ctx context.Context, ref string, resolver remotes.Resolver) remotes.Resolver {
   842  	r.mu.Lock()
   843  	defer r.mu.Unlock()
   844  
   845  	ref = r.domain(ref) + "-" + session.FromContext(ctx)
   846  
   847  	cr, ok := r.m[ref]
   848  	cr.timeout = time.Now().Add(time.Minute)
   849  	if ok {
   850  		return &cr
   851  	}
   852  
   853  	cr.Resolver = resolver
   854  	r.m[ref] = cr
   855  	return &cr
   856  }
   857  
   858  func (r *resolverCache) domain(refStr string) string {
   859  	ref, err := distreference.ParseNormalizedNamed(refStr)
   860  	if err != nil {
   861  		return refStr
   862  	}
   863  	return distreference.Domain(ref)
   864  }
   865  
   866  func (r *resolverCache) Get(ctx context.Context, ref string) remotes.Resolver {
   867  	r.mu.Lock()
   868  	defer r.mu.Unlock()
   869  
   870  	ref = r.domain(ref) + "-" + session.FromContext(ctx)
   871  
   872  	cr, ok := r.m[ref]
   873  	if !ok {
   874  		return nil
   875  	}
   876  	return &cr
   877  }
   878  
   879  func (r *resolverCache) clean(now time.Time) {
   880  	r.mu.Lock()
   881  	for k, cr := range r.m {
   882  		if now.After(cr.timeout) {
   883  			delete(r.m, k)
   884  		}
   885  	}
   886  	r.mu.Unlock()
   887  }
   888  
   889  func newResolverCache() *resolverCache {
   890  	rc := &resolverCache{
   891  		m: map[string]cachedResolver{},
   892  	}
   893  	t := time.NewTicker(time.Minute)
   894  	go func() {
   895  		for {
   896  			rc.clean(<-t.C)
   897  		}
   898  	}()
   899  	return rc
   900  }
   901  
   902  func ensureManifestRequested(ctx context.Context, res remotes.Resolver, ref string) {
   903  	cr, ok := res.(*cachedResolver)
   904  	if !ok {
   905  		return
   906  	}
   907  	if atomic.LoadInt64(&cr.counter) == 0 {
   908  		res.Resolve(ctx, ref)
   909  	}
   910  }