package remotecache import ( "context" "encoding/json" "io" "sync" "time" "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" v1 "github.com/moby/buildkit/cache/remotecache/v1" "github.com/moby/buildkit/solver" "github.com/moby/buildkit/util/imageutil" "github.com/moby/buildkit/worker" digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" ) // ResolveCacheImporterFunc returns importer and descriptor. type ResolveCacheImporterFunc func(ctx context.Context, attrs map[string]string) (Importer, ocispec.Descriptor, error) type Importer interface { Resolve(ctx context.Context, desc ocispec.Descriptor, id string, w worker.Worker) (solver.CacheManager, error) } type DistributionSourceLabelSetter interface { SetDistributionSourceLabel(context.Context, digest.Digest) error SetDistributionSourceAnnotation(desc ocispec.Descriptor) ocispec.Descriptor } func NewImporter(provider content.Provider) Importer { return &contentCacheImporter{provider: provider} } type contentCacheImporter struct { provider content.Provider } 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) if err != nil { return nil, err } var mfst ocispec.Index if err := json.Unmarshal(dt, &mfst); err != nil { return nil, err } allLayers := v1.DescriptorProvider{} var configDesc ocispec.Descriptor for _, m := range mfst.Manifests { if m.MediaType == v1.CacheConfigMediaTypeV0 { configDesc = m continue } allLayers[m.Digest] = v1.DescriptorProviderPair{ Descriptor: m, Provider: ci.provider, } } if dsls, ok := ci.provider.(DistributionSourceLabelSetter); ok { for dgst, l := range allLayers { err := dsls.SetDistributionSourceLabel(ctx, dgst) _ = err // error ignored because layer may not exist l.Descriptor = dsls.SetDistributionSourceAnnotation(l.Descriptor) allLayers[dgst] = l } } if configDesc.Digest == "" { return ci.importInlineCache(ctx, dt, id, w) } dt, err = readBlob(ctx, ci.provider, configDesc) if err != nil { return nil, err } cc := v1.NewCacheChains() if err := v1.Parse(dt, allLayers, cc); err != nil { return nil, err } keysStorage, resultStorage, err := v1.NewCacheKeyStorage(cc, w) if err != nil { return nil, err } 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 } } } return dt, errors.WithStack(err) } 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 } var mu sync.Mutex var cMap = map[digest.Digest]*v1.CacheChains{} 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 { return errors.WithStack(err) } if m.Config.Digest == "" || len(m.Layers) == 0 { return nil } if dsls, ok := ci.provider.(DistributionSourceLabelSetter); ok { for i, l := range m.Layers { err := dsls.SetDistributionSourceLabel(ctx, l.Digest) _ = err // error ignored because layer may not exist m.Layers[i] = dsls.SetDistributionSourceAnnotation(l) } } p, err := content.ReadBlob(ctx, ci.provider, m.Config) if err != nil { return errors.WithStack(err) } var img image if err := json.Unmarshal(p, &img); err != nil { return errors.WithStack(err) } 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 { return errors.WithStack(err) } createdDates, createdMsg, err := parseCreatedLayerInfo(img) if err != nil { return err } layers := v1.DescriptorProvider{} for i, m := range m.Layers { if m.Annotations == nil { m.Annotations = map[string]string{} } if createdAt := createdDates[i]; createdAt != "" { m.Annotations["buildkit/createdat"] = createdAt } if createdBy := createdMsg[i]; createdBy != "" { m.Annotations["buildkit/description"] = createdBy } 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, }) } dt, err = json.Marshal(config) if err != nil { return errors.WithStack(err) } cc := v1.NewCacheChains() if err := v1.ParseConfig(config, layers, cc); err != nil { return err } mu.Lock() cMap[dgst] = cc mu.Unlock() return nil }) }(dgst, dt) } if err := eg.Wait(); err != nil { return nil, err } 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)) } return solver.NewCombinedCacheManager(cms, nil), nil } 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 { return errors.WithStack(err) } for _, d := range index.Manifests { if _, ok := m[d.Digest]; ok { continue } p, err := content.ReadBlob(ctx, ci.provider, d) if err != nil { return errors.WithStack(err) } if err := ci.allDistributionManifests(ctx, p, m); err != nil { return err } } } return nil } 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"` CreatedBy string `json:"created_by,omitempty"` EmptyLayer bool `json:"empty_layer,omitempty"` } `json:"history,omitempty"` } func parseCreatedLayerInfo(img image) ([]string, []string, error) { dates := make([]string, 0, len(img.Rootfs.DiffIDs)) createdBy := make([]string, 0, len(img.Rootfs.DiffIDs)) for _, h := range img.History { if !h.EmptyLayer { str := "" if h.Created != nil { dt, err := h.Created.MarshalText() if err != nil { return nil, nil, err } str = string(dt) } dates = append(dates, str) createdBy = append(createdBy, h.CreatedBy) } } return dates, createdBy, nil }