package inline
import (
"context"
"encoding/json"
"slices"
"github.com/containerd/containerd/v2/pkg/labels"
"github.com/moby/buildkit/cache/remotecache"
v1 "github.com/moby/buildkit/cache/remotecache/v1"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/compression"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
func ResolveCacheExporterFunc() remotecache.ResolveCacheExporterFunc {
return func(ctx context.Context, _ session.Group, _ map[string]string) (remotecache.Exporter, error) {
return NewExporter(), nil
}
}
func NewExporter() remotecache.Exporter {
cc := v1.NewCacheChains()
return &exporter{CacheExporterTarget: cc, chains: cc}
}
type exporter struct {
solver.CacheExporterTarget
chains *v1.CacheChains
}
func (*exporter) Name() string {
return "exporting inline cache"
}
func (ce *exporter) Config() remotecache.Config {
return remotecache.Config{
Compression: compression.New(compression.Default),
}
}
func (ce *exporter) Finalize(ctx context.Context) (map[string]string, error) {
return nil, nil
}
func (ce *exporter) reset() {
cc := v1.NewCacheChains()
ce.CacheExporterTarget = cc
ce.chains = cc
}
func (ce *exporter) ExportForLayers(ctx context.Context, layers []digest.Digest) ([]byte, error) {
config, descs, err := ce.chains.Marshal(ctx)
if err != nil {
return nil, err
}
layerBlobDigests := make([][]digest.Digest, len(layers))
descs2 := map[digest.Digest]v1.DescriptorProviderPair{}
for i, k := range layers {
if v, ok := descs[k]; ok {
descs2[k] = v
layerBlobDigests[i] = append(layerBlobDigests[i], k)
}
// fallback for uncompressed digests
for _, v := range descs {
if uc := v.Descriptor.Annotations[labels.LabelUncompressed]; uc == string(k) {
descs2[v.Descriptor.Digest] = v
layerBlobDigests[i] = append(layerBlobDigests[i], v.Descriptor.Digest)
}
}
}
cc := v1.NewCacheChains()
if err := v1.ParseConfig(*config, descs2, cc); err != nil {
return nil, err
}
cfg, _, err := cc.Marshal(ctx)
if err != nil {
return nil, err
}
if len(cfg.Layers) == 0 {
bklog.G(ctx).Warn("failed to match any cache with layers")
return nil, nil
}
// reorder layers based on the order in the image
blobIndexes := make(map[digest.Digest]int, len(layers))
for i, blobs := range layerBlobDigests {
for _, blob := range blobs {
blobIndexes[blob] = i
}
}
for i, r := range cfg.Records {
for j, rr := range r.Results {
resultBlobs := layerToBlobs(rr.LayerIndex, cfg.Layers)
// match being true means the result is in the same order as the image
var match bool
if len(resultBlobs) <= len(layers) {
match = true
for k, resultBlob := range resultBlobs {
matchesBlob := slices.Contains(layerBlobDigests[k], resultBlob)
if !matchesBlob {
match = false
break
}
}
}
if match {
// The layers of the result are in the same order as the image, so we can
// specify it just using the CacheResult struct and specifying LayerIndex
// as the top-most layer of the result.
rr.LayerIndex = len(resultBlobs) - 1
r.Results[j] = rr
} else {
// The layers of the result are not in the same order as the image, so we
// have to use ChainedResult to specify each layer of the result individually.
chainedResult := v1.ChainedResult{}
for _, resultBlob := range resultBlobs {
idx, ok := blobIndexes[resultBlob]
if !ok {
return nil, errors.Errorf("failed to find blob %s in layers", resultBlob)
}
chainedResult.LayerIndexes = append(chainedResult.LayerIndexes, idx)
}
r.Results[j] = v1.CacheResult{}
r.ChainedResults = append(r.ChainedResults, chainedResult)
}
// remove any CacheResults that had to be converted to the ChainedResult format.
var filteredResults []v1.CacheResult
for _, rr := range r.Results {
if rr != (v1.CacheResult{}) {
filteredResults = append(filteredResults, rr)
}
}
r.Results = filteredResults
cfg.Records[i] = r
}
}
dt, err := json.Marshal(cfg.Records)
if err != nil {
return nil, err
}
ce.reset()
return dt, nil
}
func layerToBlobs(idx int, layers []v1.CacheLayer) []digest.Digest {
var ds []digest.Digest
for idx != -1 {
layer := layers[idx]
ds = append(ds, layer.Blob)
idx = layer.ParentIndex
}
// reverse so they go lowest to highest
slices.Reverse(ds)
return ds
}