diff --git a/cache/caching_image_test.go b/cache/caching_image_test.go index 345acc2c..57518184 100644 --- a/cache/caching_image_test.go +++ b/cache/caching_image_test.go @@ -10,6 +10,7 @@ import ( "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/fakes" + "github.com/buildpacks/lifecycle/cmd" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -37,7 +38,7 @@ func testCachingImage(t *testing.T, when spec.G, it spec.S) { fakeImage = fakes.NewImage("some-image", "", nil) tmpDir, err = os.MkdirTemp("", "") h.AssertNil(t, err) - volumeCache, err = cache.NewVolumeCache(tmpDir) + volumeCache, err = cache.NewVolumeCache(tmpDir, cmd.DefaultLogger) h.AssertNil(t, err) subject = cache.NewCachingImage(fakeImage, volumeCache) layerPath, layerSHA, layerData = h.RandomLayer(t, tmpDir) diff --git a/cache/common.go b/cache/common.go index 8b06e362..d9d4f329 100644 --- a/cache/common.go +++ b/cache/common.go @@ -5,3 +5,21 @@ import ( ) var errCacheCommitted = errors.New("cache cannot be modified after commit") + +type ReadErr struct { + msg string +} + +func NewReadErr(msg string) ReadErr { + return ReadErr{msg: msg} +} + +func (e ReadErr) Error() string { + return e.msg +} + +func IsReadErr(err error) (bool, *ReadErr) { + var e ReadErr + isReadErr := errors.As(err, &e) + return isReadErr, &e +} diff --git a/cache/image_cache.go b/cache/image_cache.go index efdbdf2e..8ac04782 100644 --- a/cache/image_cache.go +++ b/cache/image_cache.go @@ -106,8 +106,20 @@ func (c *ImageCache) ReuseLayer(diffID string) error { return c.newImage.ReuseLayer(diffID) } +func IsLayerNotFound(err error) bool { + var e imgutil.ErrLayerNotFound + return errors.As(err, &e) +} + func (c *ImageCache) RetrieveLayer(diffID string) (io.ReadCloser, error) { - return c.origImage.GetLayer(diffID) + closer, err := c.origImage.GetLayer(diffID) + if err != nil { + if IsLayerNotFound(err) { + return nil, NewReadErr(fmt.Sprintf("Layer with SHA '%s' not found", diffID)) + } + return nil, NewReadErr(fmt.Sprintf("unknown error retrieving layer with SHA '%s'", diffID)) + } + return closer, nil } func (c *ImageCache) Commit() error { @@ -129,3 +141,26 @@ func (c *ImageCache) Commit() error { return nil } + +func (c *ImageCache) LayerExists(diffID string) (bool, error) { + layers, err := c.origImage.UnderlyingImage().Layers() + if err != nil { + return false, errors.Wrap(err, "getting image layers") + } + + for _, layer := range layers { + d, err := layer.DiffID() + if err != nil { + return false, errors.Wrap(err, "getting layer diffID") + } + + if d.String() == diffID { + return true, nil + } + } + return false, nil +} + +func (c *ImageCache) Destroy() { + c.imageDeleter.DeleteImage(c.origImage) +} diff --git a/cache/image_cache_test.go b/cache/image_cache_test.go index d3fcb2d1..67e370bf 100644 --- a/cache/image_cache_test.go +++ b/cache/image_cache_test.go @@ -146,7 +146,7 @@ func testImageCache(t *testing.T, when spec.G, it spec.S) { when("layer does not exist", func() { it("returns an error", func() { _, err := subject.RetrieveLayer("some_nonexistent_sha") - h.AssertError(t, err, "failed to get layer with sha 'some_nonexistent_sha'") + h.AssertError(t, err, "unknown error retrieving layer with SHA 'some_nonexistent_sha'") }) }) }) @@ -236,7 +236,7 @@ func testImageCache(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, subject.AddLayerFile(testLayerTarPath, testLayerSHA)) _, err := subject.RetrieveLayer(testLayerSHA) - h.AssertError(t, err, fmt.Sprintf("failed to get layer with sha '%s'", testLayerSHA)) + h.AssertError(t, err, fmt.Sprintf("unknown error retrieving layer with SHA '%s'", testLayerSHA)) }) }) }) diff --git a/cache/image_deleter.go b/cache/image_deleter.go index 8807715d..f6fc03ac 100644 --- a/cache/image_deleter.go +++ b/cache/image_deleter.go @@ -12,6 +12,7 @@ import ( // ImageDeleter defines the methods available to delete and compare cached images type ImageDeleter interface { DeleteOrigImageIfDifferentFromNewImage(origImage, newImage imgutil.Image) + DeleteImage(image imgutil.Image) } // ImageDeleterImpl is a component to manage cache image deletion @@ -35,13 +36,13 @@ func (c *ImageDeleterImpl) DeleteOrigImageIfDifferentFromNewImage(origImage, new } if !same { - c.deleteImage(origImage) + c.DeleteImage(origImage) } } } -// deleteImage deletes an image -func (c *ImageDeleterImpl) deleteImage(image imgutil.Image) { +// DeleteImage deletes an image +func (c *ImageDeleterImpl) DeleteImage(image imgutil.Image) { if c.deletionEnabled { if err := image.Delete(); err != nil { c.logger.Warnf("Unable to delete cache image: %v", err.Error()) diff --git a/cache/volume_cache.go b/cache/volume_cache.go index 210e1afb..1a551f2f 100644 --- a/cache/volume_cache.go +++ b/cache/volume_cache.go @@ -2,12 +2,14 @@ package cache import ( "encoding/json" + "fmt" "io" "os" "path/filepath" "runtime" "strings" + "github.com/buildpacks/lifecycle/log" "github.com/pkg/errors" "github.com/buildpacks/lifecycle/internal/fsutil" @@ -20,9 +22,10 @@ type VolumeCache struct { backupDir string stagingDir string committedDir string + logger log.Logger } -func NewVolumeCache(dir string) (*VolumeCache, error) { +func NewVolumeCache(dir string, logger log.Logger) (*VolumeCache, error) { if _, err := os.Stat(dir); err != nil { return nil, err } @@ -32,6 +35,7 @@ func NewVolumeCache(dir string) (*VolumeCache, error) { backupDir: filepath.Join(dir, "committed-backup"), stagingDir: filepath.Join(dir, "staging"), committedDir: filepath.Join(dir, "committed"), + logger: logger, } if err := c.setupStagingDir(); err != nil { @@ -133,7 +137,20 @@ func (c *VolumeCache) ReuseLayer(diffID string) error { if c.committed { return errCacheCommitted } - if err := os.Link(diffIDPath(c.committedDir, diffID), diffIDPath(c.stagingDir, diffID)); err != nil && !os.IsExist(err) { + committedPath := diffIDPath(c.committedDir, diffID) + stagingPath := diffIDPath(c.stagingDir, diffID) + + if _, err := os.Stat(committedPath); err != nil { + if os.IsNotExist(err) { + return NewReadErr(fmt.Sprintf("Layer with SHA '%s' not found", diffID)) + } + if os.IsPermission(err) { + return NewReadErr(fmt.Sprintf("Cannot read cache layer with SHA '%s' due to insufficient permissions", diffID)) + } + return errors.Wrapf(err, "reusing layer (%s)", diffID) + } + + if err := os.Link(committedPath, stagingPath); err != nil && !os.IsExist(err) { return errors.Wrapf(err, "reusing layer (%s)", diffID) } return nil @@ -146,7 +163,10 @@ func (c *VolumeCache) RetrieveLayer(diffID string) (io.ReadCloser, error) { } file, err := os.Open(path) if err != nil { - return nil, errors.Wrapf(err, "opening layer with SHA '%s'", diffID) + if os.IsPermission(err) { + return nil, NewReadErr(fmt.Sprintf("cannot read cache layer with SHA '%s' due to insufficient permissions", diffID)) + } + return nil, NewReadErr(fmt.Sprintf("unknown error retrieving layer with SHA '%s'", diffID)) } return file, nil } @@ -165,7 +185,7 @@ func (c *VolumeCache) RetrieveLayerFile(diffID string) (string, error) { path := diffIDPath(c.committedDir, diffID) if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { - return "", errors.Wrapf(err, "layer with SHA '%s' not found", diffID) + return "", NewReadErr(fmt.Sprintf("Layer with SHA '%s' not found", diffID)) } return "", errors.Wrapf(err, "retrieving layer with SHA '%s'", diffID) } @@ -206,3 +226,20 @@ func (c *VolumeCache) setupStagingDir() error { } return os.MkdirAll(c.stagingDir, 0777) } + +func (c *VolumeCache) LayerExists(diffID string) (bool, error) { + path := diffIDPath(c.committedDir, diffID) + if _, err := os.Stat(path); 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) Destroy() { + if err := os.RemoveAll(c.dir); err != nil { + c.logger.Warnf("Unable to delete cache directory: %v", err.Error()) + } +} diff --git a/cache/volume_cache_test.go b/cache/volume_cache_test.go index 3ab97ffe..cbcea570 100644 --- a/cache/volume_cache_test.go +++ b/cache/volume_cache_test.go @@ -7,6 +7,8 @@ import ( "path/filepath" "testing" + "github.com/buildpacks/lifecycle/cmd" + "github.com/buildpacks/lifecycle/log" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -28,6 +30,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { backupDir string stagingDir string committedDir string + testLogger log.Logger ) it.Before(func() { @@ -42,6 +45,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { backupDir = filepath.Join(volumeDir, "committed-backup") stagingDir = filepath.Join(volumeDir, "staging") committedDir = filepath.Join(volumeDir, "committed") + testLogger = cmd.DefaultLogger }) it.After(func() { @@ -50,7 +54,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { when("#NewVolumeCache", func() { it("returns an error when the volume path does not exist", func() { - _, err := cache.NewVolumeCache(filepath.Join(tmpDir, "does_not_exist")) + _, err := cache.NewVolumeCache(filepath.Join(tmpDir, "does_not_exist"), testLogger) if err == nil { t.Fatal("expected NewVolumeCache to fail because volume path does not exist") } @@ -66,7 +70,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { it("clears staging", func() { var err error - subject, err = cache.NewVolumeCache(volumeDir) + subject, err = cache.NewVolumeCache(volumeDir, testLogger) h.AssertNil(t, err) _, err = os.Stat(filepath.Join(stagingDir, "some-layer.tar")) @@ -80,7 +84,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { it("creates staging dir", func() { var err error - subject, err = cache.NewVolumeCache(volumeDir) + subject, err = cache.NewVolumeCache(volumeDir, testLogger) h.AssertNil(t, err) _, err = os.Stat(stagingDir) @@ -92,7 +96,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { it("creates committed dir", func() { var err error - subject, err = cache.NewVolumeCache(volumeDir) + subject, err = cache.NewVolumeCache(volumeDir, testLogger) h.AssertNil(t, err) _, err = os.Stat(committedDir) @@ -109,7 +113,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { it("clears the backup dir", func() { var err error - subject, err = cache.NewVolumeCache(volumeDir) + subject, err = cache.NewVolumeCache(volumeDir, testLogger) h.AssertNil(t, err) _, err = os.Stat(filepath.Join(backupDir, "some-layer.tar")) @@ -124,7 +128,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { it.Before(func() { var err error - subject, err = cache.NewVolumeCache(volumeDir) + subject, err = cache.NewVolumeCache(volumeDir, testLogger) h.AssertNil(t, err) }) @@ -206,7 +210,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { when("layer does not exist", func() { it("returns an error", func() { _, err := subject.RetrieveLayer("some_nonexistent_sha") - h.AssertError(t, err, "layer with SHA 'some_nonexistent_sha' not found") + h.AssertError(t, err, "Layer with SHA 'some_nonexistent_sha' not found") }) }) }) @@ -230,7 +234,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { when("layer does not exist", func() { it("returns an error", func() { _, err := subject.RetrieveLayerFile("some_nonexistent_sha") - h.AssertError(t, err, "layer with SHA 'some_nonexistent_sha' not found") + h.AssertError(t, err, "Layer with SHA 'some_nonexistent_sha' not found") }) }) }) @@ -340,7 +344,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, subject.AddLayerFile(tarPath, "some_sha")) _, err := subject.RetrieveLayer("some_sha") - h.AssertError(t, err, "layer with SHA 'some_sha' not found") + h.AssertError(t, err, "Layer with SHA 'some_sha' not found") }) }) @@ -415,7 +419,7 @@ func testVolumeCache(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, subject.AddLayer(layerReader, layerSha)) _, err := subject.RetrieveLayer(layerSha) - h.AssertError(t, err, fmt.Sprintf("layer with SHA '%s' not found", layerSha)) + h.AssertError(t, err, fmt.Sprintf("Layer with SHA '%s' not found", layerSha)) }) }) diff --git a/cmd/lifecycle/exporter.go b/cmd/lifecycle/exporter.go index 30518661..10d97cba 100644 --- a/cmd/lifecycle/exporter.go +++ b/cmd/lifecycle/exporter.go @@ -12,6 +12,7 @@ import ( "github.com/buildpacks/imgutil/layout" "github.com/buildpacks/imgutil/local" "github.com/buildpacks/imgutil/remote" + "github.com/buildpacks/lifecycle/log" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -200,7 +201,7 @@ func (e *exportCmd) export(group buildpack.Group, cacheStore phase.Cache, analyz case e.UseLayout: appImage, runImageID, err = e.initLayoutAppImage(analyzedMD) case e.UseDaemon: - appImage, runImageID, err = e.initDaemonAppImage(analyzedMD) + appImage, runImageID, err = e.initDaemonAppImage(analyzedMD, cmd.DefaultLogger) default: appImage, runImageID, err = e.initRemoteAppImage(analyzedMD) } @@ -258,7 +259,7 @@ func (e *exportCmd) export(group buildpack.Group, cacheStore phase.Cache, analyz return nil } -func (e *exportCmd) initDaemonAppImage(analyzedMD files.Analyzed) (imgutil.Image, string, error) { +func (e *exportCmd) initDaemonAppImage(analyzedMD files.Analyzed, logger log.Logger) (imgutil.Image, string, error) { var opts = []imgutil.ImageOption{ local.FromBaseImage(e.RunImageRef), } @@ -301,7 +302,7 @@ func (e *exportCmd) initDaemonAppImage(analyzedMD files.Analyzed) (imgutil.Image } if e.LaunchCacheDir != "" { - volumeCache, err := cache.NewVolumeCache(e.LaunchCacheDir) + volumeCache, err := cache.NewVolumeCache(e.LaunchCacheDir, logger) if err != nil { return nil, "", cmd.FailErr(err, "create launch cache") } diff --git a/cmd/lifecycle/main.go b/cmd/lifecycle/main.go index 74b51a01..42fede72 100644 --- a/cmd/lifecycle/main.go +++ b/cmd/lifecycle/main.go @@ -96,14 +96,14 @@ func (ch *DefaultCacheHandler) InitCache(cacheImageRef string, cacheDir string, cacheStore phase.Cache err error ) + logger := cmd.DefaultLogger if cacheImageRef != "" { - logger := cmd.DefaultLogger cacheStore, err = cache.NewImageCacheFromName(cacheImageRef, ch.keychain, logger, cache.NewImageDeleter(cache.NewImageComparer(), logger, deletionEnabled)) if err != nil { return nil, errors.Wrap(err, "creating image cache") } } else if cacheDir != "" { - cacheStore, err = cache.NewVolumeCache(cacheDir) + cacheStore, err = cache.NewVolumeCache(cacheDir, logger) if err != nil { return nil, errors.Wrap(err, "creating volume cache") } @@ -118,14 +118,14 @@ func initCache(cacheImageTag, cacheDir string, keychain authn.Keychain, deletion cacheStore phase.Cache err error ) + logger := cmd.DefaultLogger if cacheImageTag != "" { - logger := cmd.DefaultLogger cacheStore, err = cache.NewImageCacheFromName(cacheImageTag, keychain, logger, cache.NewImageDeleter(cache.NewImageComparer(), logger, deletionEnabled)) if err != nil { return nil, cmd.FailErr(err, "create image cache") } } else if cacheDir != "" { - cacheStore, err = cache.NewVolumeCache(cacheDir) + cacheStore, err = cache.NewVolumeCache(cacheDir, logger) if err != nil { return nil, cmd.FailErr(err, "create volume cache") } diff --git a/internal/layer/sbom_restorer_test.go b/internal/layer/sbom_restorer_test.go index 544b3f29..f542f00e 100644 --- a/internal/layer/sbom_restorer_test.go +++ b/internal/layer/sbom_restorer_test.go @@ -164,7 +164,7 @@ func testSBOMRestorer(t *testing.T, when spec.G, it spec.S) { cacheDir, err = os.MkdirTemp("", "lifecycle.cache-dir.") h.AssertNil(t, err) - testCache, err = cache.NewVolumeCache(cacheDir) + testCache, err = cache.NewVolumeCache(cacheDir, &log.Logger{Handler: &discard.Handler{}}) h.AssertNil(t, err) h.AssertNil(t, testCache.AddLayerFile(layer.TarPath, layer.Digest)) h.AssertNil(t, testCache.SetMetadata(platform.CacheMetadata{BOM: files.LayerMetadata{SHA: layer.Digest}})) diff --git a/phase/analyzer.go b/phase/analyzer.go index 0217411b..c5d4ef6a 100644 --- a/phase/analyzer.go +++ b/phase/analyzer.go @@ -43,7 +43,7 @@ func (f *ConnectedFactory) NewAnalyzer(inputs platform.LifecycleInputs, logger l } var err error - if analyzer.PreviousImage, err = f.getPreviousImage(inputs.PreviousImageRef, inputs.LaunchCacheDir); err != nil { + if analyzer.PreviousImage, err = f.getPreviousImage(inputs.PreviousImageRef, inputs.LaunchCacheDir, logger); err != nil { return nil, err } if analyzer.RunImage, err = f.getRunImage(inputs.RunImageRef); err != nil { diff --git a/phase/analyzer_test.go b/phase/analyzer_test.go index 71f965f0..87f2518a 100644 --- a/phase/analyzer_test.go +++ b/phase/analyzer_test.go @@ -332,6 +332,7 @@ func testAnalyzer(platformAPI string) func(t *testing.T, when spec.G, it spec.S) it.Before(func() { var err error + discardLogger := log.Logger{Handler: &discard.Handler{}} tmpDir, err = os.MkdirTemp("", "analyzer-tests") h.AssertNil(t, err) @@ -342,15 +343,13 @@ func testAnalyzer(platformAPI string) func(t *testing.T, when spec.G, it spec.S) cacheDir, err = os.MkdirTemp("", "some-cache-dir") h.AssertNil(t, err) - testCache, err = cache.NewVolumeCache(cacheDir) + testCache, err = cache.NewVolumeCache(cacheDir, &discardLogger) h.AssertNil(t, err) previousImage = fakes.NewImage("image-repo-name", "", local.IDIdentifier{ ImageID: "s0m3D1g3sT", }) - discardLogger := log.Logger{Handler: &discard.Handler{}} - mockCtrl = gomock.NewController(t) sbomRestorer = testmock.NewMockSBOMRestorer(mockCtrl) diff --git a/phase/cache.go b/phase/cache.go index 16685d97..3d7bff77 100644 --- a/phase/cache.go +++ b/phase/cache.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/buildpacks/lifecycle/buildpack" + c "github.com/buildpacks/lifecycle/cache" "github.com/buildpacks/lifecycle/layers" "github.com/buildpacks/lifecycle/log" "github.com/buildpacks/lifecycle/platform" @@ -100,7 +101,15 @@ func (e *Exporter) addOrReuseCacheLayer(cache Cache, layerDir LayerDir, previous if layer.Digest == previousSHA { e.Logger.Infof("Reusing cache layer '%s'\n", layer.ID) e.Logger.Debugf("Layer '%s' SHA: %s\n", layer.ID, layer.Digest) - return layer.Digest, cache.ReuseLayer(previousSHA) + err = cache.ReuseLayer(previousSHA) + if err != nil { + isReadErr, readErr := c.IsReadErr(err) + if !isReadErr { + return "", errors.Wrapf(err, "reusing layer %s", layer.ID) + } else { + e.Logger.Warnf("%s, skipping reuse", readErr.Error()) + } + } } e.Logger.Infof("Adding cache layer '%s'\n", layer.ID) e.Logger.Debugf("Layer '%s' SHA: %s\n", layer.ID, layer.Digest) diff --git a/phase/cache_test.go b/phase/cache_test.go index 03971e7c..bf73376a 100644 --- a/phase/cache_test.go +++ b/phase/cache_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/apex/log" + "github.com/apex/log/handlers/discard" "github.com/apex/log/handlers/memory" "github.com/golang/mock/gomock" "github.com/sclevine/spec" @@ -46,6 +47,10 @@ func testCache(t *testing.T, when spec.G, it spec.S) { mockCtrl = gomock.NewController(t) layerFactory = testmock.NewMockLayerFactory(mockCtrl) + logHandler = memory.New() + level, err := log.ParseLevel("info") + h.AssertNil(t, err) + tmpDir, err = os.MkdirTemp("", "lifecycle.cacher.layer") h.AssertNil(t, err) h.AssertNil(t, os.Mkdir(filepath.Join(tmpDir, "artifacts"), 0777)) @@ -53,11 +58,7 @@ func testCache(t *testing.T, when spec.G, it spec.S) { cacheDir = filepath.Join(tmpDir, "cache") h.AssertNil(t, os.Mkdir(cacheDir, 0777)) - testCache, err = cache.NewVolumeCache(cacheDir) - h.AssertNil(t, err) - - logHandler = memory.New() - level, err := log.ParseLevel("info") + testCache, err = cache.NewVolumeCache(cacheDir, &log.Logger{Handler: logHandler, Level: level}) h.AssertNil(t, err) exporter = &phase.Exporter{ @@ -300,13 +301,15 @@ func assertCacheHasLayer(t *testing.T, cache phase.Cache, id string) { } func initializeCache(t *testing.T, exporter *phase.Exporter, testCache *phase.Cache, cacheDir, layersDir, metadataTemplate string) { - previousCache, err := cache.NewVolumeCache(cacheDir) + logger := &log.Logger{Handler: &discard.Handler{}} + + previousCache, err := cache.NewVolumeCache(cacheDir, logger) h.AssertNil(t, err) err = exporter.Cache(layersDir, previousCache) h.AssertNil(t, err) - *testCache, err = cache.NewVolumeCache(cacheDir) + *testCache, err = cache.NewVolumeCache(cacheDir, logger) h.AssertNil(t, err) h.AssertNil(t, os.WriteFile( diff --git a/phase/connected_factory.go b/phase/connected_factory.go index 3514938a..be749a71 100644 --- a/phase/connected_factory.go +++ b/phase/connected_factory.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/buildpacks/imgutil" + "github.com/buildpacks/lifecycle/log" "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/cache" @@ -58,7 +59,7 @@ func (f *ConnectedFactory) ensureRegistryAccess(inputs platform.LifecycleInputs) return nil } -func (f *ConnectedFactory) getPreviousImage(imageRef string, launchCacheDir string) (imgutil.Image, error) { +func (f *ConnectedFactory) getPreviousImage(imageRef string, launchCacheDir string, logger log.Logger) (imgutil.Image, error) { if imageRef == "" { return nil, nil } @@ -69,7 +70,7 @@ func (f *ConnectedFactory) getPreviousImage(imageRef string, launchCacheDir stri if launchCacheDir == "" || f.imageHandler.Kind() != image.LocalKind { return previousImage, nil } - volumeCache, err := cache.NewVolumeCache(launchCacheDir) + volumeCache, err := cache.NewVolumeCache(launchCacheDir, logger) if err != nil { return nil, fmt.Errorf("creating launch cache: %w", err) } diff --git a/phase/exporter.go b/phase/exporter.go index 4808e857..a6bce1dd 100644 --- a/phase/exporter.go +++ b/phase/exporter.go @@ -34,7 +34,9 @@ type Cache interface { AddLayerFile(tarPath string, sha string) error ReuseLayer(sha string) error RetrieveLayer(sha string) (io.ReadCloser, error) + LayerExists(sha string) (bool, error) Commit() error + Destroy() } type Exporter struct { diff --git a/phase/restorer.go b/phase/restorer.go index 48b6da59..b7dd26c9 100644 --- a/phase/restorer.go +++ b/phase/restorer.go @@ -3,6 +3,7 @@ package phase import ( "path/filepath" + c "github.com/buildpacks/lifecycle/cache" "github.com/pkg/errors" "golang.org/x/sync/errgroup" @@ -98,7 +99,16 @@ func (r *Restorer) Restore(cache Cache) error { } else { r.Logger.Infof("Restoring data for %q from cache", bpLayer.Identifier()) g.Go(func() error { - return r.restoreCacheLayer(cache, cachedLayer.SHA) + err = r.restoreCacheLayer(cache, cachedLayer.SHA) + if err != nil { + isReadErr, readErr := c.IsReadErr(err) + if isReadErr { + r.Logger.Warnf("%s, skipping restore", readErr.Error()) + return nil + } + return errors.Wrapf(err, "restoring layer %s", bpLayer.Identifier()) + } + return nil }) } } diff --git a/phase/restorer_test.go b/phase/restorer_test.go index 9a7b2730..ececcda2 100644 --- a/phase/restorer_test.go +++ b/phase/restorer_test.go @@ -54,6 +54,8 @@ func testRestorer(buildpackAPI, platformAPI string) func(t *testing.T, when spec it.Before(func() { var err error + logHandler = memory.New() + logger := log.Logger{Handler: logHandler, Level: log.DebugLevel} layersDir, err = os.MkdirTemp("", "lifecycle-layer-dir") h.AssertNil(t, err) @@ -61,13 +63,9 @@ func testRestorer(buildpackAPI, platformAPI string) func(t *testing.T, when spec cacheDir, err = os.MkdirTemp("", "") h.AssertNil(t, err) - testCache, err = cache.NewVolumeCache(cacheDir) + testCache, err = cache.NewVolumeCache(cacheDir, &logger) h.AssertNil(t, err) - logHandler = memory.New() - - logger := log.Logger{Handler: logHandler, Level: log.DebugLevel} - mockCtrl = gomock.NewController(t) sbomRestorer = testmock.NewMockSBOMRestorer(mockCtrl) if api.MustParse(platformAPI).AtLeast("0.8") { diff --git a/phase/testmock/cache/image_deleter.go b/phase/testmock/cache/image_deleter.go index f4a6051e..39d6a4b5 100644 --- a/phase/testmock/cache/image_deleter.go +++ b/phase/testmock/cache/image_deleter.go @@ -34,6 +34,18 @@ func (m *MockImageDeleter) EXPECT() *MockImageDeleterMockRecorder { return m.recorder } +// DeleteImage mocks base method. +func (m *MockImageDeleter) DeleteImage(arg0 imgutil.Image) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteImage", arg0) +} + +// DeleteImage indicates an expected call of DeleteImage. +func (mr *MockImageDeleterMockRecorder) DeleteImage(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteImage", reflect.TypeOf((*MockImageDeleter)(nil).DeleteImage), arg0) +} + // DeleteOrigImageIfDifferentFromNewImage mocks base method. func (m *MockImageDeleter) DeleteOrigImageIfDifferentFromNewImage(arg0, arg1 imgutil.Image) { m.ctrl.T.Helper()