253 lines
6.4 KiB
Go
253 lines
6.4 KiB
Go
package cache
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/buildpacks/lifecycle/log"
|
|
|
|
"github.com/buildpacks/lifecycle/internal/fsutil"
|
|
"github.com/buildpacks/lifecycle/platform"
|
|
)
|
|
|
|
type VolumeCache struct {
|
|
committed bool
|
|
dir string
|
|
backupDir string
|
|
stagingDir string
|
|
committedDir string
|
|
logger log.Logger
|
|
}
|
|
|
|
// NewVolumeCache creates a new VolumeCache
|
|
func NewVolumeCache(dir string, logger log.Logger) (*VolumeCache, error) {
|
|
if _, err := os.Stat(dir); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := &VolumeCache{
|
|
dir: dir,
|
|
backupDir: filepath.Join(dir, "committed-backup"),
|
|
stagingDir: filepath.Join(dir, "staging"),
|
|
committedDir: filepath.Join(dir, "committed"),
|
|
logger: logger,
|
|
}
|
|
|
|
if err := c.setupStagingDir(); err != nil {
|
|
return nil, errors.Wrapf(err, "initializing staging directory '%s'", c.stagingDir)
|
|
}
|
|
|
|
if err := os.RemoveAll(c.backupDir); err != nil {
|
|
return nil, errors.Wrapf(err, "removing backup directory '%s'", c.backupDir)
|
|
}
|
|
|
|
if err := os.MkdirAll(c.committedDir, 0777); err != nil {
|
|
return nil, errors.Wrapf(err, "creating committed directory '%s'", c.committedDir)
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func (c *VolumeCache) Exists() bool {
|
|
if _, err := os.Stat(c.committedDir); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *VolumeCache) Name() string {
|
|
return c.dir
|
|
}
|
|
|
|
func (c *VolumeCache) SetMetadata(metadata platform.CacheMetadata) error {
|
|
if c.committed {
|
|
return errCacheCommitted
|
|
}
|
|
metadataPath := filepath.Join(c.stagingDir, MetadataLabel)
|
|
file, err := os.Create(metadataPath)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "creating metadata file '%s'", metadataPath)
|
|
}
|
|
defer file.Close()
|
|
|
|
if err := json.NewEncoder(file).Encode(metadata); err != nil {
|
|
return errors.Wrap(err, "marshalling metadata")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *VolumeCache) RetrieveMetadata() (platform.CacheMetadata, error) {
|
|
metadataPath := filepath.Join(c.committedDir, MetadataLabel)
|
|
file, err := os.Open(metadataPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return platform.CacheMetadata{}, nil
|
|
}
|
|
return platform.CacheMetadata{}, errors.Wrapf(err, "opening metadata file '%s'", metadataPath)
|
|
}
|
|
defer file.Close()
|
|
|
|
metadata := platform.CacheMetadata{}
|
|
if json.NewDecoder(file).Decode(&metadata) != nil {
|
|
return platform.CacheMetadata{}, nil
|
|
}
|
|
return metadata, nil
|
|
}
|
|
|
|
func (c *VolumeCache) AddLayerFile(tarPath string, diffID string) error {
|
|
if c.committed {
|
|
return errCacheCommitted
|
|
}
|
|
layerTar := diffIDPath(c.stagingDir, diffID)
|
|
if _, err := os.Stat(layerTar); err == nil {
|
|
// don't waste time rewriting an identical layer
|
|
return nil
|
|
}
|
|
|
|
if err := fsutil.Copy(tarPath, layerTar); err != nil {
|
|
return errors.Wrapf(err, "caching layer (%s)", diffID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *VolumeCache) AddLayer(rc io.ReadCloser, diffID string) error {
|
|
if c.committed {
|
|
return errCacheCommitted
|
|
}
|
|
|
|
fh, err := os.Create(diffIDPath(c.stagingDir, diffID))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "create layer file in cache")
|
|
}
|
|
defer fh.Close()
|
|
|
|
if _, err := io.Copy(fh, rc); err != nil {
|
|
return errors.Wrap(err, "copying layer to tar file")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *VolumeCache) ReuseLayer(diffID string) error {
|
|
if c.committed {
|
|
return errCacheCommitted
|
|
}
|
|
committedPath := diffIDPath(c.committedDir, diffID)
|
|
stagingPath := diffIDPath(c.stagingDir, diffID)
|
|
|
|
if _, err := os.Stat(committedPath); err != nil {
|
|
if err = handleFileError(err, diffID); errors.Is(err, ReadErr{}) {
|
|
return err
|
|
}
|
|
return fmt.Errorf("failed to re-use cache layer with SHA '%s': %w", diffID, err)
|
|
}
|
|
|
|
if err := os.Link(committedPath, stagingPath); err != nil && !os.IsExist(err) {
|
|
return errors.Wrapf(err, "reusing layer (%s)", diffID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *VolumeCache) RetrieveLayer(diffID string) (io.ReadCloser, error) {
|
|
path, err := c.RetrieveLayerFile(diffID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
if err = handleFileError(err, diffID); errors.Is(err, ReadErr{}) {
|
|
return nil, err
|
|
}
|
|
return nil, fmt.Errorf("failed to get cache layer with SHA '%s'", diffID)
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func (c *VolumeCache) HasLayer(diffID string) (bool, error) {
|
|
if _, err := os.Stat(diffIDPath(c.committedDir, diffID)); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, errors.Wrapf(err, "retrieving layer with SHA '%s'", diffID)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (c *VolumeCache) RetrieveLayerFile(diffID string) (string, error) {
|
|
path := diffIDPath(c.committedDir, diffID)
|
|
if _, err := os.Stat(path); err != nil {
|
|
if err = handleFileError(err, diffID); errors.Is(err, ReadErr{}) {
|
|
return "", err
|
|
}
|
|
return "", errors.Wrapf(err, "retrieving layer with SHA '%s'", diffID)
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (c *VolumeCache) Commit() error {
|
|
if c.committed {
|
|
return errCacheCommitted
|
|
}
|
|
c.committed = true
|
|
if err := os.Rename(c.committedDir, c.backupDir); err != nil {
|
|
return errors.Wrap(err, "backing up cache")
|
|
}
|
|
defer os.RemoveAll(c.backupDir)
|
|
|
|
if err1 := os.Rename(c.stagingDir, c.committedDir); err1 != nil {
|
|
if err2 := os.Rename(c.backupDir, c.committedDir); err2 != nil {
|
|
return errors.Wrap(err2, "rolling back cache")
|
|
}
|
|
return errors.Wrap(err1, "committing cache")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func diffIDPath(basePath, diffID string) string {
|
|
return filepath.Join(basePath, diffID+".tar")
|
|
}
|
|
|
|
func (c *VolumeCache) setupStagingDir() error {
|
|
if err := os.RemoveAll(c.stagingDir); err != nil {
|
|
return err
|
|
}
|
|
return os.MkdirAll(c.stagingDir, 0777)
|
|
}
|
|
|
|
// VerifyLayer returns an error if the layer contents do not match the provided sha.
|
|
func (c *VolumeCache) VerifyLayer(diffID string) error {
|
|
layerRC, err := c.RetrieveLayer(diffID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
_ = layerRC.Close()
|
|
}()
|
|
hasher := sha256.New()
|
|
if _, err := io.Copy(hasher, layerRC); err != nil {
|
|
return errors.Wrap(err, "hashing layer")
|
|
}
|
|
foundDiffID := fmt.Sprintf("sha256:%x", hasher.Sum(nil))
|
|
if diffID != foundDiffID {
|
|
return NewReadErr(fmt.Sprintf("expected layer contents to have SHA '%s'; found '%s'", diffID, foundDiffID))
|
|
}
|
|
return err
|
|
}
|
|
|
|
func handleFileError(err error, diffID string) error {
|
|
if os.IsNotExist(err) {
|
|
return NewReadErr(fmt.Sprintf("failed to find cache layer with SHA '%s'", diffID))
|
|
}
|
|
if os.IsPermission(err) {
|
|
return NewReadErr(fmt.Sprintf("failed to read cache layer with SHA '%s' due to insufficient permissions", diffID))
|
|
}
|
|
return err
|
|
}
|