diff --git a/build.go b/build.go index cea62b78..ef70dfec 100644 --- a/build.go +++ b/build.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/BurntSushi/toml" + "github.com/Masterminds/semver/v3" "github.com/paketo-buildpacks/packit/internal" ) @@ -54,6 +55,9 @@ type BuildFunc func(BuildContext) (BuildResult, error) type BuildResult struct { // Plan is the set of refinements to the Buildpack Plan that were performed // during the build phase. + // + // Deprecated: Use LaunchMetadata or BuildMetadata instead. For more information + // see https://buildpacks.io/docs/reference/spec/migration/buildpack-api-0.4-0.5/ Plan BuildpackPlan // Layers is a list of layers that will be persisted by the lifecycle at the @@ -65,6 +69,26 @@ type BuildResult struct { // the buildpack lifecycle specification: // https://github.com/buildpacks/spec/blob/main/buildpack.md#launchtoml-toml Launch LaunchMetadata + + // Build is the metadata that will be persisted as build.toml according to + // the buildpack lifecycle specification: + // https://github.com/buildpacks/spec/blob/main/buildpack.md#buildtoml-toml + Build BuildMetadata +} + +// BOMEntry contains a bill of materials entry. +type BOMEntry struct { + // Name represents the name of the entry. + Name string `toml:"name"` + + // Metadata is the metadata of the entry. Optional. + Metadata map[string]interface{} `toml:"metadata,omitempty"` +} + +// UnmetEntry contains the name of an unmet dependency from the build process +type UnmetEntry struct { + // Name represents the name of the entry. + Name string `toml:"name"` } // LaunchMetadata represents the launch metadata details persisted in the @@ -82,6 +106,35 @@ type LaunchMetadata struct { // Labels is a map of key-value pairs that will be returned to the lifecycle to be // added as config label on the image metadata. Keys must be unique. Labels map[string]string + + // BOM is the Bill-of-Material entries containing information about the + // dependencies provided to the launch environment. + BOM []BOMEntry +} + +func (l LaunchMetadata) isEmpty() bool { + return (len(l.Processes) == 0 && + len(l.Slices) == 0 && + len(l.Labels) == 0 && + len(l.BOM) == 0) +} + +func (b BuildMetadata) isEmpty() bool { + return (len(b.BOM) == 0 && + len(b.Unmet) == 0) +} + +// BuildMetadata represents the build metadata details persisted in the +// build.toml file according to the buildpack lifecycle specification: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#buildtoml-toml. +type BuildMetadata struct { + // BOM is the Bill-of-Material entries containing information about the + // dependencies provided to the build environment. + BOM []BOMEntry `toml:"bom"` + + // Unmet is a list of unmet entries from the build process that it was unable + // to provide. + Unmet []UnmetEntry `toml:"unmet"` } // Process represents a process to be run during the launch phase as described @@ -193,14 +246,23 @@ func Build(f BuildFunc, options ...Option) { } var buildpackInfo struct { - Buildpack BuildpackInfo `toml:"buildpack"` + APIVersion string `toml:"api"` + Buildpack BuildpackInfo `toml:"buildpack"` } + _, err = toml.DecodeFile(filepath.Join(cnbPath, "buildpack.toml"), &buildpackInfo) if err != nil { config.exitHandler.Error(err) return } + apiV05, _ := semver.NewVersion("0.5") + apiVersion, err := semver.NewVersion(buildpackInfo.APIVersion) + if err != nil { + config.exitHandler.Error(err) + return + } + result, err := f(BuildContext{ CNBPath: cnbPath, Stack: os.Getenv("CNB_STACK_ID"), @@ -216,10 +278,17 @@ func Build(f BuildFunc, options ...Option) { return } - err = config.tomlWriter.Write(planPath, result.Plan) - if err != nil { - config.exitHandler.Error(err) - return + if len(result.Plan.Entries) > 0 { + if apiVersion.GreaterThan(apiV05) || apiVersion.Equal(apiV05) { + config.exitHandler.Error(fmt.Errorf(`buildpack plan is read only since BuildPack API v0.5`)) + return + } + + err = config.tomlWriter.Write(planPath, result.Plan) + if err != nil { + config.exitHandler.Error(err) + return + } } layerTomls, err := filepath.Glob(filepath.Join(layersPath, "*.toml")) @@ -229,7 +298,7 @@ func Build(f BuildFunc, options ...Option) { } for _, file := range layerTomls { - if filepath.Base(file) != "launch.toml" && filepath.Base(file) != "store.toml" { + if filepath.Base(file) != "launch.toml" && filepath.Base(file) != "store.toml" && filepath.Base(file) != "build.toml" { err = os.Remove(file) if err != nil { config.exitHandler.Error(fmt.Errorf("failed to remove layer toml: %w", err)) @@ -264,9 +333,11 @@ func Build(f BuildFunc, options ...Option) { } } - if len(result.Launch.Processes) > 0 || - len(result.Launch.Slices) > 0 || - len(result.Launch.Labels) > 0 { + if !result.Launch.isEmpty() { + if apiVersion.LessThan(apiV05) && len(result.Launch.BOM) > 0 { + config.exitHandler.Error(fmt.Errorf("BOM entries in launch.toml is only supported with Buildpack API v0.5 or higher")) + return + } type label struct { Key string `toml:"key"` @@ -274,14 +345,15 @@ func Build(f BuildFunc, options ...Option) { } var launch struct { - Processes []Process `toml:"processes"` - Slices []Slice `toml:"slices"` - Labels []label `toml:"labels"` + Processes []Process `toml:"processes"` + Slices []Slice `toml:"slices"` + Labels []label `toml:"labels"` + BOM []BOMEntry `toml:"bom"` } launch.Processes = result.Launch.Processes launch.Slices = result.Launch.Slices - + launch.BOM = result.Launch.BOM if len(result.Launch.Labels) > 0 { launch.Labels = []label{} for k, v := range result.Launch.Labels { @@ -299,4 +371,17 @@ func Build(f BuildFunc, options ...Option) { return } } + + if !result.Build.isEmpty() { + if apiVersion.LessThan(apiV05) { + config.exitHandler.Error(fmt.Errorf("build.toml is only supported with Buildpack API v0.5 or higher")) + return + + } + err = config.tomlWriter.Write(filepath.Join(layersPath, "build.toml"), result.Build) + if err != nil { + config.exitHandler.Error(err) + return + } + } } diff --git a/build_test.go b/build_test.go index 15cd1ff6..5f0020d5 100644 --- a/build_test.go +++ b/build_test.go @@ -69,6 +69,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) bpTOML := []byte(` +api = "0.5" [buildpack] id = "some-id" name = "some-name" @@ -127,20 +128,36 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { }, })) }) + context("when there are updates to the build plan", func() { + context("when the api version is less than 0.5", func() { - it("updates the buildpack plan.toml with any changes", func() { - packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { - ctx.Plan.Entries[0].Metadata["other-key"] = "other-value" + it.Before(func() { + bpTOML := []byte(` +api = "0.4" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false +`) + Expect(ioutil.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) + Expect(ioutil.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - return packit.BuildResult{ - Plan: ctx.Plan, - }, nil - }, packit.WithArgs([]string{binaryPath, "", "", planPath})) + }) - contents, err := ioutil.ReadFile(planPath) - Expect(err).NotTo(HaveOccurred()) + it("updates the buildpack plan.toml with any changes", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + ctx.Plan.Entries[0].Metadata["other-key"] = "other-value" - Expect(string(contents)).To(MatchTOML(` + return packit.BuildResult{ + Plan: ctx.Plan, + }, nil + }, packit.WithArgs([]string{binaryPath, "", "", planPath})) + + contents, err := ioutil.ReadFile(planPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` [[entries]] name = "some-entry" @@ -149,6 +166,19 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { some-key = "some-value" other-key = "other-value" `)) + }) + }) + context("when the api version is greater or equal to 0.5", func() { + it("throws an error", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Plan: ctx.Plan, + }, nil + }, packit.WithArgs([]string{binaryPath, "", "", planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("buildpack plan is read only"))) + }) + }) }) it("persists layer metadata", func() { @@ -285,6 +315,175 @@ cache = true }) }) + context("when there are bom entries in the build metadata", func() { + it("persists a build.toml", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Build: packit.BuildMetadata{ + BOM: []packit.BOMEntry{ + { + Name: "example", + }, + { + Name: "another-example", + Metadata: map[string]interface{}{ + "version": "0.5", + }, + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + + contents, err := ioutil.ReadFile(filepath.Join(layersDir, "build.toml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` + [[bom]] + name = "example" + [[bom]] + name = "another-example" + [bom.metadata] + version = "0.5" + `)) + }) + + context("when the api version is less than 0.5", func() { + it.Before(func() { + bpTOML := []byte(` +api = "0.4" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false +`) + Expect(ioutil.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) + Expect(ioutil.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) + + }) + it("throws an error", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Build: packit.BuildMetadata{ + BOM: []packit.BOMEntry{ + { + Name: "example", + }, + { + Name: "another-example", + Metadata: map[string]interface{}{ + "version": "0.5", + }, + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("build.toml is only supported with Buildpack API v0.5 or higher"))) + + }) + }) + }) + + context("when there are unmet entries in the build metadata", func() { + it("persists a build.toml", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Build: packit.BuildMetadata{ + Unmet: []packit.UnmetEntry{ + { + Name: "example", + }, + { + Name: "another-example", + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + + contents, err := ioutil.ReadFile(filepath.Join(layersDir, "build.toml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` + [[unmet]] + name = "example" + [[unmet]] + name = "another-example" + `)) + }) + context("when the api version is less than 0.5", func() { + it.Before(func() { + bpTOML := []byte(` +api = "0.4" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false +`) + Expect(ioutil.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) + Expect(ioutil.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) + + }) + + it("throws an error", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Build: packit.BuildMetadata{ + Unmet: []packit.UnmetEntry{ + { + Name: "example", + }, + { + Name: "another-example", + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath}), packit.WithExitHandler(exitHandler)) + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("build.toml is only supported with Buildpack API v0.5 or higher"))) + + }) + }) + + }) + + context("when there are bom entries in the launch metadata", func() { + it("persists a launch.toml", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + BOM: []packit.BOMEntry{ + { + Name: "example", + }, + { + Name: "another-example", + Metadata: map[string]interface{}{ + "version": "0.5", + }, + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, "", planPath})) + + contents, err := ioutil.ReadFile(filepath.Join(layersDir, "launch.toml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` + [[bom]] + name = "example" + [[bom]] + name = "another-example" + [bom.metadata] + version = "0.5" + `)) + }) + }) + context("when there are processes in the result", func() { it("persists a launch.toml", func() { packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { @@ -449,7 +648,7 @@ cache = true }) }) - context("when there are no processes, slices or labels in the result", func() { + context("when there are no processes, slices, bom or labels in the result", func() { it("does not persist a launch.toml", func() { packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{}, nil @@ -585,12 +784,22 @@ cache = true context("when the buildpack plan.toml cannot be written", func() { it.Before(func() { + bpTOML := []byte(` +api = "0.4" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false + `) + Expect(ioutil.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) + Expect(ioutil.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) Expect(os.Chmod(planPath, 0444)).To(Succeed()) }) it("calls the exit handler", func() { packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { - return packit.BuildResult{}, nil + return packit.BuildResult{Plan: ctx.Plan}, nil }, packit.WithArgs([]string{binaryPath, "", "", planPath}), packit.WithExitHandler(exitHandler)) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) diff --git a/detect_test.go b/detect_test.go index 1e124ea7..7fa9e097 100644 --- a/detect_test.go +++ b/detect_test.go @@ -56,6 +56,7 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { binaryPath = filepath.Join(cnbDir, "bin", "detect") bpTOMLContent := []byte(` +api = "0.5" [buildpack] id = "some-id" name = "some-name" diff --git a/run_test.go b/run_test.go index fb6c89fb..621bd072 100644 --- a/run_test.go +++ b/run_test.go @@ -40,6 +40,7 @@ func testRun(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Expect(ioutil.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), []byte(` +api = "0.5" [buildpack] id = "some-id" name = "some-name"