mirror of
https://github.com/moby/buildkit.git
synced 2026-06-30 19:57:39 +00:00
493 lines
14 KiB
Go
493 lines
14 KiB
Go
package containerimage
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"slices"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/v2/core/content"
|
|
"github.com/containerd/containerd/v2/core/diff"
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/core/leases"
|
|
"github.com/containerd/containerd/v2/core/remotes/docker"
|
|
"github.com/containerd/containerd/v2/pkg/reference"
|
|
"github.com/containerd/platforms"
|
|
distreference "github.com/distribution/reference"
|
|
"github.com/moby/buildkit/cache"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/client/llb/sourceresolver"
|
|
"github.com/moby/buildkit/session"
|
|
"github.com/moby/buildkit/snapshot"
|
|
"github.com/moby/buildkit/solver"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/moby/buildkit/source"
|
|
srctypes "github.com/moby/buildkit/source/types"
|
|
"github.com/moby/buildkit/util/contentutil"
|
|
"github.com/moby/buildkit/util/flightcontrol"
|
|
"github.com/moby/buildkit/util/imageutil"
|
|
"github.com/moby/buildkit/util/leaseutil"
|
|
"github.com/moby/buildkit/util/pull"
|
|
"github.com/moby/buildkit/util/resolver"
|
|
"github.com/moby/buildkit/util/tracing"
|
|
policyimage "github.com/moby/policy-helpers/image"
|
|
digest "github.com/opencontainers/go-digest"
|
|
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 Source struct {
|
|
SourceOpt
|
|
gImageRes flightcontrol.Group[*resolveImageResult]
|
|
gAttestChain flightcontrol.Group[*sourceresolver.AttestationChain]
|
|
}
|
|
|
|
var _ source.Source = &Source{}
|
|
|
|
func NewSource(opt SourceOpt) (*Source, error) {
|
|
is := &Source{
|
|
SourceOpt: opt,
|
|
}
|
|
return is, nil
|
|
}
|
|
|
|
func (is *Source) Schemes() []string {
|
|
if is.ResolverType == ResolverTypeOCILayout {
|
|
return []string{srctypes.OCIScheme}
|
|
}
|
|
return []string{srctypes.DockerImageScheme}
|
|
}
|
|
|
|
func (is *Source) Identifier(scheme, ref string, attrs map[string]string, platform *pb.Platform) (source.Identifier, error) {
|
|
if is.ResolverType == ResolverTypeOCILayout {
|
|
return is.ociIdentifier(ref, attrs, platform)
|
|
}
|
|
|
|
return is.registryIdentifier(ref, attrs, platform)
|
|
}
|
|
|
|
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 resolver.ResolveMode
|
|
recordType client.UsageRecordType
|
|
ref reference.Spec
|
|
store sourceresolver.ResolveImageConfigOptStore
|
|
layerLimit *int
|
|
checksum digest.Digest
|
|
)
|
|
switch is.ResolverType {
|
|
case ResolverTypeRegistry:
|
|
imageIdentifier, ok := id.(*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
|
|
checksum = imageIdentifier.Checksum
|
|
case ResolverTypeOCILayout:
|
|
ociIdentifier, ok := id.(*OCIIdentifier)
|
|
if !ok {
|
|
return nil, errors.Errorf("invalid OCI layout identifier %v", id)
|
|
}
|
|
|
|
if ociIdentifier.Platform != nil {
|
|
platform = *ociIdentifier.Platform
|
|
}
|
|
mode = resolver.ResolveModeForcePull // with OCI layout, we always just "pull"
|
|
store = sourceresolver.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,
|
|
checksum: checksum,
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (is *Source) ResolveImageMetadata(ctx context.Context, id *ImageIdentifier, opt *sourceresolver.ResolveImageOpt, sm *session.Manager, g session.Group) (_ *sourceresolver.ResolveImageResponse, retErr error) {
|
|
if is.ResolverType != ResolverTypeRegistry {
|
|
return nil, errors.Errorf("invalid resolver type for image metadata: %v", is.ResolverType)
|
|
}
|
|
ref := id.Reference.String()
|
|
|
|
span, ctx := tracing.StartSpan(ctx, "resolving "+ref)
|
|
defer func() {
|
|
tracing.FinishWithError(span, retErr)
|
|
}()
|
|
|
|
key := ref
|
|
if platform := opt.Platform; platform != nil {
|
|
key += platforms.FormatAll(*platform)
|
|
}
|
|
rm, err := resolver.ParseImageResolveMode(opt.ResolveMode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rslvr := resolver.DefaultPool.GetResolver(is.RegistryHosts, ref, resolver.ScopeType{}, sm, g).WithImageStore(is.ImageStore, rm)
|
|
key += rm.String()
|
|
|
|
if len(opt.ResolveAttestations) > 0 {
|
|
opt.AttestationChain = true
|
|
}
|
|
|
|
ret := &sourceresolver.ResolveImageResponse{}
|
|
if !opt.NoConfig {
|
|
res, err := is.gImageRes.Do(ctx, key, func(ctx context.Context) (*resolveImageResult, error) {
|
|
dgst, dt, err := imageutil.Config(ctx, ref, rslvr, is.ContentStore, is.LeaseManager, opt.Platform)
|
|
if err != nil {
|
|
if rm != resolver.ResolveModeDefault || is.ImageStore == nil {
|
|
return nil, err
|
|
}
|
|
localRslvr := rslvr.WithImageStore(is.ImageStore, resolver.ResolveModePreferLocal)
|
|
if _, _, localErr := localRslvr.ResolveLocal(ctx, ref); localErr != nil {
|
|
return nil, err
|
|
}
|
|
localDgst, localDt, localErr := imageutil.Config(ctx, ref, localRslvr, is.ContentStore, is.LeaseManager, opt.Platform)
|
|
if localErr != nil {
|
|
return nil, err
|
|
}
|
|
dgst, dt = localDgst, localDt
|
|
}
|
|
return &resolveImageResult{dgst: dgst, dt: dt}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.Digest = res.dgst
|
|
ret.Config = res.dt
|
|
}
|
|
if opt.AttestationChain {
|
|
ctx, done, err := leaseutil.WithLease(ctx, is.LeaseManager, leases.WithExpiration(5*time.Minute), leaseutil.MakeTemporary)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
defer func() {
|
|
// this lease is not deleted to allow other components to access manifest/config from cache. It will be deleted after 5 min deadline or on pruning inactive builder
|
|
imageutil.AddLease(done)
|
|
}()
|
|
res, err := is.gAttestChain.Do(ctx, key, func(ctx context.Context) (*sourceresolver.AttestationChain, error) {
|
|
refStr, desc, err := rslvr.Resolve(ctx, ref)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
f, err := rslvr.Fetcher(ctx, refStr)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
named, err := distreference.ParseNormalizedNamed(ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if desc.MediaType != ocispecs.MediaTypeImageIndex {
|
|
return nil, nil
|
|
}
|
|
prov := contentutil.ReferrersProviderWithBuffer(contentutil.FromFetcher(f), is.ContentStore, named.Name())
|
|
sc, err := policyimage.ResolveSignatureChain(ctx, prov, desc, opt.Platform)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ac := &sourceresolver.AttestationChain{
|
|
Root: desc.Digest,
|
|
}
|
|
descs := []ocispecs.Descriptor{desc}
|
|
if sc.ImageManifest != nil {
|
|
// not adding image manifest to descs as it is not really needed for verification
|
|
// still adding digest to provide hint of what the image manifest was resolved by platform
|
|
// for better debugging experience and error messages
|
|
ac.ImageManifest = sc.ImageManifest.Digest
|
|
}
|
|
if sc.AttestationManifest != nil {
|
|
ac.AttestationManifest = sc.AttestationManifest.Digest
|
|
descs = append(descs, sc.AttestationManifest.Descriptor)
|
|
}
|
|
if sc.SignatureManifest != nil {
|
|
ac.SignatureManifests = []digest.Digest{sc.SignatureManifest.Digest}
|
|
descs = append(descs, sc.SignatureManifest.Descriptor)
|
|
mfst, err := sc.OCIManifest(ctx, sc.SignatureManifest)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
descs = append(descs, mfst.Layers...)
|
|
}
|
|
for _, desc := range descs {
|
|
dt, err := policyimage.ReadBlob(ctx, prov, desc)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
if ac.Blobs == nil {
|
|
ac.Blobs = make(map[digest.Digest]sourceresolver.Blob)
|
|
}
|
|
ac.Blobs[desc.Digest] = sourceresolver.Blob{
|
|
Descriptor: desc,
|
|
Data: dt,
|
|
}
|
|
}
|
|
if len(opt.ResolveAttestations) > 0 && ac.AttestationManifest != "" {
|
|
if err := addAttestationBlobs(ctx, prov, ac, opt.ResolveAttestations); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := prov.SetGCLabels(ctx, desc); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
return ac, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.AttestationChain = res
|
|
if ret.Digest == "" {
|
|
ret.Digest = res.Root
|
|
} else if ret.Digest != res.Root {
|
|
return nil, errors.Errorf("attestation chain root digest %s does not match image digest %s", res.Root, ret.Digest)
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (is *Source) ResolveOCILayoutMetadata(ctx context.Context, id *OCIIdentifier, opt *sourceresolver.ResolveOCILayoutOpt, sm *session.Manager, g session.Group) (_ *sourceresolver.ResolveImageResponse, retErr error) {
|
|
if is.ResolverType != ResolverTypeOCILayout {
|
|
return nil, errors.Errorf("invalid resolver type for image metadata: %v", is.ResolverType)
|
|
}
|
|
ref := id.Reference.String()
|
|
|
|
span, ctx := tracing.StartSpan(ctx, "resolving "+ref)
|
|
defer func() {
|
|
tracing.FinishWithError(span, retErr)
|
|
}()
|
|
|
|
key := ref
|
|
if platform := opt.Platform; platform != nil {
|
|
key += platforms.FormatAll(*platform)
|
|
}
|
|
|
|
if opt.Store.StoreID == "" {
|
|
opt.Store.StoreID = id.StoreID
|
|
}
|
|
|
|
rslvr := getOCILayoutResolver(opt.Store, sm, g)
|
|
key += resolver.ResolveModeForcePull.String()
|
|
|
|
res, err := is.gImageRes.Do(ctx, key, func(ctx context.Context) (*resolveImageResult, error) {
|
|
dgst, dt, err := imageutil.Config(ctx, ref, rslvr, is.ContentStore, is.LeaseManager, opt.Platform)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resolveImageResult{dgst: dgst, dt: dt}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &sourceresolver.ResolveImageResponse{
|
|
Digest: res.dgst,
|
|
Config: res.dt,
|
|
}, nil
|
|
}
|
|
|
|
type resolveImageResult struct {
|
|
dgst digest.Digest
|
|
dt []byte
|
|
}
|
|
|
|
func addAttestationBlobs(ctx context.Context, prov policyimage.ReferrersProvider, ac *sourceresolver.AttestationChain, predicateTypes []string) error {
|
|
if ac == nil || ac.AttestationManifest == "" || ac.Blobs == nil || len(predicateTypes) == 0 {
|
|
return nil
|
|
}
|
|
att, ok := ac.Blobs[ac.AttestationManifest]
|
|
if !ok || len(att.Data) == 0 {
|
|
return nil
|
|
}
|
|
need := map[string]struct{}{}
|
|
for _, p := range predicateTypes {
|
|
if p == "" {
|
|
continue
|
|
}
|
|
need[p] = struct{}{}
|
|
}
|
|
if len(need) == 0 {
|
|
return nil
|
|
}
|
|
var manifest ocispecs.Manifest
|
|
if err := json.Unmarshal(att.Data, &manifest); err != nil {
|
|
return errors.Wrapf(err, "unmarshaling attestation manifest %s", ac.AttestationManifest)
|
|
}
|
|
|
|
for _, layer := range manifest.Layers {
|
|
predicateType := layer.Annotations["in-toto.io/predicate-type"]
|
|
if _, ok := need[predicateType]; !ok {
|
|
continue
|
|
}
|
|
if _, ok := ac.Blobs[layer.Digest]; ok {
|
|
continue
|
|
}
|
|
dt, err := policyimage.ReadBlob(ctx, prov, layer)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
ac.Blobs[layer.Digest] = sourceresolver.Blob{
|
|
Descriptor: layer,
|
|
Data: dt,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (is *Source) registryIdentifier(ref string, attrs map[string]string, platform *pb.Platform) (source.Identifier, error) {
|
|
id, err := NewImageIdentifier(ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if platform != nil {
|
|
id.Platform = &ocispecs.Platform{
|
|
OS: platform.OS,
|
|
Architecture: platform.Architecture,
|
|
Variant: platform.Variant,
|
|
OSVersion: platform.OSVersion,
|
|
}
|
|
if platform.OSFeatures != nil {
|
|
id.Platform.OSFeatures = slices.Clone(platform.OSFeatures)
|
|
}
|
|
}
|
|
|
|
for k, v := range attrs {
|
|
switch k {
|
|
case pb.AttrImageResolveMode:
|
|
rm, err := resolver.ParseImageResolveMode(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
id.ResolveMode = rm
|
|
case pb.AttrImageRecordType:
|
|
rt, err := parseImageRecordType(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
id.RecordType = rt
|
|
case pb.AttrImageLayerLimit:
|
|
l, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "invalid layer limit %s", v)
|
|
}
|
|
if l <= 0 {
|
|
return nil, errors.Errorf("invalid layer limit %s", v)
|
|
}
|
|
id.LayerLimit = &l
|
|
case pb.AttrImageChecksum:
|
|
dgst, err := digest.Parse(v)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "invalid image checksum %s", v)
|
|
}
|
|
id.Checksum = dgst
|
|
}
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (is *Source) ociIdentifier(ref string, attrs map[string]string, platform *pb.Platform) (source.Identifier, error) {
|
|
id, err := NewOCIIdentifier(ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if platform != nil {
|
|
id.Platform = &ocispecs.Platform{
|
|
OS: platform.OS,
|
|
Architecture: platform.Architecture,
|
|
Variant: platform.Variant,
|
|
OSVersion: platform.OSVersion,
|
|
}
|
|
if platform.OSFeatures != nil {
|
|
id.Platform.OSFeatures = slices.Clone(platform.OSFeatures)
|
|
}
|
|
}
|
|
|
|
for k, v := range attrs {
|
|
switch k {
|
|
case pb.AttrOCILayoutSessionID:
|
|
id.SessionID = v
|
|
case pb.AttrOCILayoutStoreID:
|
|
id.StoreID = v
|
|
case pb.AttrOCILayoutLayerLimit:
|
|
l, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "invalid layer limit %s", v)
|
|
}
|
|
if l <= 0 {
|
|
return nil, errors.Errorf("invalid layer limit %s", v)
|
|
}
|
|
id.LayerLimit = &l
|
|
}
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func parseImageRecordType(v string) (client.UsageRecordType, error) {
|
|
switch client.UsageRecordType(v) {
|
|
case "", client.UsageRecordTypeRegular:
|
|
return client.UsageRecordTypeRegular, nil
|
|
case client.UsageRecordTypeInternal:
|
|
return client.UsageRecordTypeInternal, nil
|
|
case client.UsageRecordTypeFrontend:
|
|
return client.UsageRecordTypeFrontend, nil
|
|
default:
|
|
return "", errors.Errorf("invalid record type %s", v)
|
|
}
|
|
}
|