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 }