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"})) })