lifecycle/phase/analyzer_test.go

527 lines
18 KiB
Go

package phase_test
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/apex/log"
"github.com/apex/log/handlers/discard"
"github.com/buildpacks/imgutil/fakes"
"github.com/buildpacks/imgutil/local"
"github.com/golang/mock/gomock"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
"github.com/buildpacks/lifecycle/api"
"github.com/buildpacks/lifecycle/cache"
"github.com/buildpacks/lifecycle/cmd"
"github.com/buildpacks/lifecycle/image"
"github.com/buildpacks/lifecycle/internal/layer"
"github.com/buildpacks/lifecycle/phase"
"github.com/buildpacks/lifecycle/phase/testmock"
"github.com/buildpacks/lifecycle/platform"
"github.com/buildpacks/lifecycle/platform/files"
h "github.com/buildpacks/lifecycle/testhelpers"
)
func TestAnalyzer(t *testing.T) {
spec.Run(t, "unit-new-analyzer/", testAnalyzerFactory, spec.Parallel(), spec.Report(report.Terminal{}))
for _, platformAPI := range api.Platform.Supported {
spec.Run(t, "unit-analyzer/"+platformAPI.String(), testAnalyzer(platformAPI.String()), spec.Parallel(), spec.Report(report.Terminal{}))
}
}
func testAnalyzerFactory(t *testing.T, when spec.G, it spec.S) {
when("#NewAnalyzer", func() {
var (
analyzerFactory *phase.ConnectedFactory
fakeAPIVerifier *testmock.MockBuildpackAPIVerifier
fakeCacheHandler *testmock.MockCacheHandler
fakeConfigHandler *testmock.MockConfigHandler
fakeImageHandler *testmock.MockHandler
fakeRegistryHandler *testmock.MockRegistryHandler
logger *log.Logger
mockController *gomock.Controller
tempDir string
)
it.Before(func() {
mockController = gomock.NewController(t)
fakeAPIVerifier = testmock.NewMockBuildpackAPIVerifier(mockController)
fakeCacheHandler = testmock.NewMockCacheHandler(mockController)
fakeConfigHandler = testmock.NewMockConfigHandler(mockController)
fakeImageHandler = testmock.NewMockHandler(mockController)
fakeRegistryHandler = testmock.NewMockRegistryHandler(mockController)
logger = &log.Logger{Handler: &discard.Handler{}}
var err error
tempDir, err = os.MkdirTemp("", "")
h.AssertNil(t, err)
})
it.After(func() {
mockController.Finish()
_ = os.RemoveAll(tempDir)
})
when("platform api >= 0.8", func() {
it.Before(func() {
analyzerFactory = phase.NewConnectedFactory(
api.Platform.Latest(),
fakeAPIVerifier,
fakeCacheHandler,
fakeConfigHandler,
fakeImageHandler,
fakeRegistryHandler,
)
})
when("layout case", func() {
it("configures the analyzer", func() {
previousImage := fakes.NewImage("some-previous-image-ref", "", nil)
runImage := fakes.NewImage("some-run-image-ref", "", nil)
t.Log("ensures registry access")
fakeImageHandler.EXPECT().Kind().Return(image.LayoutKind).AnyTimes()
// Only caching must be checked for writing access
fakeRegistryHandler.EXPECT().EnsureWriteAccess([]string{"some-cache-image-ref"})
// we don't expect any read access check when -layout is used
fakeRegistryHandler.EXPECT().EnsureReadAccess([]string{})
t.Log("does not process cache")
t.Log("processes previous image")
fakeImageHandler.EXPECT().InitImage("some-previous-image-ref").Return(previousImage, nil)
t.Log("processes run image")
fakeImageHandler.EXPECT().InitImage("some-run-image-ref").Return(runImage, nil)
analyzer, err := analyzerFactory.NewAnalyzer(platform.LifecycleInputs{
AdditionalTags: []string{"some-additional-tag"},
CacheImageRef: "some-cache-image-ref",
LaunchCacheDir: "some-launch-cache-dir",
LayersDir: "some-layers-dir",
OutputImageRef: "some-output-image-ref",
PreviousImageRef: "some-previous-image-ref",
RunImageRef: "some-run-image-ref",
SkipLayers: false,
}, logger)
h.AssertNil(t, err)
h.AssertEq(t, analyzer.PreviousImage.Name(), previousImage.Name())
h.AssertEq(t, analyzer.RunImage.Name(), runImage.Name())
t.Log("restores sbom data")
sbomRestorer, ok := analyzer.SBOMRestorer.(*layer.DefaultSBOMRestorer)
h.AssertEq(t, ok, true)
h.AssertEq(t, sbomRestorer.LayersDir, "some-layers-dir")
h.AssertEq(t, sbomRestorer.Logger, logger)
t.Log("sets logger")
h.AssertEq(t, analyzer.Logger, logger)
})
})
it("configures the analyzer", func() {
previousImage := fakes.NewImage("some-previous-image-ref", "", nil)
runImage := fakes.NewImage("some-run-image-ref", "", nil)
t.Log("ensures registry access")
fakeImageHandler.EXPECT().Kind().Return(image.RemoteKind).AnyTimes()
fakeRegistryHandler.EXPECT().EnsureReadAccess([]string{"some-previous-image-ref", "some-run-image-ref"})
fakeRegistryHandler.EXPECT().EnsureWriteAccess([]string{"some-cache-image-ref", "some-output-image-ref", "some-additional-tag"})
t.Log("does not process cache")
t.Log("processes previous image")
fakeImageHandler.EXPECT().InitImage("some-previous-image-ref").Return(previousImage, nil)
t.Log("processes run image")
fakeImageHandler.EXPECT().InitImage("some-run-image-ref").Return(runImage, nil)
analyzer, err := analyzerFactory.NewAnalyzer(platform.LifecycleInputs{
AdditionalTags: []string{"some-additional-tag"},
CacheImageRef: "some-cache-image-ref",
LaunchCacheDir: "some-launch-cache-dir",
LayersDir: "some-layers-dir",
OutputImageRef: "some-output-image-ref",
PreviousImageRef: "some-previous-image-ref",
RunImageRef: "some-run-image-ref",
SkipLayers: false,
}, logger)
h.AssertNil(t, err)
h.AssertEq(t, analyzer.PreviousImage.Name(), previousImage.Name())
h.AssertEq(t, analyzer.RunImage.Name(), runImage.Name())
t.Log("restores sbom data")
sbomRestorer, ok := analyzer.SBOMRestorer.(*layer.DefaultSBOMRestorer)
h.AssertEq(t, ok, true)
h.AssertEq(t, sbomRestorer.LayersDir, "some-layers-dir")
h.AssertEq(t, sbomRestorer.Logger, logger)
h.AssertEq(t, analyzer.PlatformAPI, api.Platform.Latest())
t.Log("sets logger")
h.AssertEq(t, analyzer.Logger, logger)
})
when("daemon case", func() {
it("configures the analyzer", func() {
previousImage := fakes.NewImage("some-previous-image-ref", "", nil)
runImage := fakes.NewImage("some-run-image-ref", "", nil)
t.Log("ensures registry access")
fakeImageHandler.EXPECT().Kind().Return(image.LocalKind).AnyTimes()
fakeRegistryHandler.EXPECT().EnsureReadAccess()
fakeRegistryHandler.EXPECT().EnsureWriteAccess([]string{"some-cache-image-ref"})
t.Log("processes previous image")
fakeImageHandler.EXPECT().InitImage("some-previous-image-ref").Return(previousImage, nil)
t.Log("processes run image")
fakeImageHandler.EXPECT().InitImage("some-run-image-ref").Return(runImage, nil)
launchCacheDir := filepath.Join(tempDir, "some-launch-cache-dir")
h.AssertNil(t, os.MkdirAll(launchCacheDir, 0777))
analyzer, err := analyzerFactory.NewAnalyzer(platform.LifecycleInputs{
AdditionalTags: []string{"some-additional-tag"},
CacheImageRef: "some-cache-image-ref",
LaunchCacheDir: launchCacheDir,
LayersDir: "some-layers-dir",
OutputImageRef: "some-output-image-ref",
PreviousImageRef: "some-previous-image-ref",
RunImageRef: "some-run-image-ref",
SkipLayers: false,
}, logger)
h.AssertNil(t, err)
h.AssertEq(t, analyzer.PreviousImage.Name(), previousImage.Name())
h.AssertEq(t, analyzer.RunImage.Name(), runImage.Name())
t.Log("uses the provided launch cache")
_, ok := analyzer.PreviousImage.(*cache.CachingImage)
h.AssertEq(t, ok, true)
h.AssertPathExists(t, filepath.Join(launchCacheDir, "committed"))
h.AssertPathExists(t, filepath.Join(launchCacheDir, "staging"))
})
})
when("skip layers", func() {
it("does not restore sbom data", func() {
fakeImageHandler.EXPECT().Kind().Return(image.RemoteKind).AnyTimes()
fakeRegistryHandler.EXPECT().EnsureReadAccess(gomock.Any())
fakeRegistryHandler.EXPECT().EnsureWriteAccess(gomock.Any())
fakeImageHandler.EXPECT().InitImage(gomock.Any())
fakeImageHandler.EXPECT().InitImage(gomock.Any())
analyzer, err := analyzerFactory.NewAnalyzer(platform.LifecycleInputs{
AdditionalTags: []string{"some-additional-tag"},
CacheImageRef: "some-cache-image-ref",
LaunchCacheDir: "some-launch-cache-dir",
LayersDir: "some-layers-dir",
OutputImageRef: "some-output-image-ref",
PreviousImageRef: "some-previous-image-ref",
RunImageRef: "some-run-image-ref",
SkipLayers: true,
}, logger)
h.AssertNil(t, err)
_, ok := analyzer.SBOMRestorer.(*layer.NopSBOMRestorer)
h.AssertEq(t, ok, true)
})
})
})
when("platform api = 0.7", func() {
it.Before(func() {
analyzerFactory = phase.NewConnectedFactory(
api.MustParse("0.7"),
fakeAPIVerifier,
fakeCacheHandler,
fakeConfigHandler,
fakeImageHandler,
fakeRegistryHandler,
)
})
it("configures the analyzer", func() {
previousImage := fakes.NewImage("some-previous-image-ref", "", nil)
runImage := fakes.NewImage("some-run-image-ref", "", nil)
t.Log("ensures registry access")
fakeImageHandler.EXPECT().Kind().Return(image.RemoteKind).AnyTimes()
fakeRegistryHandler.EXPECT().EnsureReadAccess([]string{"some-previous-image-ref", "some-run-image-ref"})
fakeRegistryHandler.EXPECT().EnsureWriteAccess([]string{"some-cache-image-ref", "some-output-image-ref", "some-additional-tag"})
t.Log("processes previous image")
fakeImageHandler.EXPECT().InitImage("some-previous-image-ref").Return(previousImage, nil)
t.Log("processes run image")
fakeImageHandler.EXPECT().InitImage("some-run-image-ref").Return(runImage, nil)
analyzer, err := analyzerFactory.NewAnalyzer(platform.LifecycleInputs{
AdditionalTags: []string{"some-additional-tag"},
CacheImageRef: "some-cache-image-ref",
LaunchCacheDir: "some-launch-cache-dir",
LayersDir: "some-layers-dir",
OutputImageRef: "some-output-image-ref",
PreviousImageRef: "some-previous-image-ref",
RunImageRef: "some-run-image-ref",
SkipLayers: true,
}, logger)
h.AssertNil(t, err)
h.AssertEq(t, analyzer.PreviousImage.Name(), previousImage.Name())
h.AssertEq(t, analyzer.RunImage.Name(), runImage.Name())
t.Log("does not restore sbom data")
_, ok := analyzer.SBOMRestorer.(*layer.NopSBOMRestorer)
h.AssertEq(t, ok, true)
t.Log("sets logger")
h.AssertEq(t, analyzer.Logger, logger)
})
when("daemon case", func() {
it("configures the analyzer", func() {
previousImage := fakes.NewImage("some-previous-image-ref", "", nil)
runImage := fakes.NewImage("some-run-image-ref", "", nil)
t.Log("ensures registry access")
fakeImageHandler.EXPECT().Kind().Return(image.LocalKind).AnyTimes()
fakeRegistryHandler.EXPECT().EnsureReadAccess()
fakeRegistryHandler.EXPECT().EnsureWriteAccess([]string{"some-cache-image-ref"})
t.Log("processes previous image")
fakeImageHandler.EXPECT().InitImage("some-previous-image-ref").Return(previousImage, nil)
t.Log("processes run image")
fakeImageHandler.EXPECT().InitImage("some-run-image-ref").Return(runImage, nil)
launchCacheDir := filepath.Join(tempDir, "some-launch-cache-dir")
h.AssertNil(t, os.MkdirAll(launchCacheDir, 0777))
analyzer, err := analyzerFactory.NewAnalyzer(platform.LifecycleInputs{
AdditionalTags: []string{"some-additional-tag"},
CacheImageRef: "some-cache-image-ref",
LaunchCacheDir: launchCacheDir,
LayersDir: "some-layers-dir",
OutputImageRef: "some-output-image-ref",
PreviousImageRef: "some-previous-image-ref",
RunImageRef: "some-run-image-ref",
SkipLayers: true,
}, logger)
h.AssertNil(t, err)
h.AssertEq(t, analyzer.PreviousImage.Name(), previousImage.Name())
h.AssertEq(t, analyzer.RunImage.Name(), runImage.Name())
})
})
})
})
}
func testAnalyzer(platformAPI string) func(t *testing.T, when spec.G, it spec.S) {
return func(t *testing.T, when spec.G, it spec.S) {
var (
cacheDir string
layersDir string
tmpDir string
analyzer *phase.Analyzer
previousImage *fakes.Image
mockCtrl *gomock.Controller
sbomRestorer *testmock.MockSBOMRestorer
testCache phase.Cache
)
it.Before(func() {
var err error
discardLogger := log.Logger{Handler: &discard.Handler{}}
tmpDir, err = os.MkdirTemp("", "analyzer-tests")
h.AssertNil(t, err)
layersDir, err = os.MkdirTemp("", "lifecycle-layer-dir")
h.AssertNil(t, err)
cacheDir, err = os.MkdirTemp("", "some-cache-dir")
h.AssertNil(t, err)
testCache, err = cache.NewVolumeCache(cacheDir, &discardLogger)
h.AssertNil(t, err)
previousImage = fakes.NewImage("image-repo-name", "", local.IDIdentifier{
ImageID: "s0m3D1g3sT",
})
mockCtrl = gomock.NewController(t)
sbomRestorer = testmock.NewMockSBOMRestorer(mockCtrl)
h.AssertNil(t, err)
analyzer = &phase.Analyzer{
PreviousImage: previousImage,
Logger: &discardLogger,
SBOMRestorer: sbomRestorer,
PlatformAPI: api.MustParse(platformAPI),
}
if testing.Verbose() {
analyzer.Logger = cmd.DefaultLogger
h.AssertNil(t, cmd.DefaultLogger.SetLevel("debug"))
}
})
it.After(func() {
h.AssertNil(t, os.RemoveAll(tmpDir))
h.AssertNil(t, os.RemoveAll(layersDir))
h.AssertNil(t, os.RemoveAll(cacheDir))
h.AssertNil(t, previousImage.Cleanup())
mockCtrl.Finish()
})
when("#Analyze", func() {
var (
expectedAppMetadata files.LayersMetadata
expectedCacheMetadata platform.CacheMetadata
ref *testmock.MockReference
)
it.Before(func() {
ref = testmock.NewMockReference(mockCtrl)
ref.EXPECT().Name().AnyTimes()
})
when("previous image exists", func() {
it.Before(func() {
metadata := h.MustReadFile(t, filepath.Join("testdata", "analyzer", "app_metadata.json"))
h.AssertNil(t, previousImage.SetLabel("io.buildpacks.lifecycle.metadata", string(metadata)))
h.AssertNil(t, json.Unmarshal(metadata, &expectedAppMetadata))
})
it("returns the analyzed metadata", func() {
md, err := analyzer.Analyze()
h.AssertNil(t, err)
h.AssertEq(t, md.PreviousImageRef(), "s0m3D1g3sT")
h.AssertEq(t, md.LayersMetadata, expectedAppMetadata)
})
when("cache exists", func() {
it.Before(func() {
metadata := h.MustReadFile(t, filepath.Join("testdata", "analyzer", "cache_metadata.json"))
h.AssertNil(t, json.Unmarshal(metadata, &expectedCacheMetadata))
h.AssertNil(t, testCache.SetMetadata(expectedCacheMetadata))
h.AssertNil(t, testCache.Commit())
})
it("returns the analyzed metadata", func() {
md, err := analyzer.Analyze()
h.AssertNil(t, err)
h.AssertEq(t, md.LayersMetadata, expectedAppMetadata)
})
})
})
when("previous image not found", func() {
it.Before(func() {
h.AssertNil(t, previousImage.Delete())
})
it("returns a nil image in the analyzed metadata", func() {
md, err := analyzer.Analyze()
h.AssertNil(t, err)
h.AssertEq(t, md.PreviousImageRef(), "")
h.AssertEq(t, md.LayersMetadata, files.LayersMetadata{})
})
})
when("previous image does not have metadata label", func() {
it.Before(func() {
h.AssertNil(t, previousImage.SetLabel("io.buildpacks.lifecycle.metadata", ""))
})
it("returns empty analyzed metadata", func() {
md, err := analyzer.Analyze()
h.AssertNil(t, err)
h.AssertEq(t, md.LayersMetadata, files.LayersMetadata{})
})
})
when("previous image has incompatible metadata", func() {
it.Before(func() {
h.AssertNil(t, previousImage.SetLabel("io.buildpacks.lifecycle.metadata", `{["bad", "metadata"]}`))
})
it("returns empty analyzed metadata", func() {
md, err := analyzer.Analyze()
h.AssertNil(t, err)
h.AssertEq(t, md.LayersMetadata, files.LayersMetadata{})
})
})
when("previous image has an SBOM layer digest in the analyzed metadata", func() {
it.Before(func() {
metadata := fmt.Sprintf(`{"sbom": {"sha":"%s"}}`, "some-digest")
h.AssertNil(t, previousImage.SetLabel("io.buildpacks.lifecycle.metadata", metadata))
h.AssertNil(t, json.Unmarshal([]byte(metadata), &expectedAppMetadata))
})
it("calls the SBOM restorer with the SBOM layer digest", func() {
sbomRestorer.EXPECT().RestoreFromPrevious(previousImage, "some-digest")
_, err := analyzer.Analyze()
h.AssertNil(t, err)
})
})
when("run image is provided", func() {
it.Before(func() {
analyzer.RunImage = previousImage
})
it("returns the run image digest in the analyzed metadata", func() {
md, err := analyzer.Analyze()
h.AssertNil(t, err)
h.AssertEq(t, md.RunImage.Reference, "s0m3D1g3sT")
})
it("populates target metadata from the run image", func() {
h.AssertNil(t, previousImage.SetLabel("io.buildpacks.base.id", "id software"))
h.AssertNil(t, previousImage.SetOS("zindows"))
h.AssertNil(t, previousImage.SetOSVersion("95"))
h.AssertNil(t, previousImage.SetArchitecture("Pentium"))
h.AssertNil(t, previousImage.SetVariant("MMX"))
h.AssertNil(t, previousImage.SetLabel("io.buildpacks.base.distro.name", "moobuntu"))
h.AssertNil(t, previousImage.SetLabel("io.buildpacks.base.distro.version", "Helpful Holstein"))
md, err := analyzer.Analyze()
h.AssertNil(t, err)
if api.MustParse(platformAPI).LessThan("0.12") {
h.AssertNil(t, md.RunImage.TargetMetadata)
} else {
h.AssertNotNil(t, md.RunImage.TargetMetadata)
h.AssertEq(t, md.RunImage.TargetMetadata.Arch, "Pentium")
h.AssertEq(t, md.RunImage.TargetMetadata.ArchVariant, "MMX")
h.AssertEq(t, md.RunImage.TargetMetadata.OS, "zindows")
h.AssertEq(t, md.RunImage.TargetMetadata.ID, "id software")
h.AssertNotNil(t, md.RunImage.TargetMetadata.Distro)
h.AssertEq(t, md.RunImage.TargetMetadata.Distro.Name, "moobuntu")
h.AssertEq(t, md.RunImage.TargetMetadata.Distro.Version, "Helpful Holstein")
}
})
when("run image is missing OS", func() {
it("errors", func() {
h.AssertNil(t, previousImage.SetOS(""))
_, err := analyzer.Analyze()
if api.MustParse(platformAPI).LessThan("0.12") {
h.AssertNil(t, err)
} else {
h.AssertError(t, err, "failed to find OS")
}
})
})
})
})
}
}