contentutil: add pull through cache for attestations chain resolve

Currently attestation chains were always loaded directly from
registry on each pull.

This adds cache capability to resolver so all the pulled manifests
are first pulled to content store and kept there with GC labels
references from the root manifest.

If blob or referrers request already exists in the content store
then local response is used without registry requests.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2025-12-19 16:03:06 -08:00
parent 6ef55fa1f4
commit 7e17a06aa6
4 changed files with 527 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ import (
"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"
@@ -201,7 +202,11 @@ func (is *Source) ResolveImageMetadata(ctx context.Context, id *ImageIdentifier,
if err != nil {
return nil, err
}
prov := contentutil.FromFetcher(f)
named, err := distreference.ParseNormalizedNamed(ref)
if err != nil {
return nil, err
}
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
@@ -242,6 +247,9 @@ func (is *Source) ResolveImageMetadata(ctx context.Context, id *ImageIdentifier,
Data: dt,
}
}
if err := prov.SetGCLabels(ctx, desc); err != nil {
return nil, err
}
return ac, nil
})
if err != nil {

View File

@@ -95,7 +95,14 @@ func (b *buffer) Writer(ctx context.Context, opts ...content.WriterOpt) (content
}
}
b.mu.Lock()
if wOpts.Desc.Digest != "" {
if _, ok := b.buffers[wOpts.Desc.Digest]; ok {
b.mu.Unlock()
return nil, errors.Wrapf(cerrdefs.ErrAlreadyExists, "content %v already exists", wOpts.Desc.Digest)
}
}
if _, ok := b.refs[wOpts.Ref]; ok {
b.mu.Unlock()
return nil, errors.Wrapf(cerrdefs.ErrUnavailable, "ref %s locked", wOpts.Ref)
}
b.mu.Unlock()

229
util/contentutil/cache.go Normal file
View File

