fix: Python local buildpack build (#2907)

* fix: Python local buildpack build

Signed-off-by: Matej Vašek <mvasek@redhat.com>

* fix: sane default for LISTEN_ADDRESS in pack build

Signed-off-by: Matej Vašek <mvasek@redhat.com>

---------

Signed-off-by: Matej Vašek <mvasek@redhat.com>
This commit is contained in:
Matej Vašek 2025-07-03 02:34:01 +02:00 committed by GitHub
parent 18a119abff
commit 879233d668
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 171 additions and 4 deletions

View File

@ -120,10 +120,6 @@ var DefaultLifecycleImage = "docker.io/buildpacksio/lifecycle:553c041"
// Build the Function at path.
func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platform) (err error) {
if f.Runtime == "python" {
return fmt.Errorf("python is not currently supported with pack builder (use host or s2i builder instead")
}
if len(platforms) != 0 {
return errors.New("the pack builder does not support specifying target platforms directly")
}
@ -186,6 +182,10 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
opts.ContainerConfig.Network = "host"
}
if _, ok := opts.Env["BPE_DEFAULT_LISTEN_ADDRESS"]; !ok {
opts.Env["BPE_DEFAULT_LISTEN_ADDRESS"] = "[::]:8080"
}
var bindings = make([]string, 0, len(f.Build.Mounts))
for _, m := range f.Build.Mounts {
bindings = append(bindings, fmt.Sprintf("%s:%s", m.Source, m.Destination))
@ -215,6 +215,10 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
return fmt.Errorf("podman 4.3 is not supported, use podman 4.2 or 4.4")
}
if f.Runtime == "python" {
cli = pyScaffoldInjector{cli}
}
// Client with a logger which is enabled if in Verbose mode and a dockerClient that supports SSH docker daemon connection.
if impl, err = pack.NewClient(pack.WithLogger(b.logger), pack.WithDockerClient(cli)); err != nil {
return fmt.Errorf("cannot create pack client: %w", err)

View File

@ -0,0 +1,163 @@
package buildpacks
import (
"archive/tar"
"context"
"errors"
"io"
"runtime"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
// Hack implementation of DockerClient that overrides CopyToContainer method.
// The CopyToContainer method hijacks the uploaded project stream and injects function scaffolding to it.
// It basically moves content of /workspace to /workspace/fn and then setup scaffolding code directly in /workspace.
type pyScaffoldInjector struct {
client.CommonAPIClient
}
func (s pyScaffoldInjector) CopyToContainer(ctx context.Context, ctr, p string, r io.Reader, opts container.CopyToContainerOptions) error {
if pc, _, _, ok := runtime.Caller(1); ok &&
!strings.Contains(runtime.FuncForPC(pc).Name(), "build.copyDir") {
// We are not called by "project dir copy" so we do simple direct forward call.
return s.CommonAPIClient.CopyToContainer(ctx, ctr, p, r, opts)
}
pr, pw := io.Pipe()
go func() {
var err error
defer func() {
_ = pw.CloseWithError(err)
}()
tr := tar.NewReader(r)
tw := tar.NewWriter(pw)
for {
var hdr *tar.Header
hdr, err = tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
err = nil
break
}
return
}
if strings.HasPrefix(hdr.Name, "/workspace/") {
hdr.Name = strings.Replace(hdr.Name, "/workspace/", "/workspace/fn/", 1)
}
err = tw.WriteHeader(hdr)
if err != nil {
return
}
_, err = io.Copy(tw, tr)
if err != nil {
return
}
}
err = writePythonScaffolding(tw)
if err != nil {
return
}
err = tw.Close()
}()
return s.CommonAPIClient.CopyToContainer(ctx, ctr, p, pr, opts)
}
func writePythonScaffolding(tw *tar.Writer) error {
for _, f := range []struct {
path string
content string
}{
{
path: "pyproject.toml",
content: pyprojectToml,
},
{
path: "service/main.py",
content: serviceMain,
},
{
path: "service/__init__.py",
content: "",
},
} {
err := tw.WriteHeader(&tar.Header{
Name: "/workspace/" + f.path,
Size: int64(len(f.content)),
Mode: 0644,
})
if err != nil {
return err
}
_, err = tw.Write([]byte(f.content))
if err != nil {
return err
}
}
return nil
}
const pyprojectToml = `[project]
name = "service"
description = "an autogenerated service which runs a Function"
version = "0.1.0"
requires-python = ">=3.9"
license = "MIT"
dependencies = [
"func-python",
"function @ ./fn"
]
authors = [
{ name="The Knative Authors", email="knative-dev@googlegroups.com"},
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
[tool.poetry.scripts]
script = "service.main:main"
`
const serviceMain = `"""
This code is glue between a user's Function and the middleware which will
expose it as a network service. This code is written on-demand when a
Function is being built, deployed or run. This will be included in the
final container.
"""
import logging
from func_python.cloudevent import serve
logging.basicConfig(level=logging.INFO)
try:
from function import new as handler # type: ignore[import]
except ImportError:
try:
from function import handle as handler # type: ignore[import]
except ImportError:
logging.error("Function must export either 'new' or 'handle'")
raise
def main():
logging.info("Functions middleware invoking user function")
serve(handler)
if __name__ == "__main__":
main()
`