From 6d325c1e99c8b2e39ad92f893f444fbb226b63f4 Mon Sep 17 00:00:00 2001 From: Jerico Pena Date: Tue, 2 Apr 2024 19:31:17 -0400 Subject: [PATCH 1/2] Add support for BP_PROCFILE_DEFAULT_PROCESS environment variable --- README.md | 3 +++ procfile/detect.go | 7 ++++--- procfile/detect_test.go | 29 ++++++++++++++++++++++++++++- procfile/procfile.go | 30 +++++++++++++++++++++++++----- procfile/procfile_test.go | 33 +++++++++++++++++++++++++++++---- 5 files changed, 89 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b331f7e..86cec78 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,14 @@ This buildpack will participate if one or all of the following conditions are me * The application contains a `Procfile` * A Binding exists with type `Procfile` and secret containing a `Procfile` +* The `BP_PROCFILE_DEFAULT_PROCESS` environment variable is set to a non-empty value The buildpack will do the following: +* When `BP_PROCFILE_DEFAULT_PROCESS` is set, it will contribute the `web` process type to the image. * Contribute the process types from one or both `Procfile` files to the image. * If process types are identified from both Binding _and_ file, the contents are merged into a single `Procfile`. Commands from the Binding take precedence if there are duplicate types. + * If process types are identified from environment _and_ Binding _or_ file, the contents are merged into a single `Procfile`. Commands from Binding or file take precedence if there are duplicate types, with Binding taking precedence over file. * If the application's stack is `io.paketo.stacks.tiny` the contents of the `Procfile` must be single command with zero or more space delimited arguments. Argument values containing whitespace should be quoted. The resulting process will be executed directly and will not be parsed by the shell. * If the application's stack is not `io.paketo.stacks.tiny` the contents of `Procfile` will be executed as a shell script. diff --git a/procfile/detect.go b/procfile/detect.go index 1bfed37..10464ee 100644 --- a/procfile/detect.go +++ b/procfile/detect.go @@ -17,9 +17,10 @@ package procfile import ( + "os" + "github.com/buildpacks/libcnb" "github.com/paketo-buildpacks/libpak/bard" - "os" ) type Detect struct{} @@ -27,13 +28,13 @@ type Detect struct{} func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) { l := bard.NewLogger(os.Stdout) // Create Procfile from source path or binding, if both exist, merge into one. The binding takes precedence on duplicate name/command pairs. - p, err := NewProcfileFromPathOrBinding(context.Application.Path, context.Platform.Bindings) + p, err := NewProcfileFromEnvironmentOrPathOrBinding(context.Application.Path, context.Platform.Bindings) if err != nil { return libcnb.DetectResult{}, err } if len(p) == 0 { - l.Logger.Info("SKIPPED: No procfile found from either source path or binding.") + l.Logger.Info("SKIPPED: No procfile found from environment, source path, or binding.") return libcnb.DetectResult{Pass: false}, nil } diff --git a/procfile/detect_test.go b/procfile/detect_test.go index 77299d3..d1098e7 100644 --- a/procfile/detect_test.go +++ b/procfile/detect_test.go @@ -48,7 +48,7 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) }) - it("fails without Procfile", func() { + it("fails without Procfile or BP_PROCFILE_DEFAULT_PROCESS", func() { Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{})) }) @@ -58,6 +58,13 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{})) }) + it("fails with empty Procfile and empty BP_PROCFILE_DEFAULT_PROCESS", func() { + t.Setenv("BP_PROCFILE_DEFAULT_PROCESS", "") + Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "Procfile"), []byte(""), 0644)) + + Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{})) + }) + it("passes with Procfile", func() { Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "Procfile"), []byte(`test-type-1: test-command-1 test-type-2: test-command-2`), 0644)) @@ -79,4 +86,24 @@ test-type-2: test-command-2`), 0644)) }, })) }) + + it("passes with BP_PROCFILE_DEFAULT_PROCESS", func() { + t.Setenv("BP_PROCFILE_DEFAULT_PROCESS", "test-command-1") + + Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ + Pass: true, + Plans: []libcnb.BuildPlan{ + { + Provides: []libcnb.BuildPlanProvide{ + {Name: "procfile"}, + }, + Requires: []libcnb.BuildPlanRequire{ + {Name: "procfile", Metadata: procfile.Procfile{ + "web": "test-command-1", + }}, + }, + }, + }, + })) + }) } diff --git a/procfile/procfile.go b/procfile/procfile.go index 56107c7..101c1b6 100644 --- a/procfile/procfile.go +++ b/procfile/procfile.go @@ -23,6 +23,7 @@ import ( "path/filepath" "regexp" + "github.com/paketo-buildpacks/libpak/bard" "github.com/paketo-buildpacks/libpak/bindings" "github.com/buildpacks/libcnb" @@ -35,6 +36,17 @@ const ( BindingType = "Procfile" // BindingType is used to resolve a binding containing a Procfile ) +// NewProcfileFromEnvironment creates a Procfile by reading environment variable BP_PROCFILE_DEFAULT_PROCESS if it exists. +// If it does not exist, returns an empty Procfile. +func NewProcfileFromEnvironment() (Procfile, error) { + if process, isSet := os.LookupEnv("BP_PROCFILE_DEFAULT_PROCESS"); isSet { + if process != "" { + return Procfile{"web": process}, nil + } + } + return nil, nil +} + // NewProcfileFromPath creates a Procfile by reading Procfile from path if it exists. If it does not exist, returns an // empty Procfile. func NewProcfileFromPath(path string) (Procfile, error) { @@ -87,10 +99,13 @@ func NewProcfileFromBinding(binds libcnb.Bindings) (Procfile, error) { } } -// NewProcfileFromPathOrBinding attempts to create a merged Procfile from given path and bindings. If neither can be created, returns an -// empty Procfile. -func NewProcfileFromPathOrBinding(path string, binds libcnb.Bindings) (Procfile, error) { - +// NewProcfileFromEnvironmentOrPathOrBinding attempts to create a merged Procfile from environment and/or given path and bindings. +// If none can be created, returns an empty Procfile. +func NewProcfileFromEnvironmentOrPathOrBinding(path string, binds libcnb.Bindings) (Procfile, error) { + procEnv, err := NewProcfileFromEnvironment() + if err != nil { + return nil, err + } procPath, err := NewProcfileFromPath(path) if err != nil { return nil, err @@ -99,7 +114,12 @@ func NewProcfileFromPathOrBinding(path string, binds libcnb.Bindings) (Procfile, if err != nil { return nil, err } - procBind = mergeProcfiles(procPath, procBind) + if len(procEnv) > 0 && len(procPath)+len(procBind) > 0 { + l := bard.NewLogger(os.Stdout) + l.Logger.Info("A Procfile exists and BP_PROCFILE_DEFAULT_PROCESS is set, entries in Procfile take precedence") + } + + procBind = mergeProcfiles(procEnv, procPath, procBind) return procBind, nil } diff --git a/procfile/procfile_test.go b/procfile/procfile_test.go index b6a2989..9a329f9 100644 --- a/procfile/procfile_test.go +++ b/procfile/procfile_test.go @@ -48,6 +48,16 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { Expect(os.RemoveAll(path)).To(Succeed()) }) + it("returns an empty Procfile when BP_PROCFILE_DEFAULT_PROCESS is an empty string", func() { + t.Setenv("BP_PROCFILE_DEFAULT_PROCESS", "") + Expect(procfile.NewProcfileFromEnvironment()).To(HaveLen(0)) + }) + + it("returns a parsed Profile when BP_PROCFILE_DEFAULT_PROCESS is a non-empty string", func() { + t.Setenv("BP_PROCFILE_DEFAULT_PROCESS", "test-command") + Expect(procfile.NewProcfileFromEnvironment()).To(HaveLen(1)) + }) + it("returns an empty Procfile when file does not exist", func() { Expect(procfile.NewProcfileFromPath(path)).To(HaveLen(0)) }) @@ -74,7 +84,7 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { bindings = libcnb.Bindings{} Expect(ioutil.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type-path: test-command"), 0644)).To(Succeed()) - Expect(procfile.NewProcfileFromPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type-path": "test-command"})) + Expect(procfile.NewProcfileFromEnvironmentOrPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type-path": "test-command"})) }) @@ -87,7 +97,7 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { Secret: map[string]string{"Procfile": filepath.Join(bindPath, "Procfile")}, }} - Expect(procfile.NewProcfileFromPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type-bind": "test-command"})) + Expect(procfile.NewProcfileFromEnvironmentOrPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type-bind": "test-command"})) }) @@ -101,7 +111,7 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { }} Expect(ioutil.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type-path: test-command"), 0644)).To(Succeed()) - Expect(procfile.NewProcfileFromPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type-path": "test-command", "test-type-bind": "test-command"})) + Expect(procfile.NewProcfileFromEnvironmentOrPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type-path": "test-command", "test-type-bind": "test-command"})) }) @@ -115,7 +125,22 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { }} Expect(ioutil.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type: path-test-command"), 0644)).To(Succeed()) - Expect(procfile.NewProcfileFromPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type": "bind-test-command", "test-type-2": "another-test-command"})) + Expect(procfile.NewProcfileFromEnvironmentOrPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type": "bind-test-command", "test-type-2": "another-test-command"})) + + }) + + it("returns a merged Procfile from environment and given binding + file, binding takes precedence on duplicates", func() { + t.Setenv("BP_PROCFILE_DEFAULT_PROCESS", "env-test-command") + Expect(ioutil.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("web: bind-test-command\ntest-type-2: another-test-command"), 0644)).To(Succeed()) + bindings = libcnb.Bindings{libcnb.Binding{ + Name: "name1", + Type: "Procfile", + Path: bindPath, + Secret: map[string]string{"Procfile": filepath.Join(bindPath, "Procfile")}, + }} + Expect(ioutil.WriteFile(filepath.Join(path, "Procfile"), []byte("web: path-test-command"), 0644)).To(Succeed()) + + Expect(procfile.NewProcfileFromEnvironmentOrPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"web": "bind-test-command", "test-type-2": "another-test-command"})) }) From fcae2e369f4edc8bcab5b2a800a0bac92b37567e Mon Sep 17 00:00:00 2001 From: Jerico Pena Date: Wed, 3 Apr 2024 17:44:15 -0400 Subject: [PATCH 2/2] Remove deprecated ioutil usage and use testing TempDir function --- procfile/detect_test.go | 12 ++++-------- procfile/procfile_test.go | 27 ++++++++++++--------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/procfile/detect_test.go b/procfile/detect_test.go index d1098e7..eb9a097 100644 --- a/procfile/detect_test.go +++ b/procfile/detect_test.go @@ -17,7 +17,6 @@ package procfile_test import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -38,10 +37,7 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { ) it.Before(func() { - var err error - - ctx.Application.Path, err = ioutil.TempDir("", "procfile") - Expect(err).NotTo(HaveOccurred()) + ctx.Application.Path = t.TempDir() }) it.After(func() { @@ -53,20 +49,20 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { }) it("fails with empty Procfile", func() { - Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "Procfile"), []byte(""), 0644)) + Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "Procfile"), []byte(""), 0644)) Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{})) }) it("fails with empty Procfile and empty BP_PROCFILE_DEFAULT_PROCESS", func() { t.Setenv("BP_PROCFILE_DEFAULT_PROCESS", "") - Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "Procfile"), []byte(""), 0644)) + Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "Procfile"), []byte(""), 0644)) Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{})) }) it("passes with Procfile", func() { - Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "Procfile"), []byte(`test-type-1: test-command-1 + Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "Procfile"), []byte(`test-type-1: test-command-1 test-type-2: test-command-2`), 0644)) Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ diff --git a/procfile/procfile_test.go b/procfile/procfile_test.go index 9a329f9..1a8b6b5 100644 --- a/procfile/procfile_test.go +++ b/procfile/procfile_test.go @@ -17,7 +17,6 @@ package procfile_test import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -38,10 +37,8 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { ) it.Before(func() { - var err error - path, err = ioutil.TempDir("", "procfile") - bindPath, err = ioutil.TempDir("", "bindProcfile") - Expect(err).NotTo(HaveOccurred()) + path = t.TempDir() + bindPath = t.TempDir() }) it.After(func() { @@ -63,13 +60,13 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { }) it("returns a parsed Profile", func() { - Expect(ioutil.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type: test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type: test-command"), 0644)).To(Succeed()) Expect(procfile.NewProcfileFromPath(path)).To(Equal(procfile.Procfile{"test-type": "test-command"})) }) it("returns a Procfile from given binding", func() { - Expect(ioutil.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("test-type-bind: test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("test-type-bind: test-command"), 0644)).To(Succeed()) bindings = libcnb.Bindings{libcnb.Binding{ Name: "name1", Type: "Procfile", @@ -82,14 +79,14 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { it("returns a Procfile with only file contents, if no binding", func() { bindings = libcnb.Bindings{} - Expect(ioutil.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type-path: test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type-path: test-command"), 0644)).To(Succeed()) Expect(procfile.NewProcfileFromEnvironmentOrPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type-path": "test-command"})) }) it("returns a Procfile with only binding contents, if no file", func() { - Expect(ioutil.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("test-type-bind: test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("test-type-bind: test-command"), 0644)).To(Succeed()) bindings = libcnb.Bindings{libcnb.Binding{ Name: "name1", Type: "Procfile", @@ -102,28 +99,28 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { }) it("returns a merged Procfile from given binding + file", func() { - Expect(ioutil.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("test-type-bind: test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("test-type-bind: test-command"), 0644)).To(Succeed()) bindings = libcnb.Bindings{libcnb.Binding{ Name: "name1", Type: "Procfile", Path: bindPath, Secret: map[string]string{"Procfile": filepath.Join(bindPath, "Procfile")}, }} - Expect(ioutil.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type-path: test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type-path: test-command"), 0644)).To(Succeed()) Expect(procfile.NewProcfileFromEnvironmentOrPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type-path": "test-command", "test-type-bind": "test-command"})) }) it("returns a merged Procfile from given binding + file, binding takes precedence on duplicates", func() { - Expect(ioutil.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("test-type: bind-test-command\ntest-type-2: another-test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("test-type: bind-test-command\ntest-type-2: another-test-command"), 0644)).To(Succeed()) bindings = libcnb.Bindings{libcnb.Binding{ Name: "name1", Type: "Procfile", Path: bindPath, Secret: map[string]string{"Procfile": filepath.Join(bindPath, "Procfile")}, }} - Expect(ioutil.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type: path-test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(path, "Procfile"), []byte("test-type: path-test-command"), 0644)).To(Succeed()) Expect(procfile.NewProcfileFromEnvironmentOrPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"test-type": "bind-test-command", "test-type-2": "another-test-command"})) @@ -131,14 +128,14 @@ func testProcfile(t *testing.T, context spec.G, it spec.S) { it("returns a merged Procfile from environment and given binding + file, binding takes precedence on duplicates", func() { t.Setenv("BP_PROCFILE_DEFAULT_PROCESS", "env-test-command") - Expect(ioutil.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("web: bind-test-command\ntest-type-2: another-test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(bindPath, "Procfile"), []byte("web: bind-test-command\ntest-type-2: another-test-command"), 0644)).To(Succeed()) bindings = libcnb.Bindings{libcnb.Binding{ Name: "name1", Type: "Procfile", Path: bindPath, Secret: map[string]string{"Procfile": filepath.Join(bindPath, "Procfile")}, }} - Expect(ioutil.WriteFile(filepath.Join(path, "Procfile"), []byte("web: path-test-command"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(path, "Procfile"), []byte("web: path-test-command"), 0644)).To(Succeed()) Expect(procfile.NewProcfileFromEnvironmentOrPathOrBinding(path, bindings)).To(Equal(procfile.Procfile{"web": "bind-test-command", "test-type-2": "another-test-command"}))