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