diff --git a/e2e/e2e_matrix_test.go b/e2e/e2e_matrix_test.go index 18a363f19a..8858160141 100644 --- a/e2e/e2e_matrix_test.go +++ b/e2e/e2e_matrix_test.go @@ -217,10 +217,6 @@ func matrixExceptionsShared(t *testing.T, initArgs []string, funcRuntime, builde // Python Special Treatment // -------------------------- - // Skip Pack builder (not supported) - if funcRuntime == "python" && builder == "pack" { - t.Skip("The pack builder does not currently support Python Functions") - } // Echo Implementation // Replace the simple "OK" implementation with an echo. diff --git a/e2e/e2e_python_update_test.go b/e2e/e2e_python_update_test.go new file mode 100644 index 0000000000..d1fd127cd5 --- /dev/null +++ b/e2e/e2e_python_update_test.go @@ -0,0 +1,79 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +// TestPython_Update verifies that redeploying a Python function after changing +// its source code actually serves the new code. +// +// This is the Python equivalent of TestCore_Update (Go). It documents issue +// #3079: the paketo-buildpacks/poetry-install buildpack caches the poetry-venv +// layer (which contains user code installed via site-packages). That cache is +// only invalidated when pyproject.toml or poetry.lock change — NOT when .py +// source files change. So after modifying func.py and rebuilding, the stale +// installed package is reused and the old response is served. +// +// This test is expected to FAIL on current code. +func TestCore_PythonUpdate(t *testing.T) { + name := "func-e2e-test-python-update" + root := fromCleanEnv(t, name) + + // create + if err := newCmd(t, "init", "-l=python", "-t=http").Run(); err != nil { + t.Fatal(err) + } + + // deploy + if err := newCmd(t, "deploy", "--builder", "pack").Run(); err != nil { + t.Fatal(err) + } + defer func() { + clean(t, name, Namespace) + }() + if !waitFor(t, ksvcUrl(name), + withWaitTimeout(5*time.Minute)) { + t.Fatal("function did not deploy correctly") + } + + // update: rewrite func.py with a new response body + updated := `import logging + +def new(): + return Function() + +class Function: + async def handle(self, scope, receive, send): + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + [b'content-type', b'text/plain'], + ], + }) + await send({ + 'type': 'http.response.body', + 'body': b'UPDATED', + }) +` + err := os.WriteFile(filepath.Join(root, "function", "func.py"), []byte(updated), 0644) + if err != nil { + t.Fatal(err) + } + + // redeploy + if err := newCmd(t, "deploy", "--builder", "pack").Run(); err != nil { + t.Fatal(err) + } + if !waitFor(t, ksvcUrl(name), + withWaitTimeout(5*time.Minute), + withContentMatch("UPDATED")) { + t.Fatal("function did not update correctly (issue #3079: poetry-venv cache not invalidated on source change)") + } +} diff --git a/e2e/e2e_userdeps_test.go b/e2e/e2e_userdeps_test.go new file mode 100644 index 0000000000..f3edd06ad0 --- /dev/null +++ b/e2e/e2e_userdeps_test.go @@ -0,0 +1,71 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "time" +) + +// TestPython_UserDeps_Run verifies that user code and its local dependencies +// survive the scaffolding process during a local pack build. The test template +// includes a local mylib package inside function/ that func.py imports; +// if scaffolding misplaces user files, the import fails at runtime. +func TestCore_PythonUserDepsRun(t *testing.T) { + name := "func-e2e-python-userdeps-run" + _ = fromCleanEnv(t, name) + t.Cleanup(func() { cleanImages(t, name) }) + + // Init with testdata Python HTTP template (includes function/mylib/) + initArgs := []string{"init", "-l", "python", "-t", "http", + "--repository", "file://" + filepath.Join(Testdata, "templates-userdeps")} + if err := newCmd(t, initArgs...).Run(); err != nil { + t.Fatalf("init failed: %v", err) + } + + // Run with pack builder + cmd := newCmd(t, "run", "--builder", "pack", "--json") + address := parseRunJSON(t, cmd) + + if !waitFor(t, address, + withWaitTimeout(6*time.Minute), + withContentMatch("hello from mylib")) { + t.Fatal("function did not return mylib greeting — user code not preserved") + } + + if err := cmd.Process.Signal(os.Interrupt); err != nil { + fmt.Fprintf(os.Stderr, "error interrupting: %v", err) + } +} + +// TestPython_UserDeps_Remote verifies that user code and its local +// dependencies survive a remote (Tekton) build. +func TestCore_PythonUserDepsRemote(t *testing.T) { + name := "func-e2e-python-userdeps-remote" + _ = fromCleanEnv(t, name) + t.Cleanup(func() { cleanImages(t, name) }) + t.Cleanup(func() { clean(t, name, Namespace) }) + + // Init with testdata Python HTTP template (includes function/mylib/) + initArgs := []string{"init", "-l", "python", "-t", "http", + "--repository", "file://" + filepath.Join(Testdata, "templates-userdeps")} + if err := newCmd(t, initArgs...).Run(); err != nil { + t.Fatalf("init failed: %v", err) + } + + // Deploy remotely via Tekton + if err := newCmd(t, "deploy", "--builder", "pack", "--remote", + fmt.Sprintf("--registry=%s", ClusterRegistry)).Run(); err != nil { + t.Fatal(err) + } + + if !waitFor(t, ksvcUrl(name), + withWaitTimeout(5*time.Minute), + withContentMatch("hello from mylib")) { + t.Fatal("function did not return mylib greeting — user code not preserved in remote build") + } +} diff --git a/e2e/testdata/templates-userdeps/python/http/function/__init__.py b/e2e/testdata/templates-userdeps/python/http/function/__init__.py new file mode 100644 index 0000000000..c16dbac257 --- /dev/null +++ b/e2e/testdata/templates-userdeps/python/http/function/__init__.py @@ -0,0 +1 @@ +from .func import new diff --git a/e2e/testdata/templates-userdeps/python/http/function/func.py b/e2e/testdata/templates-userdeps/python/http/function/func.py new file mode 100644 index 0000000000..8d15988ae4 --- /dev/null +++ b/e2e/testdata/templates-userdeps/python/http/function/func.py @@ -0,0 +1,20 @@ +import logging +from .mylib import greet + + +def new(): + return Function() + + +class Function: + async def handle(self, scope, receive, send): + logging.info("Request Received") + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [[b'content-type', b'text/plain']], + }) + await send({ + 'type': 'http.response.body', + 'body': greet().encode(), + }) diff --git a/e2e/testdata/templates-userdeps/python/http/function/mylib/__init__.py b/e2e/testdata/templates-userdeps/python/http/function/mylib/__init__.py new file mode 100644 index 0000000000..b840409ae9 --- /dev/null +++ b/e2e/testdata/templates-userdeps/python/http/function/mylib/__init__.py @@ -0,0 +1,2 @@ +def greet(): + return "hello from mylib" diff --git a/e2e/testdata/templates-userdeps/python/http/pyproject.toml b/e2e/testdata/templates-userdeps/python/http/pyproject.toml new file mode 100644 index 0000000000..e14f74d0a1 --- /dev/null +++ b/e2e/testdata/templates-userdeps/python/http/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "function" +version = "0.1.0" +requires-python = ">=3.9" +license = "MIT" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/pkg/buildpacks/builder.go b/pkg/buildpacks/builder.go index 76fda93799..dc8fa9026d 100644 --- a/pkg/buildpacks/builder.go +++ b/pkg/buildpacks/builder.go @@ -210,6 +210,30 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf // only trust our known builders opts.TrustBuilder = TrustBuilder + // Python scaffolding via inline pre-buildpack. + // Injects a script that rearranges user code into fn/ and copies + // scaffolding from .func/build/ to the workspace root. + if f.Runtime == "python" { + opts.ProjectDescriptor.Build.Pre = types.GroupAddition{ + Buildpacks: []types.Buildpack{ + { + ID: "knative.dev/func/python-scaffolding", + Script: types.Script{ + API: "0.10", + Inline: pythonScaffoldScript(), + Shell: "/bin/bash", + }, + }, + }, + } + // Tell the procfile buildpack what command to run. + // The Procfile written by the pre-buildpack is only + // available during the build phase, but the procfile + // buildpack needs to detect it earlier. Setting this + // env var makes it detect from environment instead. + opts.Env["BP_PROCFILE_DEFAULT_PROCESS"] = "python -m service.main" + } + var impl = b.impl // Instantiate the pack build client implementation // (and update build opts as necessary) @@ -230,19 +254,6 @@ 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" { - if fi, _ := os.Lstat(filepath.Join(f.Root, "Procfile")); fi == nil { - // HACK (of a hack): need to get the right invocation signature - // the standard scaffolding does this in toSignature() func. - // we know we have python here. - invoke := f.Invoke - if invoke == "" { - invoke = "http" - } - cli = pyScaffoldInjector{cli, invoke} - } - } - // 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) diff --git a/pkg/buildpacks/scaffolder.go b/pkg/buildpacks/scaffolder.go index f13dd28f42..4ea6d1911c 100644 --- a/pkg/buildpacks/scaffolder.go +++ b/pkg/buildpacks/scaffolder.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/scaffolding" @@ -21,14 +22,14 @@ func NewScaffolder(verbose bool) *Scaffolder { return &Scaffolder{verbose: verbose} } -// Scaffold the function so that it can be built via buildpacks builder. -// 'path' is an optional override. Assign "" (empty string) most of the time +// Scaffold writes scaffolding for the function's runtime to the target path +// using embedded templates. Pass "" for path to use the default (.func/build/). +// Runtime-specific processing is applied after scaffolding is written. func (s Scaffolder) Scaffold(ctx context.Context, f fn.Function, path string) error { - // Because of Python injector we currently dont scaffold python. - // Add it here once the injector is removed - if f.Runtime != "go" { + // TODO: can be written as switch statement when we add more runtimes for readability + if f.Runtime != "go" && f.Runtime != "python" { if s.verbose { - fmt.Println("Scaffolding skipped. Currently available for runtime go") + fmt.Println("Scaffolding skipped. Currently available for runtimes go, python") } return nil } @@ -38,13 +39,90 @@ func (s Scaffolder) Scaffold(ctx context.Context, f fn.Function, path string) er appRoot = filepath.Join(f.Root, defaultPath) } if s.verbose { - fmt.Printf("Writing buildpacks scaffolding at path '%v'\n", appRoot) + fmt.Printf("Writing %s buildpacks scaffolding at path '%v'\n", f.Runtime, appRoot) } embeddedRepo, err := fn.NewRepository("", "") if err != nil { return fmt.Errorf("unable to load embedded scaffolding: %w", err) } - _ = os.RemoveAll(appRoot) - return scaffolding.Write(appRoot, f.Root, f.Runtime, f.Invoke, embeddedRepo.FS()) + if err = os.RemoveAll(appRoot); err != nil { + return fmt.Errorf("cannot clean scaffolding directory: %w", err) + } + if err = scaffolding.Write(appRoot, f.Root, f.Runtime, f.Invoke, embeddedRepo.FS()); err != nil { + return err + } + + if f.Runtime != "python" { + return nil + } + // Python specific: patch pyproject.toml for Poetry and write Procfile. + // + // Add procfile for python+pack scaffolding. This will be used for buildpacks + // build/run phases to tell pack what to run. Note that this is not for + // detection as pre-buildpack runs only at build phase. + // Variable BP_PROCFILE_DEFAULT_PROCESS (see builder.go) is used for detection + // for local deployment. + if err = patchPyprojectForPack(filepath.Join(appRoot, "pyproject.toml")); err != nil { + return err + } + return os.WriteFile( + filepath.Join(appRoot, "Procfile"), + []byte(PythonScaffoldingProcfile()), + os.FileMode(0644), + ) +} + +// patchPyprojectForPack applies pack-specific modifications to the template +// pyproject.toml: +// - Replaces {root:uri}/f with ./f — Poetry doesn't understand hatchling's +// {root:uri} context variable. The inline buildpack creates a symlink +// f -> fn/ so ./f resolves correctly. +// - Appends [tool.poetry.dependencies] — Poetry needs this section for +// dependency solving +func patchPyprojectForPack(pyprojectPath string) error { + data, err := os.ReadFile(pyprojectPath) + if err != nil { + return fmt.Errorf("cannot read pyproject.toml for patching: %w", err) + } + content := strings.Replace(string(data), "{root:uri}/f", "./f", 1) + content += "[tool.poetry.dependencies]\npython = \">=3.9,<4.0\"\nfunction = { path = \"f\", develop = true }\n" + if err = os.WriteFile(pyprojectPath, []byte(content), 0644); err != nil { + return fmt.Errorf("cannot write patched pyproject.toml: %w", err) + } + return nil +} + +// PythonScaffoldingProcfile returns the Procfile content that tells the +// buildpack how to start the service. +func PythonScaffoldingProcfile() string { + return "web: python -m service.main\n" +} + +// pythonScaffoldScript returns a bash script for use as an inline buildpack. +// The script rearranges user code into fn/ and moves pre-written scaffolding +// from .func/build/ (populated by Scaffold) to the workspace root. +func pythonScaffoldScript() string { + return `#!/bin/bash +set -eo pipefail + +# Move user code into fn/ subdirectory, preserving infrastructure entries +shopt -s dotglob +mkdir -p fn +for item in *; do + case "$item" in + fn|.func|.git|.gitignore|func.yaml) continue ;; + esac + mv "$item" fn/ +done +shopt -u dotglob + +# Move scaffolding from .func/build/ to root +mv .func/build/* . +rm -rf .func + +# Create symlink so ./f in pyproject.toml resolves to fn/ +# -n: treat existing symlink as file, not follow it to directory +ln -snf fn f +` } diff --git a/pkg/buildpacks/scaffolder_test.go b/pkg/buildpacks/scaffolder_test.go new file mode 100644 index 0000000000..ef0e1e1ede --- /dev/null +++ b/pkg/buildpacks/scaffolder_test.go @@ -0,0 +1,190 @@ +package buildpacks + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + fn "knative.dev/func/pkg/functions" + . "knative.dev/func/pkg/testing" +) + +// TestScaffoldPython_LocalBuild ensures that Scaffold writes scaffolding +// to .func/build/ when path is empty (local pack builds). User files at +// the function root must not be moved or modified. +func TestScaffoldPython_LocalBuild(t *testing.T) { + root, done := Mktemp(t) + defer done() + + // Create a user file that should NOT be moved + if err := os.WriteFile(filepath.Join(root, "func.py"), []byte("def handle(): pass"), 0644); err != nil { + t.Fatal(err) + } + + s := NewScaffolder(false) + f := fn.Function{Root: root, Runtime: "python", Invoke: "http"} + + // Empty path = local build → writes to .func/build/ + if err := s.Scaffold(context.Background(), f, ""); err != nil { + t.Fatal(err) + } + + // User files must not be moved + if _, err := os.Stat(filepath.Join(root, "func.py")); err != nil { + t.Error("user file func.py should still be at root after local scaffold") + } + // Scaffolding files should NOT exist at root + if _, err := os.Stat(filepath.Join(root, "Procfile")); !os.IsNotExist(err) { + t.Error("Procfile should not exist at root after local scaffold") + } + if _, err := os.Stat(filepath.Join(root, "pyproject.toml")); !os.IsNotExist(err) { + t.Error("pyproject.toml should not exist at root after local scaffold") + } + + // Scaffolding files should exist in .func/build/ + buildDir := filepath.Join(root, ".func", "build") + for _, name := range []string{"pyproject.toml", "Procfile", "service/main.py", "service/__init__.py"} { + if _, err := os.Stat(filepath.Join(buildDir, name)); err != nil { + t.Errorf(".func/build/%s should exist after local scaffold", name) + } + } +} + +// TestScaffoldPython_ScaffoldingContent verifies the scaffolding output: +// pyproject.toml is patched for Poetry, and Procfile uses module invocation. +func TestScaffoldPython_ScaffoldingContent(t *testing.T) { + root, done := Mktemp(t) + defer done() + + s := NewScaffolder(false) + f := fn.Function{Root: root, Runtime: "python", Invoke: "http"} + outDir := filepath.Join(root, "out") + if err := s.Scaffold(context.Background(), f, outDir); err != nil { + t.Fatal(err) + } + + // pyproject.toml: template patched for pack/Poetry + pyproject, err := os.ReadFile(filepath.Join(outDir, "pyproject.toml")) + if err != nil { + t.Fatal(err) + } + text := string(pyproject) + if !strings.Contains(text, "func-python==") { + t.Error("pyproject.toml should contain pinned func-python dependency") + } + if strings.Contains(text, `{root:uri}`) { + t.Error("pyproject.toml should not contain {root:uri} after pack patching") + } + if !strings.Contains(text, `./f`) { + t.Error("pyproject.toml should use ./f after pack patching") + } + if !strings.Contains(text, "[tool.poetry.dependencies]") { + t.Error("pyproject.toml should contain [tool.poetry.dependencies] for pack builds") + } + + // Procfile: module invocation + procfile, err := os.ReadFile(filepath.Join(outDir, "Procfile")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(procfile), "python -m service.main") { + t.Errorf("Procfile should use 'python -m service.main', got: %s", procfile) + } +} + +// TestScaffoldGo_WritesToSubdir ensures Go scaffolding writes to .func/build/ +// and does not modify the function root. +func TestScaffoldGo_WritesToSubdir(t *testing.T) { + root, done := Mktemp(t) + defer done() + + f := fn.Function{ + Root: root, + Runtime: "go", + Registry: "example.com/alice", + } + + var err error + if f, err = fn.New().Init(f); err != nil { + t.Fatal(err) + } + + s := NewScaffolder(false) + if err := s.Scaffold(context.Background(), f, ""); err != nil { + t.Fatal(err) + } + + buildDir := filepath.Join(root, ".func", "build") + if _, err := os.Stat(buildDir); err != nil { + t.Fatalf(".func/build/ directory should exist: %v", err) + } + + // Verify scaffolding content exists in the build directory + entries, err := os.ReadDir(buildDir) + if err != nil { + t.Fatal(err) + } + if len(entries) == 0 { + t.Error(".func/build/ should not be empty after Go scaffolding") + } +} + +// TestScaffold_UnsupportedRuntime ensures unsupported runtimes are a no-op. +func TestScaffold_UnsupportedRuntime(t *testing.T) { + root, done := Mktemp(t) + defer done() + + s := NewScaffolder(false) + f := fn.Function{Root: root, Runtime: "rust"} + + if err := s.Scaffold(context.Background(), f, ""); err != nil { + t.Fatalf("unsupported runtime should not error, got: %v", err) + } +} + +// TestScaffoldPython_InlineBuildpack verifies the inline buildpack script +// moves scaffolding from .func/build/ to root and preserves keep-list entries. +func TestScaffoldPython_InlineBuildpack(t *testing.T) { + script := pythonScaffoldScript() + + // Should move scaffolding from .func/build/ to root + if !strings.Contains(script, "mv .func/build/*") { + t.Error("inline buildpack should move scaffolding from .func/build/") + } + + // Should clean up .func after moving + if !strings.Contains(script, "rm -rf .func") { + t.Error("inline buildpack should remove .func after moving") + } + + // Should create f -> fn symlink + if !strings.Contains(script, "ln -snf fn f") { + t.Error("inline buildpack should create f -> fn symlink") + } + + // Should skip infrastructure entries when moving user code into fn/ + for _, entry := range []string{".func", ".git", ".gitignore", "func.yaml"} { + if !strings.Contains(script, entry) { + t.Errorf("inline buildpack script should reference %q in keep-list", entry) + } + } +} + +// TestScaffoldPython_InvalidInvoke ensures that invalid invoke types +// are rejected at scaffolding time (via scaffolding.Write/detectSignature). +func TestScaffoldPython_InvalidInvoke(t *testing.T) { + root, done := Mktemp(t) + defer done() + + if err := os.WriteFile(filepath.Join(root, "func.yaml"), []byte("name: test"), 0644); err != nil { + t.Fatal(err) + } + + s := NewScaffolder(false) + f := fn.Function{Root: root, Runtime: "python", Invoke: "invalid-type"} + if err := s.Scaffold(context.Background(), f, filepath.Join(root, "out")); err == nil { + t.Fatal("expected error for invalid invoke type") + } +} diff --git a/pkg/buildpacks/scaffolding_injector.go b/pkg/buildpacks/scaffolding_injector.go deleted file mode 100644 index 9b1e42f047..0000000000 --- a/pkg/buildpacks/scaffolding_injector.go +++ /dev/null @@ -1,169 +0,0 @@ -package buildpacks - -import ( - "archive/tar" - "context" - "errors" - "fmt" - "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.APIClient - invoke string -} - -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.APIClient.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, s.invoke) - if err != nil { - return - } - err = tw.Close() - }() - - return s.APIClient.CopyToContainer(ctx, ctr, p, pr, opts) -} - -func writePythonScaffolding(tw *tar.Writer, invoke string) error { - for _, f := range []struct { - path string - content string - }{ - { - path: "pyproject.toml", - content: pyprojectToml, - }, - { - path: "service/main.py", - content: serviceMain(invoke), - }, - { - 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" -function = { path = "fn", develop = true } - -[tool.poetry.scripts] -script = "service.main:main" -` - -func serviceMain(invoke string) string { - template := `""" -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.%s 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() -` - return fmt.Sprintf(template, invoke) -} diff --git a/pkg/pipelines/tekton/task-buildpack.yaml.tmpl b/pkg/pipelines/tekton/task-buildpack.yaml.tmpl index d5f2f85ec2..abda863372 100644 --- a/pkg/pipelines/tekton/task-buildpack.yaml.tmpl +++ b/pkg/pipelines/tekton/task-buildpack.yaml.tmpl @@ -154,6 +154,34 @@ spec: echo "--> Saving 'func.yaml'" cp $func_file /emptyDir/func.yaml + # Python only: replicate the inline pre-buildpack logic. + # Lifecycle doesn't support inline buildpacks (only pack does), + # so the rearrangement must happen here for remote builds. + src_path="$(workspaces.source.path)" + if [ "$(params.SOURCE_SUBPATH)" != "" ]; then + src_path="$(workspaces.source.path)/$(params.SOURCE_SUBPATH)" + fi + if [ -f "$src_path/.func/build/service/main.py" ]; then + echo "--> Python function detected, applying scaffolding" + cd "$src_path" + + shopt -s dotglob + mkdir -p fn + for item in *; do + case "$item" in + fn|.func|.git|.gitignore|func.yaml) continue ;; + esac + mv "$item" fn/ + done + shopt -u dotglob + + mv .func/build/* . + rm -rf .func + ln -snf fn f + + echo "===> Python scaffolding complete" + fi + ############################################ volumeMounts: - name: layers-dir