167 lines
4.8 KiB
Go
167 lines
4.8 KiB
Go
package cache
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"runtime"
|
|
|
|
"github.com/buildpacks/imgutil"
|
|
"github.com/buildpacks/imgutil/remote"
|
|
"github.com/google/go-containerregistry/pkg/authn"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/buildpacks/lifecycle/image"
|
|
"github.com/buildpacks/lifecycle/log"
|
|
"github.com/buildpacks/lifecycle/platform"
|
|
)
|
|
|
|
const MetadataLabel = "io.buildpacks.lifecycle.cache.metadata"
|
|
|
|
type ImageCache struct {
|
|
committed bool
|
|
origImage imgutil.Image
|
|
newImage imgutil.Image
|
|
logger log.Logger
|
|
imageDeleter ImageDeleter
|
|
}
|
|
|
|
// NewImageCache creates a new ImageCache instance
|
|
func NewImageCache(origImage imgutil.Image, newImage imgutil.Image, logger log.Logger, imageDeleter ImageDeleter) *ImageCache {
|
|
return &ImageCache{
|
|
origImage: origImage,
|
|
newImage: newImage,
|
|
logger: logger,
|
|
imageDeleter: imageDeleter,
|
|
}
|
|
}
|
|
|
|
// NewImageCacheFromName creates a new ImageCache from the name that has been provided
|
|
func NewImageCacheFromName(name string, keychain authn.Keychain, logger log.Logger, imageDeleter ImageDeleter) (*ImageCache, error) {
|
|
origImage, err := remote.NewImage(
|
|
name,
|
|
keychain,
|
|
remote.FromBaseImage(name),
|
|
remote.WithDefaultPlatform(imgutil.Platform{OS: runtime.GOOS}),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("accessing cache image %q: %v", name, err)
|
|
}
|
|
emptyImage, err := remote.NewImage(
|
|
name,
|
|
keychain,
|
|
remote.WithPreviousImage(name),
|
|
remote.WithDefaultPlatform(imgutil.Platform{OS: runtime.GOOS}),
|
|
remote.AddEmptyLayerOnSave(),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating new cache image %q: %v", name, err)
|
|
}
|
|
|
|
return NewImageCache(origImage, emptyImage, logger, imageDeleter), nil
|
|
}
|
|
|
|
func (c *ImageCache) Exists() bool {
|
|
return c.origImage.Found()
|
|
}
|
|
|
|
func (c *ImageCache) Name() string {
|
|
return c.origImage.Name()
|
|
}
|
|
|
|
func (c *ImageCache) SetMetadata(metadata platform.CacheMetadata) error {
|
|
if c.committed {
|
|
return errCacheCommitted
|
|
}
|
|
data, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return errors.Wrap(err, "serializing metadata")
|
|
}
|
|
return c.newImage.SetLabel(MetadataLabel, string(data))
|
|
}
|
|
|
|
func (c *ImageCache) RetrieveMetadata() (platform.CacheMetadata, error) {
|
|
if c.origImage.Found() && !c.origImage.Valid() {
|
|
c.logger.Infof("Ignoring cache image %q because it was corrupt", c.origImage.Name())
|
|
return platform.CacheMetadata{}, nil
|
|
}
|
|
var meta platform.CacheMetadata
|
|
if err := image.DecodeLabel(c.origImage, MetadataLabel, &meta); err != nil {
|
|
return platform.CacheMetadata{}, nil
|
|
}
|
|
return meta, nil
|
|
}
|
|
|
|
func (c *ImageCache) AddLayerFile(tarPath string, diffID string) error {
|
|
if c.committed {
|
|
return errCacheCommitted
|
|
}
|
|
return c.newImage.AddLayerWithDiffID(tarPath, diffID)
|
|
}
|
|
|
|
// isLayerNotFound checks if the error is a layer not found error
|
|
//
|
|
// FIXME: we should not have to rely on trapping ErrUnexpectedEOF.
|
|
// If a blob is not present in the registry, we should get imgutil.ErrLayerNotFound,
|
|
// but we do not and instead get io.ErrUnexpectedEOF
|
|
func isLayerNotFound(err error) bool {
|
|
var e imgutil.ErrLayerNotFound
|
|
return errors.As(err, &e) || errors.Is(err, io.ErrUnexpectedEOF)
|
|
}
|
|
|
|
func (c *ImageCache) ReuseLayer(diffID string) error {
|
|
if c.committed {
|
|
return errCacheCommitted
|
|
}
|
|
err := c.newImage.ReuseLayer(diffID)
|
|
if err != nil {
|
|
// FIXME: this path is not currently executed.
|
|
// If a blob is not present in the registry, we should get imgutil.ErrLayerNotFound.
|
|
// We should then skip attempting to reuse the layer.
|
|
// However, we do not get imgutil.ErrLayerNotFound when the blob is not present.
|
|
if isLayerNotFound(err) {
|
|
return NewReadErr(fmt.Sprintf("failed to find cache layer with SHA '%s'", diffID))
|
|
}
|
|
return fmt.Errorf("failed to reuse cache layer with SHA '%s'", diffID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RetrieveLayer retrieves a layer from the cache
|
|
func (c *ImageCache) RetrieveLayer(diffID string) (io.ReadCloser, error) {
|
|
closer, err := c.origImage.GetLayer(diffID)
|
|
if err != nil {
|
|
if isLayerNotFound(err) {
|
|
return nil, NewReadErr(fmt.Sprintf("failed to find cache layer with SHA '%s'", diffID))
|
|
}
|
|
return nil, fmt.Errorf("failed to get cache layer with SHA '%s'", diffID)
|
|
}
|
|
return closer, nil
|
|
}
|
|
|
|
func (c *ImageCache) Commit() error {
|
|
if c.committed {
|
|
return errCacheCommitted
|
|
}
|
|
|
|
if err := c.newImage.Save(); err != nil {
|
|
return errors.Wrapf(err, "saving image '%s'", c.newImage.Name())
|
|
}
|
|
c.committed = true
|
|
|
|
// Check if the cache image exists prior to saving the new cache at that same location
|
|
if c.origImage.Found() {
|
|
c.imageDeleter.DeleteOrigImageIfDifferentFromNewImage(c.origImage, c.newImage)
|
|
}
|
|
|
|
c.origImage = c.newImage
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyLayer returns an error if the layer contents do not match the provided sha.
|
|
func (c *ImageCache) VerifyLayer(_ string) error {
|
|
// we assume the registry is verifying digests for us
|
|
return nil
|
|
}
|