@@ -0,0 +1,229 @@
package contentutil
import (
"context"
"encoding/json"
"slices"
"strings"
"sync"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/remotes"
cerrdefs "github.com/containerd/errdefs"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
)
func ReferrersProviderWithBuffer(p ReferrersProvider, buffer Buffer, name string) *ReferrersProviderBuffer {
return &ReferrersProviderBuffer{
p: p,
cache: buffer,
name: name,
}
}
var _ ReferrersProvider = &ReferrersProviderBuffer{}
type ReferrersProviderBuffer struct {
p ReferrersProvider
cache Buffer
name string
mu sync.Mutex
blobs map[digest.Digest]ocispecs.Descriptor
refs map[digest.Digest][]ocispecs.Descriptor
}
func (p *ReferrersProviderBuffer) ReaderAt(ctx context.Context, desc ocispecs.Descriptor) (content.ReaderAt, error) {
cw, err := content.OpenWriter(ctx, p.cache, content.WithDescriptor(desc), content.WithRef(desc.Digest.String()))
if err != nil {
if cerrdefs.IsAlreadyExists(err) {
ra, err := p.cache.ReaderAt(ctx, desc)
if err != nil {
return nil, err
}
p.mu.Lock()
if p.blobs == nil {
p.blobs = make(map[digest.Digest]ocispecs.Descriptor)
}
p.blobs[desc.Digest] = desc
p.mu.Unlock()
return ra, nil
}
return nil, err
}
ra, err := p.p.ReaderAt(ctx, desc)
if err != nil {
cw.Close()
return nil, err
}
if err := content.CopyReaderAt(cw, ra, ra.Size()); err != nil {
cw.Close()
return nil, err
}
if err := cw.Commit(ctx, desc.Size, desc.Digest); err != nil {
cw.Close()
return nil, err
}
ra, err = p.cache.ReaderAt(ctx, desc)
if err != nil {
return nil, err
}
p.mu.Lock()
if p.blobs == nil {
p.blobs = make(map[digest.Digest]ocispecs.Descriptor)
}
p.blobs[desc.Digest] = desc
p.mu.Unlock()
return ra, nil
}
func (p *ReferrersProviderBuffer) FetchReferrers(ctx context.Context, dgst digest.Digest, opts ...remotes.FetchReferrersOpt) ([]ocispecs.Descriptor, error) {
cfg := remotes.FetchReferrersConfig{}
for _, o := range opts {
if err := o(ctx, &cfg); err != nil {
return nil, err
}
}
info, err := p.cache.Info(ctx, dgst)
if err == nil && len(info.Labels) != 0 {
refs := []ocispecs.Descriptor{}
for l, v := range info.Labels {
if !strings.HasPrefix(l, "containerd.io/gc.ref.content.buildkit.refs.") {
continue
}
dgst, err := digest.Parse(v)
if err != nil {
continue
}
dt, err := content.ReadBlob(ctx, p.cache, ocispecs.Descriptor{Digest: dgst})
if err != nil {
continue
}
desc := ocispecs.Descriptor{
Digest: dgst,
Size: int64(len(dt)),
ArtifactType: readArtifactType(dt),
}
refs = append(refs, desc)
}
refs = filterRefs(refs, &cfg)
if len(refs) > 0 {
return refs, nil
}
v, ok := info.Labels["buildkit/refs.null"]
if ok {
for name := range strings.SplitSeq(v, ",") {
if name == p.name {
return nil, nil
}
}
}
}
refs, err := p.p.FetchReferrers(ctx, dgst, opts...)
if err != nil {
return nil, err
}
refs = filterRefs(refs, &cfg)
p.mu.Lock()
if p.refs == nil {
p.refs = make(map[digest.Digest][]ocispecs.Descriptor)
}
p.refs[dgst] = append(p.refs[dgst], refs...)
p.mu.Unlock()
return refs, nil
}
func (p *ReferrersProviderBuffer) SetGCLabels(ctx context.Context, root ocispecs.Descriptor) error {
labels := map[string]string{}
fieldpaths := []string{}
p.mu.Lock()
for _, desc := range p.blobs {
shaPrefix := desc.Digest.Hex()[:12]
key := "containerd.io/gc.ref.content.buildkit." + shaPrefix
labels[key] = desc.Digest.String()
fieldpaths = append(fieldpaths, "labels."+key)
}
p.mu.Unlock()
_, err := p.cache.Update(ctx, content.Info{
Digest: root.Digest,
Labels: labels,
}, fieldpaths...)
if err != nil {
return err
}
for dgst, refs := range p.refs {
info, err := p.cache.Info(ctx, dgst)
if err != nil {
continue
}
labels := map[string]string{}
fieldpaths := []string{}
for _, ref := range refs {
shaPrefix := ref.Digest.Hex()[:12]
key := "containerd.io/gc.ref.content.buildkit.refs." + shaPrefix
labels[key] = ref.Digest.String()
fieldpaths = append(fieldpaths, "labels."+key)
}
if len(refs) == 0 {
key := "buildkit/refs.null"
labels[key] = addName(info.Labels[key], p.name)
fieldpaths = append(fieldpaths, "labels."+key)
}
if len(labels) == 0 {
continue
}
_, err = p.cache.Update(ctx, content.Info{
Digest: dgst,
Labels: labels,
}, fieldpaths...)
if err != nil {
return err
}
}
return nil
}
func filterRefs(refs []ocispecs.Descriptor, cfg *remotes.FetchReferrersConfig) []ocispecs.Descriptor {
if len(cfg.ArtifactTypes) == 0 {
return refs
}
out := []ocispecs.Descriptor{}
for _, ref := range refs {
if slices.Contains(cfg.ArtifactTypes, ref.ArtifactType) {
out = append(out, ref)
}
}
return out
}
func addName(existing, name string) string {
if existing == "" {
return name
}
m := map[string]struct{}{}
for n := range strings.SplitSeq(existing, ",") {
m[n] = struct{}{}
}
m[name] = struct{}{}
var names []string
for n := range m {
names = append(names, n)
}
slices.Sort(names)
return strings.Join(names, ",")
}
func readArtifactType(dt []byte) string {
var mfst ocispecs.Manifest
if err := json.Unmarshal(dt, &mfst); err != nil {
return ""
}
return mfst.ArtifactType
}

View File

