From d96d141a70e6dc19757f61d92b3e09017448efca Mon Sep 17 00:00:00 2001 From: Ryan Moran Date: Fri, 15 May 2020 14:16:59 -0400 Subject: [PATCH] Adds Run Run takes a DetectFunc and a BuildFunc as arguments. Then when compiled into executables called "detect" or "build", it uses the name of the executable to determine which phase to run. This helps simplify buildpacks wishing to combine their detect and build executables into a single executable with symlinking. --- cargo/jam/init_test.go | 3 + doc.go | 35 +++++++++- init_test.go | 1 + run.go | 36 ++++++++++ run_test.go | 153 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 run.go create mode 100644 run_test.go diff --git a/cargo/jam/init_test.go b/cargo/jam/init_test.go index f17f106d..6ba06d17 100644 --- a/cargo/jam/init_test.go +++ b/cargo/jam/init_test.go @@ -10,6 +10,7 @@ import ( "os" "sync" "testing" + "time" "github.com/onsi/gomega/gexec" "github.com/sclevine/spec" @@ -21,6 +22,8 @@ import ( var path string func TestUnitJam(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + suite := spec.New("cargo/jam", spec.Report(report.Terminal{})) suite("pack", testPack) suite("summarize", testSummarize) diff --git a/doc.go b/doc.go index a73ad4ac..3c6772c3 100644 --- a/doc.go +++ b/doc.go @@ -96,9 +96,7 @@ // // package main // -// import ( -// "github.com/paketo-buildpacks/packit" -// ) +// import "github.com/paketo-buildpacks/packit" // // func main() { // // The build phase includes the yarn cli in a new layer that is made @@ -152,6 +150,37 @@ // return nil // } // +// Run +// +// Buildpacks can be created with a single entrypoint executable using the +// packit.Run function. Here, you can combine both the Detect and Build phases +// and run will ensure that the correct phase is called when the matching +// executable is called by the Cloud Native Buildpack Lifecycle. Below is an +// example that combines a simple detect and build into a single main program. +// +// package main +// +// import "github.com/paketo-buildpacks/packit" +// +// func main() { +// detect := func(context packit.DetectContext) (packit.DetectResult, error) { +// return packit.DetectResult{}, nil +// } + +// build := func(context packit.BuildContext) (packit.BuildResult, error) { +// return packit.BuildResult{ +// Processes: []packit.Process{ +// { +// Type: "web", +// Command: `while true; do nc -l -p $PORT -c 'echo -e "HTTP/1.1 200 OK\n\n Hello, world!\n"'; done`, +// }, +// }, +// }, nil +// } +// +// packit.Run(detect, build) +// } +// // Summary // // These examples show the very basics of what a buildpack implementation using diff --git a/init_test.go b/init_test.go index 3049567e..dc5a7fd8 100644 --- a/init_test.go +++ b/init_test.go @@ -14,5 +14,6 @@ func TestUnitPackit(t *testing.T) { suite("Environment", testEnvironment) suite("Layer", testLayer) suite("Layers", testLayers) + suite("Run", testRun) suite.Run(t) } diff --git a/run.go b/run.go new file mode 100644 index 00000000..23f4b294 --- /dev/null +++ b/run.go @@ -0,0 +1,36 @@ +package packit + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/paketo-buildpacks/packit/internal" +) + +// Run combines the invocation of both build and detect into a single entry +// point. Calling Run from an executable with a name matching "build" or +// "detect" will result in the matching DetectFunc or BuildFunc being called. +func Run(detect DetectFunc, build BuildFunc, options ...Option) { + config := OptionConfig{ + exitHandler: internal.NewExitHandler(), + args: os.Args, + } + + for _, option := range options { + config = option(config) + } + + phase := filepath.Base(config.args[0]) + + switch phase { + case "detect": + Detect(detect, options...) + + case "build": + Build(build, options...) + + default: + config.exitHandler.Error(fmt.Errorf("failed to run buildpack: unknown lifecycle phase %q", phase)) + } +} diff --git a/run_test.go b/run_test.go new file mode 100644 index 00000000..fb6c89fb --- /dev/null +++ b/run_test.go @@ -0,0 +1,153 @@ +package packit_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/paketo-buildpacks/packit" + "github.com/paketo-buildpacks/packit/fakes" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testRun(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + workingDir string + tmpDir string + cnbDir string + exitHandler *fakes.ExitHandler + ) + + it.Before(func() { + var err error + workingDir, err = os.Getwd() + Expect(err).NotTo(HaveOccurred()) + + tmpDir, err = ioutil.TempDir("", "") + Expect(err).NotTo(HaveOccurred()) + + tmpDir, err = filepath.EvalSymlinks(tmpDir) + Expect(err).NotTo(HaveOccurred()) + + Expect(os.Chdir(tmpDir)).To(Succeed()) + + cnbDir, err = ioutil.TempDir("", "cnb") + Expect(err).NotTo(HaveOccurred()) + + Expect(ioutil.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), []byte(` +[buildpack] +id = "some-id" +name = "some-name" +version = "some-version" +clear-env = false +`), 0644)).To(Succeed()) + + exitHandler = &fakes.ExitHandler{} + }) + + it.After(func() { + Expect(os.Chdir(workingDir)).To(Succeed()) + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + Expect(os.RemoveAll(cnbDir)).To(Succeed()) + }) + + context("when running the detect executable", func() { + var ( + args []string + buildPlanPath string + ) + + it.Before(func() { + buildPlanPath = filepath.Join(tmpDir, "buildplan.toml") + + args = []string{filepath.Join(cnbDir, "bin", "detect"), "", buildPlanPath} + }) + + it.After(func() { + Expect(os.Remove(buildPlanPath)).To(Succeed()) + }) + + it("calls the DetectFunc", func() { + var detectCalled bool + + detect := func(packit.DetectContext) (packit.DetectResult, error) { + detectCalled = true + return packit.DetectResult{}, nil + } + + packit.Run(detect, nil, packit.WithArgs(args), packit.WithExitHandler(exitHandler)) + + Expect(detectCalled).To(BeTrue()) + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) + }) + }) + + context("when running the build executable", func() { + var ( + args []string + layersDir string + planPath string + ) + + it.Before(func() { + file, err := ioutil.TempFile("", "plan.toml") + Expect(err).NotTo(HaveOccurred()) + defer file.Close() + + _, err = file.WriteString(` +[[entries]] +name = "some-entry" +version = "some-version" + +[entries.metadata] +some-key = "some-value" +`) + Expect(err).NotTo(HaveOccurred()) + + planPath = file.Name() + + layersDir, err = ioutil.TempDir("", "layers") + Expect(err).NotTo(HaveOccurred()) + + args = []string{filepath.Join(cnbDir, "bin", "build"), layersDir, "", planPath} + }) + + it.After(func() { + Expect(os.RemoveAll(layersDir)).To(Succeed()) + Expect(os.Remove(planPath)).To(Succeed()) + }) + + it("calls the BuildFunc", func() { + var buildCalled bool + + build := func(packit.BuildContext) (packit.BuildResult, error) { + buildCalled = true + return packit.BuildResult{}, nil + } + + packit.Run(nil, build, packit.WithArgs(args), packit.WithExitHandler(exitHandler)) + + Expect(buildCalled).To(BeTrue()) + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) + }) + }) + + context("when running any other executable", func() { + var args []string + + it.Before(func() { + args = []string{filepath.Join(cnbDir, "bin", "something-else"), "some", "args"} + }) + + it("returns an error", func() { + packit.Run(nil, nil, packit.WithArgs(args), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("failed to run buildpack: unknown lifecycle phase \"something-else\"")) + }) + }) +}