Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.bin
bin
build
.idea/
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ The buildpack will do the following:
environment variable `POETRY_VIRTUAL_ENVS_PATH`.
- Prepends the layer `poetry-venv` onto `PYTHONPATH`.
- Prepends the `bin` directory of the `poetry-venv` layer to the `PATH` environment variable.
- Installs only dependencies defined in the main group,
- Installs dependencies from the main group with dependencies listed via the
environment variable `BP_POETRY_INSTALL_WITH`, following `--with` option behavior of the [`poetry install` command](https://python-poetry.org/docs/cli/#install)
- eg, `BP_POETRY_INSTALL_WITH=dev` will install dependencies from main and dev groups
- eg, `BP_POETRY_INSTALL_WITH=dev,docs` will install dependencies from main, dev and docs groups
* At run time:
- Does nothing

Expand Down
5 changes: 5 additions & 0 deletions install_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ func NewPoetryInstallProcess(executable Executable, logger scribe.Emitter) Poetr
// a virtual env in the targetPath.
func (p PoetryInstallProcess) Execute(workingDir, targetPath, cachePath string) (string, error) {
args := []string{"install"}
if group, exists := os.LookupEnv("BP_POETRY_INSTALL_WITH"); exists && len(group) > 0 {
args = append(args, "--with", group)
} else {
args = append(args, "--with", "main")
}

env := append(
os.Environ(),
Expand Down
91 changes: 88 additions & 3 deletions install_process_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,90 @@ func testInstallProcess(t *testing.T, context spec.G, it spec.S) {
})

context("Execute", func() {
it("runs installation", func() {
it("runs installation for main dependencies group when BP_POETRY_INSTALL_WITH is not set", func() {
dependencyGroups := os.Getenv("BP_POETRY_INSTALL_WITH")
Expect(dependencyGroups).To(BeEmpty())

venvDir, err := poetryInstallProcess.Execute(workingDir, packagesLayerPath, cacheLayerPath)
Expect(err).NotTo(HaveOccurred())

Expect(executable.ExecuteCall.CallCount).To(Equal(2))
Expect(executableInvocations).To(HaveLen(2))

Expect(executableInvocations[0]).To(MatchFields(IgnoreExtras, Fields{
"Args": Equal([]string{
"install", "--with", "main",
}),
"Dir": Equal(workingDir),
"Env": ContainElements([]string{
fmt.Sprintf("POETRY_VIRTUALENVS_PATH=%s", packagesLayerPath),
fmt.Sprintf("POETRY_CACHE_DIR=%s", cacheLayerPath),
}),
}))

Expect(executableInvocations[1]).To(MatchFields(IgnoreExtras, Fields{
"Args": Equal([]string{
"env", "info", "--path",
}),
"Dir": Equal(workingDir),
"Env": ContainElements([]string{
fmt.Sprintf("POETRY_VIRTUALENVS_PATH=%s", packagesLayerPath),
fmt.Sprintf("POETRY_CACHE_DIR=%s", cacheLayerPath),
}),
}))

Expect(venvDir).To(Equal("/some/path/to/some/venv"))
Expect(buffer.String()).To(ContainLines(
fmt.Sprintf(" Running 'POETRY_CACHE_DIR=%s POETRY_VIRTUALENVS_PATH=%s poetry install --with main'", cacheLayerPath, packagesLayerPath),
" //some/path/xyz/../to/some/venv//",
" stderr output",
))
})

it("runs installation for main dependencies group when BP_POETRY_INSTALL_WITH is empty", func() {
t.Setenv("BP_POETRY_INSTALL_WITH", "")
dependencyGroups := os.Getenv("BP_POETRY_INSTALL_WITH")
Expect(dependencyGroups).To(BeEmpty())

venvDir, err := poetryInstallProcess.Execute(workingDir, packagesLayerPath, cacheLayerPath)
Expect(err).NotTo(HaveOccurred())

Expect(executable.ExecuteCall.CallCount).To(Equal(2))
Expect(executableInvocations).To(HaveLen(2))

Expect(executableInvocations[0]).To(MatchFields(IgnoreExtras, Fields{
"Args": Equal([]string{
"install", "--with", "main",
}),
"Dir": Equal(workingDir),
"Env": ContainElements([]string{
fmt.Sprintf("POETRY_VIRTUALENVS_PATH=%s", packagesLayerPath),
fmt.Sprintf("POETRY_CACHE_DIR=%s", cacheLayerPath),
}),
}))

Expect(executableInvocations[1]).To(MatchFields(IgnoreExtras, Fields{
"Args": Equal([]string{
"env", "info", "--path",
}),
"Dir": Equal(workingDir),
"Env": ContainElements([]string{
fmt.Sprintf("POETRY_VIRTUALENVS_PATH=%s", packagesLayerPath),
fmt.Sprintf("POETRY_CACHE_DIR=%s", cacheLayerPath),
}),
}))

Expect(venvDir).To(Equal("/some/path/to/some/venv"))
Expect(buffer.String()).To(ContainLines(
fmt.Sprintf(" Running 'POETRY_CACHE_DIR=%s POETRY_VIRTUALENVS_PATH=%s poetry install --with main'", cacheLayerPath, packagesLayerPath),
" //some/path/xyz/../to/some/venv//",
" stderr output",
))
})

it("runs installation with dev dependencies group when BP_POETRY_INSTALL_WITH=dev", func() {
devGroup := "dev"
t.Setenv("BP_POETRY_INSTALL_WITH", devGroup)
venvDir, err := poetryInstallProcess.Execute(workingDir, packagesLayerPath, cacheLayerPath)
Expect(err).NotTo(HaveOccurred())

Expand All @@ -78,12 +161,13 @@ func testInstallProcess(t *testing.T, context spec.G, it spec.S) {

Expect(executableInvocations[0]).To(MatchFields(IgnoreExtras, Fields{
"Args": Equal([]string{
"install",
"install", "--with", devGroup,
}),
"Dir": Equal(workingDir),
"Env": ContainElements([]string{
fmt.Sprintf("POETRY_VIRTUALENVS_PATH=%s", packagesLayerPath),
fmt.Sprintf("POETRY_CACHE_DIR=%s", cacheLayerPath),
fmt.Sprintf("BP_POETRY_INSTALL_WITH=%s", devGroup),
}),
}))

Expand All @@ -95,12 +179,13 @@ func testInstallProcess(t *testing.T, context spec.G, it spec.S) {
"Env": ContainElements([]string{
fmt.Sprintf("POETRY_VIRTUALENVS_PATH=%s", packagesLayerPath),
fmt.Sprintf("POETRY_CACHE_DIR=%s", cacheLayerPath),
fmt.Sprintf("BP_POETRY_INSTALL_WITH=%s", devGroup),
}),
}))

Expect(venvDir).To(Equal("/some/path/to/some/venv"))
Expect(buffer.String()).To(ContainLines(
fmt.Sprintf(" Running 'POETRY_CACHE_DIR=%s POETRY_VIRTUALENVS_PATH=%s poetry install'", cacheLayerPath, packagesLayerPath),
fmt.Sprintf(" Running 'POETRY_CACHE_DIR=%s POETRY_VIRTUALENVS_PATH=%s poetry install --with dev'", cacheLayerPath, packagesLayerPath),
" //some/path/xyz/../to/some/venv//",
" stderr output",
))
Expand Down
187 changes: 187 additions & 0 deletions integration/build_with_dependency_group_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package integration_test

import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/paketo-buildpacks/occam"
"github.com/sclevine/spec"

. "github.com/onsi/gomega"
. "github.com/paketo-buildpacks/occam/matchers"
)

func testWithDependencyGroup(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
Eventually = NewWithT(t).Eventually

pack occam.Pack
docker occam.Docker
)

it.Before(func() {
pack = occam.NewPack()
docker = occam.NewDocker()
})

context("when the buildpack is run with pack build", func() {
var (
image occam.Image
container occam.Container
name string
source string
)

it.Before(func() {
var err error
name, err = occam.RandomName()
Expect(err).NotTo(HaveOccurred())

source, err = occam.Source(filepath.Join("testdata", "app_with_dependency_group"))
Expect(err).NotTo(HaveOccurred())
})

it.After(func() {
Expect(docker.Container.Remove.Execute(container.ID)).To(Succeed())
Expect(docker.Image.Remove.Execute(image.ID)).To(Succeed())
Expect(docker.Volume.Remove.Execute(occam.CacheVolumeNames(name))).To(Succeed())
Expect(os.RemoveAll(source)).To(Succeed())
})

it("builds and runs successfully with dev dependency group", func() {
var err error
var logs fmt.Stringer
buildPackIdUnderscore := strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_")

image, logs, err = pack.WithNoColor().Build.
WithPullPolicy("never").
WithBuildpacks(
settings.Buildpacks.CPython.Online,
settings.Buildpacks.Pip.Online,
settings.Buildpacks.Poetry.Online,
settings.Buildpacks.PoetryInstall.Online,
settings.Buildpacks.BuildPlan.Online,
).
WithEnv(map[string]string{"BP_POETRY_INSTALL_WITH": "dev"}).
Execute(name, source)
Expect(err).ToNot(HaveOccurred(), logs.String)

Expect(logs).To(ContainLines(
MatchRegexp(fmt.Sprintf(`%s \d+\.\d+\.\d+`, buildpackInfo.Buildpack.Name)),
" Executing build process",
MatchRegexp(fmt.Sprintf(
" Running 'POETRY_CACHE_DIR=/layers/%s/cache POETRY_VIRTUALENVS_PATH=/layers/%s/poetry-venv poetry install --with dev'",
buildPackIdUnderscore,
buildPackIdUnderscore,
)),
))

aDependencyFromMainGroup := "Installing flask"
aDependencyFromDevGroup := "Installing pytest"
Expect(logs).To(ContainSubstring(aDependencyFromMainGroup))
Expect(logs).To(ContainSubstring(aDependencyFromDevGroup))

Expect(logs).To(ContainLines(MatchRegexp(` Completed in \d+\.\d+`)))

Expect(logs).To(ContainLines(
" Configuring build environment",
MatchRegexp(fmt.Sprintf(` PATH -> "/layers/%s/poetry-venv/default-app-.*-py\d+\.\d+/bin:\$PATH"`, buildPackIdUnderscore)),
MatchRegexp(fmt.Sprintf(` POETRY_VIRTUALENVS_PATH -> "/layers/%s/poetry-venv"`, buildPackIdUnderscore)),
MatchRegexp(fmt.Sprintf(` PYTHONPATH -> "/layers/%s/poetry-venv/default-app-.*-py\d+\.\d+/lib/python\d+\.\d+/site-packages:\$PYTHONPATH"`, buildPackIdUnderscore)),
))
Expect(logs).To(ContainLines(
" Configuring launch environment",
MatchRegexp(fmt.Sprintf(` PATH -> "/layers/%s/poetry-venv/default-app-.*-py\d+\.\d+/bin:\$PATH"`, buildPackIdUnderscore)),
MatchRegexp(fmt.Sprintf(` POETRY_VIRTUALENVS_PATH -> "/layers/%s/poetry-venv"`, buildPackIdUnderscore)),
MatchRegexp(fmt.Sprintf(` PYTHONPATH -> "/layers/%s/poetry-venv/default-app-.*-py\d+\.\d+/lib/python\d+\.\d+/site-packages:\$PYTHONPATH"`, buildPackIdUnderscore)),
))

container, err = docker.Container.Run.
WithCommand("gunicorn server:app").
WithEnv(map[string]string{"PORT": "8080"}).
WithPublish("8080").
Execute(image.ID)
Expect(err).ToNot(HaveOccurred())

Eventually(container).Should(BeAvailable())
Eventually(container).Should(Serve(ContainSubstring("Hello, World!")).OnPort(8080))
})

context("validating SBOM", func() {
var (
sbomDir string
)

it.Before(func() {
var err error
sbomDir, err = os.MkdirTemp("", "sbom")
Expect(err).NotTo(HaveOccurred())
Expect(os.Chmod(sbomDir, os.ModePerm)).To(Succeed())
})

it.After(func() {
Expect(os.RemoveAll(sbomDir)).To(Succeed())
})

it("writes SBOM files to the layer and label metadata", func() {
var err error
var logs fmt.Stringer
buildPackIdUnderscore := strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_")

image, logs, err = pack.WithNoColor().Build.
WithPullPolicy("never").
WithBuildpacks(
settings.Buildpacks.CPython.Online,
settings.Buildpacks.Pip.Online,
settings.Buildpacks.Poetry.Online,
settings.Buildpacks.PoetryInstall.Online,
settings.Buildpacks.BuildPlan.Online,
).
WithEnv(map[string]string{
"BP_LOG_LEVEL": "DEBUG",
"BP_POETRY_INSTALL_WITH": "dev",
}).
WithSBOMOutputDir(sbomDir).
Execute(name, source)
Expect(err).ToNot(HaveOccurred(), logs.String)

container, err = docker.Container.Run.
WithCommand("gunicorn server:app").
WithEnv(map[string]string{"PORT": "8080"}).
WithPublish("8080").
Execute(image.ID)
Expect(err).ToNot(HaveOccurred())

Eventually(container).Should(BeAvailable())
Eventually(container).Should(Serve(ContainSubstring("Hello, World!")).OnPort(8080))

Expect(logs).To(ContainLines(
fmt.Sprintf(" Generating SBOM for /layers/%s/poetry-venv", buildPackIdUnderscore),
MatchRegexp(` Completed in \d+(\.?\d+)*`),
))
Expect(logs).To(ContainLines(
" Writing SBOM in the following format(s):",
" application/vnd.cyclonedx+json",
" application/spdx+json",
" application/vnd.syft+json",
))

// check that all required SBOM files are present
Expect(filepath.Join(sbomDir, "sbom", "launch", buildPackIdUnderscore, "poetry-venv", "sbom.cdx.json")).To(BeARegularFile())
Expect(filepath.Join(sbomDir, "sbom", "launch", buildPackIdUnderscore, "poetry-venv", "sbom.spdx.json")).To(BeARegularFile())
Expect(filepath.Join(sbomDir, "sbom", "launch", buildPackIdUnderscore, "poetry-venv", "sbom.syft.json")).To(BeARegularFile())

// check an SBOM file to make sure it has an entry for a poetry dependency
contents, err := os.ReadFile(filepath.Join(sbomDir, "sbom", "launch", buildPackIdUnderscore, "poetry-venv", "sbom.cdx.json"))
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(ContainSubstring(`"name": "flask"`))
// optional dev dependencies should be included
Expect(string(contents)).To(ContainSubstring(`"name": "pytest"`))
})
})
})
}
10 changes: 9 additions & 1 deletion integration/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,17 @@ func testDefault(t *testing.T, context spec.G, it spec.S) {
MatchRegexp(fmt.Sprintf(`%s \d+\.\d+\.\d+`, buildpackInfo.Buildpack.Name)),
" Executing build process",
MatchRegexp(fmt.Sprintf(
" Running 'POETRY_CACHE_DIR=/layers/%s/cache POETRY_VIRTUALENVS_PATH=/layers/%s/poetry-venv poetry install'",
" Running 'POETRY_CACHE_DIR=/layers/%s/cache POETRY_VIRTUALENVS_PATH=/layers/%s/poetry-venv poetry install --with main'",
strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"),
strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"),
)),
))

aDependencyFromMainGroup := "Installing flask"
aDependencyFromDevGroup := "Installing pytest"
Expect(logs).To(ContainSubstring(aDependencyFromMainGroup))
Expect(logs).NotTo(ContainSubstring(aDependencyFromDevGroup))

Expect(logs).To(ContainLines(MatchRegexp(` Completed in \d+\.\d+`)))
Expect(logs).To(ContainLines(
" Configuring build environment",
Expand Down Expand Up @@ -166,6 +172,8 @@ func testDefault(t *testing.T, context spec.G, it spec.S) {
contents, err := os.ReadFile(filepath.Join(sbomDir, "sbom", "launch", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"), "poetry-venv", "sbom.cdx.json"))
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(ContainSubstring(`"name": "flask"`))
// optional dev dependencies should not be included
Expect(string(contents)).NotTo(ContainSubstring(`"name": "pytest"`))
})
})
})
Expand Down
1 change: 1 addition & 0 deletions integration/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,6 @@ func TestIntegration(t *testing.T) {

suite := spec.New("Integration", spec.Report(report.Terminal{}))
suite("Default", testDefault, spec.Parallel())
suite("WithDependencyGroup", testWithDependencyGroup)
suite.Run(t)
}
11 changes: 11 additions & 0 deletions integration/testdata/app_with_dependency_group/plan.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[[requires]]
name = "poetry-venv"

[requires.metadata]
launch = true

[[requires]]
name = "cpython"

[requires.metadata]
launch = true
Loading