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 } if st, err := cw.Status(); err == nil { if st.Offset > 0 { if err := cw.Truncate(0); err != nil { cw.Close() return nil, err } } } abort := func() { _ = p.cache.Abort(ctx, desc.Digest.String()) } defer func() { if abort != nil { abort() } }() ra, err := p.p.ReaderAt(ctx, desc) if err != nil { cw.Close() return nil, err } defer ra.Close() 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 } abort = nil 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 }