package resolver import ( "context" "fmt" "strings" "sync" "sync/atomic" "time" "github.com/containerd/containerd/images" "github.com/containerd/containerd/remotes" "github.com/containerd/containerd/remotes/docker" distreference "github.com/docker/distribution/reference" "github.com/moby/buildkit/session" "github.com/moby/buildkit/source" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // DefaultPool is the default shared resolver pool instance var DefaultPool = NewPool() // Pool is a cache of recently used resolvers type Pool struct { mu sync.Mutex m map[string]*authHandlerNS } // NewPool creates a new pool for caching resolvers func NewPool() *Pool { p := &Pool{ m: map[string]*authHandlerNS{}, } time.AfterFunc(5*time.Minute, p.gc) return p } func (p *Pool) gc() { p.mu.Lock() defer p.mu.Unlock() for k, ns := range p.m { ns.mu.Lock() for key, h := range ns.handlers { if time.Since(h.lastUsed) < 10*time.Minute { continue } parts := strings.SplitN(key, "/", 2) if len(parts) != 2 { delete(ns.handlers, key) continue } c, err := ns.sm.Get(context.TODO(), parts[1], true) if c == nil || err != nil { delete(ns.handlers, key) } } if len(ns.handlers) == 0 { delete(p.m, k) } ns.mu.Unlock() } time.AfterFunc(5*time.Minute, p.gc) } // Clear deletes currently cached items. This may be called on config changes for example. func (p *Pool) Clear() { p.mu.Lock() defer p.mu.Unlock() p.m = map[string]*authHandlerNS{} } // GetResolver gets a resolver for a specified scope from the pool func (p *Pool) GetResolver(hosts docker.RegistryHosts, ref, scope string, sm *session.Manager, g session.Group) *Resolver { name := ref named, err := distreference.ParseNormalizedNamed(ref) if err == nil { name = named.Name() } key := fmt.Sprintf("%s::%s", name, scope) p.mu.Lock() defer p.mu.Unlock() h, ok := p.m[key] if !ok { h = newAuthHandlerNS(sm) p.m[key] = h } return newResolver(hosts, h, sm, g) } func newResolver(hosts docker.RegistryHosts, handler *authHandlerNS, sm *session.Manager, g session.Group) *Resolver { if hosts == nil { hosts = docker.ConfigureDefaultRegistries( docker.WithClient(newDefaultClient()), docker.WithPlainHTTP(docker.MatchLocalhost), ) } r := &Resolver{ hosts: hosts, sm: sm, g: g, handler: handler, } r.Resolver = docker.NewResolver(docker.ResolverOptions{ Hosts: r.hostsFunc, }) return r } // Resolver is a wrapper around remotes.Resolver type Resolver struct { remotes.Resolver hosts docker.RegistryHosts sm *session.Manager g session.Group handler *authHandlerNS auth *dockerAuthorizer is images.Store mode source.ResolveMode } func (r *Resolver) hostsFunc(host string) ([]docker.RegistryHost, error) { return func(domain string) ([]docker.RegistryHost, error) { v, err := r.handler.g.Do(context.TODO(), domain, func(ctx context.Context) (interface{}, error) { // long lock not needed because flightcontrol.Do r.handler.mu.Lock() v, ok := r.handler.hosts[domain] r.handler.mu.Unlock() if ok { return v, nil } res, err := r.hosts(domain) if err != nil { return nil, err } r.handler.mu.Lock() r.handler.hosts[domain] = res r.handler.mu.Unlock() return res, nil }) if err != nil || v == nil { return nil, err } res := v.([]docker.RegistryHost) if len(res) == 0 { return nil, nil } auth := newDockerAuthorizer(res[0].Client, r.handler, r.sm, r.g) for i := range res { res[i].Authorizer = auth } return res, nil }(host) } // WithSession returns a new resolver that works with new session group func (r *Resolver) WithSession(s session.Group) *Resolver { r2 := *r r2.auth = nil r2.g = s return &r2 } // WithImageStore returns new resolver that can also resolve from local images store func (r *Resolver) WithImageStore(is images.Store, mode source.ResolveMode) *Resolver { r2 := *r r2.Resolver = r.Resolver r2.is = is r2.mode = mode return &r2 } // Fetcher returns a new fetcher for the provided reference. func (r *Resolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { if atomic.LoadInt64(&r.handler.counter) == 0 { r.Resolve(ctx, ref) } return r.Resolver.Fetcher(ctx, ref) } // Resolve attempts to resolve the reference into a name and descriptor. func (r *Resolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, error) { if r.mode == source.ResolveModePreferLocal && r.is != nil { if img, err := r.is.Get(ctx, ref); err == nil { return ref, img.Target, nil } } n, desc, err := r.Resolver.Resolve(ctx, ref) if err == nil { atomic.AddInt64(&r.handler.counter, 1) return n, desc, err } if r.mode == source.ResolveModeDefault && r.is != nil { if img, err := r.is.Get(ctx, ref); err == nil { return ref, img.Target, nil } } return "", ocispec.Descriptor{}, err }