Skip to content
Open
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
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
179 changes: 179 additions & 0 deletions integration/build_with_dependency_group_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
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

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'",
strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"),
strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this strings.ReplaceAll twice - the second invocation should be redundant.

)),
))

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"`, strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"))),
MatchRegexp(fmt.Sprintf(` POETRY_VIRTUALENVS_PATH -> "/layers/%s/poetry-venv"`, strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"))),
MatchRegexp(fmt.Sprintf(` PYTHONPATH -> "/layers/%s/poetry-venv/default-app-.*-py\d+\.\d+/lib/python\d+\.\d+/site-packages:\$PYTHONPATH"`, strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"))),
"",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the "" and replace it with another invocation of Expect(logs).To(ContainLines(...))?

We've found that asserting on empty log lines is brittle, because sometimes extra lines get inserted due to environment issues (deprecation warnings, etc).

A second invocation of ContainLines() will continue to preserve the assertions on the line ordering.

" Configuring launch environment",
MatchRegexp(fmt.Sprintf(` PATH -> "/layers/%s/poetry-venv/default-app-.*-py\d+\.\d+/bin:\$PATH"`, strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"))),
MatchRegexp(fmt.Sprintf(` POETRY_VIRTUALENVS_PATH -> "/layers/%s/poetry-venv"`, strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"))),
MatchRegexp(fmt.Sprintf(` PYTHONPATH -> "/layers/%s/poetry-venv/default-app-.*-py\d+\.\d+/lib/python\d+\.\d+/site-packages:\$PYTHONPATH"`, strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"))),
))

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
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",
}).
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", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_")),
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", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"), "poetry-venv", "sbom.cdx.json")).To(BeARegularFile())
Expect(filepath.Join(sbomDir, "sbom", "launch", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"), "poetry-venv", "sbom.spdx.json")).To(BeARegularFile())
Expect(filepath.Join(sbomDir, "sbom", "launch", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"), "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", strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"), "poetry-venv", "sbom.cdx.json"))
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(ContainSubstring(`"name": "flask"`))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this test, could you also add the environment variable: WithEnv(map[string]string{"BP_POETRY_INSTALL_WITH": "dev"}). and could you also add an assertion that the SBOM contains a dev dependency?

Similarly, could you add an assertion to the default_test to assert that the SBOM does not contain a dev dependency?

})
})
})
}
8 changes: 7 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
3 changes: 2 additions & 1 deletion integration/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func TestIntegration(t *testing.T) {
SetDefaultEventuallyTimeout(30 * time.Second)

suite := spec.New("Integration", spec.Report(report.Terminal{}))
suite("Default", testDefault, spec.Parallel())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we removed the spec.Parallel() - was that intentional? Did you observe the tests failing with that present?

suite("Default", testDefault)
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