diff --git a/phase/generator_test.go b/phase/generator_test.go index c014878ed..607ff6b6a 100644 --- a/phase/generator_test.go +++ b/phase/generator_test.go @@ -2,6 +2,7 @@ package phase_test import ( "bytes" + "fmt" "os" "path/filepath" "runtime" @@ -132,6 +133,9 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { // when we set up the table tests. var tmpErr error tmpDir, tmpErr = os.MkdirTemp("", "lifecycle") + generatedDir = filepath.Join(tmpDir, "output") + appDir = filepath.Join(generatedDir, "app") + platformDir = filepath.Join(tmpDir, "platform") it.Before(func() { h.AssertNil(t, tmpErr) @@ -140,9 +144,6 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { dirStore = testmock.NewMockDirStore(mockCtrl) executor = testmock.NewMockGenerateExecutor(mockCtrl) - generatedDir = filepath.Join(tmpDir, "output") - appDir = filepath.Join(generatedDir, "app") - platformDir = filepath.Join(tmpDir, "platform") h.Mkdir(t, generatedDir, appDir, filepath.Join(platformDir, "env")) stdout, stderr = &bytes.Buffer{}, &bytes.Buffer{} @@ -174,697 +175,495 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { extB := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "ext/B", Version: "v1"}}} extC := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "C", Version: "v1"}}} - when("using the platform API 0.12", func() { - it.Before(func() { - generator.PlatformAPI = api.MustParse("0.12") - }) - - it("provides a subset of the build plan to each extension", func() { - generator.Plan = files.Plan{ - Entries: []files.BuildPlanEntry{ - { - Providers: []buildpack.GroupElement{ - {ID: "A", Version: "v1"}, // not provided to any extension because Extension is false - }, - Requires: []buildpack.Require{ - {Name: "buildpack-dep", Version: "v1"}, - }, - }, - { - Providers: []buildpack.GroupElement{ - {ID: "A", Version: "v1", Extension: true}, - {ID: "ext/B", Version: "v2", Extension: true}, - }, - Requires: []buildpack.Require{ - {Name: "some-dep", Version: "v1"}, // not provided to B because it is met - }, - }, - { - Providers: []buildpack.GroupElement{ - {ID: "A", Version: "v1", Extension: true}, - {ID: "ext/B", Version: "v2", Extension: true}, - }, - Requires: []buildpack.Require{ - {Name: "some-unmet-dep", Version: "v2"}, // provided to ext/B because it is unmet - }, - }, - { - Providers: []buildpack.GroupElement{ - {ID: "ext/B", Version: "v2", Extension: true}, - }, - Requires: []buildpack.Require{ - {Name: "other-dep", Version: "v4"}, // only provided to ext/B - }, - }, - }, - } - expectedInputs := buildpack.GenerateInputs{ - AppDir: appDir, - PlatformDir: platformDir, - // OutputDir is ephemeral directory - } - - dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil) - expectedAInputs := expectedInputs - expectedAInputs.Plan = buildpack.Plan{Entries: []buildpack.Require{ - {Name: "some-dep", Version: "v1"}, - {Name: "some-unmet-dep", Version: "v2"}, - }} - executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).DoAndReturn( - func(_ buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ llog.Logger) (buildpack.GenerateOutputs, error) { - h.AssertEq(t, inputs.Plan, expectedAInputs.Plan) - h.AssertEq(t, inputs.AppDir, expectedAInputs.AppDir) - h.AssertEq(t, inputs.PlatformDir, expectedAInputs.PlatformDir) - h.AssertEq(t, inputs.BuildConfigDir, generator.BuildConfigDir) - return buildpack.GenerateOutputs{MetRequires: []string{"some-dep"}}, nil - }) - - dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil) - expectedBInputs := expectedInputs - expectedBInputs.Plan = buildpack.Plan{Entries: []buildpack.Require{ - {Name: "some-unmet-dep", Version: "v2"}, - {Name: "other-dep", Version: "v4"}, - }} - executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Do( - func(_ buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ llog.Logger) (buildpack.GenerateOutputs, error) { - h.AssertEq(t, inputs.Plan, expectedBInputs.Plan) - h.AssertEq(t, inputs.AppDir, expectedBInputs.AppDir) - h.AssertEq(t, inputs.PlatformDir, expectedBInputs.PlatformDir) - h.AssertEq(t, inputs.BuildConfigDir, generator.BuildConfigDir) - return buildpack.GenerateOutputs{}, nil - }) - - _, err := generator.Generate() - h.AssertNil(t, err) - }) - - it("passes through CNB_TARGET environment variables", func() { - generator.AnalyzedMD = files.Analyzed{ - RunImage: &files.RunImage{ - TargetMetadata: &files.TargetMetadata{ - OS: "linux", - Arch: "amd64", - }, - }, - } - // mock generate for extensions - these are tested elsewhere, so we just need to return anything... - dirStore.EXPECT().LookupExt(gomock.Any(), gomock.Any()).Return(&extA, nil) - dirStore.EXPECT().LookupExt(gomock.Any(), gomock.Any()).Return(&extB, nil) - // extension A has a build.Dockerfile and an extend-config.toml - h.Mkdir(t, filepath.Join(tmpDir, "A")) - buildDockerfilePathA := filepath.Join(tmpDir, "A", "build.Dockerfile") - h.Mkfile(t, "some-build.Dockerfile-content-A", buildDockerfilePathA) - extendConfigPathA := filepath.Join(tmpDir, "A", "extend-config.toml") - h.Mkfile(t, "some-extend-config.toml-content-A", extendConfigPathA) - executor.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(d buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ *log.Logger) (buildpack.GenerateOutputs, error) { - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH=amd64") - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH_VARIANT=") - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_NAME=") - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_VERSION=") - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_OS=linux") - return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID, - Kind: "build", Path: buildDockerfilePathA}}}, nil - }) - executor.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(d buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ *log.Logger) (buildpack.GenerateOutputs, error) { - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH=amd64") - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH_VARIANT=") - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_NAME=") - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_VERSION=") - h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_OS=linux") - return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID, - Kind: "build", Path: buildDockerfilePathA}}}, nil - }) - _, err := generator.Generate() - h.AssertNil(t, err) - }) - - it("copies Dockerfiles and extend-config.toml files to the correct locations", func() { - // mock generate for extension A - dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil) - // extension A has a build.Dockerfile and an extend-config.toml - h.Mkdir(t, filepath.Join(tmpDir, "A")) - buildDockerfilePathA := filepath.Join(tmpDir, "A", "build.Dockerfile") - h.Mkfile(t, "some-build.Dockerfile-content-A", buildDockerfilePathA) - extendConfigPathA := filepath.Join(tmpDir, "A", "extend-config.toml") - h.Mkfile(t, "some-extend-config.toml-content-A", extendConfigPathA) - executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ - Dockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "A", - Kind: "build", - Path: buildDockerfilePathA, - }, - }, - }, nil) - - // mock generate for extension B - dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil) - // extension B has a run.Dockerfile - h.Mkdir(t, filepath.Join(tmpDir, "B")) - runDockerfilePathB := filepath.Join(tmpDir, "B", "run.Dockerfile") - h.Mkfile(t, "some-run.Dockerfile-content-B", runDockerfilePathB) - executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ - Dockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "B", - Kind: "run", - Path: runDockerfilePathB, - }, - }, - }, nil) - - // mock generate for extension C - dirStore.EXPECT().LookupExt("C", "v1").Return(&extC, nil) - // extension C has a build.Dockerfile, run.Dockerfile, and extend-config.toml - h.Mkdir(t, filepath.Join(tmpDir, "C")) - buildDockerfilePathC := filepath.Join(tmpDir, "C", "build.Dockerfile") - h.Mkfile(t, "some-build.Dockerfile-content-C", buildDockerfilePathC) - runDockerfilePathC := filepath.Join(tmpDir, "C", "run.Dockerfile") - h.Mkfile(t, "some-run.Dockerfile-content-C", runDockerfilePathC) - extendConfigPathC := filepath.Join(tmpDir, "C", "extend-config.toml") - h.Mkfile(t, "some-extend-config.toml-content-C", extendConfigPathC) - executor.EXPECT().Generate(extC, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ - Dockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "C", - Kind: "build", - Path: buildDockerfilePathC, - }, - { - ExtensionID: "C", - Kind: "run", - Path: runDockerfilePathC, - }, - }, - }, nil) - - // add extension C to the group - generator.Extensions = append(generator.Extensions, buildpack.GroupElement{ID: "C", Version: "v1", API: api.Buildpack.Latest().String()}) - // do generate - _, err := generator.Generate() - h.AssertNil(t, err) - - t.Log("copies Dockerfiles") - contents := h.MustReadFile(t, filepath.Join(generatedDir, "build", "A", "Dockerfile")) - h.AssertEq(t, string(contents), "some-build.Dockerfile-content-A") - contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "B", "Dockerfile")) - h.AssertEq(t, string(contents), "some-run.Dockerfile-content-B") - contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "C", "Dockerfile")) - h.AssertEq(t, string(contents), "some-build.Dockerfile-content-C") - contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "C", "Dockerfile")) - h.AssertEq(t, string(contents), "some-run.Dockerfile-content-C") - - t.Log("copies extend-config.toml files if they exist") - contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "A", "extend-config.toml")) - h.AssertEq(t, string(contents), "some-extend-config.toml-content-A") - contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "C", "extend-config.toml")) - h.AssertEq(t, string(contents), "some-extend-config.toml-content-C") - contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "C", "extend-config.toml")) - h.AssertEq(t, string(contents), "some-extend-config.toml-content-C") - - t.Log("does not pollute the output directory") - h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "A", "run.Dockerfile")) - h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "B", "build.Dockerfile")) - h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "C", "run.Dockerfile")) - h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "C", "build.Dockerfile")) - }) - - when("returning run image metadata", func() { - var ( - runDockerfilePathA = filepath.Join(tmpDir, "run.Dockerfile.A") - runDockerfilePathB = filepath.Join(tmpDir, "run.Dockerfile.B") - ) + for _, platformAPI := range []*api.Version{ + api.MustParse("0.12"), + api.MustParse("0.13"), + } { + platformAPI := platformAPI + when(fmt.Sprintf("using the platform API %s", platformAPI), func() { it.Before(func() { - h.Mkfile(t, "some-dockerfile-content-A", runDockerfilePathA) - h.Mkfile(t, "some-dockerfile-content-B", runDockerfilePathB) - - generator.AnalyzedMD = files.Analyzed{ - RunImage: &files.RunImage{ - Reference: "some-existing-run-image@sha256:s0m3d1g3st", - Image: "some-existing-run-image", - }, - } + generator.PlatformAPI = platformAPI }) - type testCase struct { - before func() - descCondition string - descResult string - aDockerfiles []buildpack.DockerfileInfo - bDockerfiles []buildpack.DockerfileInfo - expectedRunImageImage string - expectedRunImageReference string - expectedRunImageExtend bool - expectedErr string - assertAfter func() - } - for _, tc := range []testCase{ - { - descCondition: "all run.Dockerfiles declare `FROM ${base_image}`", - descResult: "returns the original run image in the result", - aDockerfiles: []buildpack.DockerfileInfo{{ - ExtensionID: "A", - Kind: "run", - Path: runDockerfilePathA, - WithBase: "", - Extend: true, - }}, - bDockerfiles: []buildpack.DockerfileInfo{{ - ExtensionID: "B", - Kind: "run", - Path: runDockerfilePathB, - WithBase: "", - Extend: true, - }}, - expectedRunImageImage: "some-existing-run-image", - expectedRunImageReference: "some-existing-run-image@sha256:s0m3d1g3st", - expectedRunImageExtend: true, - }, - { - descCondition: "a run.Dockerfile declares a new base image and run.Dockerfiles follow", - descResult: "returns the last image referenced in the `FROM` statement of the last run.Dockerfile not to declare `FROM ${base_image}`, and sets extend to true", - aDockerfiles: []buildpack.DockerfileInfo{ + it("provides a subset of the build plan to each extension", func() { + generator.Plan = files.Plan{ + Entries: []files.BuildPlanEntry{ { - ExtensionID: "A", - Kind: "run", - Path: runDockerfilePathA, - WithBase: "some-new-run-image", - Extend: false, - }, - }, - bDockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "B", - Kind: "run", - Path: runDockerfilePathB, - WithBase: "", - Extend: true, + Providers: []buildpack.GroupElement{ + {ID: "some-buildpack", Version: "v1"}, // not provided to any extension because Extension is false + }, + Requires: []buildpack.Require{ + {Name: "buildpack-dep", Version: "v1"}, + }, }, - }, - expectedRunImageImage: "some-new-run-image", - expectedRunImageReference: "some-new-run-image", - expectedRunImageExtend: true, - }, - { - descCondition: "a run.Dockerfile declares a new base image (only) and no run.Dockerfiles follow", - descResult: "returns the referenced base image, and sets extend to false", - aDockerfiles: []buildpack.DockerfileInfo{ { - ExtensionID: "A", - Kind: "run", - Path: runDockerfilePathA, - WithBase: "some-new-run-image", - Extend: true, + Providers: []buildpack.GroupElement{ + {ID: "A", Version: "v1", Extension: true}, + {ID: "ext/B", Version: "v2", Extension: true}, + }, + Requires: []buildpack.Require{ + {Name: "some-dep", Version: "v1"}, // not provided to B because it is met + }, }, - }, - bDockerfiles: []buildpack.DockerfileInfo{ { - ExtensionID: "B", - Kind: "run", - Path: runDockerfilePathB, - WithBase: "some-other-base-image", - Extend: false, + Providers: []buildpack.GroupElement{ + {ID: "A", Version: "v1", Extension: true}, + {ID: "ext/B", Version: "v2", Extension: true}, + }, + Requires: []buildpack.Require{ + {Name: "some-unmet-dep", Version: "v2"}, // provided to ext/B because it is unmet + }, }, - }, - expectedRunImageImage: "some-other-base-image", - expectedRunImageReference: "some-other-base-image", - expectedRunImageExtend: false, - assertAfter: func() { - t.Log("copies Dockerfiles to the correct locations") - t.Log("renames earlier run.Dockerfiles to Dockerfile.ignore in the output directory") - aContents := h.MustReadFile(t, filepath.Join(generatedDir, "run", "A", "Dockerfile.ignore")) - h.AssertEq(t, string(aContents), `some-dockerfile-content-A`) - BContents := h.MustReadFile(t, filepath.Join(generatedDir, "run", "B", "Dockerfile")) - h.AssertEq(t, string(BContents), `some-dockerfile-content-B`) - }, - }, - { - descCondition: "a run.Dockerfile declares a new base image and also extends", - descResult: "returns the referenced base image, and sets extend to true", - aDockerfiles: []buildpack.DockerfileInfo{ { - ExtensionID: "A", - Kind: "run", - Path: runDockerfilePathA, - WithBase: "some-new-run-image", - Extend: true, - }, - }, - bDockerfiles: []buildpack.DockerfileInfo{}, - expectedRunImageImage: "some-new-run-image", - expectedRunImageReference: "some-new-run-image", - expectedRunImageExtend: true, - }, - { - before: func() { - generator.RunMetadata = files.Run{ - Images: []files.RunImageForExport{ - {Image: "some-run-image"}, + Providers: []buildpack.GroupElement{ + {ID: "ext/B", Version: "v2", Extension: true}, }, - } - }, - descCondition: "run metadata is provided and contains new run image", - descResult: "succeeds", - aDockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "A", - Kind: "run", - Path: runDockerfilePathA, - WithBase: "some-new-run-image", - Extend: false, - }, - }, - bDockerfiles: []buildpack.DockerfileInfo{}, - expectedRunImageImage: "some-new-run-image", - expectedRunImageReference: "some-new-run-image", - expectedRunImageExtend: false, - }, - { - before: func() { - generator.RunMetadata = files.Run{ - Images: []files.RunImageForExport{ - {Image: "some-run-image"}, + Requires: []buildpack.Require{ + {Name: "other-dep", Version: "v4"}, // only provided to ext/B }, - } - }, - descCondition: "run metadata is provided and does not contain new run image", - descResult: "succeeds with warning", - aDockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "A", - Kind: "run", - Path: runDockerfilePathA, - WithBase: "some-other-run-image", - Extend: false, }, }, - bDockerfiles: []buildpack.DockerfileInfo{}, - expectedRunImageImage: "some-other-run-image", - expectedRunImageReference: "some-other-run-image", - assertAfter: func() { - h.AssertLogEntry(t, logHandler, "new runtime base image 'some-other-run-image' not found in run metadata") - }, - }, - } { - tc := tc - when := when - when(tc.descCondition, func() { - if tc.before != nil { - it.Before(tc.before) - } - - it(tc.descResult, func() { - // mock generate for extension A - dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil) - executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ - Dockerfiles: tc.aDockerfiles, - }, nil) + } + expectedInputs := buildpack.GenerateInputs{ + AppDir: appDir, + PlatformDir: platformDir, + // OutputDir is ephemeral directory + } - // mock generate for extension B - dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil) - executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ - Dockerfiles: tc.bDockerfiles, - }, nil) - - // do generate - result, err := generator.Generate() - if err == nil { - h.AssertEq(t, result.AnalyzedMD.RunImage.Image, tc.expectedRunImageImage) - h.AssertEq(t, result.AnalyzedMD.RunImage.Reference, tc.expectedRunImageReference) - h.AssertEq(t, result.AnalyzedMD.RunImage.Extend, tc.expectedRunImageExtend) - } else { - t.Log(err) - h.AssertError(t, err, tc.expectedErr) - } + dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil) + expectedAInputs := expectedInputs + expectedAInputs.Plan = buildpack.Plan{Entries: []buildpack.Require{ + {Name: "some-dep", Version: "v1"}, + {Name: "some-unmet-dep", Version: "v2"}, + }} + executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).DoAndReturn( + func(_ buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ llog.Logger) (buildpack.GenerateOutputs, error) { + h.AssertEq(t, inputs.Plan, expectedAInputs.Plan) + h.AssertEq(t, inputs.AppDir, expectedAInputs.AppDir) + h.AssertEq(t, inputs.PlatformDir, expectedAInputs.PlatformDir) + h.AssertEq(t, inputs.BuildConfigDir, generator.BuildConfigDir) + return buildpack.GenerateOutputs{MetRequires: []string{"some-dep"}}, nil + }) - if tc.assertAfter != nil { - tc.assertAfter() - } + dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil) + expectedBInputs := expectedInputs + expectedBInputs.Plan = buildpack.Plan{Entries: []buildpack.Require{ + {Name: "some-unmet-dep", Version: "v2"}, + {Name: "other-dep", Version: "v4"}, + }} + executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Do( + func(_ buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ llog.Logger) (buildpack.GenerateOutputs, error) { + h.AssertEq(t, inputs.Plan, expectedBInputs.Plan) + h.AssertEq(t, inputs.AppDir, expectedBInputs.AppDir) + h.AssertEq(t, inputs.PlatformDir, expectedBInputs.PlatformDir) + h.AssertEq(t, inputs.BuildConfigDir, generator.BuildConfigDir) + return buildpack.GenerateOutputs{}, nil }) - }) - } - }) - when("extension generate failed", func() { - when("first extension fails", func() { - it("errors", func() { - extA := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}} - dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil) - executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, errors.New("some error")) + _, err := generator.Generate() + h.AssertNil(t, err) + }) - if _, err := generator.Generate(); err == nil { - t.Fatal("Expected error.\n") - } else if !strings.Contains(err.Error(), "some error") { - t.Fatalf("Incorrect error: %s\n", err) - } - }) + it("passes through CNB_TARGET environment variables", func() { + generator.AnalyzedMD = files.Analyzed{ + RunImage: &files.RunImage{ + TargetMetadata: &files.TargetMetadata{ + OS: "linux", + Arch: "amd64", + }, + }, + } + // mock generate for extensions - these are tested elsewhere, so we just need to return anything... + dirStore.EXPECT().LookupExt(gomock.Any(), gomock.Any()).Return(&extA, nil) + dirStore.EXPECT().LookupExt(gomock.Any(), gomock.Any()).Return(&extB, nil) + buildDockerfilePathA := filepath.Join(tmpDir, "dummy.Dockerfile") + h.Mkfile(t, "some-build.Dockerfile-content-A", buildDockerfilePathA) + executor.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(d buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ *log.Logger) (buildpack.GenerateOutputs, error) { + h.AssertContains(t, inputs.Env.List(), + "CNB_TARGET_ARCH=amd64", + "CNB_TARGET_ARCH_VARIANT=", + "CNB_TARGET_DISTRO_NAME=", + "CNB_TARGET_DISTRO_VERSION=", + "CNB_TARGET_OS=linux", + ) + return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID, + Kind: "build", Path: buildDockerfilePathA}}}, nil + }) + executor.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(d buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ *log.Logger) (buildpack.GenerateOutputs, error) { + h.AssertContains(t, inputs.Env.List(), + "CNB_TARGET_ARCH=amd64", + "CNB_TARGET_ARCH_VARIANT=", + "CNB_TARGET_DISTRO_NAME=", + "CNB_TARGET_DISTRO_VERSION=", + "CNB_TARGET_OS=linux", + ) + return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID, + Kind: "build", Path: buildDockerfilePathA}}}, nil + }) + _, err := generator.Generate() + h.AssertNil(t, err) }) - when("later extension fails", func() { - it("errors", func() { - extA := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}} + if platformAPI.LessThan("0.13") { // Platform API >= 0.13 does no longer require Dockerfile copying + it("copies Dockerfiles and extend-config.toml files to the correct locations", func() { + // mock generate for extension A dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil) - executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, nil) - extB := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "ext/B", Version: "v1"}}} - dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil) - executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, errors.New("some error")) + // extension A has a build.Dockerfile and an extend-config.toml + h.Mkdir(t, filepath.Join(tmpDir, "A")) + buildDockerfilePathA := filepath.Join(tmpDir, "A", "build.Dockerfile") + h.Mkfile(t, "some-build.Dockerfile-content-A", buildDockerfilePathA) + extendConfigPathA := filepath.Join(tmpDir, "A", "extend-config.toml") + h.Mkfile(t, "some-extend-config.toml-content-A", extendConfigPathA) + executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ + Dockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "A", + Kind: "build", + Path: buildDockerfilePathA, + }, + }, + }, nil) - if _, err := generator.Generate(); err == nil { - t.Fatal("Expected error.\n") - } else if !strings.Contains(err.Error(), "some error") { - t.Fatalf("Incorrect error: %s\n", err) - } + // mock generate for extension B + dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil) + // extension B has a run.Dockerfile + h.Mkdir(t, filepath.Join(tmpDir, "B")) + runDockerfilePathB := filepath.Join(tmpDir, "B", "run.Dockerfile") + h.Mkfile(t, "some-run.Dockerfile-content-B", runDockerfilePathB) + executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ + Dockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "B", + Kind: "run", + Path: runDockerfilePathB, + }, + }, + }, nil) + + // mock generate for extension C + dirStore.EXPECT().LookupExt("C", "v1").Return(&extC, nil) + // extension C has a build.Dockerfile, run.Dockerfile, and extend-config.toml + h.Mkdir(t, filepath.Join(tmpDir, "C")) + buildDockerfilePathC := filepath.Join(tmpDir, "C", "build.Dockerfile") + h.Mkfile(t, "some-build.Dockerfile-content-C", buildDockerfilePathC) + runDockerfilePathC := filepath.Join(tmpDir, "C", "run.Dockerfile") + h.Mkfile(t, "some-run.Dockerfile-content-C", runDockerfilePathC) + extendConfigPathC := filepath.Join(tmpDir, "C", "extend-config.toml") + h.Mkfile(t, "some-extend-config.toml-content-C", extendConfigPathC) + executor.EXPECT().Generate(extC, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ + Dockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "C", + Kind: "build", + Path: buildDockerfilePathC, + }, + { + ExtensionID: "C", + Kind: "run", + Path: runDockerfilePathC, + }, + }, + }, nil) + + // add extension C to the group + generator.Extensions = append(generator.Extensions, buildpack.GroupElement{ID: "C", Version: "v1", API: api.Buildpack.Latest().String()}) + // do generate + _, err := generator.Generate() + h.AssertNil(t, err) + + t.Log("copies Dockerfiles") + + contents := h.MustReadFile(t, filepath.Join(generatedDir, "build", "A", "Dockerfile")) + h.AssertEq(t, string(contents), "some-build.Dockerfile-content-A") + contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "B", "Dockerfile")) + h.AssertEq(t, string(contents), "some-run.Dockerfile-content-B") + contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "C", "Dockerfile")) + h.AssertEq(t, string(contents), "some-build.Dockerfile-content-C") + contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "C", "Dockerfile")) + h.AssertEq(t, string(contents), "some-run.Dockerfile-content-C") + + t.Log("copies extend-config.toml files if they exist") + contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "A", "extend-config.toml")) + h.AssertEq(t, string(contents), "some-extend-config.toml-content-A") + contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "C", "extend-config.toml")) + h.AssertEq(t, string(contents), "some-extend-config.toml-content-C") + contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "C", "extend-config.toml")) + h.AssertEq(t, string(contents), "some-extend-config.toml-content-C") + + t.Log("does not pollute the output directory") + h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "A", "run.Dockerfile")) + h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "B", "build.Dockerfile")) + h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "C", "run.Dockerfile")) + h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "C", "build.Dockerfile")) }) - }) - }) - }) - - when("using the platform API 0.13", func() { - it.Before(func() { - generator.PlatformAPI = api.MustParse("0.13") - }) + } - when("returning run image metadata", func() { - var ( - runDockerfilePathA string - runDockerfilePathB string - ) + when("returning run image metadata", func() { + var runDockerfilePathA, runDockerfilePathB string + if platformAPI.AtLeast("0.13") { + runDockerfilePathA = filepath.Join(generatedDir, "A", "run.Dockerfile.A") + runDockerfilePathB = filepath.Join(generatedDir, "B", "run.Dockerfile.B") + } else { + runDockerfilePathA = filepath.Join(tmpDir, "run.Dockerfile.A") + runDockerfilePathB = filepath.Join(tmpDir, "run.Dockerfile.B") + } - it.Before(func() { - runDockerfilePathA = filepath.Join(generatedDir, "A", "run.Dockerfile.A") - runDockerfilePathB = filepath.Join(generatedDir, "B", "run.Dockerfile.B") + it.Before(func() { + h.Mkdir(t, filepath.Dir(runDockerfilePathA), filepath.Dir(runDockerfilePathB)) + h.Mkfile(t, "some-dockerfile-content-A", runDockerfilePathA) + h.Mkfile(t, "some-dockerfile-content-B", runDockerfilePathB) - h.Mkdir(t, filepath.Join(generatedDir, "A"), filepath.Join(generatedDir, "B")) - h.Mkfile(t, "some-dockerfile-content-A", runDockerfilePathA) - h.Mkfile(t, "some-dockerfile-content-B", runDockerfilePathB) + generator.AnalyzedMD = files.Analyzed{ + RunImage: &files.RunImage{ + Reference: "some-existing-run-image@sha256:s0m3d1g3st", + Image: "some-existing-run-image", + }, + } + }) - generator.AnalyzedMD = files.Analyzed{ - RunImage: &files.RunImage{ - Reference: "some-existing-run-image@sha256:s0m3d1g3st", - Image: "some-existing-run-image", - }, + type testCase struct { + before func() + descCondition string + descResult string + aDockerfiles []buildpack.DockerfileInfo + bDockerfiles []buildpack.DockerfileInfo + expectedRunImageImage string + expectedRunImageReference string + expectedRunImageExtend bool + expectedErr string + assertAfter func() } - }) - - type testCase struct { - before func() - descCondition string - descResult string - aDockerfiles []buildpack.DockerfileInfo - bDockerfiles []buildpack.DockerfileInfo - expectedRunImageImage string - expectedRunImageReference string - expectedRunImageExtend bool - expectedErr string - assertAfter func() - } - for _, tc := range []testCase{ - { - descCondition: "all run.Dockerfiles declare `FROM ${base_image}`", - descResult: "returns the original run image in the result", - aDockerfiles: []buildpack.DockerfileInfo{{ - ExtensionID: "A", - Kind: "run", - WithBase: "", - Extend: true, - }}, - bDockerfiles: []buildpack.DockerfileInfo{{ - ExtensionID: "B", - Kind: "run", - WithBase: "", - Extend: true, - }}, - expectedRunImageImage: "some-existing-run-image", - expectedRunImageReference: "some-existing-run-image@sha256:s0m3d1g3st", - expectedRunImageExtend: true, - }, - { - descCondition: "a run.Dockerfile declares a new base image and run.Dockerfiles follow", - descResult: "returns the last image referenced in the `FROM` statement of the last run.Dockerfile not to declare `FROM ${base_image}`, and sets extend to true", - aDockerfiles: []buildpack.DockerfileInfo{ - { + for _, tc := range []testCase{ + { + descCondition: "all run.Dockerfiles declare `FROM ${base_image}`", + descResult: "returns the original run image in the result", + aDockerfiles: []buildpack.DockerfileInfo{{ ExtensionID: "A", Kind: "run", - WithBase: "some-new-run-image", - Extend: false, - }, - }, - bDockerfiles: []buildpack.DockerfileInfo{ - { + Path: runDockerfilePathA, + WithBase: "", + Extend: true, + }}, + bDockerfiles: []buildpack.DockerfileInfo{{ ExtensionID: "B", Kind: "run", + Path: runDockerfilePathB, WithBase: "", Extend: true, - }, + }}, + expectedRunImageImage: "some-existing-run-image", + expectedRunImageReference: "some-existing-run-image@sha256:s0m3d1g3st", + expectedRunImageExtend: true, }, - expectedRunImageImage: "some-new-run-image", - expectedRunImageReference: "some-new-run-image", - expectedRunImageExtend: true, - }, - { - descCondition: "a run.Dockerfile declares a new base image (only) and no run.Dockerfiles follow", - descResult: "returns the referenced base image, and sets extend to false", - aDockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "A", - Kind: "run", - WithBase: "some-new-run-image", - Extend: true, + { + descCondition: "a run.Dockerfile declares a new base image and run.Dockerfiles follow", + descResult: "returns the last image referenced in the `FROM` statement of the last run.Dockerfile not to declare `FROM ${base_image}`, and sets extend to true", + aDockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "A", + Kind: "run", + Path: runDockerfilePathA, + WithBase: "some-new-run-image", + Extend: false, + }, }, - }, - bDockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "B", - Kind: "run", - WithBase: "some-other-base-image", - Extend: false, + bDockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "B", + Kind: "run", + Path: runDockerfilePathB, + WithBase: "", + Extend: true, + }, }, + expectedRunImageImage: "some-new-run-image", + expectedRunImageReference: "some-new-run-image", + expectedRunImageExtend: true, }, - expectedRunImageImage: "some-other-base-image", - expectedRunImageReference: "some-other-base-image", - expectedRunImageExtend: false, - assertAfter: func() { - t.Log("renames earlier run.Dockerfiles to Dockerfile.ignore in the output directory") - aContents := h.MustReadFile(t, filepath.Join(generatedDir, "A", "run.Dockerfile.A.ignore")) - h.AssertEq(t, string(aContents), `some-dockerfile-content-A`) - BContents := h.MustReadFile(t, filepath.Join(generatedDir, "B", "run.Dockerfile.B")) - h.AssertEq(t, string(BContents), `some-dockerfile-content-B`) - }, - }, - { - descCondition: "a run.Dockerfile declares a new base image and also extends", - descResult: "returns the referenced base image, and sets extend to true", - aDockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "A", - Kind: "run", - WithBase: "some-new-run-image", - Extend: true, + { + descCondition: "a run.Dockerfile declares a new base image (only) and no run.Dockerfiles follow", + descResult: "returns the referenced base image, and sets extend to false", + aDockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "A", + Kind: "run", + Path: runDockerfilePathA, + WithBase: "some-new-run-image", + Extend: true, + }, }, - }, - bDockerfiles: []buildpack.DockerfileInfo{}, - expectedRunImageImage: "some-new-run-image", - expectedRunImageReference: "some-new-run-image", - expectedRunImageExtend: true, - }, - { - before: func() { - generator.RunMetadata = files.Run{ - Images: []files.RunImageForExport{ - {Image: "some-run-image"}, + bDockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "B", + Kind: "run", + Path: runDockerfilePathB, + WithBase: "some-other-base-image", + Extend: false, }, - } - }, - descCondition: "run metadata is provided and contains new run image", - descResult: "succeeds", - aDockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "A", - Kind: "run", - WithBase: "some-new-run-image", - Extend: false, + }, + expectedRunImageImage: "some-other-base-image", + expectedRunImageReference: "some-other-base-image", + expectedRunImageExtend: false, + assertAfter: func() { + var runDockerfileDestinationPathA, runDockerfileDestinationPathB string + if platformAPI.AtLeast("0.13") { + runDockerfileDestinationPathA = runDockerfilePathA + ".ignore" + runDockerfileDestinationPathB = runDockerfilePathB + } else { + runDockerfileDestinationPathA = filepath.Join(generatedDir, "run", "A", "Dockerfile.ignore") + runDockerfileDestinationPathB = filepath.Join(generatedDir, "run", "B", "Dockerfile") + t.Log("copies Dockerfiles to the correct locations") + } + t.Log("renames earlier run.Dockerfiles to Dockerfile.ignore in the output directory") + aContents := h.MustReadFile(t, runDockerfileDestinationPathA) + h.AssertEq(t, string(aContents), `some-dockerfile-content-A`) + bContents := h.MustReadFile(t, runDockerfileDestinationPathB) + h.AssertEq(t, string(bContents), `some-dockerfile-content-B`) }, }, - bDockerfiles: []buildpack.DockerfileInfo{}, - expectedRunImageImage: "some-new-run-image", - expectedRunImageReference: "some-new-run-image", - expectedRunImageExtend: false, - }, - { - before: func() { - generator.RunMetadata = files.Run{ - Images: []files.RunImageForExport{ - {Image: "some-run-image"}, + { + descCondition: "a run.Dockerfile declares a new base image and also extends", + descResult: "returns the referenced base image, and sets extend to true", + aDockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "A", + Kind: "run", + Path: runDockerfilePathA, + WithBase: "some-new-run-image", + Extend: true, }, - } + }, + bDockerfiles: []buildpack.DockerfileInfo{}, + expectedRunImageImage: "some-new-run-image", + expectedRunImageReference: "some-new-run-image", + expectedRunImageExtend: true, }, - descCondition: "run metadata is provided and does not contain new run image", - descResult: "succeeds with warning", - aDockerfiles: []buildpack.DockerfileInfo{ - { - ExtensionID: "A", - Kind: "run", - WithBase: "some-other-run-image", - Extend: false, + { + before: func() { + generator.RunMetadata = files.Run{ + Images: []files.RunImageForExport{ + {Image: "some-run-image"}, + }, + } }, + descCondition: "run metadata is provided and contains new run image", + descResult: "succeeds", + aDockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "A", + Kind: "run", + Path: runDockerfilePathA, + WithBase: "some-new-run-image", + Extend: false, + }, + }, + bDockerfiles: []buildpack.DockerfileInfo{}, + expectedRunImageImage: "some-new-run-image", + expectedRunImageReference: "some-new-run-image", + expectedRunImageExtend: false, }, - bDockerfiles: []buildpack.DockerfileInfo{}, - expectedRunImageImage: "some-other-run-image", - expectedRunImageReference: "some-other-run-image", - assertAfter: func() { - h.AssertLogEntry(t, logHandler, "new runtime base image 'some-other-run-image' not found in run metadata") + { + before: func() { + generator.RunMetadata = files.Run{ + Images: []files.RunImageForExport{ + {Image: "some-run-image"}, + }, + } + }, + descCondition: "run metadata is provided and does not contain new run image", + descResult: "succeeds with warning", + aDockerfiles: []buildpack.DockerfileInfo{ + { + ExtensionID: "A", + Kind: "run", + Path: runDockerfilePathA, + WithBase: "some-other-run-image", + Extend: false, + }, + }, + bDockerfiles: []buildpack.DockerfileInfo{}, + expectedRunImageImage: "some-other-run-image", + expectedRunImageReference: "some-other-run-image", + assertAfter: func() { + h.AssertLogEntry(t, logHandler, "new runtime base image 'some-other-run-image' not found in run metadata") + }, }, - }, - } { - tc := tc - when := when - when(tc.descCondition, func() { - if tc.before != nil { - it.Before(tc.before) - } - - it(tc.descResult, func() { - for i := range tc.aDockerfiles { - tc.aDockerfiles[i].Path = runDockerfilePathA + } { + tc := tc + when := when + when(tc.descCondition, func() { + if tc.before != nil { + it.Before(tc.before) } - for i := range tc.bDockerfiles { - tc.bDockerfiles[i].Path = runDockerfilePathB - } + it(tc.descResult, func() { + // mock generate for extension A + dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil) + executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ + Dockerfiles: tc.aDockerfiles, + }, nil) + + // mock generate for extension B + dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil) + executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ + Dockerfiles: tc.bDockerfiles, + }, nil) + + // do generate + result, err := generator.Generate() + if err == nil { + h.AssertEq(t, result.AnalyzedMD.RunImage.Image, tc.expectedRunImageImage) + h.AssertEq(t, result.AnalyzedMD.RunImage.Reference, tc.expectedRunImageReference) + h.AssertEq(t, result.AnalyzedMD.RunImage.Extend, tc.expectedRunImageExtend) + } else { + t.Log(err) + h.AssertError(t, err, tc.expectedErr) + } + + if tc.assertAfter != nil { + tc.assertAfter() + } + }) + }) + } + }) - // mock generate for extension A + when("extension generate failed", func() { + when("first extension fails", func() { + it("errors", func() { + extA := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}} dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil) - executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ - Dockerfiles: tc.aDockerfiles, - }, nil) + executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, errors.New("some error")) - // mock generate for extension B - dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil) - executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{ - Dockerfiles: tc.bDockerfiles, - }, nil) - - // do generate - result, err := generator.Generate() - if err == nil { - h.AssertEq(t, result.AnalyzedMD.RunImage.Image, tc.expectedRunImageImage) - h.AssertEq(t, result.AnalyzedMD.RunImage.Reference, tc.expectedRunImageReference) - h.AssertEq(t, result.AnalyzedMD.RunImage.Extend, tc.expectedRunImageExtend) - } else { - t.Log(err) - h.AssertError(t, err, tc.expectedErr) + if _, err := generator.Generate(); err == nil { + t.Fatal("Expected error.\n") + } else if !strings.Contains(err.Error(), "some error") { + t.Fatalf("Incorrect error: %s\n", err) } + }) + }) - if tc.assertAfter != nil { - tc.assertAfter() + when("later extension fails", func() { + it("errors", func() { + extA := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}} + dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil) + executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, nil) + extB := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "ext/B", Version: "v1"}}} + dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil) + executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, errors.New("some error")) + + if _, err := generator.Generate(); err == nil { + t.Fatal("Expected error.\n") + } else if !strings.Contains(err.Error(), "some error") { + t.Fatalf("Incorrect error: %s\n", err) } }) }) - } + }) }) - }) + } }) }