Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for BP_PROCFILE_DEFAULT_PROCESS environment variable #226

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 4 additions & 3 deletions procfile/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,24 @@
package procfile

import (
"os"

"github.com/buildpacks/libcnb"
"github.com/paketo-buildpacks/libpak/bard"
"os"
)

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
}

Expand Down
39 changes: 31 additions & 8 deletions procfile/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package procfile_test

import (
"io/ioutil"
"os"
"path/filepath"
"testing"
Expand All @@ -38,28 +37,32 @@ 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() {
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{}))
})

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(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{
Expand All @@ -79,4 +82,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",
}},
},
},
},
}))
})
}
30 changes: 25 additions & 5 deletions procfile/procfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"path/filepath"
"regexp"

"github.com/paketo-buildpacks/libpak/bard"
"github.com/paketo-buildpacks/libpak/bindings"

"github.com/buildpacks/libcnb"
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down
56 changes: 39 additions & 17 deletions procfile/procfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package procfile_test

import (
"io/ioutil"
"os"
"path/filepath"
"testing"
Expand All @@ -38,28 +37,36 @@ 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() {
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))
})

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",
Expand All @@ -72,50 +79,65 @@ 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.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"}))

})

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",
Path: bindPath,
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"}))

})

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

})

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

})

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(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("test-type: path-test-command"), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "Procfile"), []byte("web: 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{"web": "bind-test-command", "test-type-2": "another-test-command"}))

})

Expand Down
Loading