blob: 8792111aaa5dc6c598adc7b8ce92f89c2b021b27 [file]
package containerimage
import (
"context"
"encoding/json"
"runtime"
"time"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/diff"
containerderrdefs "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/leases"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/reference"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/containerd/containerd/snapshots"
"github.com/moby/buildkit/cache"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/solver/errdefs"
"github.com/moby/buildkit/source"
srctypes "github.com/moby/buildkit/source/types"
"github.com/moby/buildkit/util/estargz"
"github.com/moby/buildkit/util/flightcontrol"
"github.com/moby/buildkit/util/imageutil"
"github.com/moby/buildkit/util/leaseutil"
"github.com/moby/buildkit/util/progress"
"github.com/moby/buildkit/util/progress/controller"
"github.com/moby/buildkit/util/pull"
"github.com/moby/buildkit/util/resolver"
digest "github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/identity"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// TODO: break apart containerd specifics like contentstore so the resolver
// code can be used with any implementation
type ResolverType int
const (
ResolverTypeRegistry ResolverType = iota
ResolverTypeOCILayout
)
type SourceOpt struct {
Snapshotter snapshot.Snapshotter
ContentStore content.Store
Applier diff.Applier
CacheAccessor cache.Accessor
ImageStore images.Store // optional
RegistryHosts docker.RegistryHosts
ResolverType
LeaseManager leases.Manager
}
type resolveImageResult struct {
ref string
dgst digest.Digest
dt []byte
}
type Source struct {
SourceOpt
g flightcontrol.Group[*resolveImageResult]
}
var _ source.Source = &Source{}
func NewSource(opt SourceOpt) (*Source, error) {
is := &Source{
SourceOpt: opt,
}
return is, nil
}
func (is *Source) ID() string {
if is.ResolverType == ResolverTypeOCILayout {
return srctypes.OCIScheme
}
return srctypes.DockerImageScheme
}
func (is *Source) ResolveImageConfig(ctx context.Context, ref string, opt llb.ResolveImageConfigOpt, sm *session.Manager, g session.Group) (string, digest.Digest, []byte, error) {
key := ref
if platform := opt.Platform; platform != nil {
key += platforms.Format(*platform)
}
var (
rm source.ResolveMode
rslvr remotes.Resolver
err error
)
switch is.ResolverType {
case ResolverTypeRegistry:
rm, err = source.ParseImageResolveMode(opt.ResolveMode)
if err != nil {
return "", "", nil, err
}
rslvr = resolver.DefaultPool.GetResolver(is.RegistryHosts, ref, "pull", sm, g).WithImageStore(is.ImageStore, rm)
case ResolverTypeOCILayout:
rm = source.ResolveModeForcePull
rslvr = getOCILayoutResolver(opt.Store, sm, g)
}
key += rm.String()
res, err := is.g.Do(ctx, key, func(ctx context.Context) (*resolveImageResult, error) {
newRef, dgst, dt, err := imageutil.Config(ctx, ref, rslvr, is.ContentStore, is.LeaseManager, opt.Platform, opt.SourcePolicies)
if err != nil {
return nil, err
}
return &resolveImageResult{dgst: dgst, dt: dt, ref: newRef}, nil
})
if err != nil {
return "", "", nil, err
}
return res.ref, res.dgst, res.dt, nil
}
func (is *Source) Resolve(ctx context.Context, id source.Identifier, sm *session.Manager, vtx solver.Vertex) (source.SourceInstance, error) {
var (
p *puller
platform = platforms.DefaultSpec()
pullerUtil *pull.Puller
mode source.ResolveMode
recordType client.UsageRecordType
ref reference.Spec
store llb.ResolveImageConfigOptStore
layerLimit *int
)
switch is.ResolverType {
case ResolverTypeRegistry:
imageIdentifier, ok := id.(*source.ImageIdentifier)
if !ok {
return nil, errors.Errorf("invalid image identifier %v", id)
}
if imageIdentifier.Platform != nil {
platform = *imageIdentifier.Platform
}
mode = imageIdentifier.ResolveMode
recordType = imageIdentifier.RecordType
ref = imageIdentifier.Reference
layerLimit = imageIdentifier.LayerLimit
case ResolverTypeOCILayout:
ociIdentifier, ok := id.(*source.OCIIdentifier)
if !ok {
return nil, errors.Errorf("invalid OCI layout identifier %v", id)
}
if ociIdentifier.Platform != nil {
platform = *ociIdentifier.Platform
}
mode = source.ResolveModeForcePull // with OCI layout, we always just "pull"
store = llb.ResolveImageConfigOptStore{
SessionID: ociIdentifier.SessionID,
StoreID: ociIdentifier.StoreID,
}
ref = ociIdentifier.Reference
layerLimit = ociIdentifier.LayerLimit
default:
return nil, errors.Errorf("unknown resolver type: %v", is.ResolverType)
}
pullerUtil = &pull.Puller{
ContentStore: is.ContentStore,
Platform: platform,
Src: ref,
}
p = &puller{
CacheAccessor: is.CacheAccessor,
LeaseManager: is.LeaseManager,
Puller: pullerUtil,
RegistryHosts: is.RegistryHosts,
ResolverType: is.ResolverType,
ImageStore: is.ImageStore,
Mode: mode,
RecordType: recordType,
Ref: ref.String(),
SessionManager: sm,
vtx: vtx,
store: store,
layerLimit: layerLimit,
}
return p, nil
}
type puller struct {
CacheAccessor cache.Accessor
LeaseManager leases.Manager
RegistryHosts docker.RegistryHosts
ImageStore images.Store
Mode source.ResolveMode
RecordType client.UsageRecordType
Ref string
SessionManager *session.Manager
layerLimit *int
vtx solver.Vertex
ResolverType
store llb.ResolveImageConfigOptStore
g flightcontrol.Group[struct{}]
cacheKeyErr error
cacheKeyDone bool
releaseTmpLeases func(context.Context) error
descHandlers cache.DescHandlers
manifest *pull.PulledManifests
manifestKey string
configKey string
*pull.Puller
}
func mainManifestKey(ctx context.Context, desc ocispecs.Descriptor, platform ocispecs.Platform, layerLimit *int) (digest.Digest, error) {
dt, err := json.Marshal(struct {
Digest digest.Digest
OS string
Arch string
Variant string `json:",omitempty"`
Limit *int `json:",omitempty"`
}{
Digest: desc.Digest,
OS: platform.OS,
Arch: platform.Architecture,
Variant: platform.Variant,
Limit: layerLimit,
})
if err != nil {
return "", err
}
return digest.FromBytes(dt), nil
}
func (p *puller) CacheKey(ctx context.Context, g session.Group, index int) (cacheKey string, imgDigest string, cacheOpts solver.CacheOpts, cacheDone bool, err error) {
var getResolver pull.SessionResolver
switch p.ResolverType {
case ResolverTypeRegistry:
resolver := resolver.DefaultPool.GetResolver(p.RegistryHosts, p.Ref, "pull", p.SessionManager, g).WithImageStore(p.ImageStore, p.Mode)
p.Puller.Resolver = resolver
getResolver = func(g session.Group) remotes.Resolver { return resolver.WithSession(g) }
case ResolverTypeOCILayout:
resolver := getOCILayoutResolver(p.store, p.SessionManager, g)
p.Puller.Resolver = resolver
// OCILayout has no need for session
getResolver = func(g session.Group) remotes.Resolver { return resolver }
default:
}
// progressFactory needs the outer context, the context in `p.g.Do` will
// be canceled before the progress output is complete
progressFactory := progress.FromContext(ctx)
_, err = p.g.Do(ctx, "", func(ctx context.Context) (_ struct{}, err error) {
if p.cacheKeyErr != nil || p.cacheKeyDone {
return struct{}{}, p.cacheKeyErr
}
defer func() {
if !errdefs.IsCanceled(ctx, err) {
p.cacheKeyErr = err
}
}()
ctx, done, err := leaseutil.WithLease(ctx, p.LeaseManager, leases.WithExpiration(5*time.Minute), leaseutil.MakeTemporary)
if err != nil {
return struct{}{}, err
}
p.releaseTmpLeases = done
defer imageutil.AddLease(done)
resolveProgressDone := progress.OneOff(ctx, "resolve "+p.Src.String())
defer func() {
resolveProgressDone(err)
}()
p.manifest, err = p.PullManifests(ctx, getResolver)
if err != nil {
return struct{}{}, err
}
if ll := p.layerLimit; ll != nil {
if *ll > len(p.manifest.Descriptors) {
return struct{}{}, errors.Errorf("layer limit %d is greater than the number of layers in the image %d", *ll, len(p.manifest.Descriptors))
}
p.manifest.Descriptors = p.manifest.Descriptors[:*ll]
}
if len(p.manifest.Descriptors) > 0 {
progressController := &controller.Controller{
WriterFactory: progressFactory,
}
if p.vtx != nil {
progressController.Digest = p.vtx.Digest()
progressController.Name = p.vtx.Name()
progressController.ProgressGroup = p.vtx.Options().ProgressGroup
}
p.descHandlers = cache.DescHandlers(make(map[digest.Digest]*cache.DescHandler))
for i, desc := range p.manifest.Descriptors {
labels := snapshots.FilterInheritedLabels(desc.Annotations)
if labels == nil {
labels = make(map[string]string)
}
for k, v := range estargz.SnapshotLabels(p.manifest.Ref, p.manifest.Descriptors, i) {
labels[k] = v
}
p.descHandlers[desc.Digest] = &cache.DescHandler{
Provider: p.manifest.Provider,
Progress: progressController,
SnapshotLabels: labels,
Annotations: desc.Annotations,
Ref: p.manifest.Ref,
}
}
}
desc := p.manifest.MainManifestDesc
k, err := mainManifestKey(ctx, desc, p.Platform, p.layerLimit)
if err != nil {
return struct{}{}, err
}
p.manifestKey = k.String()
dt, err := content.ReadBlob(ctx, p.ContentStore, p.manifest.ConfigDesc)
if err != nil {
return struct{}{}, err
}
ck, err := cacheKeyFromConfig(dt, p.layerLimit)
if err != nil {
return struct{}{}, err
}
p.configKey = ck.String()
p.cacheKeyDone = true
return struct{}{}, nil
})
if err != nil {
return "", "", nil, false, err
}
cacheOpts = solver.CacheOpts(make(map[interface{}]interface{}))
for dgst, descHandler := range p.descHandlers {
cacheOpts[cache.DescHandlerKey(dgst)] = descHandler
}
cacheDone = index > 0
if index == 0 || p.configKey == "" {
return p.manifestKey, p.manifest.MainManifestDesc.Digest.String(), cacheOpts, cacheDone, nil
}
return p.configKey, p.manifest.MainManifestDesc.Digest.String(), cacheOpts, cacheDone, nil
}
func (p *puller) Snapshot(ctx context.Context, g session.Group) (ir cache.ImmutableRef, err error) {
var getResolver pull.SessionResolver
switch p.ResolverType {
case ResolverTypeRegistry:
resolver := resolver.DefaultPool.GetResolver(p.RegistryHosts, p.Ref, "pull", p.SessionManager, g).WithImageStore(p.ImageStore, p.Mode)
p.Puller.Resolver = resolver
getResolver = func(g session.Group) remotes.Resolver { return resolver.WithSession(g) }
case ResolverTypeOCILayout:
resolver := getOCILayoutResolver(p.store, p.SessionManager, g)
p.Puller.Resolver = resolver
// OCILayout has no need for session
getResolver = func(g session.Group) remotes.Resolver { return resolver }
default:
}
if len(p.manifest.Descriptors) == 0 {
return nil, nil
}
defer func() {
if p.releaseTmpLeases != nil {
p.releaseTmpLeases(context.TODO())
}
}()
var current cache.ImmutableRef
defer func() {
if err != nil && current != nil {
current.Release(context.TODO())
}
}()
var parent cache.ImmutableRef
setWindowsLayerType := p.Platform.OS == "windows" && runtime.GOOS != "windows"
for _, layerDesc := range p.manifest.Descriptors {
parent = current
current, err = p.CacheAccessor.GetByBlob(ctx, layerDesc, parent,
p.descHandlers, cache.WithImageRef(p.manifest.Ref))
if parent != nil {
parent.Release(context.TODO())
}
if err != nil {
return nil, err
}
if setWindowsLayerType {
if err := current.SetLayerType("windows"); err != nil {
return nil, err
}
}
}
for _, desc := range p.manifest.Nonlayers {
if _, err := p.ContentStore.Info(ctx, desc.Digest); containerderrdefs.IsNotFound(err) {
// manifest or config must have gotten gc'd after CacheKey, re-pull them
ctx, done, err := leaseutil.WithLease(ctx, p.LeaseManager, leaseutil.MakeTemporary)
if err != nil {
return nil, err
}
defer done(ctx)
if _, err := p.PullManifests(ctx, getResolver); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
if err := p.LeaseManager.AddResource(ctx, leases.Lease{ID: current.ID()}, leases.Resource{
ID: desc.Digest.String(),
Type: "content",
}); err != nil {
return nil, err
}
}
if p.RecordType != "" && current.GetRecordType() == "" {
if err := current.SetRecordType(p.RecordType); err != nil {
return nil, err
}
}
return current, nil
}
// cacheKeyFromConfig returns a stable digest from image config. If image config
// is a known oci image we will use chainID of layers.
func cacheKeyFromConfig(dt []byte, layerLimit *int) (digest.Digest, error) {
var img ocispecs.Image
err := json.Unmarshal(dt, &img)
if err != nil {
if layerLimit != nil {
return "", errors.Wrap(err, "failed to parse image config")
}
return digest.FromBytes(dt), nil // digest of config
}
if layerLimit != nil {
l := *layerLimit
if len(img.RootFS.DiffIDs) < l {
return "", errors.Errorf("image has %d layers, limit is %d", len(img.RootFS.DiffIDs), l)
}
img.RootFS.DiffIDs = img.RootFS.DiffIDs[:l]
}
if img.RootFS.Type != "layers" || len(img.RootFS.DiffIDs) == 0 {
return "", nil
}
return identity.ChainID(img.RootFS.DiffIDs), nil
}