Chapter 12. Functions development reference guide 12.1. Developing Quarkus functions After you have created a Quarkus function project , you can modify the template files provided to add business logic to your function. This includes configuring function invocation and the returned headers and status codes. 12.1.1. Prerequisites Before you can develop functions, you must complete the setup steps in Configuring OpenShift Serverless Functions . 12.1.2. Quarkus function template structure When you create a Quarkus function by using the Knative ( kn ) CLI, the project directory looks similar to a typical Maven project. Additionally, the project contains the func.yaml file, which is used for configuring the function. Both http and event trigger functions have the same template structure: Template structure . ├── func.yaml 1 ├── mvnw ├── mvnw.cmd ├── pom.xml 2 ├── README.md └── src ├── main │ ├── java │ │ └── functions │ │ ├── Function.java 3 │ │ ├── Input.java │ │ └── Output.java │ └── resources │ └── application.properties └── test └── java └── functions 4 ├── FunctionTest.java └── NativeFunctionIT.java 1 Used to determine the image name and registry. 2 The Project Object Model (POM) file contains project configuration, such as information about dependencies. You can add additional dependencies by modifying this file. Example of additional dependencies ... <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.8.0</version> <scope>test</scope> </dependency> </dependencies> ... Dependencies are downloaded during the first compilation. 3 The function project must contain a Java method annotated with @Funq . You can place this method in the Function.java class. 4 Contains simple test cases that can be used to test your function locally. 12.1.3. About invoking Quarkus functions You can create a Quarkus project that responds to cloud events, or one that responds to simple HTTP requests. Cloud events in Knative are transported over HTTP as a POST request, so either function type can listen and respond to incoming HTTP requests. When an incoming request is received, Quarkus functions are invoked with an instance of a permitted type. Table 12.1. Function invocation options Invocation method Data type contained in the instance Example of data HTTP POST request JSON object in the body of the request { "customerId": "0123456", "productId": "6543210" } HTTP GET request Data in the query string ?customerId=0123456&productId=6543210 CloudEvent JSON object in the data property { "customerId": "0123456", "productId": "6543210" } The following example shows a function that receives and processes the customerId and productId purchase data that is listed in the table: Example Quarkus function public class Functions { @Funq public void processPurchase(Purchase purchase) { // process the purchase } } The corresponding Purchase JavaBean class that contains the purchase data looks as follows: Example class public class Purchase { private long customerId; private long productId; // getters and setters } 12.1.3.1. Invocation examples The following example code defines three functions named withBeans , withCloudEvent , and withBinary ; Example import io.quarkus.funqy.Funq; import io.quarkus.funqy.knative.events.CloudEvent; public class Input { private String message; // getters and setters } public class Output { private String message; // getters and setters } public class Functions { @Funq public Output withBeans(Input in) { // function body } @Funq public CloudEvent<Output> withCloudEvent(CloudEvent<Input> in) { // function body } @Funq public void withBinary(byte[] in) { // function body } } The withBeans function of the Functions class can be invoked by: An HTTP POST request with a JSON body: USD curl "http://localhost:8080/withBeans" -X POST \ -H "Content-Type: application/json" \ -d '{"message": "Hello there."}' An HTTP GET request with query parameters: USD curl "http://localhost:8080/withBeans?message=Hello%20there." -X GET A CloudEvent object in binary encoding: USD curl "http://localhost:8080/" -X POST \ -H "Content-Type: application/json" \ -H "Ce-SpecVersion: 1.0" \ -H "Ce-Type: withBeans" \ -H "Ce-Source: cURL" \ -H "Ce-Id: 42" \ -d '{"message": "Hello there."}' A CloudEvent object in structured encoding: USD curl http://localhost:8080/ \ -H "Content-Type: application/cloudevents+json" \ -d '{ "data": {"message":"Hello there."}, "datacontenttype": "application/json", "id": "42", "source": "curl", "type": "withBeans", "specversion": "1.0"}' The withCloudEvent function of the Functions class can be invoked by using a CloudEvent object, similarly to the withBeans function. However, unlike withBeans , withCloudEvent cannot be invoked with a plain HTTP request. The withBinary function of the Functions class can be invoked by: A CloudEvent object in binary encoding: A CloudEvent object in structured encoding: 12.1.4. CloudEvent attributes If you need to read or write the attributes of a CloudEvent, such as type or subject , you can use the CloudEvent<T> generic interface and the CloudEventBuilder builder. The <T> type parameter must be one of the permitted types. In the following example, CloudEventBuilder is used to return success or failure of processing the purchase: public class Functions { private boolean _processPurchase(Purchase purchase) { // do stuff } public CloudEvent<Void> processPurchase(CloudEvent<Purchase> purchaseEvent) { System.out.println("subject is: " + purchaseEvent.subject()); if (!_processPurchase(purchaseEvent.data())) { return CloudEventBuilder.create() .type("purchase.error") .build(); } return CloudEventBuilder.create() .type("purchase.success") .build(); } } 12.1.5. Quarkus function return values Functions can return an instance of any type from the list of permitted types. Alternatively, they can return the Uni<T> type, where the <T> type parameter can be of any type from the permitted types. The Uni<T> type is useful if a function calls asynchronous APIs, because the returned object is serialized in the same format as the received object. For example: If a function receives an HTTP request, then the returned object is sent in the body of an HTTP response. If a function receives a CloudEvent object in binary encoding, then the returned object is sent in the data property of a binary-encoded CloudEvent object. The following example shows a function that fetches a list of purchases: Example command public class Functions { @Funq public List<Purchase> getPurchasesByName(String name) { // logic to retrieve purchases } } Invoking this function through an HTTP request produces an HTTP response that contains a list of purchases in the body of the response. Invoking this function through an incoming CloudEvent object produces a CloudEvent response with a list of purchases in the data property. 12.1.5.1. Permitted types The input and output of a function can be any of the void , String , or byte[] types. Additionally, they can be primitive types and their wrappers, for example, int and Integer . They can also be the following complex objects: Javabeans, maps, lists, arrays, and the special CloudEvents<T> type. Maps, lists, arrays, the <T> type parameter of the CloudEvents<T> type, and attributes of Javabeans can only be of types listed here. Example public class Functions { public List<Integer> getIds(); public Purchase[] getPurchasesByName(String name); public String getNameById(int id); public Map<String,Integer> getNameIdMapping(); public void processImage(byte[] img); } 12.1.6. Testing Quarkus functions Quarkus functions can be tested locally on your computer. In the default project that is created when you create a function using kn func create , there is the src/test/ directory, which contains basic Maven tests. These tests can be extended as needed. Prerequisites You have created a Quarkus function. You have installed the Knative ( kn ) CLI. Procedure Navigate to the project folder for your function. Run the Maven tests: USD ./mvnw test 12.1.7. Overriding liveness and readiness probe values You can override liveness and readiness probe values for your Quarkus functions. This allows you to configure health checks performed on the function. Prerequisites The OpenShift Serverless Operator and Knative Serving are installed on the cluster. You have installed the Knative ( kn ) CLI. You have created a function by using kn func create . Procedure Override the /health/liveness and /health/readiness paths with your own values. You can do this either by changing properties in the function source or by setting the QUARKUS_SMALLRYE_HEALTH_LIVENESS_PATH and QUARKUS_SMALLRYE_HEALTH_READINESS_PATH environment variables on func.yaml file. To override the paths using the function source, update the path properties in the src/main/resources/application.properties file: 1 The root path, which is automatically prepended to the liveness and readiness paths. 2 The liveness path, set to /health/alive here. 3 The readiness path, set to /health/ready here. To override the paths using environment variables, define the path variables in the build block of the func.yaml file: build: builder: s2i buildEnvs: - name: QUARKUS_SMALLRYE_HEALTH_LIVENESS_PATH value: alive 1 - name: QUARKUS_SMALLRYE_HEALTH_READINESS_PATH value: ready 2 1 The liveness path, set to /health/alive here. 2 The readiness path, set to /health/ready here. Add the new endpoints to the func.yaml file, so that they are properly bound to the container for the Knative service: deploy: healthEndpoints: liveness: /health/alive readiness: /health/ready 12.1.8. steps Build and deploy a function. 12.2. Developing Node.js functions After you have created a Node.js function project , you can modify the template files provided to add business logic to your function. This includes configuring function invocation and the returned headers and status codes. 12.2.1. Prerequisites Before you can develop functions, you must complete the steps in Configuring OpenShift Serverless Functions . 12.2.2. Node.js function template structure When you create a Node.js function using the Knative ( kn ) CLI, the project directory looks like a typical Node.js project. The only exception is the additional func.yaml file, which is used to configure the function. Both http and event trigger functions have the same template structure: Template structure . ├── func.yaml 1 ├── index.js 2 ├── package.json 3 ├── README.md └── test 4 ├── integration.js └── unit.js 1 The func.yaml configuration file is used to determine the image name and registry. 2 Your project must contain an index.js file which exports a single function. 3 You are not restricted to the dependencies provided in the template package.json file. You can add additional dependencies as you would in any other Node.js project. Example of adding npm dependencies npm install --save opossum When the project is built for deployment, these dependencies are included in the created runtime container image. 4 Integration and unit test scripts are provided as part of the function template. 12.2.3. About invoking Node.js functions When using the Knative ( kn ) CLI to create a function project, you can generate a project that responds to CloudEvents, or one that responds to simple HTTP requests. CloudEvents in Knative are transported over HTTP as a POST request, so both function types listen for and respond to incoming HTTP events. Node.js functions can be invoked with a simple HTTP request. When an incoming request is received, functions are invoked with a context object as the first parameter. 12.2.3.1. Node.js context objects Functions are invoked by providing a context object as the first parameter. This object provides access to the incoming HTTP request information. This information includes the HTTP request method, any query strings or headers sent with the request, the HTTP version, and the request body. Incoming requests that contain a CloudEvent attach the incoming instance of the CloudEvent to the context object so that it can be accessed by using context.cloudevent . 12.2.3.1.1. Context object methods The context object has a single method, cloudEventResponse() , that accepts a data value and returns a CloudEvent. In a Knative system, if a function deployed as a service is invoked by an event broker sending a CloudEvent, the broker examines the response. If the response is a CloudEvent, this event is handled by the broker. Example context object method // Expects to receive a CloudEvent with customer data function handle(context, customer) { // process the customer const processed = handle(customer); return context.cloudEventResponse(customer) .source('/handle') .type('fn.process.customer') .response(); } 12.2.3.1.2. CloudEvent data If the incoming request is a CloudEvent, any data associated with the CloudEvent is extracted from the event and provided as a second parameter. For example, if a CloudEvent is received that contains a JSON string in its data property that is similar to the following: { "customerId": "0123456", "productId": "6543210" } When invoked, the second parameter to the function, after the context object, will be a JavaScript object that has customerId and productId properties. Example signature function handle(context, data) The data parameter in this example is a JavaScript object that contains the customerId and productId properties. 12.2.3.1.3. Arbitrary data A function can receive any data, not just CloudEvents . For example, you might want to call a function by using POST with an arbitrary object in the body: { "id": "12345", "contact": { "title": "Mr.", "firstname": "John", "lastname": "Smith" } } In this case, you can define the function as follows: function handle(context, customer) { return "Hello " + customer.contact.title + " " + customer.contact.lastname; } Supplying the contact object to the function would then return the following output: Hello Mr. Smith 12.2.3.1.4. Supported data types CloudEvents can contain various data types, including JSON, XML, plain text, and binary data. These data types are provided to the function in their respective formats: JSON Data : Provided as a JavaScript object. XML Data : Provided as an XML document. Plain Text : Provided as a string. Binary Data : Provided as a Buffer object. 12.2.3.1.5. Multiple data types in a function Ensure your function can handle different data types by checking the Content-Type header and parsing the data accordingly. For example: function handle(context, data) { if (context.headers['content-type'] === 'application/json') { // handle JSON data } else if (context.headers['content-type'] === 'application/xml') { // handle XML data } else { // handle other data types } } 12.2.4. Node.js function return values Functions can return any valid JavaScript type or can have no return value. When a function has no return value specified, and no failure is indicated, the caller receives a 204 No Content response. Functions can also return a CloudEvent or a Message object in order to push events into the Knative Eventing system. In this case, the developer is not required to understand or implement the CloudEvent messaging specification. Headers and other relevant information from the returned values are extracted and sent with the response. Example function handle(context, customer) { // process customer and return a new CloudEvent return new CloudEvent({ source: 'customer.processor', type: 'customer.processed' }) } 12.2.4.1. Returning primitive types Functions can return any valid JavaScript type, including primitives such as strings, numbers, and booleans: Example function returning a string function handle(context) { return "This function Works!" } Calling this function returns the following string: USD curl https://myfunction.example.com This function Works! Example function returning a number function handle(context) { let somenumber = 100 return { body: somenumber } } Calling this function returns the following number: USD curl https://myfunction.example.com 100 Example function returning a boolean function handle(context) { let someboolean = false return { body: someboolean } } Calling this function returns the following boolean: USD curl https://myfunction.example.com false Returning primitives directly without wrapping them in an object results in a 204 No Content status code with an empty body: Example function returning a primitive directly function handle(context) { let someboolean = false return someboolean } Calling this function returns the following: USD http :8080 HTTP/1.1 204 No Content Connection: keep-alive ... 12.2.4.2. Returning headers You can set a response header by adding a headers property to the return object. These headers are extracted and sent with the response to the caller. Example response header function handle(context, customer) { // process customer and return custom headers // the response will be '204 No content' return { headers: { customerid: customer.id } }; } 12.2.4.3. Returning status codes You can set a status code that is returned to the caller by adding a statusCode property to the return object: Example status code function handle(context, customer) { // process customer if (customer.restricted) { return { statusCode: 451 } } } Status codes can also be set for errors that are created and thrown by the function: Example error status code function handle(context, customer) { // process customer if (customer.restricted) { const err = new Error('Unavailable for legal reasons'); err.statusCode = 451; throw err; } } 12.2.5. Testing Node.js functions Node.js functions can be tested locally on your computer. In the default project that is created when you create a function by using kn func create , there is a test folder that contains some simple unit and integration tests. Prerequisites The OpenShift Serverless Operator and Knative Serving are installed on the cluster. You have installed the Knative ( kn ) CLI. You have created a function by using kn func create . Procedure Navigate to the test folder for your function. Run the tests: USD npm test 12.2.6. Overriding liveness and readiness probe values You can override liveness and readiness probe values for your Node.js functions. This allows you to configure health checks performed on the function. Prerequisites The OpenShift Serverless Operator and Knative Serving are installed on the cluster. You have installed the Knative ( kn ) CLI. You have created a function by using kn func create . Procedure In your function code, create the Function object, which implements the following interface: export interface Function { init?: () => any; 1 shutdown?: () => any; 2 liveness?: HealthCheck; 3 readiness?: HealthCheck; 4 logLevel?: LogLevel; handle: CloudEventFunction | HTTPFunction; 5 } 1 The initialization function, called before the server is started. This function is optional and should be synchronous. 2 The shutdown function, called after the server is stopped. This function is optional and should be synchronous. 3 The liveness function, called to check if the server is alive. This function is optional and should return 200/OK if the server is alive. 4 The readiness function, called to check if the server is ready to accept requests. This function is optional and should return 200/OK if the server is ready. 5 The function to handle HTTP requests. For example, add the following code to the index.js file: const Function = { handle: (context, body) => { // The function logic goes here return 'function called' }, liveness: () => { process.stdout.write('In liveness\n'); return 'ok, alive'; }, 1 readiness: () => { process.stdout.write('In readiness\n'); return 'ok, ready'; } 2 }; Function.liveness.path = '/alive'; 3 Function.readiness.path = '/ready'; 4 module.exports = Function; 1 Custom liveness function. 2 Custom readiness function. 3 Custom liveness endpoint. 4 Custom readiness endpoint. As an alternative to Function.liveness.path and Function.readiness.path , you can specify custom endpoints using the LIVENESS_URL and READINESS_URL environment variables: run: envs: - name: LIVENESS_URL value: /alive 1 - name: READINESS_URL value: /ready 2 1 The liveness path, set to /alive here. 2 The readiness path, set to /ready here. Add the new endpoints to the func.yaml file, so that they are properly bound to the container for the Knative service: deploy: healthEndpoints: liveness: /alive readiness: /ready 12.2.7. Node.js context object reference The context object has several properties that can be accessed by the function developer. Accessing these properties can provide information about HTTP requests and write output to the cluster logs. 12.2.7.1. log Provides a logging object that can be used to write output to the cluster logs. The log adheres to the Pino logging API . Example log function handle(context) { context.log.info("Processing customer"); } You can access the function by using the kn func invoke command: Example command USD kn func invoke --target 'http://example.function.com' Example output {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"Processing customer"} You can change the log level to one of fatal , error , warn , info , debug , trace , or silent . To do that, change the value of logLevel by assigning one of these values to the environment variable FUNC_LOG_LEVEL using the config command. 12.2.7.2. query Returns the query string for the request, if any, as key-value pairs. These attributes are also found on the context object itself. Example query function handle(context) { // Log the 'name' query parameter context.log.info(context.query.name); // Query parameters are also attached to the context context.log.info(context.name); } You can access the function by using the kn func invoke command: Example command USD kn func invoke --target 'http://example.com?name=tiger' Example output {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"tiger"} 12.2.7.3. body Returns the request body if any. If the request body contains JSON code, this will be parsed so that the attributes are directly available. Example body function handle(context) { // log the incoming request body's 'hello' parameter context.log.info(context.body.hello); } You can access the function by using the curl command to invoke it: Example command USD kn func invoke -d '{"Hello": "world"}' Example output {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"world"} 12.2.7.4. headers Returns the HTTP request headers as an object. Example header function handle(context) { context.log.info(context.headers["custom-header"]); } You can access the function by using the kn func invoke command: Example command USD kn func invoke --target 'http://example.function.com' Example output {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"some-value"} 12.2.7.5. HTTP requests method Returns the HTTP request method as a string. httpVersion Returns the HTTP version as a string. httpVersionMajor Returns the HTTP major version number as a string. httpVersionMinor Returns the HTTP minor version number as a string. 12.2.8. steps Build and deploy a function. 12.3. Developing TypeScript functions After you have created a TypeScript function project , you can modify the template files provided to add business logic to your function. This includes configuring function invocation and the returned headers and status codes. 12.3.1. Prerequisites Before you can develop functions, you must complete the steps in Configuring OpenShift Serverless Functions . 12.3.2. TypeScript function template structure When you create a TypeScript function using the Knative ( kn ) CLI, the project directory looks like a typical TypeScript project. The only exception is the additional func.yaml file, which is used for configuring the function. Both http and event trigger functions have the same template structure: Template structure . ├── func.yaml 1 ├── package.json 2 ├── package-lock.json ├── README.md ├── src │ └── index.ts 3 ├── test 4 │ ├── integration.ts │ └── unit.ts └── tsconfig.json 1 The func.yaml configuration file is used to determine the image name and registry. 2 You are not restricted to the dependencies provided in the template package.json file. You can add additional dependencies as you would in any other TypeScript project. Example of adding npm dependencies npm install --save opossum When the project is built for deployment, these dependencies are included in the created runtime container image. 3 Your project must contain an src/index.js file which exports a function named handle . 4 Integration and unit test scripts are provided as part of the function template. 12.3.3. About invoking TypeScript functions When using the Knative ( kn ) CLI to create a function project, you can generate a project that responds to CloudEvents or one that responds to simple HTTP requests. CloudEvents in Knative are transported over HTTP as a POST request, so both function types listen for and respond to incoming HTTP events. TypeScript functions can be invoked with a simple HTTP request. When an incoming request is received, functions are invoked with a context object as the first parameter. 12.3.3.1. TypeScript context objects To invoke a function, you provide a context object as the first parameter. Accessing properties of the context object can provide information about the incoming HTTP request. Example context object function handle(context:Context): string This information includes the HTTP request method, any query strings or headers sent with the request, the HTTP version, and the request body. Incoming requests that contain a CloudEvent attach the incoming instance of the CloudEvent to the context object so that it can be accessed by using context.cloudevent . 12.3.3.1.1. Context object methods The context object has a single method, cloudEventResponse() , that accepts a data value and returns a CloudEvent. In a Knative system, if a function deployed as a service is invoked by an event broker sending a CloudEvent, the broker examines the response. If the response is a CloudEvent, this event is handled by the broker. Example context object method // Expects to receive a CloudEvent with customer data export function handle(context: Context, cloudevent?: CloudEvent): CloudEvent { // process the customer const customer = cloudevent.data; const processed = processCustomer(customer); return context.cloudEventResponse(customer) .source('/customer/process') .type('customer.processed') .response(); } 12.3.3.1.2. Context types The TypeScript type definition files export the following types for use in your functions. Exported type definitions // Invokable is the expeted Function signature for user functions export interface Invokable { (context: Context, cloudevent?: CloudEvent): any } // Logger can be used for structural logging to the console export interface Logger { debug: (msg: any) => void, info: (msg: any) => void, warn: (msg: any) => void, error: (msg: any) => void, fatal: (msg: any) => void, trace: (msg: any) => void, } // Context represents the function invocation context, and provides // access to the event itself as well as raw HTTP objects. export interface Context { log: Logger; req: IncomingMessage; query?: Record<string, any>; body?: Record<string, any>|string; method: string; headers: IncomingHttpHeaders; httpVersion: string; httpVersionMajor: number; httpVersionMinor: number; cloudevent: CloudEvent; cloudEventResponse(data: string|object): CloudEventResponse; } // CloudEventResponse is a convenience class used to create // CloudEvents on function returns export interface CloudEventResponse { id(id: string): CloudEventResponse; source(source: string): CloudEventResponse; type(type: string): CloudEventResponse; version(version: string): CloudEventResponse; response(): CloudEvent; } 12.3.3.1.3. CloudEvent data If the incoming request is a CloudEvent, any data associated with the CloudEvent is extracted from the event and provided as a second parameter. For example, if a CloudEvent is received that contains a JSON string in its data property that is similar to the following: { "customerId": "0123456", "productId": "6543210" } When invoked, the second parameter to the function, after the context object, will be a JavaScript object that has customerId and productId properties. Example signature function handle(context: Context, cloudevent?: CloudEvent): CloudEvent The cloudevent parameter in this example is a JavaScript object that contains the customerId and productId properties. 12.3.4. TypeScript function return values Functions can return any valid JavaScript type or can have no return value. When a function has no return value specified, and no failure is indicated, the caller receives a 204 No Content response. Functions can also return a CloudEvent or a Message object in order to push events into the Knative Eventing system. In this case, the developer is not required to understand or implement the CloudEvent messaging specification. Headers and other relevant information from the returned values are extracted and sent with the response. Example export const handle: Invokable = function ( context: Context, cloudevent?: CloudEvent ): Message { // process customer and return a new CloudEvent const customer = cloudevent.data; return HTTP.binary( new CloudEvent({ source: 'customer.processor', type: 'customer.processed' }) ); }; 12.3.4.1. Returning headers You can set a response header by adding a headers property to the return object. These headers are extracted and sent with the response to the caller. Example response header export function handle(context: Context, cloudevent?: CloudEvent): Record<string, any> { // process customer and return custom headers const customer = cloudevent.data as Record<string, any>; return { headers: { 'customer-id': customer.id } }; } 12.3.4.2. Returning status codes You can set a status code that is returned to the caller by adding a statusCode property to the return object: Example status code export function handle(context: Context, cloudevent?: CloudEvent): Record<string, any> { // process customer const customer = cloudevent.data as Record<string, any>; if (customer.restricted) { return { statusCode: 451 } } // business logic, then return { statusCode: 240 } } Status codes can also be set for errors that are created and thrown by the function: Example error status code export function handle(context: Context, cloudevent?: CloudEvent): Record<string, string> { // process customer const customer = cloudevent.data as Record<string, any>; if (customer.restricted) { const err = new Error('Unavailable for legal reasons'); err.statusCode = 451; throw err; } } 12.3.5. Testing TypeScript functions TypeScript functions can be tested locally on your computer. In the default project that is created when you create a function using kn func create , there is a test folder that contains some simple unit and integration tests. Prerequisites The OpenShift Serverless Operator and Knative Serving are installed on the cluster. You have installed the Knative ( kn ) CLI. You have created a function by using kn func create . Procedure If you have not previously run tests, install the dependencies first: USD npm install Navigate to the test folder for your function. Run the tests: USD npm test 12.3.6. Overriding liveness and readiness probe values You can override liveness and readiness probe values for your TypeScript functions. This allows you to configure health checks performed on the function. Prerequisites The OpenShift Serverless Operator and Knative Serving are installed on the cluster. You have installed the Knative ( kn ) CLI. You have created a function by using kn func create . Procedure In your function code, create the Function object, which implements the following interface: export interface Function { init?: () => any; 1 shutdown?: () => any; 2 liveness?: HealthCheck; 3 readiness?: HealthCheck; 4 logLevel?: LogLevel; handle: CloudEventFunction | HTTPFunction; 5 } 1 The initialization function, called before the server is started. This function is optional and should be synchronous. 2 The shutdown function, called after the server is stopped. This function is optional and should be synchronous. 3 The liveness function, called to check if the server is alive. This function is optional and should return 200/OK if the server is alive. 4 The readiness function, called to check if the server is ready to accept requests. This function is optional and should return 200/OK if the server is ready. 5 The function to handle HTTP requests. For example, add the following code to the index.js file: const Function = { handle: (context, body) => { // The function logic goes here return 'function called' }, liveness: () => { process.stdout.write('In liveness\n'); return 'ok, alive'; }, 1 readiness: () => { process.stdout.write('In readiness\n'); return 'ok, ready'; } 2 }; Function.liveness.path = '/alive'; 3 Function.readiness.path = '/ready'; 4 module.exports = Function; 1 Custom liveness function. 2 Custom readiness function. 3 Custom liveness endpoint. 4 Custom readiness endpoint. As an alternative to Function.liveness.path and Function.readiness.path , you can specify custom endpoints using the LIVENESS_URL and READINESS_URL environment variables: run: envs: - name: LIVENESS_URL value: /alive 1 - name: READINESS_URL value: /ready 2 1 The liveness path, set to /alive here. 2 The readiness path, set to /ready here. Add the new endpoints to the func.yaml file, so that they are properly bound to the container for the Knative service: deploy: healthEndpoints: liveness: /alive readiness: /ready 12.3.7. TypeScript context object reference The context object has several properties that can be accessed by the function developer. Accessing these properties can provide information about incoming HTTP requests and write output to the cluster logs. 12.3.7.1. log Provides a logging object that can be used to write output to the cluster logs. The log adheres to the Pino logging API . Example log export function handle(context: Context): string { // log the incoming request body's 'hello' parameter if (context.body) { context.log.info((context.body as Record<string, string>).hello); } else { context.log.info('No data received'); } return 'OK'; } You can access the function by using the kn func invoke command: Example command USD kn func invoke --target 'http://example.function.com' Example output {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"Processing customer"} You can change the log level to one of fatal , error , warn , info , debug , trace , or silent . To do that, change the value of logLevel by assigning one of these values to the environment variable FUNC_LOG_LEVEL using the config command. 12.3.7.2. query Returns the query string for the request, if any, as key-value pairs. These attributes are also found on the context object itself. Example query export function handle(context: Context): string { // log the 'name' query parameter if (context.query) { context.log.info((context.query as Record<string, string>).name); } else { context.log.info('No data received'); } return 'OK'; } You can access the function by using the kn func invoke command: Example command USD kn func invoke --target 'http://example.function.com' --data '{"name": "tiger"}' Example output {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"tiger"} {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"tiger"} 12.3.7.3. body Returns the request body, if any. If the request body contains JSON code, this will be parsed so that the attributes are directly available. Example body export function handle(context: Context): string { // log the incoming request body's 'hello' parameter if (context.body) { context.log.info((context.body as Record<string, string>).hello); } else { context.log.info('No data received'); } return 'OK'; } You can access the function by using the kn func invoke command: Example command USD kn func invoke --target 'http://example.function.com' --data '{"hello": "world"}' Example output {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"world"} 12.3.7.4. headers Returns the HTTP request headers as an object. Example header export function handle(context: Context): string { // log the incoming request body's 'hello' parameter if (context.body) { context.log.info((context.headers as Record<string, string>)['custom-header']); } else { context.log.info('No data received'); } return 'OK'; } You can access the function by using the curl command to invoke it: Example command USD curl -H'x-custom-header: some-value'' http://example.function.com Example output {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"some-value"} 12.3.7.5. HTTP requests method Returns the HTTP request method as a string. httpVersion Returns the HTTP version as a string. httpVersionMajor Returns the HTTP major version number as a string. httpVersionMinor Returns the HTTP minor version number as a string. 12.3.8. steps Build and deploy a function. See the Pino API documentation for more information about logging with functions. 12.4. Developing Python functions Important OpenShift Serverless Functions with Python is a Technology Preview feature only. Technology Preview features are not supported with Red Hat production service level agreements (SLAs) and might not be functionally complete. Red Hat does not recommend using them in production. These features provide early access to upcoming product features, enabling customers to test functionality and provide feedback during the development process. For more information about the support scope of Red Hat Technology Preview features, see Technology Preview Features Support Scope . After you have created a Python function project , you can modify the template files provided to add business logic to your function. This includes configuring function invocation and the returned headers and status codes. 12.4.1. Prerequisites Before you can develop functions, you must complete the steps in Configuring OpenShift Serverless Functions . 12.4.2. Python function template structure When you create a Python function by using the Knative ( kn ) CLI, the project directory looks similar to a typical Python project. Python functions have very few restrictions. The only requirements are that your project contains a func.py file that contains a main() function, and a func.yaml configuration file. Developers are not restricted to the dependencies provided in the template requirements.txt file. Additional dependencies can be added as they would be in any other Python project. When the project is built for deployment, these dependencies will be included in the created runtime container image. Both http and event trigger functions have the same template structure: Template structure fn ├── func.py 1 ├── func.yaml 2 ├── requirements.txt 3 └── test_func.py 4 1 Contains a main() function. 2 Used to determine the image name and registry. 3 Additional dependencies can be added to the requirements.txt file as they are in any other Python project. 4 Contains a simple unit test that can be used to test your function locally. 12.4.3. About invoking Python functions Python functions can be invoked with a simple HTTP request. When an incoming request is received, functions are invoked with a context object as the first parameter. The context object is a Python class with two attributes: The request attribute is always present, and contains the Flask request object. The second attribute, cloud_event , is populated if the incoming request is a CloudEvent object. Developers can access any CloudEvent data from the context object. Example context object def main(context: Context): """ The context parameter contains the Flask request object and any CloudEvent received with the request. """ print(f"Method: {context.request.method}") print(f"Event data {context.cloud_event.data}") # ... business logic here 12.4.4. Python function return values Functions can return any value supported by Flask . This is because the invocation framework proxies these values directly to the Flask server. Example def main(context: Context): body = { "message": "Howdy!" } headers = { "content-type": "application/json" } return body, 200, headers Functions can set both headers and response codes as secondary and tertiary response values from function invocation. 12.4.4.1. Returning CloudEvents Developers can use the @event decorator to tell the invoker that the function return value must be converted to a CloudEvent before sending the response. Example @event("event_source"="/my/function", "event_type"="my.type") def main(context): # business logic here data = do_something() # more data processing return data This example sends a CloudEvent as the response value, with a type of "my.type" and a source of "/my/function" . The CloudEvent data property is set to the returned data variable. The event_source and event_type decorator attributes are both optional. 12.4.5. Testing Python functions You can test Python functions locally on your computer. The default project contains a test_func.py file, which provides a simple unit test for functions. Note The default test framework for Python functions is unittest . You can use a different test framework if you prefer. Prerequisites To run Python functions tests locally, you must install the required dependencies: USD pip install -r requirements.txt Procedure Navigate to the folder for your function that contains the test_func.py file. Run the tests: USD python3 test_func.py 12.4.6. steps Build and deploy a function.

Commands:
. ├── func.yaml 1 ├── mvnw ├── mvnw.cmd ├── pom.xml 2 ├── README.md └── src ├── main │ ├── java │ │ └── functions │ │ ├── Function.java 3 │ │ ├── Input.java │ │ └── Output.java │ └── resources │ └── application.properties └── test └── java └── functions 4 ├── FunctionTest.java └── NativeFunctionIT.java
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.8.0</version> <scope>test</scope> </dependency> </dependencies>
public class Functions { @Funq public void processPurchase(Purchase purchase) { // process the purchase } }
public class Purchase { private long customerId; private long productId; // getters and setters }
import io.quarkus.funqy.Funq; import io.quarkus.funqy.knative.events.CloudEvent; public class Input { private String message; // getters and setters } public class Output { private String message; // getters and setters } public class Functions { @Funq public Output withBeans(Input in) { // function body } @Funq public CloudEvent<Output> withCloudEvent(CloudEvent<Input> in) { // function body } @Funq public void withBinary(byte[] in) { // function body } }
curl "http://localhost:8080/withBeans" -X POST -H "Content-Type: application/json" -d '{"message": "Hello there."}'
curl "http://localhost:8080/withBeans?message=Hello%20there." -X GET
curl "http://localhost:8080/" -X POST -H "Content-Type: application/json" -H "Ce-SpecVersion: 1.0" -H "Ce-Type: withBeans" -H "Ce-Source: cURL" -H "Ce-Id: 42" -d '{"message": "Hello there."}'
curl http://localhost:8080/ -H "Content-Type: application/cloudevents+json" -d '{ "data": {"message":"Hello there."}, "datacontenttype": "application/json", "id": "42", "source": "curl", "type": "withBeans", "specversion": "1.0"}'
curl "http://localhost:8080/" -X POST -H "Content-Type: application/octet-stream" -H "Ce-SpecVersion: 1.0" -H "Ce-Type: withBinary" -H "Ce-Source: cURL" -H "Ce-Id: 42" --data-binary '@img.jpg'
curl http://localhost:8080/ -H "Content-Type: application/cloudevents+json" -d "{ \"data_base64\": \"USD(base64 --wrap=0 img.jpg)\", \"datacontenttype\": \"application/octet-stream\", \"id\": \"42\", \"source\": \"curl\", \"type\": \"withBinary\", \"specversion\": \"1.0\"}"
public class Functions { private boolean _processPurchase(Purchase purchase) { // do stuff } public CloudEvent<Void> processPurchase(CloudEvent<Purchase> purchaseEvent) { System.out.println("subject is: " + purchaseEvent.subject()); if (!_processPurchase(purchaseEvent.data())) { return CloudEventBuilder.create() .type("purchase.error") .build(); } return CloudEventBuilder.create() .type("purchase.success") .build(); } }
public class Functions { @Funq public List<Purchase> getPurchasesByName(String name) { // logic to retrieve purchases } }
public class Functions { public List<Integer> getIds(); public Purchase[] getPurchasesByName(String name); public String getNameById(int id); public Map<String,Integer> getNameIdMapping(); public void processImage(byte[] img); }
./mvnw test
quarkus.smallrye-health.root-path=/health 1 quarkus.smallrye-health.liveness-path=alive 2 quarkus.smallrye-health.readiness-path=ready 3
build: builder: s2i buildEnvs: - name: QUARKUS_SMALLRYE_HEALTH_LIVENESS_PATH value: alive 1 - name: QUARKUS_SMALLRYE_HEALTH_READINESS_PATH value: ready 2
deploy: healthEndpoints: liveness: /health/alive readiness: /health/ready
. ├── func.yaml 1 ├── index.js 2 ├── package.json 3 ├── README.md └── test 4 ├── integration.js └── unit.js
npm install --save opossum
// Expects to receive a CloudEvent with customer data function handle(context, customer) { // process the customer const processed = handle(customer); return context.cloudEventResponse(customer) .source('/handle') .type('fn.process.customer') .response(); }
customerId: 0123456
productId: 6543210
function handle(context, data)
{ "id": "12345", "contact": { "title": "Mr.", "firstname": "John", "lastname": "Smith" } }
function handle(context, customer) { return "Hello " + customer.contact.title + " " + customer.contact.lastname; }
Hello Mr. Smith
function handle(context, data) { if (context.headers['content-type'] === 'application/json') { // handle JSON data } else if (context.headers['content-type'] === 'application/xml') { // handle XML data } else { // handle other data types } }
function handle(context, customer) { // process customer and return a new CloudEvent return new CloudEvent({ source: 'customer.processor', type: 'customer.processed' }) }
function handle(context) { return "This function Works!" }
curl https://myfunction.example.com
This function Works!
function handle(context) { let somenumber = 100 return { body: somenumber } }
curl https://myfunction.example.com
100
function handle(context) { let someboolean = false return { body: someboolean } }
curl https://myfunction.example.com
false
function handle(context) { let someboolean = false return someboolean }
http :8080
HTTP/1.1 204 No Content Connection: keep-alive
function handle(context, customer) { // process customer and return custom headers // the response will be '204 No content' return { headers: { customerid: customer.id } }; }
function handle(context, customer) { // process customer if (customer.restricted) { return { statusCode: 451 } } }
function handle(context, customer) { // process customer if (customer.restricted) { const err = new Error('Unavailable for legal reasons'); err.statusCode = 451; throw err; } }
npm test
export interface Function { init?: () => any; 1 shutdown?: () => any; 2 liveness?: HealthCheck; 3 readiness?: HealthCheck; 4 logLevel?: LogLevel; handle: CloudEventFunction | HTTPFunction; 5 }
const Function = { handle: (context, body) => { // The function logic goes here return 'function called' }, liveness: () => { process.stdout.write('In liveness\n'); return 'ok, alive'; }, 1 readiness: () => { process.stdout.write('In readiness\n'); return 'ok, ready'; } 2 }; Function.liveness.path = '/alive'; 3 Function.readiness.path = '/ready'; 4 module.exports = Function;
run: envs: - name: LIVENESS_URL value: /alive 1 - name: READINESS_URL value: /ready 2
deploy: healthEndpoints: liveness: /alive readiness: /ready
function handle(context) { context.log.info("Processing customer"); }
kn func invoke --target 'http://example.function.com'
level: 30
time: 1604511655265
pid: 3430203
hostname: localhost.localdomain
reqId: 1
msg: Processing customer
function handle(context) { // Log the 'name' query parameter context.log.info(context.query.name); // Query parameters are also attached to the context context.log.info(context.name); }
kn func invoke --target 'http://example.com?name=tiger'
level: 30
time: 1604511655265
pid: 3430203
hostname: localhost.localdomain
reqId: 1
msg: tiger
function handle(context) { // log the incoming request body's 'hello' parameter context.log.info(context.body.hello); }
kn func invoke -d '{"Hello": "world"}'
level: 30
time: 1604511655265
pid: 3430203
hostname: localhost.localdomain
reqId: 1
msg: world
function handle(context) { context.log.info(context.headers["custom-header"]); }
kn func invoke --target 'http://example.function.com'
level: 30
time: 1604511655265
pid: 3430203
hostname: localhost.localdomain
reqId: 1
msg: some-value
. ├── func.yaml 1 ├── package.json 2 ├── package-lock.json ├── README.md ├── src │ └── index.ts 3 ├── test 4 │ ├── integration.ts │ └── unit.ts └── tsconfig.json
npm install --save opossum
function handle(context:Context): string
// Expects to receive a CloudEvent with customer data export function handle(context: Context, cloudevent?: CloudEvent): CloudEvent { // process the customer const customer = cloudevent.data; const processed = processCustomer(customer); return context.cloudEventResponse(customer) .source('/customer/process') .type('customer.processed') .response(); }
// Invokable is the expeted Function signature for user functions export interface Invokable { (context: Context, cloudevent?: CloudEvent): any } // Logger can be used for structural logging to the console export interface Logger { debug: (msg: any) => void, info: (msg: any) => void, warn: (msg: any) => void, error: (msg: any) => void, fatal: (msg: any) => void, trace: (msg: any) => void, } // Context represents the function invocation context, and provides // access to the event itself as well as raw HTTP objects. export interface Context { log: Logger; req: IncomingMessage; query?: Record<string, any>; body?: Record<string, any>|string; method: string; headers: IncomingHttpHeaders; httpVersion: string; httpVersionMajor: number; httpVersionMinor: number; cloudevent: CloudEvent; cloudEventResponse(data: string|object): CloudEventResponse; } // CloudEventResponse is a convenience class used to create // CloudEvents on function returns export interface CloudEventResponse { id(id: string): CloudEventResponse; source(source: string): CloudEventResponse; type(type: string): CloudEventResponse; version(version: string): CloudEventResponse; response(): CloudEvent; }
customerId: 0123456
productId: 6543210
function handle(context: Context, cloudevent?: CloudEvent): CloudEvent
export const handle: Invokable = function ( context: Context, cloudevent?: CloudEvent ): Message { // process customer and return a new CloudEvent const customer = cloudevent.data; return HTTP.binary( new CloudEvent({ source: 'customer.processor', type: 'customer.processed' }) ); };
export function handle(context: Context, cloudevent?: CloudEvent): Record<string, any> { // process customer and return custom headers const customer = cloudevent.data as Record<string, any>; return { headers: { 'customer-id': customer.id } }; }
export function handle(context: Context, cloudevent?: CloudEvent): Record<string, any> { // process customer const customer = cloudevent.data as Record<string, any>; if (customer.restricted) { return { statusCode: 451 } } // business logic, then return { statusCode: 240 } }
export function handle(context: Context, cloudevent?: CloudEvent): Record<string, string> { // process customer const customer = cloudevent.data as Record<string, any>; if (customer.restricted) { const err = new Error('Unavailable for legal reasons'); err.statusCode = 451; throw err; } }
npm install
npm test
export interface Function { init?: () => any; 1 shutdown?: () => any; 2 liveness?: HealthCheck; 3 readiness?: HealthCheck; 4 logLevel?: LogLevel; handle: CloudEventFunction | HTTPFunction; 5 }
const Function = { handle: (context, body) => { // The function logic goes here return 'function called' }, liveness: () => { process.stdout.write('In liveness\n'); return 'ok, alive'; }, 1 readiness: () => { process.stdout.write('In readiness\n'); return 'ok, ready'; } 2 }; Function.liveness.path = '/alive'; 3 Function.readiness.path = '/ready'; 4 module.exports = Function;
run: envs: - name: LIVENESS_URL value: /alive 1 - name: READINESS_URL value: /ready 2
deploy: healthEndpoints: liveness: /alive readiness: /ready
export function handle(context: Context): string { // log the incoming request body's 'hello' parameter if (context.body) { context.log.info((context.body as Record<string, string>).hello); } else { context.log.info('No data received'); } return 'OK'; }
kn func invoke --target 'http://example.function.com'
level: 30
time: 1604511655265
pid: 3430203
hostname: localhost.localdomain
reqId: 1
msg: Processing customer
export function handle(context: Context): string { // log the 'name' query parameter if (context.query) { context.log.info((context.query as Record<string, string>).name); } else { context.log.info('No data received'); } return 'OK'; }
kn func invoke --target 'http://example.function.com' --data '{"name": "tiger"}'
{"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"tiger"} {"level":30,"time":1604511655265,"pid":3430203,"hostname":"localhost.localdomain","reqId":1,"msg":"tiger"}
export function handle(context: Context): string { // log the incoming request body's 'hello' parameter if (context.body) { context.log.info((context.body as Record<string, string>).hello); } else { context.log.info('No data received'); } return 'OK'; }
kn func invoke --target 'http://example.function.com' --data '{"hello": "world"}'
level: 30
time: 1604511655265
pid: 3430203
hostname: localhost.localdomain
reqId: 1
msg: world
export function handle(context: Context): string { // log the incoming request body's 'hello' parameter if (context.body) { context.log.info((context.headers as Record<string, string>)['custom-header']); } else { context.log.info('No data received'); } return 'OK'; }
curl -H'x-custom-header: some-value'' http://example.function.com
level: 30
time: 1604511655265
pid: 3430203
hostname: localhost.localdomain
reqId: 1
msg: some-value
fn ├── func.py 1 ├── func.yaml 2 ├── requirements.txt 3 └── test_func.py 4
def main(context: Context): """ The context parameter contains the Flask request object and any CloudEvent received with the request. """ print(f"Method: {context.request.method}") print(f"Event data {context.cloud_event.data}") # ... business logic here
def main(context: Context): body = { "message": "Howdy!" } headers = { "content-type": "application/json" } return body, 200, headers
@event("event_source"="/my/function", "event_type"="my.type") def main(context): # business logic here data = do_something() # more data processing return data
pip install -r requirements.txt
python3 test_func.py