lifecycle/cache/image_cache.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
}