Files
buildkit/source/containerimage/source.go
Tonis Tiigi aa003bef56 llb: add checksum option to llb.Image
This allows images to be pulled by tag and then
checked against the digest. If digest is added directly
to the image reference, then tag is ignored.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2025-09-19 17:40:40 -07:00

314 lines
8.4 KiB
Go

package containerimage
import (
"context"
"slices"
"strconv"
"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"
"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/containerd/containerd/v2/pkg/reference"
"github.com/containerd/platforms"
"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/flightcontrol"
"github.com/moby/buildkit/util/imageutil"
"github.com/moby/buildkit/util/pull"
"github.com/moby/buildkit/util/resolver"
"github.com/moby/buildkit/util/tracing"
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
g flightcontrol.Group[*resolveImageResult]
}
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) ResolveImageConfig(ctx context.Context, ref string, opt sourceresolver.Opt, sm *session.Manager, g session.Group) (digest digest.Digest, config []byte, retErr error) {
span, ctx := tracing.StartSpan(ctx, "resolving "+ref)
defer func() {
tracing.FinishWithError(span, retErr)
}()
key := ref
var (
rm resolver.ResolveMode
rslvr remotes.Resolver
err error
)
if platform := opt.Platform; platform != nil {
key += platforms.FormatAll(*platform)
}
switch is.ResolverType {
case ResolverTypeRegistry:
iopt := opt.ImageOpt
if iopt == nil {
return "", nil, errors.Errorf("missing imageopt for resolve")
}
rm, err = resolver.ParseImageResolveMode(iopt.ResolveMode)
if err != nil {
return "", nil, err
}
rslvr = resolver.DefaultPool.GetResolver(is.RegistryHosts, ref, "pull", sm, g).WithImageStore(is.ImageStore, rm)
case ResolverTypeOCILayout:
iopt := opt.OCILayoutOpt
if iopt == nil {
return "", nil, errors.Errorf("missing ocilayoutopt for resolve")
}
rm = resolver.ResolveModeForcePull
rslvr = getOCILayoutResolver(iopt.Store, sm, g)
}
key += rm.String()
res, err := is.g.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 res.dgst, res.dt, nil
}
type resolveImageResult struct {
dgst digest.Digest
dt []byte
}
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)
}
}