package solver
import (
"context"
"errors"
"slices"
cerrdefs "github.com/containerd/errdefs"
digest "github.com/opencontainers/go-digest"
)
type exporter struct {
k *CacheKey
records []*CacheRecord
record *CacheRecord
recordCtxOpts func(context.Context) context.Context
edge *edge // for secondaryExporters
override *bool
}
func addBacklinks(t CacheExporterTarget, cm *cacheManager, id string, bkm map[string][]CacheExporterRecord) ([]CacheExporterRecord, error) {
out, ok := bkm[id]
if ok && out != nil {
return out, nil
} else if ok && out == nil {
return nil, nil
}
bkm[id] = nil
m := map[digest.Digest][][]CacheLink{}
isRoot := true
if err := cm.backend.WalkBacklinks(id, func(id string, link CacheInfoLink) error {
isRoot = false
recs, err := addBacklinks(t, cm, id, bkm)
if err != nil { // TODO: should we continue on error?
return err
}
links := m[link.Digest]
for int(link.Input) >= len(links) {
links = append(links, nil)
}
for _, rec := range recs {
links[int(link.Input)] = append(links[int(link.Input)], CacheLink{Src: rec, Selector: link.Selector.String()})
}
m[link.Digest] = links
return nil
}); err != nil {
return nil, err
}
if isRoot {
dgst, err := digest.Parse(id)
if err == nil {
rec, ok, err := t.Add(dgst, nil, nil)
if err != nil {
return nil, err
}
if ok && rec != nil {
out = append(out, rec)
}
}
}
// validate that all inputs are present
for dgst, links := range m {
for _, links := range links {
if len(links) == 0 {
out = nil
m[dgst] = nil
break
}
}
}
for dgst, links := range m {
if len(links) == 0 {
continue
}
rec, ok, err := t.Add(dgst, links, nil)
if err != nil {
return nil, err
}
if !ok || rec == nil {
continue
}
out = append(out, rec)
}
bkm[id] = out
return out, nil
}
type contextT string
var (
backlinkKey = contextT("solver/exporter/backlinks")
resKey = contextT("solver/exporter/res")
)
func (e *exporter) ExportTo(ctx context.Context, t CacheExporterTarget, opt CacheExportOpt) ([]CacheExporterRecord, error) {
var bkm map[string][]CacheExporterRecord
if bk := ctx.Value(backlinkKey); bk == nil {
bkm = map[string][]CacheExporterRecord{}
ctx = context.WithValue(ctx, backlinkKey, bkm)
} else {
bkm = bk.(map[string][]CacheExporterRecord)
}
var res map[*exporter][]CacheExporterRecord
if r := ctx.Value(resKey); r == nil {
res = map[*exporter][]CacheExporterRecord{}
ctx = context.WithValue(ctx, resKey, res)
} else {
res = r.(map[*exporter][]CacheExporterRecord)
}
if v, ok := res[e]; ok {
return v, nil
}
res[e] = nil
deps := e.k.Deps()
k := e.k.clone() // protect against *CacheKey internal ids mutation from other exports
recKey := rootKey(k.Digest(), k.Output())
results := []CacheExportResult{}
addRecord := true
if e.override != nil {
addRecord = *e.override
}
exportRecord := opt.ExportRoots
if len(deps) > 0 {
exportRecord = true
}
records := slices.Clone(e.records)
slices.SortStableFunc(records, compareCacheRecord)
var remote *Remote
var i int
mainCtx := ctx
if CacheOptGetterOf(ctx) == nil && e.recordCtxOpts != nil {
ctx = e.recordCtxOpts(ctx)
}
v := e.record
for exportRecord && addRecord {
if v == nil {
if i < len(records) {
v = records[i]
i++
} else {
break
}
}
cm := v.cacheManager
key := cm.getID(v.key)
res, err := cm.backend.Load(key, v.ID)
if err != nil {
if errors.Is(err, ErrNotFound) {
v = nil
continue
}
return nil, err
}
remotes, err := cm.results.LoadRemotes(ctx, res, opt.CompressionOpt, opt.Session)
if err != nil {
return nil, err
}
if len(remotes) > 0 {
remote, remotes = remotes[0], remotes[1:] // pop the first element
}
if opt.CompressionOpt != nil {
for _, r := range remotes { // record all remaining remotes as well
results = append(results, CacheExportResult{
CreatedAt: v.CreatedAt,
Result: r,
EdgeVertex: k.vtx,
EdgeIndex: k.output,
})
}
}
if (remote == nil || opt.CompressionOpt != nil) && opt.Mode != CacheExportModeRemoteOnly {
res, err := cm.results.Load(ctx, res)
if err != nil {
if !errors.Is(err, cerrdefs.ErrNotFound) {
return nil, err
}
remote = nil
} else {
remotes, err := opt.ResolveRemotes(ctx, res)
if err != nil {
return nil, err
}
res.Release(context.TODO())
if remote == nil && len(remotes) > 0 {
remote, remotes = remotes[0], remotes[1:] // pop the first element
}
if opt.CompressionOpt != nil {
for _, r := range remotes { // record all remaining remotes as well
results = append(results, CacheExportResult{
CreatedAt: v.CreatedAt,
Result: r,
EdgeVertex: k.vtx,
EdgeIndex: k.output,
})
}
}
}
}
if remote != nil {
results = append(results, CacheExportResult{
CreatedAt: v.CreatedAt,
Result: remote,
EdgeVertex: k.vtx,
EdgeIndex: k.output,
})
}
break
}
if remote != nil && opt.Mode == CacheExportModeMin {
opt.Mode = CacheExportModeRemoteOnly
}
srcs := make([][]CacheLink, len(deps))
for i, deps := range deps {
for _, dep := range deps {
rec, err := dep.CacheKey.Exporter.ExportTo(ctx, t, opt)
if err != nil {
continue
}
for _, r := range rec {
srcs[i] = append(srcs[i], CacheLink{Src: r, Selector: string(dep.Selector)})
}
}
}
if e.edge != nil {
for _, de := range e.edge.secondaryExporters {
recs, err := de.cacheKey.CacheKey.Exporter.ExportTo(mainCtx, t, opt)
if err != nil {
continue
}
for _, r := range recs {
srcs[de.index] = append(srcs[de.index], CacheLink{Src: r, Selector: de.cacheKey.Selector.String()})
}
}
}
if !opt.IgnoreBacklinks {
for cm, id := range k.ids {
_, err := addBacklinks(t, cm, id, bkm)
if err != nil {
return nil, err
}
}
}
// validate deps are present
for _, deps := range srcs {
if len(deps) == 0 {
res[e] = nil
return res[e], nil
}
}
if v != nil && len(deps) == 0 {
cm := v.cacheManager
key := cm.getID(v.key)
if err := cm.backend.WalkIDsByResult(v.ID, func(id string) error {
if id == key {
return nil
}
hasBacklinks := false
cm.backend.WalkBacklinks(id, func(id string, link CacheInfoLink) error {
hasBacklinks = true
return nil
})
if hasBacklinks {
return nil
}
dgst, err := digest.Parse(id)
if err != nil {
return nil
}
_, _, err = t.Add(dgst, nil, results)
return err
}); err != nil {
return nil, err
}
}
out, ok, err := t.Add(recKey, srcs, results)
if err != nil {
return nil, err
}
res[e] = []CacheExporterRecord{}
if ok {
res[e] = append(res[e], out)
}
return res[e], nil
}
func getBestResult(records []*CacheRecord) *CacheRecord {
records = slices.Clone(records)
slices.SortStableFunc(records, compareCacheRecord)
if len(records) == 0 {
return nil
}
return records[0]
}
func compareCacheRecord(a, b *CacheRecord) int {
if a == nil && b == nil {
return 0
}
if a == nil {
return 1
}
if b == nil {
return -1
}
if v := b.CreatedAt.Compare(a.CreatedAt); v != 0 {
return v
}
return a.Priority - b.Priority
}
type mergedExporter struct {
exporters []CacheExporter
}
func (e *mergedExporter) ExportTo(ctx context.Context, t CacheExporterTarget, opt CacheExportOpt) (er []CacheExporterRecord, err error) {
for _, e := range e.exporters {
r, err := e.ExportTo(ctx, t, opt)
if err != nil {
return nil, err
}
er = append(er, r...)
}
return
}