mirror of
https://github.com/moby/buildkit.git
synced 2026-06-30 19:57:39 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
229
util/contentutil/cache.go
Normal 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
|
||||
}
|
||||
282
util/contentutil/cache_test.go
Normal file
282
util/contentutil/cache_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user