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
85 changes: 85 additions & 0 deletions cmd/legacy_parliament_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cmd

// LEGACY PYTHON: func-run host rejection test.

import (
"errors"
"os"
"path/filepath"
"testing"

fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/mock"
"knative.dev/func/pkg/oci"
. "knative.dev/func/pkg/testing"
)

// TestRun_LegacyParliamentHostRejected verifies `func run --builder=host` on a
// legacy parliament function is rejected up front, before any build or run.
func TestRun_LegacyParliamentHostRejected(t *testing.T) {
root := FromTempDirectory(t)
if _, err := fn.New().Init(fn.Function{Root: root, Runtime: "python"}); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "Procfile"), []byte("web: python -m parliament .\n"), 0644); err != nil {
t.Fatal(err)
}

builder := mock.NewBuilder()
runner := mock.NewRunner()
cmd := NewRunCmd(NewTestClient(
fn.WithBuilder(builder),
fn.WithRunner(runner),
fn.WithRegistry("ghcr.com/reg"),
))
cmd.SetArgs([]string{"--builder=host"})

_, err := cmd.ExecuteContextC(t.Context())
if !errors.Is(err, oci.ErrLegacyParliamentHost) {
t.Fatalf("expected ErrLegacyParliamentHost, got %v", err)
}
if builder.BuildInvoked {
t.Error("build should not run for a rejected legacy parliament host run")
}
if runner.RunInvoked {
t.Error("run should not run for a rejected legacy parliament host run")
}
}

// TestRun_UnsupportedLegacyPythonHostRejected verifies `func run --builder=host`
// on a pre-v1.18 non-parliament Procfile layout (old flask/wsgi templates) is
// rejected up front with the migration error.
func TestRun_UnsupportedLegacyPythonHostRejected(t *testing.T) {
root := FromTempDirectory(t)
if _, err := fn.New().Init(fn.Function{Root: root, Runtime: "python"}); err != nil {
t.Fatal(err)
}
// turn the modern scaffold into the old flask/wsgi shape: a gunicorn
// Procfile at root, no root pyproject.toml.
if err := os.Remove(filepath.Join(root, "pyproject.toml")); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "Procfile"), []byte("web: gunicorn func:main --bind=0.0.0.0:8080\n"), 0644); err != nil {
t.Fatal(err)
}

builder := mock.NewBuilder()
runner := mock.NewRunner()
cmd := NewRunCmd(NewTestClient(
fn.WithBuilder(builder),
fn.WithRunner(runner),
fn.WithRegistry("ghcr.com/reg"),
))
cmd.SetArgs([]string{"--builder=host"})

