Skip to content
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
12 changes: 8 additions & 4 deletions pkg/builders/buildpacks/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
163 changes: 163 additions & 0 deletions pkg/builders/buildpacks/scaffolding_injector.go
Original file line number Diff line number Diff line change
@@ -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="[email protected]"},
]

[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()
`
Loading