diff --git a/Makefile b/Makefile index 2d6717c1..b6da6835 100644 --- a/Makefile +++ b/Makefile @@ -116,7 +116,7 @@ prepare-workdir: # Depends on: prepare-workdir target. # Usage: make build-image ifeq ($(IS_WORKFLOW),true) -build-image: BUILD_ARGS=--build-arg-file=workflows/$(WORKFLOW_ID)/argfile.conf --build-arg=BUILDER_IMAGE=$(BUILDER_IMAGE) --build-arg WF_RESOURCES=workflows/$(WORKFLOW_ID) +build-image: BUILD_ARGS=--build-arg-file=workflows/$(WORKFLOW_ID)/argfile.conf --build-arg=WORKFLOW_ID=$(WORKFLOW_ID) --build-arg=BUILDER_IMAGE=$(BUILDER_IMAGE) --build-arg WF_RESOURCES=workflows/$(WORKFLOW_ID) endif build-image: EXTRA_ARGS=--ulimit nofile=4096:4096 --platform='linux/amd64' build-image: prepare-workdir diff --git a/pipeline/workflow-builder.Dockerfile b/pipeline/workflow-builder.Dockerfile index 818e7b8e..52d42bc2 100644 --- a/pipeline/workflow-builder.Dockerfile +++ b/pipeline/workflow-builder.Dockerfile @@ -22,6 +22,8 @@ FROM ${BUILDER_IMAGE:-quay.io/orchestrator/logic-swf-builder-rhel8:1.36.0-discon # ARG QUARKUS_EXTENSIONS=org.kie:kogito-addons-quarkus-jobs-knative-eventing:999-SNAPSHOT,org.kie:kie-addons-quarkus-persistence-jdbc:999-SNAPSHOT,io.quarkus:quarkus-jdbc-postgresql:3.8.4,io.quarkus:quarkus-agroal:3.8.4,org.kie:kie-addons-quarkus-monitoring-prometheus:999-SNAPSHOT,org.kie:kie-addons-quarkus-monitoring-sonataflow:999-SNAPSHOT # When using prod: ARG QUARKUS_EXTENSIONS=io.quarkiverse.openapi.generator:quarkus-openapi-generator:2.9.1-lts,org.kie:kie-addons-quarkus-monitoring-sonataflow,org.kie:kogito-addons-quarkus-jobs-knative-eventing,org.kie:kie-addons-quarkus-persistence-jdbc,io.quarkus:quarkus-jdbc-postgresql:3.15.4.redhat-00001,io.quarkus:quarkus-agroal:3.15.4.redhat-00001 +ARG WORKFLOW_ID +ENV WORKFLOW_ID=${WORKFLOW_ID} # Args to pass to the Quarkus CLI # add extension command @@ -43,13 +45,42 @@ RUN ls -la ./resources ENV swf_home_dir=/home/kogito/serverless-workflow-project RUN if [[ -d "./resources/src" ]]; then cp -r ./resources/src/* ./src/; fi -RUN /home/kogito/launch/build-app.sh ./resources +RUN if [ "$WORKFLOW_ID" = "python" ]; then \ + QUARKUS_EXTENSIONS="${QUARKUS_EXTENSIONS},org.apache.kie.sonataflow:sonataflow-addons-quarkus-python:10.1.0"; \ + fi && \ + echo "Final QUARKUS_EXTENSIONS=$QUARKUS_EXTENSIONS" && \ + /home/kogito/launch/build-app.sh ./resources #============================= # Runtime Run #============================= FROM registry.access.redhat.com/ubi9/openjdk-17:1.21-2 +# Conditional JEP installation based on WORKFLOW_ID +ARG WORKFLOW_ID +ENV WORKFLOW_ID=${WORKFLOW_ID} +USER root +RUN if [ "$WORKFLOW_ID" = "python" ]; then \ + echo "Installing JEP because WORKFLOW_ID=$WORKFLOW_ID"; \ + microdnf install -y \ + python3 \ + python3-devel \ + gcc \ + gcc-c++ \ + make \ + libffi-devel \ + && microdnf clean all \ + && pip3 install --no-cache-dir jep \ + && PY_VER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') \ + && cp /usr/local/lib64/python${PY_VER}/site-packages/jep/libjep.so /usr/lib64 \ + && echo "JEP installation complete"; \ + else \ + echo "Skipping JEP installation"; \ + fi +USER 185 + +# Necessary command for python workflow. Copy command can not be conditional +COPY --chown=185:185 workflows/python/my_scripts /usr/local/lib64/python3.9/site-packages/my_scripts ARG FLOW_NAME ARG FLOW_SUMMARY diff --git a/workflows/python/Dockerfile b/workflows/python/Dockerfile new file mode 100644 index 00000000..c375245a --- /dev/null +++ b/workflows/python/Dockerfile @@ -0,0 +1,38 @@ +# +# This Dockerfile adds Python support to the SonataFlow devmode image. +# +# To build the image, run the following command: +# docker build -t sonataflow-devmode-python . +# +# To run the image with your workflow, run the following commands: +# IMAGE=sonataflow-devmode-python +# PATH_TO_WORKFLOW= +# docker run --ulimit nofile=262144:262144 -d --name sonataflow-devmode-python --add-host host.docker.internal:host-gateway -e QUARKUS_HTTP_PORT=8080 -p 8080:8080 -e KOGITO_SERVICE_URL=http://localhost:8080 -v $PATH_TO_WORKFLOW:/home/kogito/serverless-workflow-project/src/main/resources:Z -e KOGITO.CODEGEN.PROCESS.FAILONERROR=false -e QUARKUS_EMBEDDED_POSTGRESQL_DATA_DIR=/home/kogito/persistence -e QUARKUS_EXTENSIONS=org.apache.kie.sonataflow:sonataflow-addons-quarkus-python $IMAGE +# +# +# + +FROM quay.io/kubesmarts/incubator-kie-sonataflow-devmode:1.36.0 + +USER root + +# Install Python & build dependencies +RUN microdnf install -y \ + python3 \ + python3-devel \ + gcc \ + gcc-c++ \ + make \ + libffi-devel \ + && microdnf clean all + +# Install JEP (compiles native lib) +RUN pip3 install --no-cache-dir jep + +# Copy jep SO file to known location +RUN PY_VER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') \ + && cp /usr/local/lib64/python${PY_VER}/site-packages/jep/libjep.so /usr/lib64 + +COPY --chown=185:185 path-to-your-python-scripts-folder /usr/local/lib64/python3.6/site-packages/my_scripts + +USER 185 diff --git a/workflows/python/README.md b/workflows/python/README.md new file mode 100644 index 00000000..0eeb0fde --- /dev/null +++ b/workflows/python/README.md @@ -0,0 +1,11 @@ +# Python workflow - Workflow demonstrating usage of Python scripts and functions +This workflow shows how to execute python functions and scripts from within a workflow. The workflow does the following: +- Uses existing python packages to determine current time and date and sets it to the workflow output. +- Calls a custom python function from a custom script. The function creates a greeting message. + +## Workflow diagram +![Python workflow diagram](https://github.com/rhdhorchestrator/serverless-workflows/blob/main/python/mtv.svg?raw=true) + +## Installation + +See [official installation guide](https://github.com/rhdhorchestrator/serverless-workflows/blob/main/deploy/docs/main/python) diff --git a/workflows/python/application.properties b/workflows/python/application.properties new file mode 100644 index 00000000..203ac1ad --- /dev/null +++ b/workflows/python/application.properties @@ -0,0 +1,9 @@ +quarkus.tls.trust-all=true +quarkus.kubernetes-client.trust-certs=true + +# This property is used to select the log level, which controls the amount +# of information logged on HTTP requests based on the severity of the events. +# Possible values: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL. +# and see https://quarkus.io/guides/logging for documentation +quarkus.log.category."org.apache.http".level=DEBUG +quarkus.log.level=INFO \ No newline at end of file diff --git a/workflows/python/argfile.conf b/workflows/python/argfile.conf new file mode 100644 index 00000000..e673431e --- /dev/null +++ b/workflows/python/argfile.conf @@ -0,0 +1,3 @@ +FLOW_NAME=python +FLOW_SUMMARY=python +FLOW_DESCRIPTION=YAML based Python workflow diff --git a/workflows/python/my_scripts/my_script.py b/workflows/python/my_scripts/my_script.py new file mode 100644 index 00000000..f3f59772 --- /dev/null +++ b/workflows/python/my_scripts/my_script.py @@ -0,0 +1,41 @@ +""" +Example script with functions to import from SonataFlow workflows +""" + +def greet(name): + """Simple greeting function""" + return f"Hello, {name}!" + +def process_data(data_list): + """Process a list of data""" + if not data_list: + return [] + + processed = [item.upper() if isinstance(item, str) else str(item) for item in data_list] + return processed + +def calculate_sum(numbers): + """Calculate sum of numbers""" + return sum(numbers) + +class DataProcessor: + """Example class for data processing""" + + def __init__(self, name): + self.name = name + + def process(self, data): + """Process data with processor name""" + return f"Processed by {self.name}: {data}" + +# Module-level variable +MODULE_VERSION = "1.0.0" + +def workflow_helper(input_data): + """Helper function specifically for workflow usage""" + result = { + "processed": process_data(input_data.get("items", [])), + "greeting": greet(input_data.get("user", "Anonymous")), + "version": MODULE_VERSION + } + return result diff --git a/workflows/python/python.svg b/workflows/python/python.svg new file mode 100644 index 00000000..aaf7c431 --- /dev/null +++ b/workflows/python/python.svg @@ -0,0 +1 @@ +StartStartEnd \ No newline at end of file diff --git a/workflows/python/python.sw.yaml b/workflows/python/python.sw.yaml new file mode 100644 index 00000000..4da141af --- /dev/null +++ b/workflows/python/python.sw.yaml @@ -0,0 +1,60 @@ +id: "python" +version: "0.1" +specVersion: "0.8" +name: "python" +description: "Workflow demonstrating usage of Python scripts and functions" +start: Start +extensions: + - extensionid: workflow-output-schema + outputSchema: schemas/workflow-output-schema.json +functions: + - name: pythonScript + type: custom + operation: "script:python" + - name: pythonFunctionCtime + type: custom + operation: "service:python:time::ctime" + - name: successResult + type: expression + operation: '{ + result: { + message: "Current date and time is: " + .timeStr + "\nYour greeting is: " + $WORKFLOW.python.myGreet, + outputs:[], + nextWorkflows:[ + { + id:"next", + name:"next" + } + ] + } + }' +states: + - name: Start + type: operation + actionMode: sequential + actions: + - name: pythonScriptImport + functionRef: + refName: pythonScript + arguments: + script: "from time import time; from my_scripts import my_script" + - name: pythonScriptTime + functionRef: + refName: pythonScript + arguments: + script: "pyNowSecs = time()" + - name: pythonFunctionCtime + functionRef: + refName: pythonFunctionCtime + arguments: "$WORKFLOW.python.pyNowSecs" + actionDataFilter: + toStateData: .timeStr + - name: pythonMyScript + functionRef: + refName: pythonScript + arguments: + script: "myGreet = my_script.greet('Superman')" + - name: setResult + functionRef: + refName: successResult + end: true diff --git a/workflows/python/schemas/workflow-output-schema.json b/workflows/python/schemas/workflow-output-schema.json new file mode 100644 index 00000000..8d3c7005 --- /dev/null +++ b/workflows/python/schemas/workflow-output-schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "WorkflowResult", + "description": "Schema of workflow output", + "type": "object", + "properties": { + "result": { + "$ref": "../shared/schemas/workflow-result-schema.json", + "type": "object" + } + } +} \ No newline at end of file