_, err := cmd.ExecuteContextC(t.Context())
if !errors.Is(err, fn.ErrUnsupportedLegacyPython) {
t.Fatalf("expected ErrUnsupportedLegacyPython, got %v", err)
}
if builder.BuildInvoked {
t.Error("build should not run for a rejected legacy python host run")
}
if runner.RunInvoked {
t.Error("run should not run for a rejected legacy python host run")
}
}
9 changes: 9 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) {

container := f.Build.Builder != "host"

// LEGACY PYTHON: host runner can't run old parliament functions — reject.
if !container && f.IsLegacyParliament() {
return oci.ErrLegacyParliamentHost
}
// LEGACY PYTHON: other pre-v1.18 Procfile-based python layouts are rejected too.
if !container && f.IsUnsupportedLegacyPython() {
return fn.ErrUnsupportedLegacyPython
}

// Ignore the verbose flag if JSON output
if cfg.JSON {
cfg.Verbose = false
Expand Down
23 changes: 23 additions & 0 deletions docs/function-templates/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,26 @@ func deploy --builder=host --registry docker.io/myuser


For all deploy options, see `func deploy --help`

## Legacy parliament functions (deprecated)

Before func v1.18, Python functions used the "parliament" style: a bare `func.py`
exposing `def main(context)`, a `requirements.txt` depending on
`parliament-functions`, and a `Procfile` (`web: python -m parliament .`).

These functions still build and deploy with the `pack` and `s2i` builders, locally
and remotely. func builds them the pre-v1.18 way, from the function's own `Procfile`
and `requirements.txt`, pinning `cloudevents` below 2.0 during the build (the
parliament stack requires the 1.x line; with the `pack` builder the pin is written
to a `constraints.txt` file at the function root). The `host` builder does not
support them. This support is deprecated and will be removed in a future release;
every build prints a deprecation warning.

The other pre-v1.18 Procfile-based layouts (the old `flask` and `wsgi` templates)
are not supported and are rejected with an error.

To migrate, recreate the function with the current layout (`func create -l python`)
and move your logic into the `function` package: `def main(context)` becomes the
ASGI `handle` (or an instanced `new()`) described above, where the Flask request
object is replaced by the `scope`/`receive`/`send` interface. Functions that work
with CloudEvents must also adopt the cloudevents 2.x API.
71 changes: 71 additions & 0 deletions e2e/e2e_core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,77 @@ func TestCore_StaticSignature_Python(t *testing.T) {
}
}

// LEGACY PYTHON: old parliament functions (func.py def main(context) + parliament
// Procfile) must build and serve via pack and s2i, built locally and in-cluster.
func TestCore_LegacyParliamentSignature_Python(t *testing.T) {
// The fixtures in testdata/legacy-parliament were generated with func
// v1.17.0 (the last parliament-era release):
//
// func create -l python -t http parliament-http
// func create -l python -t cloudevents parliament-cloudevents
//
// The cells pair builders, flavors and localities so that each builder
// builds both locally and remotely (in-cluster, where scaffolding runs
// inside the Tekton pipeline) and each flavor passes through both builders.
for _, tc := range []struct {
builder string
template string
remote bool
}{
{"pack", "http", false},
{"s2i", "cloudevents", false},
{"pack", "cloudevents", true},
{"s2i", "http", true},
} {
cell := tc.builder
if tc.remote {
cell += "-remote"
}
t.Run(cell, func(t *testing.T) {
name := "func-e2e-parliament-" + cell
root := fromCleanEnv(t, name)

// Copy in the v1.17.0-generated fixture, renaming it for this run;
// the rest of the era func.yaml stays as generated.
fixture := "parliament-" + tc.template
if err := os.CopyFS(root, os.DirFS(filepath.Join(Testdata, "legacy-parliament", fixture))); err != nil {
t.Fatal(err)
}
yamlPath := filepath.Join(root, "func.yaml")
y, err := os.ReadFile(yamlPath)
if err != nil {
t.Fatal(err)
}
y = bytes.Replace(y, []byte("name: "+fixture), []byte("name: "+name), 1)
if err := os.WriteFile(yamlPath, y, 0644); err != nil {
t.Fatal(err)
}

args := []string{"deploy", "--builder", tc.builder}
if tc.remote {
args = append(args, "--remote")
}
if err := newCmd(t, args...).Run(); err != nil {
t.Fatal(err)
}
defer func() {
clean(t, name, Namespace)
}()

if tc.template == "cloudevents" {
if !waitFor(t, ksvcUrl(name), withTemplate("cloudevents")) {
t.Fatalf("legacy parliament cloudevents function did not deploy correctly with %s builder", tc.builder)
}
} else {
// The parliament http template echoes the query as JSON.
if !waitFor(t, ksvcUrl(name)+"?foo=bar", withContentMatch(`{"foo": "bar"}`)) {
t.Fatalf("legacy parliament http function did not deploy correctly with %s builder", tc.builder)
}
}
})
}
}

// TestCore_Delete ensures that a function registered as deleted when deleted.
// Also tests list as a side-effect.
//
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

# Use the .funcignore file to exclude files which should not be
# tracked in the image build. To instruct the system not to track
# files in the image build, add the regex pattern or file information
# to this file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: python -m parliament .
29 changes: 29 additions & 0 deletions e2e/testdata/legacy-parliament/parliament-cloudevents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Python CloudEvents Function

Welcome to your new Python function project! The boilerplate function
code can be found in [`func.py`](./func.py). This function is meant
to respond to [Cloud Events](https://cloudevents.io/).

## Endpoints

Running this function will expose three endpoints.

* `/` The endpoint for your function.
* `/health/readiness` The endpoint for a readiness health check
* `/health/liveness` The endpoint for a liveness health check

The health checks can be accessed in your browser at
[http://localhost:8080/health/readiness]() and
[http://localhost:8080/health/liveness]().

You can use `func invoke` to send an event to the function endpoint.


## Testing

This function project includes a [unit test](./test_func.py). Update this
as you add business logic to your function in order to test its behavior.

```console
python test_func.py
```
3 changes: 3 additions & 0 deletions e2e/testdata/legacy-parliament/parliament-cloudevents/app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

exec python -m parliament "$(dirname "$0")"
16 changes: 16 additions & 0 deletions e2e/testdata/legacy-parliament/parliament-cloudevents/func.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from parliament import Context, event


@event
def main(context: Context):
"""
Function template
The context parameter contains the Flask request object and any
CloudEvent received with the request.
"""

# Add your business logic here

# The return value here will be applied as the data attribute
# of a CloudEvent returned to the function invoker
return context.cloud_event.data
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
specVersion: 0.36.0
name: parliament-cloudevents
runtime: python
created: 2026-07-02T14:07:38.971733295+02:00
invoke: cloudevent
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parliament-functions==0.1.0
26 changes: 26 additions & 0 deletions e2e/testdata/legacy-parliament/parliament-cloudevents/test_func.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from cloudevents.http import CloudEvent
from parliament import Context

import unittest

func = __import__("func")

class TestFunc(unittest.TestCase):

def test_func(self):
# Create a CloudEvent
# - The CloudEvent "id" is generated if omitted. "specversion" defaults to "1.0".
attributes = {
"type": "dev.knative.function",
"source": "https://knative.dev/python.event",
}
data = {"message": "Hello World!"}
event = CloudEvent(attributes, data)
context = Context(req=None)
context.cloud_event = event

body = func.main(context)
self.assertEqual(body.data, event.data)

if __name__ == "__main__":
unittest.main()
5 changes: 5 additions & 0 deletions e2e/testdata/legacy-parliament/parliament-http/.funcignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

# Use the .funcignore file to exclude files which should not be
# tracked in the image build. To instruct the system not to track
# files in the image build, add the regex pattern or file information
# to this file.
1 change: 1 addition & 0 deletions e2e/testdata/legacy-parliament/parliament-http/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: python -m parliament .
29 changes: 29 additions & 0 deletions e2e/testdata/legacy-parliament/parliament-http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Python HTTP Function

Welcome to your new Python function project! The boilerplate function
code can be found in [`func.py`](./func.py). This function will respond
to incoming HTTP GET and POST requests.

## Endpoints

Running this function will expose three endpoints.

* `/` The endpoint for your function.
* `/health/readiness` The endpoint for a readiness health check
* `/health/liveness` The endpoint for a liveness health check

The health checks can be accessed in your browser at
[http://localhost:8080/health/readiness]() and
[http://localhost:8080/health/liveness]().

You can use `func invoke` to send an HTTP request to the function endpoint.


## Testing

This function project includes a [unit test](./test_func.py). Update this
as you add business logic to your function in order to test its behavior.

```console
python test_func.py
```
3 changes: 3 additions & 0 deletions e2e/testdata/legacy-parliament/parliament-http/app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

exec python -m parliament "$(dirname "$0")"
Loading
Loading