2018-05-07 21:24:28 +00:00
|
|
|
package remotecache
|
2017-10-12 22:04:33 +00:00
|
|
|
|
|
|
|
import (
|
2018-01-16 22:30:10 +00:00
|
|
|
"context"
|
2017-10-12 22:04:33 +00:00
|
|
|
"encoding/json"
|
2018-07-03 09:59:33 +00:00
|
|
|
"io"
|
2019-01-17 02:06:03 +00:00
|
|
|
"time"
|
2017-10-12 22:04:33 +00:00
|
|
|
|
|
|
|
"github.com/containerd/containerd/content"
|
2019-01-04 23:32:28 +00:00
|
|
|
"github.com/containerd/containerd/images"
|
2018-05-07 21:24:28 +00:00
|
|
|
v1 "github.com/moby/buildkit/cache/remotecache/v1"
|
2018-05-11 05:58:41 +00:00
|
|
|
"github.com/moby/buildkit/solver"
|
2019-01-04 23:32:28 +00:00
|
|
|
"github.com/moby/buildkit/util/imageutil"
|
2018-04-13 21:10:59 +00:00
|
|
|
"github.com/moby/buildkit/worker"
|
2019-01-04 23:32:28 +00:00
|
|
|
digest "github.com/opencontainers/go-digest"
|
2017-10-12 22:04:33 +00:00
|
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
|
"github.com/pkg/errors"
|
2019-01-04 23:32:28 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"golang.org/x/sync/errgroup"
|
2017-10-12 22:04:33 +00:00
|
|
|
)
|
|
|
|
|
2018-07-03 09:59:33 +00:00
|
|
|
// ResolveCacheImporterFunc returns importer and descriptor.
|
2018-09-11 08:02:46 +00:00
|
|
|
type ResolveCacheImporterFunc func(ctx context.Context, attrs map[string]string) (Importer, ocispec.Descriptor, error)
|
2017-10-12 22:04:33 +00:00
|
|
|
|
2018-07-03 09:59:33 +00:00
|
|
|
type Importer interface {
|
|
|
|
Resolve(ctx context.Context, desc ocispec.Descriptor, id string, w worker.Worker) (solver.CacheManager, error)
|
2017-10-12 22:04:33 +00:00
|
|
|
}
|
|
|
|
|
2018-07-03 09:59:33 +00:00
|
|
|
func NewImporter(provider content.Provider) Importer {
|
|
|
|
return &contentCacheImporter{provider: provider}
|
2017-10-12 22:04:33 +00:00
|
|
|
}
|
|
|
|
|
2018-07-03 09:59:33 +00:00
|
|
|
type contentCacheImporter struct {
|
|
|
|
provider content.Provider
|
2017-10-15 06:49:55 +00:00
|
|
|
}
|
|
|
|
|
2018-07-03 09:59:33 +00:00
|
|
|
func (ci *contentCacheImporter) Resolve(ctx context.Context, desc ocispec.Descriptor, id string, w worker.Worker) (solver.CacheManager, error) {
|
|
|
|
dt, err := readBlob(ctx, ci.provider, desc)
|
2017-10-13 00:53:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-10-12 22:04:33 +00:00
|
|
|
|
2017-10-13 00:53:28 +00:00
|
|
|
var mfst ocispec.Index
|
|
|
|
if err := json.Unmarshal(dt, &mfst); err != nil {
|
|
|
|
return nil, err
|
2017-10-12 22:04:33 +00:00
|
|
|
}
|
|
|
|
|
2018-04-13 21:10:59 +00:00
|
|
|
allLayers := v1.DescriptorProvider{}
|
2017-10-12 22:04:33 +00:00
|
|
|
|
2017-10-13 21:24:27 +00:00
|
|
|
var configDesc ocispec.Descriptor
|
|
|
|
|
2017-10-13 00:53:28 +00:00
|
|
|
for _, m := range mfst.Manifests {
|
2018-04-13 21:10:59 +00:00
|
|
|
if m.MediaType == v1.CacheConfigMediaTypeV0 {
|
2017-10-13 21:24:27 +00:00
|
|
|
configDesc = m
|
|
|
|
continue
|
2017-10-12 22:04:33 +00:00
|
|
|
}
|
2018-04-13 21:10:59 +00:00
|
|
|
allLayers[m.Digest] = v1.DescriptorProviderPair{
|
|
|
|
Descriptor: m,
|
2018-07-03 09:59:33 +00:00
|
|
|
Provider: ci.provider,
|
2018-04-13 21:10:59 +00:00
|
|
|
}
|
2017-10-13 21:24:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if configDesc.Digest == "" {
|
2019-01-04 23:32:28 +00:00
|
|
|
return ci.importInlineCache(ctx, dt, id, w)
|
2017-10-13 21:24:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-03 09:59:33 +00:00
|
|
|
dt, err = readBlob(ctx, ci.provider, configDesc)
|
2017-10-13 21:24:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-04-13 21:10:59 +00:00
|
|
|
cc := v1.NewCacheChains()
|
|
|
|
if err := v1.Parse(dt, allLayers, cc); err != nil {
|
2017-10-13 21:24:27 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-07-03 09:59:33 +00:00
|
|
|
keysStorage, resultStorage, err := v1.NewCacheKeyStorage(cc, w)
|
2017-10-13 00:53:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2017-10-12 22:04:33 +00:00
|
|
|
}
|
2018-07-03 09:59:33 +00:00
|
|
|
return solver.NewCacheManager(id, keysStorage, resultStorage), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func readBlob(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]byte, error) {
|
|
|
|
maxBlobSize := int64(1 << 20)
|
|
|
|
if desc.Size > maxBlobSize {
|
|
|
|
return nil, errors.Errorf("blob %s is too large (%d > %d)", desc.Digest, desc.Size, maxBlobSize)
|
|
|
|
}
|
|
|
|
dt, err := content.ReadBlob(ctx, provider, desc)
|
|
|
|
if err != nil {
|
|
|
|
// NOTE: even if err == EOF, we might have got expected dt here.
|
|
|
|
// For instance, http.Response.Body is known to return non-zero bytes with EOF.
|
|
|
|
if err == io.EOF {
|
|
|
|
if dtDigest := desc.Digest.Algorithm().FromBytes(dt); dtDigest != desc.Digest {
|
|
|
|
err = errors.Wrapf(err, "got EOF, expected %s (%d bytes), got %s (%d bytes)",
|
|
|
|
desc.Digest, desc.Size, dtDigest, len(dt))
|
|
|
|
} else {
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-06-02 00:02:42 +00:00
|
|
|
return dt, errors.WithStack(err)
|
2017-10-15 05:39:28 +00:00
|
|
|
}
|
2019-01-04 23:32:28 +00:00
|
|
|
|
|
|
|
func (ci *contentCacheImporter) importInlineCache(ctx context.Context, dt []byte, id string, w worker.Worker) (solver.CacheManager, error) {
|
|
|
|
m := map[digest.Digest][]byte{}
|
|
|
|
|
|
|
|
if err := ci.allDistributionManifests(ctx, dt, m); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-08-13 21:16:30 +00:00
|
|
|
var cMap = map[digest.Digest]*v1.CacheChains{}
|
2019-01-04 23:32:28 +00:00
|
|
|
|
|
|
|
eg, ctx := errgroup.WithContext(ctx)
|
|
|
|
for dgst, dt := range m {
|
|
|
|
func(dgst digest.Digest, dt []byte) {
|
|
|
|
eg.Go(func() error {
|
|
|
|
var m ocispec.Manifest
|
|
|
|
|
|
|
|
if err := json.Unmarshal(dt, &m); err != nil {
|
2019-06-02 00:02:42 +00:00
|
|
|
return errors.WithStack(err)
|
2019-01-04 23:32:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if m.Config.Digest == "" || len(m.Layers) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
p, err := content.ReadBlob(ctx, ci.provider, m.Config)
|
|
|
|
if err != nil {
|
2019-06-02 00:02:42 +00:00
|
|
|
return errors.WithStack(err)
|
2019-01-04 23:32:28 +00:00
|
|
|
}
|
|
|
|
|
2019-01-17 02:06:03 +00:00
|
|
|
var img image
|
2019-01-04 23:32:28 +00:00
|
|
|
|
|
|
|
if err := json.Unmarshal(p, &img); err != nil {
|
2019-06-02 00:02:42 +00:00
|
|
|
return errors.WithStack(err)
|
2019-01-04 23:32:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(img.Rootfs.DiffIDs) != len(m.Layers) {
|
|
|
|
logrus.Warnf("invalid image with mismatching manifest and config")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if img.Cache == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var config v1.CacheConfig
|
|
|
|
if err := json.Unmarshal(img.Cache, &config.Records); err != nil {
|
2019-06-02 00:02:42 +00:00
|
|
|
return errors.WithStack(err)
|
2019-01-04 23:32:28 +00:00
|
|
|
}
|
|
|
|
|
2019-01-18 22:46:13 +00:00
|
|
|
createdDates, createdMsg, err := parseCreatedLayerInfo(img)
|
2019-01-17 02:06:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-01-04 23:32:28 +00:00
|
|
|
layers := v1.DescriptorProvider{}
|
|
|
|
for i, m := range m.Layers {
|
|
|
|
if m.Annotations == nil {
|
|
|
|
m.Annotations = map[string]string{}
|
|
|
|
}
|
2019-01-17 02:06:03 +00:00
|
|
|
if createdAt := createdDates[i]; createdAt != "" {
|
|
|
|
m.Annotations["buildkit/createdat"] = createdAt
|
|
|
|
}
|
2019-01-18 22:46:13 +00:00
|
|
|
if createdBy := createdMsg[i]; createdBy != "" {
|
|
|
|
m.Annotations["buildkit/description"] = createdBy
|
|
|
|
}
|
2019-01-04 23:32:28 +00:00
|
|
|
m.Annotations["containerd.io/uncompressed"] = img.Rootfs.DiffIDs[i].String()
|
|
|
|
layers[m.Digest] = v1.DescriptorProviderPair{
|
|
|
|
Descriptor: m,
|
|
|
|
Provider: ci.provider,
|
|
|
|
}
|
|
|
|
config.Layers = append(config.Layers, v1.CacheLayer{
|
|
|
|
Blob: m.Digest,
|
|
|
|
ParentIndex: i - 1,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-01-18 22:46:13 +00:00
|
|
|
dt, err = json.Marshal(config)
|
2019-01-17 02:06:03 +00:00
|
|
|
if err != nil {
|
2019-06-02 00:02:42 +00:00
|
|
|
return errors.WithStack(err)
|
2019-01-17 02:06:03 +00:00
|
|
|
}
|
2019-08-13 21:16:30 +00:00
|
|
|
cc := v1.NewCacheChains()
|
2019-01-04 23:32:28 +00:00
|
|
|
if err := v1.ParseConfig(config, layers, cc); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-08-13 21:16:30 +00:00
|
|
|
cMap[dgst] = cc
|
2019-01-04 23:32:28 +00:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}(dgst, dt)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-08-13 21:16:30 +00:00
|
|
|
cms := make([]solver.CacheManager, 0, len(cMap))
|
|
|
|
|
|
|
|
for _, cc := range cMap {
|
|
|
|
keysStorage, resultStorage, err := v1.NewCacheKeyStorage(cc, w)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cms = append(cms, solver.NewCacheManager(id, keysStorage, resultStorage))
|
2019-01-04 23:32:28 +00:00
|
|
|
}
|
2019-08-13 21:16:30 +00:00
|
|
|
|
|
|
|
return solver.NewCombinedCacheManager(cms, nil), nil
|
2019-01-04 23:32:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ci *contentCacheImporter) allDistributionManifests(ctx context.Context, dt []byte, m map[digest.Digest][]byte) error {
|
|
|
|
mt, err := imageutil.DetectManifestBlobMediaType(dt)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch mt {
|
|
|
|
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
|
|
|
m[digest.FromBytes(dt)] = dt
|
|
|
|
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
|
|
|
|
var index ocispec.Index
|
|
|
|
if err := json.Unmarshal(dt, &index); err != nil {
|
2019-06-02 00:02:42 +00:00
|
|
|
return errors.WithStack(err)
|
2019-01-04 23:32:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, d := range index.Manifests {
|
|
|
|
if _, ok := m[d.Digest]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
p, err := content.ReadBlob(ctx, ci.provider, d)
|
|
|
|
if err != nil {
|
2019-06-02 00:02:42 +00:00
|
|
|
return errors.WithStack(err)
|
2019-01-04 23:32:28 +00:00
|
|
|
}
|
|
|
|
if err := ci.allDistributionManifests(ctx, p, m); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2019-01-17 02:06:03 +00:00
|
|
|
|
|
|
|
type image struct {
|
|
|
|
Rootfs struct {
|
|
|
|
DiffIDs []digest.Digest `json:"diff_ids"`
|
|
|
|
} `json:"rootfs"`
|
|
|
|
Cache []byte `json:"moby.buildkit.cache.v0"`
|
|
|
|
History []struct {
|
|
|
|
Created *time.Time `json:"created,omitempty"`
|
2019-01-18 22:46:13 +00:00
|
|
|
CreatedBy string `json:"created_by,omitempty"`
|
2019-01-17 02:06:03 +00:00
|
|
|
EmptyLayer bool `json:"empty_layer,omitempty"`
|
|
|
|
} `json:"history,omitempty"`
|
|
|
|
}
|
|
|
|
|
2019-01-18 22:46:13 +00:00
|
|
|
func parseCreatedLayerInfo(img image) ([]string, []string, error) {
|
2019-01-17 02:06:03 +00:00
|
|
|
dates := make([]string, 0, len(img.Rootfs.DiffIDs))
|
2019-01-18 22:46:13 +00:00
|
|
|
createdBy := make([]string, 0, len(img.Rootfs.DiffIDs))
|
2019-01-17 02:06:03 +00:00
|
|
|
for _, h := range img.History {
|
|
|
|
if !h.EmptyLayer {
|
|
|
|
str := ""
|
|
|
|
if h.Created != nil {
|
|
|
|
dt, err := h.Created.MarshalText()
|
|
|
|
if err != nil {
|
2019-01-18 22:46:13 +00:00
|
|
|
return nil, nil, err
|
2019-01-17 02:06:03 +00:00
|
|
|
}
|
|
|
|
str = string(dt)
|
|
|
|
}
|
|
|
|
dates = append(dates, str)
|
2019-01-18 22:46:13 +00:00
|
|
|
createdBy = append(createdBy, h.CreatedBy)
|
2019-01-17 02:06:03 +00:00
|
|
|
}
|
|
|
|
}
|
2019-01-18 22:46:13 +00:00
|
|
|
return dates, createdBy, nil
|
2019-01-17 02:06:03 +00:00
|
|
|
}
|