diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 3100eb2931..1c64a29f5c 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -704,6 +704,53 @@ func testAcceptance( builderName = value }) + when("complex builder", func() { + it.Before(func() { + // create our nested builder + h.SkipIf(t, imageManager.HostOS() == "windows", "These tests are not yet compatible with Windows-based containers") + h.SkipIf(t, !createBuilderPack.SupportsFeature(invoke.BuilderNoDuplicateLayers), "bug fixed in 0.18.0") + + // create a task, handled by a 'task manager' which executes our pack commands during tests. + // looks like this is used to de-dup tasks + key := taskKey( + "create-complex-builder", + append( + []string{runImageMirror, createBuilderPackConfig.Path(), lifecycle.Identifier()}, + createBuilderPackConfig.FixturePaths()..., + )..., + ) + + value, err := suiteManager.RunTaskOnceString(key, func() (string, error) { + return createComplexBuilder( + t, + assert, + createBuilderPack, + lifecycle, + buildpackManager, + runImageMirror, + ) + }) + assert.Nil(err) + + // register task to be run to 'clean up' a task + suiteManager.RegisterCleanUp("clean-"+key, func() error { + imageManager.CleanupImages(value) + return nil + }) + builderName = value + + output := pack.RunSuccessfully( + "config", "run-image-mirrors", "add", "pack-test/run", "--mirror", "some-registry.com/pack-test/run1") + assertOutput := assertions.NewOutputAssertionManager(t, output) + assertOutput.ReportsSuccesfulRunImageMirrorsAdd("pack-test/run", "some-registry.com/pack-test/run1") + }) + when("builder has duplicate buildpacks", func() { + it("buildpack layers have no duplication", func() { + assertImage.DoesNotHaveDuplicateLayers(builderName) + }) + }) + }) + when("builder.toml is invalid", func() { it("displays an error", func() { builderConfigPath := createBuilderPack.FixtureManager().FixtureLocation("invalid_builder.toml") @@ -1941,6 +1988,7 @@ include = [ "*.jar", "media/mountain.jpg", "/media/person.png", ] it.Before(func() { // create our nested builder h.SkipIf(t, imageManager.HostOS() == "windows", "These tests are not yet compatible with Windows-based containers") + h.SkipIf(t, !pack.SupportsFeature(invoke.BuilderNoDuplicateLayers), "bug fixed in 0.18.0") // create a task, handled by a 'task manager' which executes our pack commands during tests. // looks like this is used to de-dup tasks @@ -2457,12 +2505,14 @@ func createComplexBuilder(t *testing.T, packageImageName := registryConfig.RepoName("nested-level-1-buildpack-" + h.RandString(8)) nestedLevelTwoBuildpackName := registryConfig.RepoName("nested-level-2-buildpack-" + h.RandString(8)) simpleLayersBuildpackName := registryConfig.RepoName("simple-layers-buildpack-" + h.RandString(8)) + simpleLayersBuildpackDifferentShaName := registryConfig.RepoName("simple-layers-buildpack-different-name-" + h.RandString(8)) templateMapping["package_id"] = "simple/nested-level-1" templateMapping["package_image_name"] = packageImageName templateMapping["nested_level_1_buildpack"] = packageImageName templateMapping["nested_level_2_buildpack"] = nestedLevelTwoBuildpackName templateMapping["simple_layers_buildpack"] = simpleLayersBuildpackName + templateMapping["simple_layers_buildpack_different_sha"] = simpleLayersBuildpackDifferentShaName fixtureManager := pack.FixtureManager() @@ -2483,6 +2533,7 @@ func createComplexBuilder(t *testing.T, nestedLevelTwoConfigFile, templateMapping, ) + err = nestedLevelTwoConfigFile.Close() assert.Nil(err) @@ -2512,11 +2563,20 @@ func createComplexBuilder(t *testing.T, ), ) - defer imageManager.CleanupImages(packageImageName, nestedLevelTwoBuildpackName, simpleLayersBuildpackName) + simpleLayersDifferentShaBuildpack := buildpacks.NewPackageImage( + t, + pack, + simpleLayersBuildpackDifferentShaName, + fixtureManager.FixtureLocation("simple-layers-buildpack-different-sha_package.toml"), + buildpacks.WithRequiredBuildpacks(buildpacks.SimpleLayersDifferentSha), + ) + + defer imageManager.CleanupImages(packageImageName, nestedLevelTwoBuildpackName, simpleLayersBuildpackName, simpleLayersBuildpackDifferentShaName) builderBuildpacks = append( builderBuildpacks, packageImageBuildpack, + simpleLayersDifferentShaBuildpack, ) buildpackManager.PrepareBuildpacks(tmpDir, builderBuildpacks...) diff --git a/acceptance/assertions/image.go b/acceptance/assertions/image.go index 83ea2a85da..13f3589a8f 100644 --- a/acceptance/assertions/image.go +++ b/acceptance/assertions/image.go @@ -100,3 +100,19 @@ func (a ImageAssertionManager) NotExistsInRegistry(name string) { "Didn't expect to see image %s in the registry", ) } + +func (a ImageAssertionManager) DoesNotHaveDuplicateLayers(name string) { + a.testObject.Helper() + + out, err := a.imageManager.InspectLocal(name) + a.assert.Nil(err) + + layerSet := map[string]interface{}{} + for _, layer := range out.RootFS.Layers { + _, ok := layerSet[layer] + if ok { + a.testObject.Fatalf("duplicate layer found in builder %s", layer) + } + layerSet[layer] = true + } +} diff --git a/acceptance/buildpacks/archive_buildpack.go b/acceptance/buildpacks/archive_buildpack.go index fc188928fd..311e274b50 100644 --- a/acceptance/buildpacks/archive_buildpack.go +++ b/acceptance/buildpacks/archive_buildpack.go @@ -84,16 +84,17 @@ func (a archiveBuildpack) createTgz(sourceDir string) (string, error) { } var ( - SimpleLayersParent = &archiveBuildpack{name: "simple-layers-parent-buildpack"} - SimpleLayers = &archiveBuildpack{name: "simple-layers-buildpack"} - InternetCapable = &archiveBuildpack{name: "internet-capable-buildpack"} - ReadVolume = &archiveBuildpack{name: "read-volume-buildpack"} - ReadWriteVolume = &archiveBuildpack{name: "read-write-volume-buildpack"} - ArchiveNotInBuilder = &archiveBuildpack{name: "not-in-builder-buildpack"} - Noop = &archiveBuildpack{name: "noop-buildpack"} - Noop2 = &archiveBuildpack{name: "noop-buildpack-2"} - OtherStack = &archiveBuildpack{name: "other-stack-buildpack"} - ReadEnv = &archiveBuildpack{name: "read-env-buildpack"} - NestedLevelOne = &archiveBuildpack{name: "nested-level-1-buildpack"} - NestedLevelTwo = &archiveBuildpack{name: "nested-level-2-buildpack"} + SimpleLayersParent = &archiveBuildpack{name: "simple-layers-parent-buildpack"} + SimpleLayers = &archiveBuildpack{name: "simple-layers-buildpack"} + SimpleLayersDifferentSha = &archiveBuildpack{name: "simple-layers-buildpack-different-sha"} + InternetCapable = &archiveBuildpack{name: "internet-capable-buildpack"} + ReadVolume = &archiveBuildpack{name: "read-volume-buildpack"} + ReadWriteVolume = &archiveBuildpack{name: "read-write-volume-buildpack"} + ArchiveNotInBuilder = &archiveBuildpack{name: "not-in-builder-buildpack"} + Noop = &archiveBuildpack{name: "noop-buildpack"} + Noop2 = &archiveBuildpack{name: "noop-buildpack-2"} + OtherStack = &archiveBuildpack{name: "other-stack-buildpack"} + ReadEnv = &archiveBuildpack{name: "read-env-buildpack"} + NestedLevelOne = &archiveBuildpack{name: "nested-level-1-buildpack"} + NestedLevelTwo = &archiveBuildpack{name: "nested-level-2-buildpack"} ) diff --git a/acceptance/buildpacks/package_image_buildpack.go b/acceptance/buildpacks/package_image_buildpack.go index 26dbb1e0a5..7bd13a6f31 100644 --- a/acceptance/buildpacks/package_image_buildpack.go +++ b/acceptance/buildpacks/package_image_buildpack.go @@ -83,6 +83,7 @@ func (p PackageImage) Prepare(sourceDir, _ string) error { packArgs = append(packArgs, "--publish") } + p.testObject.Log("packaging image: ", p.name) output := p.pack.RunSuccessfully("buildpack", append([]string{"package"}, packArgs...)...) assertOutput := assertions.NewOutputAssertionManager(p.testObject, output) if p.publish { diff --git a/acceptance/invoke/pack.go b/acceptance/invoke/pack.go index 08c22a9bd0..ceb6611cc1 100644 --- a/acceptance/invoke/pack.go +++ b/acceptance/invoke/pack.go @@ -219,12 +219,16 @@ type Feature int const ( InspectRemoteImage = iota + BuilderNoDuplicateLayers ) var featureTests = map[Feature]func(i *PackInvoker) bool{ InspectRemoteImage: func(i *PackInvoker) bool { return i.laterThan("0.17.0") }, + BuilderNoDuplicateLayers: func(i *PackInvoker) bool { + return i.laterThan("0.18.0") + }, } func (i *PackInvoker) SupportsFeature(f Feature) bool { diff --git a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/build b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/build new file mode 100755 index 0000000000..7b0518eb50 --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/build @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +echo "---> Build: Simple Layers Different Sha Buildpack" + +set -o errexit +set -o nounset +set -o pipefail + +launch_dir=$1 + +## makes a launch layer +echo "making launch layer" + +# Add color line, to test for --no-color +echo "Color: Styled" + +mkdir "$launch_dir/launch-layer" +echo "Launch Dep Contents" > "$launch_dir/launch-layer/launch-dep" +ln -snf "$launch_dir/launch-layer" launch-deps +echo "launch = true" > "$launch_dir/launch-layer.toml" + +## makes a cached launch layer +if [[ ! -f "$launch_dir/cached-launch-layer.toml" ]]; then + echo "making cached launch layer" + mkdir "$launch_dir/cached-launch-layer" + echo "Cached Dep Contents" > "$launch_dir/cached-launch-layer/cached-dep" + ln -snf "$launch_dir/cached-launch-layer" cached-deps + echo "launch = true" > "$launch_dir/cached-launch-layer.toml" + echo "cache = true" >> "$launch_dir/cached-launch-layer.toml" +else + echo "reusing cached launch layer" + ln -snf "$launch_dir/cached-launch-layer" cached-deps +fi + +## adds a process +cat < "$launch_dir/launch.toml" +[[processes]] + type = "web" + command = "./run" + args = ["8080"] + +[[processes]] + type = "hello" + command = "echo" + args = ["hello", "world"] + direct = true +EOF + +echo "---> Done" diff --git a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/build.bat b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/build.bat new file mode 100644 index 0000000000..01421d77fc --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/build.bat @@ -0,0 +1,40 @@ +@echo off +echo --- Build: Simple Layers Different Sha Buildpack + +set launch_dir=%1 + +:: makes a launch layer +echo making launch layer %launch_dir%\launch-layer +mkdir %launch_dir%\launch-layer +echo Launch Dep Contents > "%launch_dir%\launch-layer\launch-dep +mklink /j launch-deps %launch_dir%\launch-layer +echo launch = true > %launch_dir%\launch-layer.toml + +:: makes a cached launch layer +if not exist %launch_dir%\cached-launch-layer.toml ( + echo making cached launch layer %launch_dir%\cached-launch-layer + mkdir %launch_dir%\cached-launch-layer + echo Cached Dep Contents > %launch_dir%\cached-launch-layer\cached-dep + mklink /j cached-deps %launch_dir%\cached-launch-layer + echo launch = true > %launch_dir%\cached-launch-layer.toml + echo cache = true >> %launch_dir%\cached-launch-layer.toml +) else ( + echo reusing cached launch layer %launch_dir%\cached-launch-layer + mklink /j cached-deps %launch_dir%\cached-launch-layer +) + +:: adds a process +( +echo [[processes]] +echo type = "web" +echo command = '.\run' +echo args = ["8080"] +echo. +echo [[processes]] +echo type = "hello" +echo command = "cmd" +echo args = ["/c", "echo hello world"] +echo direct = true +) > %launch_dir%\launch.toml + +echo --- Done diff --git a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/detect b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/detect new file mode 100755 index 0000000000..e4cffa69d9 --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/detect @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +## always detect diff --git a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/detect.bat b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/detect.bat new file mode 100644 index 0000000000..15823e73f1 --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/detect.bat @@ -0,0 +1,2 @@ +@echo off +:: always detect diff --git a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/extra_file.txt b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/extra_file.txt new file mode 100644 index 0000000000..771900cedf --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/bin/extra_file.txt @@ -0,0 +1 @@ +Just some extra content to change the sha256 of this buildpack :) \ No newline at end of file diff --git a/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/buildpack.toml b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/buildpack.toml new file mode 100644 index 0000000000..22b290e57e --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/0.2/simple-layers-buildpack-different-sha/buildpack.toml @@ -0,0 +1,9 @@ +api = "0.2" + +[buildpack] + id = "simple/layers" + version = "simple-layers-version" + name = "Simple Layers Buildpack" + +[[stacks]] + id = "pack.test.stack" diff --git a/acceptance/testdata/pack_fixtures/nested_builder.toml b/acceptance/testdata/pack_fixtures/nested_builder.toml index 4ab9d225bf..8ef20f56f3 100644 --- a/acceptance/testdata/pack_fixtures/nested_builder.toml +++ b/acceptance/testdata/pack_fixtures/nested_builder.toml @@ -11,10 +11,9 @@ # noop-buildpack-2 has the same id but a different version compared to noop-buildpack uri = "noop-buildpack-2.tgz" - -{{- if .simple_layers_buildpack}} +{{- if .simple_layers_buildpack_different_sha}} [[buildpacks]] - image = "{{.simple_layers_buildpack}}" + image = "{{.simple_layers_buildpack_different_sha}}" version = "simple-layers-version" {{- end}} diff --git a/acceptance/testdata/pack_fixtures/simple-layers-buildpack-different-sha_package.toml b/acceptance/testdata/pack_fixtures/simple-layers-buildpack-different-sha_package.toml new file mode 100644 index 0000000000..4ce41cd5c7 --- /dev/null +++ b/acceptance/testdata/pack_fixtures/simple-layers-buildpack-different-sha_package.toml @@ -0,0 +1,2 @@ +[buildpack] +uri = "simple-layers-buildpack-different-sha.tgz" \ No newline at end of file diff --git a/acceptance/testdata/pack_previous_fixtures_overrides/simple-layers-buildpack-different-sha_package.toml b/acceptance/testdata/pack_previous_fixtures_overrides/simple-layers-buildpack-different-sha_package.toml new file mode 100644 index 0000000000..4ce41cd5c7 --- /dev/null +++ b/acceptance/testdata/pack_previous_fixtures_overrides/simple-layers-buildpack-different-sha_package.toml @@ -0,0 +1,2 @@ +[buildpack] +uri = "simple-layers-buildpack-different-sha.tgz" \ No newline at end of file diff --git a/internal/builder/builder.go b/internal/builder/builder.go index a574774f89..87393976eb 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -45,6 +45,14 @@ const ( EnvUID = "CNB_USER_ID" EnvGID = "CNB_GROUP_ID" + + BuildpackOnBuilderMessage = `buildpack %s already exists on builder and will be overwritten + - existing diffID: %s + - new diffID: %s` + + BuildpackPreviouslyDefinedMessage = `buildpack %s was previously defined with different contents and will be overwritten + - previous diffID: %s + - using diffID: %s` ) // Builder represents a pack builder, used to build images @@ -300,36 +308,9 @@ func (b *Builder) Save(logger logging.Logger, creatorMetadata CreatorMetadata) e return errors.Wrapf(err, "getting label %s", dist.BuildpackLayersLabel) } - for _, bp := range b.additionalBuildpacks { - bpLayerTar, err := dist.BuildpackToLayerTar(tmpDir, bp) - if err != nil { - return err - } - - diffID, err := dist.LayerDiffID(bpLayerTar) - if err != nil { - return errors.Wrapf(err, - "getting content hashes for buildpack %s", - style.Symbol(bp.Descriptor().Info.FullName()), - ) - } - - if err := b.image.AddLayerWithDiffID(bpLayerTar, diffID.String()); err != nil { - return errors.Wrapf(err, - "adding layer tar for buildpack %s", - style.Symbol(bp.Descriptor().Info.FullName()), - ) - } - - bpInfo := bp.Descriptor().Info - if _, ok := bpLayers[bpInfo.ID][bpInfo.Version]; ok { - logger.Debugf( - "buildpack %s already exists on builder and will be overwritten", - style.Symbol(bpInfo.FullName()), - ) - } - - dist.AddBuildpackToLayersMD(bpLayers, bp.Descriptor(), diffID.String()) + err = addBuildpacks(logger, tmpDir, b.image, b.additionalBuildpacks, bpLayers) + if err != nil { + return err } if err := dist.SetLabel(b.image, dist.BuildpackLayersLabel, bpLayers); err != nil { @@ -394,6 +375,80 @@ func (b *Builder) Save(logger logging.Logger, creatorMetadata CreatorMetadata) e // Helpers +func addBuildpacks(logger logging.Logger, tmpDir string, image imgutil.Image, additionalBuildpacks []dist.Buildpack, bpLayers dist.BuildpackLayers) error { + type buildpackToAdd struct { + tarPath string + diffID string + buildpack dist.Buildpack + } + + buildpacksToAdd := map[string]buildpackToAdd{} + for i, bp := range additionalBuildpacks { + // create buildpack directory + bpTmpDir := filepath.Join(tmpDir, strconv.Itoa(i)) + if err := os.MkdirAll(bpTmpDir, os.ModePerm); err != nil { + return errors.Wrap(err, "creating buildpack temp dir") + } + + // create tar file + bpLayerTar, err := dist.BuildpackToLayerTar(bpTmpDir, bp) + if err != nil { + return err + } + + // generate diff id + diffID, err := dist.LayerDiffID(bpLayerTar) + if err != nil { + return errors.Wrapf(err, + "getting content hashes for buildpack %s", + style.Symbol(bp.Descriptor().Info.FullName()), + ) + } + + bpInfo := bp.Descriptor().Info + // check against builder layers + if existingBPInfo, ok := bpLayers[bpInfo.ID][bpInfo.Version]; ok { + if existingBPInfo.LayerDiffID == diffID.String() { + logger.Debugf("Buildpack %s already exists on builder with same contents, skipping...", style.Symbol(bpInfo.FullName())) + continue + } + + logger.Debugf(BuildpackOnBuilderMessage, style.Symbol(bpInfo.FullName()), style.Symbol(existingBPInfo.LayerDiffID), style.Symbol(diffID.String())) + } + + // check against other buildpacks to be added + if otherAdditionalBP, ok := buildpacksToAdd[bp.Descriptor().Info.FullName()]; ok { + if otherAdditionalBP.diffID == diffID.String() { + logger.Debugf("Buildpack %s with same contents is already being added, skipping...", style.Symbol(bpInfo.FullName())) + continue + } + + logger.Debugf(BuildpackPreviouslyDefinedMessage, style.Symbol(bpInfo.FullName()), style.Symbol(otherAdditionalBP.diffID), style.Symbol(diffID.String())) + } + + // note: if same id@version is in additionalBuildpacks, last one wins (see warnings above) + buildpacksToAdd[bp.Descriptor().Info.FullName()] = buildpackToAdd{ + tarPath: bpLayerTar, + diffID: diffID.String(), + buildpack: bp, + } + } + + for _, bp := range buildpacksToAdd { + logger.Debugf("Adding buildpack %s (diffID=%s)", style.Symbol(bp.buildpack.Descriptor().Info.FullName()), bp.diffID) + if err := image.AddLayerWithDiffID(bp.tarPath, bp.diffID); err != nil { + return errors.Wrapf(err, + "adding layer tar for buildpack %s", + style.Symbol(bp.buildpack.Descriptor().Info.FullName()), + ) + } + + dist.AddBuildpackToLayersMD(bpLayers, bp.buildpack.Descriptor(), bp.diffID) + } + + return nil +} + func processOrder(buildpacks []dist.BuildpackInfo, order dist.Order) (dist.Order, error) { resolvedOrder := dist.Order{} diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go index 21f8d2315c..16bb56adb0 100644 --- a/internal/builder/builder_test.go +++ b/internal/builder/builder_test.go @@ -2,14 +2,19 @@ package builder_test import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" + "io" "io/ioutil" "os" "path" "path/filepath" + "runtime" "testing" + "github.com/pkg/errors" + "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" @@ -630,6 +635,149 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertError(t, err, "getting label io.buildpacks.buildpack.layers") }) }) + + when("saving with duplicated buildpacks", func() { + it("adds a single buildpack to the builder image", func() { + subject.AddBuildpack(bp1v1) + subject.AddBuildpack(bp2v1) + subject.AddBuildpack(bp1v1) + + err := subject.Save(logger, builder.CreatorMetadata{}) + h.AssertNil(t, err) + + h.AssertEq(t, baseImage.IsSaved(), true) + + // Expect 6 layers from the following locations: + // - 1 from defaultDirsLayer + // - 1 from lifecycleLayer + // - 2 from buildpacks + // - 1 from orderLayer + // - 1 from stackLayer + h.AssertEq(t, baseImage.NumberOfAddedLayers(), 6) + }) + + when("duplicated buildpack, has different contents", func() { + var bp1v1Alt dist.Buildpack + it.Before(func() { + var err error + bp1v1Alt, err = ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ + API: api.MustParse("0.2"), + Info: dist.BuildpackInfo{ + ID: "buildpack-1-id", + Version: "buildpack-1-version-1", + }, + Stacks: []dist.Stack{{ + ID: "some.stack.id", + Mixins: []string{"mixinX", "mixinY"}, + }}, + }, 0644, ifakes.WithExtraBuildpackContents("coolbeans", "a file cool as beans")) + h.AssertNil(t, err) + }) + + it("uses the last buildpack", func() { + logger := ilogging.NewLogWithWriters(&outBuf, &outBuf, ilogging.WithVerbose()) + + subject.AddBuildpack(bp1v1) + subject.AddBuildpack(bp1v1Alt) + + err := subject.Save(logger, builder.CreatorMetadata{}) + h.AssertNil(t, err) + + h.AssertEq(t, baseImage.IsSaved(), true) + + // Expect 5 layers from the following locations: + // - 1 from defaultDirsLayer + // - 1 from lifecycleLayer + // - 1 from buildpacks + // - 1 from orderLayer + // - 1 from stackLayer + h.AssertEq(t, baseImage.NumberOfAddedLayers(), 5) + oldSha256 := "4dc0072c61fc2bd7118bbc93a432eae0012082de094455cf0a9fed20e3c44789" + newSha256 := "29cb2bce4c2350f0e86f3dd30fa3810beb409b910126a18651de750f457fedfb" + if runtime.GOOS == "windows" { + newSha256 = "eaed4a1617bba5738ae5672f6aefda8add7abb2f8630c75dc97a6232879d4ae4" + } + + h.AssertContains(t, outBuf.String(), fmt.Sprintf(`buildpack 'buildpack-1-id@buildpack-1-version-1' was previously defined with different contents and will be overwritten + - previous diffID: 'sha256:%s' + - using diffID: 'sha256:%s'`, oldSha256, newSha256)) + + layer, err := baseImage.FindLayerWithPath(filepath.Join("/cnb", "buildpacks", "buildpack-1-id", "buildpack-1-version-1", "coolbeans")) + h.AssertNil(t, err) + + bpLayer, err := os.Open(layer) + h.AssertNil(t, err) + defer bpLayer.Close() + + hsh := sha256.New() + _, err = io.Copy(hsh, bpLayer) + h.AssertNil(t, err) + + h.AssertEq(t, newSha256, fmt.Sprintf("%x", hsh.Sum(nil))) + }) + }) + + when("adding buildpack that already exists on the image", func() { + it("skips adding buildpack that already exists", func() { + logger := ilogging.NewLogWithWriters(&outBuf, &outBuf, ilogging.WithVerbose()) + diffID := "4dc0072c61fc2bd7118bbc93a432eae0012082de094455cf0a9fed20e3c44789" + bpLayer := dist.BuildpackLayers{ + "buildpack-1-id": map[string]dist.BuildpackLayerInfo{ + "buildpack-1-version-1": dist.BuildpackLayerInfo{ + API: api.MustParse("0.2"), + Stacks: nil, + Order: nil, + LayerDiffID: fmt.Sprintf("sha256:%s", diffID), + Homepage: "", + }, + }, + } + bpLayerString, err := json.Marshal(bpLayer) + h.AssertNil(t, err) + + h.AssertNil(t, baseImage.SetLabel( + dist.BuildpackLayersLabel, + string(bpLayerString), + )) + + subject.AddBuildpack(bp1v1) + err = subject.Save(logger, builder.CreatorMetadata{}) + h.AssertNil(t, err) + + fmt.Println(outBuf.String()) + expectedLog := "Buildpack 'buildpack-1-id@buildpack-1-version-1' already exists on builder with same contents, skipping..." + h.AssertContains(t, outBuf.String(), expectedLog) + }) + }) + }) + + when("error adding buildpacks to builder", func() { + when("unable to convert buildpack to layer tar", func() { + var bp1v1Err dist.Buildpack + it.Before(func() { + var err error + bp1v1Err, err = ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ + API: api.MustParse("0.2"), + Info: dist.BuildpackInfo{ + ID: "buildpack-1-id", + Version: "buildpack-1-version-1", + }, + Stacks: []dist.Stack{{ + ID: "some.stack.id", + Mixins: []string{"mixinX", "mixinY"}, + }}, + }, 0644, ifakes.WithOpenError(errors.New("unable to open buildpack"))) + h.AssertNil(t, err) + }) + it("errors", func() { + subject.AddBuildpack(bp1v1Err) + + err := subject.Save(logger, builder.CreatorMetadata{}) + + h.AssertError(t, err, "unable to open buildpack") + }) + }) + }) }) when("#SetLifecycle", func() { diff --git a/internal/fakes/fake_buildpack.go b/internal/fakes/fake_buildpack.go index e186f00815..7857aa17ae 100644 --- a/internal/fakes/fake_buildpack.go +++ b/internal/fakes/fake_buildpack.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "path/filepath" "github.com/BurntSushi/toml" @@ -14,6 +15,31 @@ import ( type fakeBuildpack struct { descriptor dist.BuildpackDescriptor chmod int64 + options []FakeBuildpackOption +} + +type fakeBuildpackConfig struct { + // maping of extrafilename to stringified contents + ExtraFiles map[string]string + OpenError error +} + +func newFakeBuildpackConfig() *fakeBuildpackConfig { + return &fakeBuildpackConfig{ExtraFiles: map[string]string{}} +} + +type FakeBuildpackOption func(*fakeBuildpackConfig) + +func WithExtraBuildpackContents(filename, contents string) FakeBuildpackOption { + return func(f *fakeBuildpackConfig) { + f.ExtraFiles[filename] = contents + } +} + +func WithOpenError(err error) FakeBuildpackOption { + return func(f *fakeBuildpackConfig) { + f.OpenError = err + } } // NewFakeBuildpack creates a fake buildpacks with contents: @@ -26,10 +52,11 @@ type fakeBuildpack struct { // build-contents // \_ /cnbs/buildpacks/{ID}/{version}/bin/detect // detect-contents -func NewFakeBuildpack(descriptor dist.BuildpackDescriptor, chmod int64) (dist.Buildpack, error) { +func NewFakeBuildpack(descriptor dist.BuildpackDescriptor, chmod int64, options ...FakeBuildpackOption) (dist.Buildpack, error) { return &fakeBuildpack{ descriptor: descriptor, chmod: chmod, + options: options, }, nil } @@ -38,6 +65,15 @@ func (b *fakeBuildpack) Descriptor() dist.BuildpackDescriptor { } func (b *fakeBuildpack) Open() (io.ReadCloser, error) { + fConfig := newFakeBuildpackConfig() + for _, option := range b.options { + option(fConfig) + } + + if fConfig.OpenError != nil { + return nil, fConfig.OpenError + } + buf := &bytes.Buffer{} if err := toml.NewEncoder(buf).Encode(b.descriptor); err != nil { return nil, err @@ -56,5 +92,9 @@ func (b *fakeBuildpack) Open() (io.ReadCloser, error) { tarBuilder.AddFile(bpDir+"/bin/detect", b.chmod, ts, []byte("detect-contents")) } + for extraFilename, extraContents := range fConfig.ExtraFiles { + tarBuilder.AddFile(filepath.Join(bpDir, extraFilename), b.chmod, ts, []byte(extraContents)) + } + return tarBuilder.Reader(archive.DefaultTarWriterFactory()), nil }