@@ -0,0 +1,282 @@
package contentutil
import (
"bytes"
"context"
"encoding/json"
"testing"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/remotes"
digest "github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)
type buf struct {
*bytes.Reader
}
func (r *buf) Close() error { return nil }
func newBuf(b []byte) *buf {
return &buf{
Reader: bytes.NewReader(b),
}
}
type stubProvider struct {
data map[digest.Digest][]byte
calls int
refs map[digest.Digest][]ocispecs.Descriptor
refsCalls int
}
func (p *stubProvider) ReaderAt(ctx context.Context, desc ocispecs.Descriptor) (content.ReaderAt, error) {
p.calls++
b, ok := p.data[desc.Digest]
if !ok {
return nil, errors.Errorf("not found: %s", desc.Digest.String())
}
return newBuf(b), nil
}
func (p *stubProvider) FetchReferrers(ctx context.Context, dgst digest.Digest, opts ...remotes.FetchReferrersOpt) ([]ocispecs.Descriptor, error) {
p.refsCalls++
refs, ok := p.refs[dgst]
if !ok {
return nil, nil
}
return refs, nil
}
func (p *stubProvider) add(dt []byte) ocispecs.Descriptor {
if p.data == nil {
p.data = make(map[digest.Digest][]byte)
}
dgst := digest.FromBytes(dt)
p.data[dgst] = dt
return ocispecs.Descriptor{
Digest: dgst,
Size: int64(len(dt)),
ArtifactType: readArtifactType(dt),
}
}
func (p *stubProvider) addReferrer(target digest.Digest, dt []byte) ocispecs.Descriptor {
if _, ok := p.data[target]; !ok {
panic("target not found") // this is test only helper
}
if p.refs == nil {
p.refs = make(map[digest.Digest][]ocispecs.Descriptor)
}
old, ok := p.refs[target]
if !ok {
old = []ocispecs.Descriptor{}
}
desc := p.add(dt)
p.refs[target] = append(old, desc)
return desc
}
func stubManifest(t *testing.T, name, artifactType string) []byte {
manif := ocispecs.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
MediaType: ocispecs.MediaTypeImageManifest,
ArtifactType: artifactType,
Annotations: map[string]string{
"test.name": name,
},
}
dt, err := json.Marshal(manif)
require.NoError(t, err)
return dt
}
func TestReferrersProviderBuffer(t *testing.T) {
ctx := context.TODO()
buf := NewBuffer()
rp := &stubProvider{}
rootDigest := digest.FromString("root")
rw, err := content.OpenWriter(ctx, buf)
require.NoError(t, err)
err = content.Copy(ctx, rw, bytes.NewReader([]byte("root")), 4, rootDigest)
require.NoError(t, err)
hello := rp.add([]byte("hello"))
world := rp.add([]byte("world!"))
rpb := ReferrersProviderWithBuffer(rp, buf, "")
ra, err := rpb.ReaderAt(ctx, hello)
require.NoError(t, err)
require.Equal(t, hello.Size, ra.Size())
ra.Close()
ra, err = buf.ReaderAt(ctx, hello)
require.NoError(t, err)
b := make([]byte, hello.Size)
n, err := ra.ReadAt(b, 0)
require.NoError(t, err)
require.Equal(t, int(hello.Size), n)
require.Equal(t, []byte("hello"), b)
ra.Close()
require.Equal(t, 1, rp.calls)
ra, err = rpb.ReaderAt(ctx, world)
require.NoError(t, err)
require.Equal(t, world.Size, ra.Size())
ra.Close()
ra, err = buf.ReaderAt(ctx, world)
require.NoError(t, err)
b = make([]byte, world.Size)
n, err = ra.ReadAt(b, 0)
require.NoError(t, err)
require.Equal(t, int(world.Size), n)
require.Equal(t, []byte("world!"), b)
ra.Close()
require.Equal(t, 2, rp.calls)
// second read should hit cache
ra, err = rpb.ReaderAt(ctx, hello)
require.NoError(t, err)
require.Equal(t, hello.Size, ra.Size())
ra.Close()
require.Equal(t, 2, rp.calls)
err = rpb.SetGCLabels(ctx, ocispecs.Descriptor{
Digest: rootDigest,
})
require.NoError(t, err)
info, err := buf.Info(ctx, rootDigest)
require.NoError(t, err)
require.Equal(t, rootDigest, info.Digest)
require.Equal(t, int64(4), info.Size)
labels := info.Labels
require.Equal(t, 2, len(labels))
pfx := hello.Digest.Hex()[:12]
lbl1, ok := labels["containerd.io/gc.ref.content.buildkit."+pfx]
require.True(t, ok)
require.Equal(t, hello.Digest.String(), lbl1)
pfx = world.Digest.Hex()[:12]
lbl2, ok := labels["containerd.io/gc.ref.content.buildkit."+pfx]
require.True(t, ok)
require.Equal(t, world.Digest.String(), lbl2)
}
func TestReferrersProviderRefsBuffer(t *testing.T) {
ctx := context.TODO()
buf := NewBuffer()
rp := &stubProvider{}
rootDigest := digest.FromString("root")
rw, err := content.OpenWriter(ctx, buf)
require.NoError(t, err)
err = content.Copy(ctx, rw, bytes.NewReader([]byte("root")), 4, rootDigest)
require.NoError(t, err)
hello := rp.add([]byte("hello"))
r1 := stubManifest(t, "ref1", "type")
ref1 := rp.addReferrer(hello.Digest, r1)
r2 := stubManifest(t, "ref2", "type")
ref2 := rp.addReferrer(hello.Digest, r2)
world := rp.add([]byte("world!"))
rpb := ReferrersProviderWithBuffer(rp, buf, "repo1")
ra, err := rpb.ReaderAt(ctx, hello)
require.NoError(t, err)
ra.Close()
refs, err := rpb.FetchReferrers(ctx, hello.Digest, remotes.WithReferrerArtifactTypes("type"))
require.NoError(t, err)
require.Equal(t, 2, len(refs))
require.Contains(t, refs, ref1)
require.Contains(t, refs, ref2)
require.Equal(t, 1, rp.refsCalls)
ra, err = rpb.ReaderAt(ctx, ref1)
require.NoError(t, err)
ra.Close()
ra, err = rpb.ReaderAt(ctx, ref2)
require.NoError(t, err)
ra.Close()
err = rpb.SetGCLabels(ctx, ocispecs.Descriptor{
Digest: rootDigest,
})
require.NoError(t, err)
refs, err = rpb.FetchReferrers(ctx, hello.Digest, remotes.WithReferrerArtifactTypes("type"))
require.NoError(t, err)
require.Equal(t, 2, len(refs))
require.Contains(t, []digest.Digest{ref1.Digest, ref2.Digest}, refs[0].Digest)
require.Contains(t, []digest.Digest{ref1.Digest, ref2.Digest}, refs[1].Digest)
require.Equal(t, 1, rp.refsCalls)
info, err := buf.Info(ctx, hello.Digest)
require.NoError(t, err)
labels := info.Labels
require.Equal(t, 2, len(labels))
pfx := ref1.Digest.Hex()[:12]
lbl1, ok := labels["containerd.io/gc.ref.content.buildkit.refs."+pfx]
require.True(t, ok)
require.Equal(t, ref1.Digest.String(), lbl1)
pfx = ref2.Digest.Hex()[:12]
lbl2, ok := labels["containerd.io/gc.ref.content.buildkit.refs."+pfx]
require.True(t, ok)
require.Equal(t, ref2.Digest.String(), lbl2)
// tests for empty refs calls
rpb = ReferrersProviderWithBuffer(rp, buf, "repo1")
ra, err = rpb.ReaderAt(ctx, world)
require.NoError(t, err)
ra.Close()
refs, err = rpb.FetchReferrers(ctx, world.Digest, remotes.WithReferrerArtifactTypes("type"))
require.NoError(t, err)
require.Equal(t, 0, len(refs))
require.Equal(t, 2, rp.refsCalls)
err = rpb.SetGCLabels(ctx, ocispecs.Descriptor{
Digest: rootDigest,
})
require.NoError(t, err)
refs, err = rpb.FetchReferrers(ctx, world.Digest, remotes.WithReferrerArtifactTypes("type"))
require.NoError(t, err)
require.Equal(t, 0, len(refs))
require.Equal(t, 2, rp.refsCalls)
info, err = buf.Info(ctx, world.Digest)
require.NoError(t, err)
labels = info.Labels
require.Equal(t, 1, len(labels))
lbl1, ok = labels["buildkit/refs.null"]
require.True(t, ok)
require.Equal(t, "repo1", lbl1)
}