diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/.gitignore b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/.gitignore index 965f0d4ef0d..a333709f03a 100644 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/.gitignore +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/.gitignore @@ -3,3 +3,4 @@ # generally not be tracked in source control. To instruct the system to track # .func in source control, comment the following line (prefix it with '# '). /.func +/.s2i \ No newline at end of file diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/Procfile b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/Procfile deleted file mode 100644 index bb57f5a81c7..00000000000 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: python -m parliament . diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/README.md b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/README.md new file mode 100644 index 00000000000..66ce1946005 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/README.md @@ -0,0 +1,50 @@ +# Python CloudEvents Function + +Welcome to your new Python Function! A minimal Function implementation can +be found in `./function/func.py`. + +For more, run `func --help` or read the [Python Functions doc](https://github.com/knative/func/blob/main/docs/function-templates/python.md) + +## Usage + +Knative Functions allow for the deployment of your source code directly to a +Kubernetes cluster with Knative installed. + +### Function Structure + +Python functions must implement a `new()` method that returns a function +instance. The function class can optionally implement: +- `handle()` - Process CloudEvent requests +- `start()` - Initialize the function with configuration +- `stop()` - Clean up resources when the function stops +- `alive()` / `ready()` - Health and readiness checks + +See the default implementation in `./function/func.py` + +### Running Locally + +Use `func run` to test your Function locally before deployment. +For development environments where Python is installed, it's suggested to use +the `host` builder as follows: + +```bash +# Run function on the host (outsdie of a container) +func run --builder=host +``` + +### Deploying + +Use `func deploy` to deploy your Function to a Knative-enabled cluster: + +```bash +# Deploy with interactive prompts (recommended for first deployment) +func deploy --registry ghcr.io/myuser +``` + +Functions are automatically built, containerized, and pushed to a registry +before deployment. Subsequent deployments will update the existing function. + +## Roadmap + +Our project roadmap can be found: https://github.com/orgs/knative/projects/49 + diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/app.sh b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/app.sh deleted file mode 100755 index 4da37d4d8d2..00000000000 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/app.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -exec python -m parliament "$(dirname "$0")" diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/func.py b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/func.py deleted file mode 100644 index 1692ccbef76..00000000000 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/func.py +++ /dev/null @@ -1,44 +0,0 @@ -from parliament import Context -from profanity_check import predict -from cloudevents.http import CloudEvent - - -# The function to convert the bad word filter result into a CloudEvent -def create_cloud_event(inputText, data): - attributes = { - "type": "new-review-comment", - "source": "book-review-broker", - "datacontenttype": "application/json", - "badwordfilter": data, - } - - # Put the bad word filter result into a dictionary - data = {"reviewText": inputText, "badWordResult": data} - - # Create a CloudEvent object - event = CloudEvent(attributes, data) - - return event - - -def inappropriate_language_filter(text): - profanity_result = predict([text["reviewText"]]) - result = "good" - if profanity_result[0] == 1: - result = "bad" - - profanity_event = create_cloud_event(text["reviewText"], result) - return profanity_event - - -def main(context: Context): - """ - Function template - The context parameter contains the Flask request object and any - CloudEvent received with the request. - """ - - print("Received CloudEvent: ", context.cloud_event) - - # Add your business logic here - return inappropriate_language_filter(context.cloud_event.data) diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/func.yaml b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/func.yaml index a0224174f2f..33e03966014 100644 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/func.yaml +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/func.yaml @@ -1,8 +1,14 @@ +# $schema: https://raw.githubusercontent.com/knative/func/d04ff0a3/schema/func_yaml-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/knative/func/d04ff0a3/schema/func_yaml-schema.json specVersion: 0.36.0 name: bad-word-filter runtime: python -created: 2024-03-27T23:12:06.178272+08:00 +registry: localhost:5000 +namespace: default +created: 2025-09-20T18:38:53.634514281+02:00 +invoke: cloudevent build: builder: s2i deploy: namespace: default + image: localhost:5000/bad-word-filter@sha256:690a4eb5aaae56dfbfabcb8a3f0943a48b97851e2b90ce40397091bb0866655c diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/function/__init__.py b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/function/__init__.py new file mode 100644 index 00000000000..c16dbac257c --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/function/__init__.py @@ -0,0 +1 @@ +from .func import new diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/function/func.py b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/function/func.py new file mode 100644 index 00000000000..f0ead6c886a --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/function/func.py @@ -0,0 +1,44 @@ +import logging +from cloudevents.http import CloudEvent +from profanity_check import predict + +def new(): + return Function() + +class Function: + async def handle(self, scope, receive, send): + """ Handle all HTTP requests to this Function. The incoming CloudEvent is in scope["event"]. """ + logging.info("Request Received") + + # 1. Extract the CloudEvent from the scope + request_event = scope["event"] + + # 2. Extract the data payload from the event, analyze and create CloudEvent + response_event = self.inappropriate_language_filter(request_event.data) + + # 3. Send the response + logging.info(f"Sending response: {response_event.data}") + await send(response_event) + + def create_cloud_event(self, inputText, data): + attributes = { + "type": "new-review-comment", + "source": "book-review-broker", + "datacontenttype": "application/json", + "badwordfilter": data, + } + + # Put the bad word filter result into a dictionary + data = {"reviewText": inputText, "badWordResult": data} + + # Create a CloudEvent object + return CloudEvent(attributes, data) + + def inappropriate_language_filter(self, text): + review_text = text.get("reviewText", "") + profanity_result = predict([review_text]) + result = "good" + if profanity_result[0] == 1: + result = "bad" + + return self.create_cloud_event(review_text, result) diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/pyproject.toml b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/pyproject.toml new file mode 100644 index 00000000000..675a89c0794 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "function" +description = "" +version = "0.1.0" +requires-python = ">=3.9" +readme = "README.md" +license = "MIT" +dependencies = [ + "httpx", + "cloudevents", + "pytest", + "pytest-asyncio", + "alt-profanity-check==1.4.1.post1" +] +authors = [ + { name="Your Name", email="you@example.com"}, +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pytest.ini_options] +asyncio_mode = "strict" +asyncio_default_fixture_loop_scope = "function" + + diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/requirements.txt b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/requirements.txt deleted file mode 100644 index 69aa57d8e14..00000000000 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -parliament-functions==0.1.0 -alt-profanity-check==1.4.1.post1 -cloudevents==1.10.1 diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/tests/test_func.py b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/tests/test_func.py new file mode 100644 index 00000000000..ad126f72302 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-bad-word-filter/tests/test_func.py @@ -0,0 +1,50 @@ +""" +Unit tests for the CloudEvent function implementation. +Tests both the function health endpoints and its handling of CloudEvents. + +Functions are currently served as ASGI applications using hypercorn, so we +use httpx for testing. To run the tests using poetry: + +poetry run python -m unittest discover +""" +import json +import asyncio +import pytest +from cloudevents.http import CloudEvent +from function import new + + +@pytest.mark.asyncio +async def test_func(): + f = new() # Instantiate Function to Test + + # A test CloudEvent + attributes = { + "id": "test-id", + "type": "com.example.test1", + "source": "https://example.com/event-producer", + } + data = {"message": "test message"} + event = CloudEvent(attributes, data) + + invoked = False # Flag indicating send method was invoked + + # Send + # confirms the Functions responds with a CloudEvent which echoes + # the data sent. + async def send(e): + nonlocal invoked + invoked = True # Flag send was invoked + + # Ensure we got a CloudEvent + assert isinstance(e, CloudEvent), f"Expected CloudEvent, got {type(e)}" + + # Ensure it echoes the data sent + assert e.data == data, f"Expected data {data}, got {e.data}" + + # Invoke the Function + scope = {"event": event} # Add the CloudEvent to the scope + await f.handle(scope, {}, send) + + # Assert send was called + assert invoked, "Function did not call send" diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/.gitignore b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/.gitignore index 965f0d4ef0d..a333709f03a 100644 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/.gitignore +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/.gitignore @@ -3,3 +3,4 @@ # generally not be tracked in source control. To instruct the system to track # .func in source control, comment the following line (prefix it with '# '). /.func +/.s2i \ No newline at end of file diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/Procfile b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/Procfile deleted file mode 100644 index bb57f5a81c7..00000000000 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: python -m parliament . diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/README.md b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/README.md index f71bd55cce8..66ce1946005 100644 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/README.md +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/README.md @@ -1,393 +1,50 @@ -# Sentiment Analysis Service for Bookstore Reviews +# Python CloudEvents Function -As a bookstore owner, you aim to receive instant notifications in a Slack channel whenever a customer submits a new **negative** review comment. By leveraging Knative Function, you can set up a serverless function that contains a simple sentiment analysis service to categorize review comments by sentiment. -## What Knative features will we learn about? +Welcome to your new Python Function! A minimal Function implementation can +be found in `./function/func.py`. -- The easiness to use **Knative Function** to deploy your service, and make it be managed by **Knative Serving**, which give you the ability to auto-scale your service to zero, and scale up to handle the demand. +For more, run `func --help` or read the [Python Functions doc](https://github.com/knative/func/blob/main/docs/function-templates/python.md) -## What does the final deliverable look like? -A running serverless Knative function that contains a python application that can receives the new review comments as CloudEvent and returns the sentiment classification of the input text as CloudEvent. +## Usage -The function's output will be only from -- Positive -- Neutral -- Negative -## Install Prerequisites +Knative Functions allow for the deployment of your source code directly to a +Kubernetes cluster with Knative installed. ---- -(Warning box: please make sure you have a running cluster with Knative Eventing and Serving installed. If not, click here. And you have the container registry ready.) +### Function Structure ---- +Python functions must implement a `new()` method that returns a function +instance. The function class can optionally implement: +- `handle()` - Process CloudEvent requests +- `start()` - Initialize the function with configuration +- `stop()` - Clean up resources when the function stops +- `alive()` / `ready()` - Health and readiness checks -### Prerequisite 1: Install Knative `func` CLI -Knative Function enables you to easily create, build, and deploy stateless, event-driven functions as [Knative Services](https://knative.dev/docs/serving/services/#:~:text=Knative%20Services%20are%20used%20to,the%20Service%20to%20be%20configured){:target="_blank"} by using the func CLI. +See the default implementation in `./function/func.py` -In order to do so, you need to install the `func` CLI. -You can follow the [official documentation](https://knative.dev/docs/getting-started/install-func/){:target="_blank"} to install the `func` CLI. +### Running Locally -Running `func version` in your terminal to verify the installation, and you should see the version of the `func` CLI you installed. - -## Implementation -The process is straightforward: -- Begin by utilizing the `func create` command to generate your code template. -- Next, incorporate your unique code into this template. -- Finally, execute `func deploy` to deploy your application seamlessly to the Kubernetes cluster. - -This workflow ensures a smooth transition from development to deployment within the Knative Functions ecosystem. - ---- -a warning box: -(Troubleshooting: if you see `command not found`, you may need to add the `func` CLI to your PATH.) ---- -### Step 1: Create a Knative Function template - -Create a new function using the `func` CLI: - -```bash -func create -l -``` - -In this case, we are creating a python function, so the command will be: - -```bash -func create -l python sentiment-analysis -``` - -This command will create a new directory with the name `sentiment-analysis` and a bunch of files in it. The `func` CLI will generate a basic function template for you to start with. - -You can find all the supported languages templates [here](https://knative.dev/docs/functions/){:target="_blank"}. - -The file tree will look like this: -```bash -sentiment-analysis -├── func.yaml -├── .funcignore -├── .gitignore -├── requirements.txt -├── app.sh -├── Procfile -└── func.py - -``` -### Step 2: Replace the generated code with the sentiment analysis logic -`func.py` is the file that contains the code for the function. You can replace the generated code with the sentiment analysis logic. You can use the following code as a starting point: - -```python title="func.py" -from parliament import Context -from flask import Request,request, jsonify -import json -from textblob import TextBlob -from time import sleep -from cloudevents.http import CloudEvent, to_structured - -# The function to convert the sentiment analysis result into a CloudEvent -def create_cloud_event(data): - attributes = { - "type": "knative.sampleapp.sentiment.response", - "source": "sentiment-analysis", - "datacontenttype": "application/json", - } - - # Put the sentiment analysis result into a dictionary - data = {"result": data} - - # Create a CloudEvent object - event = CloudEvent(attributes, data) - - return event - -def analyze_sentiment(text): - analysis = TextBlob(text) - sentiment = "Neutral" - if analysis.sentiment.polarity > 0: - sentiment = "Positive" - elif analysis.sentiment.polarity < 0: - sentiment = "Negative" - - # Convert the sentiment into a CloudEvent - sentiment = create_cloud_event(sentiment) - - # Sleep for 3 seconds to simulate a long-running process - sleep(3) - - return sentiment - -def main(context: Context): - """ - Function template - The context parameter contains the Flask request object and any - CloudEvent received with the request. - """ - - print("Received CloudEvent: ", context.cloud_event) - - # Add your business logic here - return analyze_sentiment(context.cloud_event.data) - -``` - -### Step 3: Configure the dependencies -The `requirements.txt` file contains the dependencies for the function. You can add the following dependencies to the `requirements.txt` file: - -```bash -Flask==3.0.2 -textblob==0.18.0.post0 -parliament-functions==0.1.0 -cloudevents==1.10.1 -``` -Knative function will automatically install the dependencies listed here when you build the function. - -### Step 4: Configre the pre-built environment -In order to properly use the `textblob` library, you need to download the corpora, which is a large collection of text data that is used to train the sentiment analysis model. You can do this by creating a new file called `setup.py`, -knative function will ensure that the `setup.py` file is executed after the dependencies have been installed. - - -The `setup.py` file should contain the following code for your bookstore: - - -```python -from setuptools import setup, find_packages -from setuptools.command.install import install -import subprocess - - -class PostInstallCommand(install): - """Post-installation for installation mode.""" - def run(self): - # Call the superclass run method - install.run(self) - # Run the command to download the TextBlob corpora - subprocess.call(['python', '-m', 'textblob.download_corpora', 'lite']) - - -setup( - name="download_corpora", - version="1.0", - packages=find_packages(), - cmdclass={ - 'install': PostInstallCommand, - } -) -``` - - - - -### Step 5: Try to build and run your Knative Function on your local machine - -In knative function, there are two ways to build: using the [pack build](https://github.com/knative/func/blob/8f3f718a5a036aa6b6eaa9f70c03aeea740015b9/docs/reference/func_build.md?plain=1#L46){:target="_blank"} or using the [source-to-image (s2i) build](https://github.com/knative/func/blob/4f48549c8ad4dad34bf750db243d81d503f0090f/docs/reference/func_build.md?plain=1#L43){:target="_blank"}. - -Currently. only the **s2i** build is supported if you need to run setup.py. When building with s2i, the `setup.py` file will be executed automatically after the dependencies have been installed. - -Before we get started, configure the container registry to push the image to the container registry. You can use the following command to configure the container registry: +Use `func run` to test your Function locally before deployment. +For development environments where Python is installed, it's suggested to use +the `host` builder as follows: ```bash -export FUNC_REGISTRY= +# Run function on the host (outsdie of a container) +func run --builder=host ``` -In this case, we will use the s2i build by adding the flag `-b=s2i`, and `-v` to see the verbose output. +### Deploying -```bash -func build -b=s2i -v -``` - -When the build is complete, you will see the following output: - -```bash -🙌 Function built: /sentiment-analysis-app:latest -``` - -This command will build the function and push the image to the container registry. After the build is complete, you can run the function using the following command: - ---- -An alert box -Issue you may experience: -``` -Error: '/home/Kuack/Documents/knative/docs/code-samples' does not contain an initialized function -``` -Solution: You may want to check whether you are in the correct directory. You can use the following command to check the current directory. - - -If you are in the right directory, and the error still occurs, try to check your func.yaml, - -as it has to contain the field `created` and the right time stamp to be treated as a valid knative function. - ---- +Use `func deploy` to deploy your Function to a Knative-enabled cluster: ```bash -func run -b=s2i -v +# Deploy with interactive prompts (recommended for first deployment) +func deploy --registry ghcr.io/myuser ``` -In the future, you can **skip the step of `func build`**, because func run will automatically build the function for you. - -You will see the following output if the function is running successfully: - -``` -function up-to-date. Force rebuild with --build -Running on host port 8080 ----> Running application from script (app.sh) ... -```` - -Now you can test the function by sending a request to the function using the following command: - -```bash -curl -X POST http://localhost:8080 \ --H "ce-id: 12345" \ --H "ce-source: /your/source" \ --H "ce-type: sentiment-analysis-request" \ --H "ce-specversion: 1.0" \ --H "Content-Type: application/json" \ --d '{"input":"I love Knative so much!"}' -``` -where `-H` are the headers, and `-d` is the input text. The input text is a **sting**. Be careful with the quotes. - -If the function is running successfully, you will see the following output (the `data` field in the Response CloudEvent only): - -```bash -{ - "input":"I love Knative so much!", - "result": "Positive" -} -``` - -Knative function also have an easy way to simulate the CloudEvent, you can use the following command to simulate the CloudEvent: - -```bash -func invoke -f=cloudevent --data='{"input": "I love Knative so much"}' --content-type=application/json --type="new-comment" -v -``` -where the `-f` flag indicates the type of the data, is either `HTTP` or `cloudevent`, and the `--data` flag is the input text. -You can read more about `func invoke` [here](https://github.com/knative/func/blob/main/docs/reference/func_invoke.md){:target="_blank"}. - -In this case, you will get the full CloudEvent response: - -```bash -Context Attributes, - specversion: 1.0 - type: knative.sampleapp.sentiment.response - source: sentiment-analysis - id: af0c0f59-9130-4a6c-96ef-6d72c2f4ce50 - time: 2024-02-31T18:48:00.232436Z - datacontenttype: application/json -Data, - { - "input":"I love Knative so much!", - "result": "Positive" - } - -``` - -### Step 6: Deploy the function to the cluster -After you have finished the code, you can deploy the function to the cluster using the following command: - -```bash -func deploy -b=s2i -v -``` - -When the deployment is complete, you will see the following output: - -```bash -✅ Function updated in namespace "default" and exposed at URL: - http://sentiment-analysis-app.default.10.99.46.8.sslip.io -``` - -You can also find the URL by running the following command: - -```bash -kubectl get kservice -A -``` - -You will see the URL in the output: - -```bash -NAMESPACE NAME URL LATESTCREATED LATESTREADY READY REASON -default sentiment-analysis-app http://sentiment-analysis-app.default.10.99.46.8.sslip.io sentiment-analysis-app-00002 sentiment-analysis-app-00002 True -``` - -Please note: if your URL ends with .svc.cluster.local, that means you can only access the function from within the cluster. You probably forget to configure the network or [start the tunnel](https://knative.dev/docs/getting-started/quickstart-install/#__tabbed_3_2){:target="_blank"} if you are using minikube. - -### Step 7: Verify the Deployment -After deployment, the `func` CLI provides a URL to access your function. You can verify the function's operation by sending a request with a sample review comment. - -Simply use Knative function's command `func invoke` to directly send a CloudEvent to the function on your cluster: - -```bash -func invoke -f=cloudevent --data="i love knative community so much" -v -t="http://sentiment-analysis-app.default.10.99.46.8.sslip.io" -``` - -- `-f` flag indicates the type of the data, is either `HTTP` or `cloudevent` -- `--data` flag is the input text -- `-t` flag is the URI to the Knative Function. - -If the function is running successfully, you will see the following output: - -```bash -Context Attributes, - specversion: 1.0 - type: knative.sampleapp.sentiment.response - source: sentiment-analysis - id: 6a246e0c-2b24-4f22-93c4-f1265c569b2d - time: 2024-02-31T19:03:50.434822Z - datacontenttype: application/json -Data, - { - "input":"I love Knative so much!", - "result": "Positive" - } -``` - ---- -Recall note box: you can get the URL to the function by running the following command: -```bash -kubectl get kservice -A -``` ---- -Another option is to use curl to send a CloudEvents to the function. -Using curl command to send a CloudEvents to the Broker: -```bash -[root@curler:/]$ curl -v "http://sentiment-analysis-app.default.10.99.46.8.sslip.io" \ --X POST \ --H "ce-id: 12345" \ --H "ce-source: my-local" \ --H "ce-type: sentiment-analysis-request" \ --H "ce-specversion: 1.0" \ --H "Content-Type: application/json" \ --d '"I love Knative so much! Sent to the cluster"' -``` - -Expect to receive a JSON response indicating the sentiment classification of the input text. - -```bash -{ - "input":"I love Knative so much!", - "result": "Positive" -} -``` -If you see the response, it means that the function is running successfully. - ---- -### The magic time about Serverless: autoscaling to zero -If you use the following command to query all the pods in the cluster, you will see that the pod is running: - -```bash -kubectl get pods -A -``` -where `-A` is the flag to query all the pods in all namespaces. - -And you will find that your sentiment analysis app is running: - -```bash -NAMESPACE NAME READY STATUS RESTARTS AGE -default sentiment-analysis-app-00002-deployment 2/2 Running 0 2m -``` - -But if you wait for a while without sending any CloudEvent to your function, and query the pods again, you will find that the pod that has your sentiment analysis app **disappeared**! - - -This is because **Knative Serving's autoscaler** will automatically scale down to zero if there is no request to the function! ---- +Functions are automatically built, containerized, and pushed to a registry +before deployment. Subsequent deployments will update the existing function. -Congratulations! You have successfully set up the sentiment analysis service for your bookstore. +## Roadmap -## Conclusion +Our project roadmap can be found: https://github.com/orgs/knative/projects/49 -In this tutorial, you learned how to create a serverless function that contains a simple sentiment analysis service with Knative function. \ No newline at end of file diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/app.sh b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/app.sh deleted file mode 100755 index 4da37d4d8d2..00000000000 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/app.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -exec python -m parliament "$(dirname "$0")" diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/func.py b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/func.py deleted file mode 100644 index 8d73da75f57..00000000000 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/func.py +++ /dev/null @@ -1,61 +0,0 @@ -from parliament import Context -from flask import Request, request, jsonify -import json -from textblob import TextBlob -from time import sleep -from cloudevents.http import CloudEvent, to_structured - - -# The function to convert the sentiment analysis result into a CloudEvent -def create_cloud_event(inputText, badWordResult, data): - attributes = { - "type": "moderated-comment", - "source": "sentiment-analysis", - "datacontenttype": "application/json", - "sentimentResult": data, - "badwordfilter": badWordResult, - } - - # Put the sentiment analysis result into a dictionary - data = { - "reviewText": inputText, - "badWordResult": badWordResult, - "sentimentResult": data, - } - - # Create a CloudEvent object - event = CloudEvent(attributes, data) - - return event - - -def analyze_sentiment(text): - analysis = TextBlob(text["reviewText"]) - sentiment = "neutral" - if analysis.sentiment.polarity > 0: - sentiment = "positive" - elif analysis.sentiment.polarity < 0: - sentiment = "negative" - - badWordResult = "" - try: - badWordResult = text["badWordResult"] - except: - pass - # Convert the sentiment into a CloudEvent - sentiment = create_cloud_event(text["reviewText"], badWordResult, sentiment) - - return sentiment - - -def main(context: Context): - """ - Function template - The context parameter contains the Flask request object and any - CloudEvent received with the request. - """ - - print("Sentiment Analysis Received CloudEvent: ", context.cloud_event) - - # Add your business logic here - return analyze_sentiment(context.cloud_event.data) diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/func.yaml b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/func.yaml index 5431eb92763..5ac7d1e09c0 100644 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/func.yaml +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/func.yaml @@ -1,8 +1,14 @@ +# $schema: https://raw.githubusercontent.com/knative/func/d04ff0a3/schema/func_yaml-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/knative/func/d04ff0a3/schema/func_yaml-schema.json specVersion: 0.36.0 name: sentiment-analysis-app runtime: python -created: 2024-02-01T00:18:00.06485162-04:00 +registry: localhost:5000 +namespace: default +created: 2025-09-20T16:25:54.767714524+02:00 +invoke: cloudevent build: builder: s2i deploy: namespace: default + image: localhost:5000/sentiment-analysis-app@sha256:fa2335ff00b780ae12a8d01e1512db874803e16ccba8ce0893223e023417b79a diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/function/__init__.py b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/function/__init__.py new file mode 100644 index 00000000000..c16dbac257c --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/function/__init__.py @@ -0,0 +1 @@ +from .func import new diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/function/func.py b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/function/func.py new file mode 100644 index 00000000000..8ed46f20b69 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/function/func.py @@ -0,0 +1,61 @@ +import logging +from cloudevents.http import CloudEvent +from textblob import TextBlob +import textblob + +def new(): + return Function() + + +class Function: + async def handle(self, scope, receive, send): + """ Handle all HTTP requests to this Function. The incoming CloudEvent is in scope["event"]. """ + logging.info("Request Received") + + # 1. Get the incoming CloudEvent + request_event = scope["event"] + + # 2. Extract the data payload from the event, analyze and create CloudEvent + response_event = self.analyze_sentiment(request_event.data) + + # 3. Send the response + logging.info(f"Sending response: {response_event.data}") + await send(response_event) + + def create_cloud_event(self, inputText, badWordResult, data): + attributes = { + "type": "moderated-comment", + "source": "sentiment-analysis", + "datacontenttype": "application/json", + "sentimentResult": data, + "badwordfilter": badWordResult, + } + + # Put the sentiment analysis result into a dictionary + data = { + "reviewText": inputText, + "badWordResult": badWordResult, + "sentimentResult": data, + } + + return CloudEvent(attributes, data) + + def analyze_sentiment(self, text): + review_text = text.get("reviewText", "") + analysis = TextBlob(review_text) + sentiment = "neutral" + + if analysis.sentiment.polarity > 0: + sentiment = "positive" + elif analysis.sentiment.polarity < 0: + sentiment = "negative" + + badWordResult = "" + try: + badWordResult = text["badWordResult"] + except KeyError: + pass + + # Convert the sentiment into a CloudEvent + return self.create_cloud_event(review_text, badWordResult, sentiment) + diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/pyproject.toml b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/pyproject.toml new file mode 100644 index 00000000000..447dd7520ef --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "function" +description = "" +version = "0.1.0" +requires-python = ">=3.9" +readme = "README.md" +license = "MIT" +dependencies = [ + "httpx", + "cloudevents", + "pytest", + "pytest-asyncio", + "textblob", +] +authors = [ + { name="Your Name", email="you@example.com"}, +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pytest.ini_options] +asyncio_mode = "strict" +asyncio_default_fixture_loop_scope = "function" + + diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/requirements.txt b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/requirements.txt deleted file mode 100644 index ab724e8a7a3..00000000000 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==3.0.2 -textblob==0.18.0.post0 -parliament-functions==0.1.0 -cloudevents==1.10.1 \ No newline at end of file diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/setup.py b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/setup.py deleted file mode 100644 index 2cdd67ede38..00000000000 --- a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/setup.py +++ /dev/null @@ -1,22 +0,0 @@ -from setuptools import setup, find_packages -from setuptools.command.install import install -import subprocess - - -class PostInstallCommand(install): - """Post-installation for installation mode.""" - def run(self): - # Call the superclass run method - install.run(self) - # Run the command to download the TextBlob corpora - subprocess.call(['python', '-m', 'textblob.download_corpora', 'lite']) - - -setup( - name="download_corpora", - version="1.0", - packages=find_packages(), - cmdclass={ - 'install': PostInstallCommand, - } -) diff --git a/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/tests/test_func.py b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/tests/test_func.py new file mode 100644 index 00000000000..ad126f72302 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/solution/ML-sentiment-analysis/tests/test_func.py @@ -0,0 +1,50 @@ +""" +Unit tests for the CloudEvent function implementation. +Tests both the function health endpoints and its handling of CloudEvents. + +Functions are currently served as ASGI applications using hypercorn, so we +use httpx for testing. To run the tests using poetry: + +poetry run python -m unittest discover +""" +import json +import asyncio +import pytest +from cloudevents.http import CloudEvent +from function import new + + +@pytest.mark.asyncio +async def test_func(): + f = new() # Instantiate Function to Test + + # A test CloudEvent + attributes = { + "id": "test-id", + "type": "com.example.test1", + "source": "https://example.com/event-producer", + } + data = {"message": "test message"} + event = CloudEvent(attributes, data) + + invoked = False # Flag indicating send method was invoked + + # Send + # confirms the Functions responds with a CloudEvent which echoes + # the data sent. + async def send(e): + nonlocal invoked + invoked = True # Flag send was invoked + + # Ensure we got a CloudEvent + assert isinstance(e, CloudEvent), f"Expected CloudEvent, got {type(e)}" + + # Ensure it echoes the data sent + assert e.data == data, f"Expected data {data}, got {e.data}" + + # Invoke the Function + scope = {"event": event} # Add the CloudEvent to the scope + await f.handle(scope, {}, send) + + # Assert send was called + assert invoked, "Function did not call send" diff --git a/code-samples/eventing/bookstore-sample-app/solution/README.md b/code-samples/eventing/bookstore-sample-app/solution/README.md index 944c0494177..c84566e217b 100644 --- a/code-samples/eventing/bookstore-sample-app/solution/README.md +++ b/code-samples/eventing/bookstore-sample-app/solution/README.md @@ -19,18 +19,21 @@ Here's an overview of the components in the solution: ## Additional Files -- `setup.sh`: Script for setting up the required services including installing Knative, frontend, and backend node-server -- `solution.sh`: Script for installing everything, deploying the entire solution. **It includes the setup script as well.** +- `solution.sh`: Script for installing everything, deploying the entire solution. ## Running the Solution -1. Have a running Kubernetes cluster. -2. Install all the prerequisites and deploy the entire solution using the `solution.sh` script: - ``` +1. Have a locally running Kubernetes cluster e.g. [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) or [Minikube](https://minikube.sigs.k8s.io/docs/start). +1. Have a locally running container registry, e.g. [Kind registry setup](https://kind.sigs.k8s.io/docs/user/local-registry/) or [Minikube registry setup](https://minikube.sigs.k8s.io/docs/handbook/registry/#enabling-insecure-registries). +1. Install all the prerequisites and deploy the entire solution using the `solution.sh` script: + + ```sh ./solution.sh ``` + If you encountered any permission issues, run the following command: - ``` + + ```sh chmod +x solution.sh ``` diff --git a/code-samples/eventing/bookstore-sample-app/solution/node-server/index.js b/code-samples/eventing/bookstore-sample-app/solution/node-server/index.js index 1a171a3c17d..78d2fb42b98 100644 --- a/code-samples/eventing/bookstore-sample-app/solution/node-server/index.js +++ b/code-samples/eventing/bookstore-sample-app/solution/node-server/index.js @@ -24,7 +24,7 @@ const pool = new Pool({ app.ws('/comments', (ws, req) => { console.log('WebSocket connection established on /comments'); - + // Function to send all comments to the connected client const sendComments = async () => { try { @@ -44,7 +44,7 @@ app.ws('/comments', (ws, req) => { // Optionally, you can trigger this function based on certain conditions // Here, we just send data immediately after connection and on an interval sendComments(); - const interval = setInterval(sendComments, 1000); // Send comments every 10 seconds + const interval = setInterval(sendComments, 1000); // Send comments every second ws.on('close', () => { console.log('WebSocket connection on /comments closed'); diff --git a/code-samples/eventing/bookstore-sample-app/solution/setup.sh b/code-samples/eventing/bookstore-sample-app/solution/setup.sh deleted file mode 100755 index e69de29bb2d..00000000000 diff --git a/code-samples/eventing/bookstore-sample-app/solution/slack-sink/README.md b/code-samples/eventing/bookstore-sample-app/solution/slack-sink/README.md index 467d27ff6e8..b64e12f408a 100644 --- a/code-samples/eventing/bookstore-sample-app/solution/slack-sink/README.md +++ b/code-samples/eventing/bookstore-sample-app/solution/slack-sink/README.md @@ -1,63 +1,82 @@ # Bookstore Notification Service: with Apache Camel K and Knative Eventing - As a bookstore owner, you aim to receive instant notifications in a Slack channel whenever a customer submits a new review comment. By leveraging Knative Eventing and Apache Camel K, you can set up an event-driven service that automates these notifications, ensuring you're always informed. + ## What Knative features will we learn about? + - Knative's ability to connect with third-party services, such as Slack, through event-driven integration using Apache Camel K. ## What does the final deliverable look like? + When a CloudEvent with the type `new-review-comment` is sent to the Knative Eventing Broker, it triggers a message to be sent in a designated Slack channel. ## Install prerequisites -### Prerequisite 1: Install Camel CLI -Install the Camel K CLI (`kamel`) on your local machine. You can find the installation instructions [here](https://camel.apache.org/camel-k/2.4.x/cli/cli.html){:target="_blank"}. +### Prerequisite 1: Install Apache Camel-Kamelets -**Troubleshot**: If after installation you run `kamel version` and you get an error message, you may need to add the `kamel` binary to your system's PATH. You can do this by moving the `kamel` binary to a directory that is already in your PATH, or by adding the directory where `kamel` is located to your PATH. +Install Apache Camel K operator on your cluster using any of the methods listed in [the official installation docs](https://camel.apache.org/camel-k/2.8.x/installation/installation.html). We will use the installation via Kustomize: -```bash -$ export PATH=$PATH: +```sh +kubectl create ns camel-k && \ +kubectl apply -k github.com/apache/camel-k/install/overlays/kubernetes/descoped?ref=v2.8.0 --server-side ``` +Now you need to setup an `IntegrationPlatform` with a container registry. You can read more about it in [the official installation docs](https://camel.apache.org/camel-k/2.8.x/installation/installation.html#integration-platform). For all our needs we only need to create the `IntegrationPlatform` CR with a container registry entry. For example let's say we're using a Kind cluster with a local registry named `kind-registry` on port `5000`. Then your `IntegrationPlatform` CR will look like the following: -### Prerequisite 2: Install Apache Camel-Kamelets -Next, install Camel K on your cluster using the Camel K CLI: - -```bash -$ kamel install --registry docker.io --organization --registry-auth-username --registry-auth-password +```yaml +apiVersion: camel.apache.org/v1 +kind: IntegrationPlatform +metadata: + name: camel-k + namespace: camel-k # Make sure this is the namespace where your operator is running +spec: + build: + registry: + address: kind-registry:5000 + insecure: true ``` -Replace the placeholders with your actual Docker registry information. - -If you are using other container registries, you may need to read more [here](https://camel.apache.org/camel-k/2.4.x/installation/registry/registry.html){:target="_blank"} for the installation. +Install it with one command: -You will see this message if the installation is successful: - -``` -OLM is not available in the cluster. Fallback to regular installation. -Camel K installed in namespace default +```sh +cat <" \ -X POST \ -H "Ce-Id: review1" \ @@ -169,13 +191,14 @@ Using curl command to send a CloudEvent to the Broker: You can find the URI to your Broker by running the following command: ```bash -$ kubectl get broker book-review-broker +kubectl get broker book-review-broker NAME URL AGE READY REASON book-review-broker http://broker-ingress.knative-eventing.svc.cluster.local/default/book-review-broker 5m37s True ``` Wait a few seconds, and you should see a notification in your Slack channel. Congratulations! You have successfully set up the notification service for your bookstore. + ## Conclusion -In this tutorial, you learned how to set up an event-driven service that automates notifications to a Slack channel using Knative Eventing and Apache Camel K. By leveraging these technologies, you can easily connect your applications to third-party services, and pass information between them in real-time. +In this tutorial, you learned how to set up an event-driven service that automates notifications to a Slack channel using Knative Eventing and Apache Camel K. By leveraging these technologies, you can easily connect your applications to third-party services, and pass information between them in real-time. diff --git a/code-samples/eventing/bookstore-sample-app/solution/solution.sh b/code-samples/eventing/bookstore-sample-app/solution/solution.sh index 031fead8d87..ae263294666 100755 --- a/code-samples/eventing/bookstore-sample-app/solution/solution.sh +++ b/code-samples/eventing/bookstore-sample-app/solution/solution.sh @@ -52,35 +52,39 @@ then exit fi -# Detect whether the user has kamel CLI installed -if ! command -v kamel &> /dev/null -then - echo "" - echo "⚠️ Kamel CLI not found. Please install the Kamel CLI by following the instructions at https://camel.apache.org/camel-k/latest/installation/installation.html." - exit -fi - # Prompt for the Docker registry details echo "" -echo "📝 Please provide the details of your Container registry to install the Camel-K." -read -p "🔑 Enter the registry hostname (e.g., docker.io or quay.io): " REGISTRY_HOST -read -p "🔑 Enter the registry username: " REGISTRY_USER -read -s -p "🔑 Enter the registry password: " REGISTRY_PASSWORD +echo "📝 Please provide the details of your local running Container registry to install the Camel-K." +read -p "🔑 Enter the registry host (e.g. kind-registry): " REGISTRY_HOST +read -p "🔑 Enter the registry port: " REGISTRY_PORT echo "" echo "✅ All the required details have been captured and saved locally." # Set the registry details as environment variables export REGISTRY_HOST=$REGISTRY_HOST -export REGISTRY_USER=$REGISTRY_USER -export REGISTRY_PASSWORD=$REGISTRY_PASSWORD +export REGISTRY_PORT=$REGISTRY_PORT # Set the KO_DOCKER_REPO environment variable -export KO_DOCKER_REPO=$REGISTRY_HOST/$REGISTRY_USER +export KO_DOCKER_REPO=$REGISTRY_HOST/$REGISTRY_PORT # Install Camel-K echo "" echo "📦 Installing Camel-K..." -kamel install --registry $REGISTRY_HOST --organization $REGISTRY_USER --registry-auth-username $REGISTRY_USER --registry-auth-password $REGISTRY_PASSWORD +kubectl create ns camel-k && \ +kubectl apply -k github.com/apache/camel-k/install/overlays/kubernetes/descoped?ref=v2.8.0 --server-side + +cat < { console.log('WebSocket connection established on /comments'); - + // Function to send all comments to the connected client const sendComments = async () => { try { @@ -44,7 +44,7 @@ app.ws('/comments', (ws, req) => { // Optionally, you can trigger this function based on certain conditions // Here, we just send data immediately after connection and on an interval sendComments(); - const interval = setInterval(sendComments, 1000); // Send comments every 10 seconds + const interval = setInterval(sendComments, 1000); // Send comments every second ws.on('close', () => { console.log('WebSocket connection on /comments closed');