This tutorial walks you through on how to build a Java functions on a Function as a Service(FaaS) platform Apache OpenWhisk.
You will need in this tutorial
-
oc (eval $(minishift oc-env))
-
stern (brew install stern)
-
curl, gunzip, tar are built-in to MacOS or part of your bash shell
-
git (everybody needs the git CLI)
-
Java 8
Assumes minishift, tested with minishift v1.13.1+75352e5
#!/bin/bash
# add the location of minishift executable to PATH
export MINISHIFT_HOME=~/minishift_1.13.1
export PATH=$MINISHIFT_HOME:$PATH
minishift profile set faas-tutorial
minishift config set memory 8GB
minishift config set cpus 3
minishift config set image-caching true
minishift addon enable admin-user
minishift addon enable anyuid # # (1)
minishift start
minishift ssh -- sudo ip link set docker0 promisc on # # (2)
-
Some images that are in Apache OpenWhisk Docker hub requires anyuid SCC in OpenShift
-
This is needed for pods to communicate with each other within the cluster (TODO: need to add more clear details here)
❗
|
|
#!/bin/bash
eval $(minishift oc-env) && eval $(minishift docker-env)
oc login $(minishift ip):8443 -u admin -p admin
The project OpenWhisk on OpenShift provides the OpenShift templates required to deploy Apache OpenWhisk.
oc new-project faas # # (1)
oc project -q # # (2)
oc process -f https://git.io/openwhisk-template | oc create -f - # # (3)
oc adm policy add-role-to-user admin developer -n faas # # (4)
-
Its always better to group certain class of applications, create a new OpenShift project called
faas
to deploy all OpenWhisk applications -
Make sure we are in right project
-
Deploy OpenWhisk applications to
openwhisk
project -
(Optional) Add
developer
user as admin tofaas
project so as to allow you to login with developer user and accessfaas
project
📎
|
You need to wait for sometime to have all the required OpenWhisk pods come up and the FaaS is ready for some load. You can watch the status watch -n 5 'oc logs -f controller-0 -n faas | grep "invoker status changed"'` |
Launch OpenShift console via minishift console
, a successful deployment will look like:
Download OpenWhisk CLI and add it your PATH. Verify your path using the command
wsk --help
The OpenWhisk CLI needs to be configured to know where the OpenWhisk is located
and the authorization that could be used to invoke wsk
commands. Run the following command to have that setup:
#!/bin/bash
AUTH_SECRET=$(oc get secret whisk.auth -o yaml | grep "system:" | awk '{print $2}' | base64 --decode)
wsk property set --auth $AUTH_SECRET --apihost $(oc get route/openwhisk --template="{{.spec.host}}")
Successful setup of WSK CLI will show output like:
In this case the OpenWhisk API Host is pointing to the local minishift nip.io address
To verify if wsk CLI is configured properly run wsk -i action list
,that should list some actions which are installed as part of the
OpenWhisk setup. If you see empty result then you Reinstall default Catalog
💡
|
The |
Clone the complete project from git clone https://github.com/redhat-developer-demos/faas-java-tutorial
, we will refer to this location as $PROJECT_HOME through out the document
for convenience.
Actions are stateless code snippets that run on the OpenWhisk platform. It is analogous to Function in Java idioms. OpenWhisk Actions are thread-safe meaning at a given point of time only one invocation happens.
Fore more details refer the official documentation here.
Lets quickly create a simple function in JavaScript to see if all working:
mkdir -p getstarted
cd $PROJECT_HOME/getstarted
Create a file called $PROJECT_HOME/getstarted/greeter.js
and add the following content to it:
function main() {
return {payload: 'Welcome to OpenWhisk on OpenShift'};
}
Create an action called greeter:
wsk -i action update greeter greeter.js
Lets invoke the action using command:
wsk -i action invoke greeter --result
The action invoke should respond with the following JSON:
{
"payload": "Welcome to OpenWhisk on OpenShift"
}
Maven Archetype could be used to generate the template Java Action project, as of writing this tutorial the archetype is not maven central hence it need to install it locally,
git clone https://github.com/apache/incubator-openwhisk-devtools
cd incubator-openwhisk-devtools/java-action-archetype
mvn -DskipTests clean install
cd $PROJECT_HOME
Lets now create the first Java Action a simple "hello world" kind of function, have it deployed to OpenWhisk and finally invoke to see the result. This section will also details the complete Create-Update-Delete cycle of Java Actions on OpenWhisk.
📎
|
For easier jar names all the examples will be using maven |
cd $PROJECT_HOME
mvn archetype:generate \
-DarchetypeGroupId=org.apache.openwhisk.java \
-DarchetypeArtifactId=java-action-archetype \
-DarchetypeVersion=1.0-SNAPSHOT \
-DgroupId=com.example \
-DartifactId=hello-openwhisk
wsk -i action invoke hello-openwhisk --result
As all the OpenWhisk actions are asynchronous, we need to add --result
to get the result shown on the console.
Successful execution of the command will show the following output:
{"greetings": "Hello! Welcome to OpenWhisk" }
wsk -i action invoke hello-openwhisk
A successful action invoke will return an activation id :
We can then use the to activation id check the response using wsk
CLI:
wsk -i activation result <activation_id>
e.g.
wsk -i activation result ffb2966350904356b29663509043566e
Successful execution of the command will show the same output like Action Response.
Update the FunctionApp class with code:
package com.example;
import com.google.gson.JsonObject;
/**
* Hello FunctionApp
*/
public class FunctionApp {
public static JsonObject main(JsonObject args) {
JsonObject response = new JsonObject();
response.addProperty("greetings", "Hello! Welcome to OpenWhisk on OpenShift");
return response;
}
}
Update the FunctionAppTest class with code:
package com.example;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import com.google.gson.JsonObject;
import org.junit.Test;
/**
* Unit test for simple function.
*/
public class FunctionAppTest {
@Test
public void testFunction() {
JsonObject args = new JsonObject();
JsonObject response = FunctionApp.main(args);
assertNotNull(response);
String greetings = response.getAsJsonPrimitive("greetings").getAsString();
assertNotNull(greetings);
assertEquals("Hello! Welcome to OpenWhisk on OpenShift", greetings);
}
}
cd $PROJECT_HOME/hello-openwhisk
mvn clean package
wsk -i action update hello-openwhisk target/hello-openwhisk.jar --main com.example.FunctionApp
Successful update should show a output like:
Repeating the Invocation and Verification steps should result in the updated response like:
{
"greetings": "Hello! Welcome to OpenWhisk on OpenShift"
}
WebActions allows the OpenWhisk action to be invoked via HTTP verbs like GET, POST, PUT etc., The WebActions can be enabled for
any Action using the prameter --web=true
during the creation of the action using WSK CLI.
cd $PROJECT_HOME
mvn archetype:generate \
-DarchetypeGroupId=org.apache.openwhisk.java \
-DarchetypeArtifactId=java-action-archetype \
-DarchetypeVersion=1.0-SNAPSHOT \
-DgroupId=com.example \
-DartifactId=hello-web
Update the FunctionApp class with code:
package com.example;
import com.google.gson.JsonObject;
/**
* Hello Web FunctionApp
*/
public class FunctionApp {
public static JsonObject main(JsonObject args) {
JsonObject response = new JsonObject();
response.add("response", args);
return response;
}
}
Update the FunctionAppTest class with code:
package com.example;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import com.google.gson.JsonObject;
import org.junit.Test;
/**
* Unit test for simple function.
*/
public class FunctionAppTest {
@Test
public void testFunction() {
JsonObject args = new JsonObject();
args.addProperty("name", "test");
JsonObject response = FunctionApp.main(args);
assertNotNull(response);
String actual = response.get("response").getAsJsonObject().get("name").getAsString();
assertEquals("test", actual);
}
}
wsk -i action update --web=true redhat-developers-demo/hello-web target/hello-web.jar --main com.example.FunctionApp
WEB_URL=`wsk -i action get redhat-developers-demo/hello-web --url | awk 'FNR==2{print $1}'` # #(1)
AUTH=`oc get secret whisk.auth -o yaml | grep "system:" | awk '{print $2}'` # #(2)
-
Get the HTTP URL for invoking the action
-
Some resources requires authentication, for those requests its required to add
Authorization
header with value as$AUTH
curl -k $WEB_URL.json
You can also access the url via browser using $WEB_URL.json, where you can get the $WEB_URL using command wsk -i action get /whisk.system/redhat-developers-demo/hello-web --url
.
📎
|
The following section shows some example requests and their expected responses Without any request data {
"response": {
"__ow_method": "get",
"__ow_headers": {
"x-forwarded-port": "443",
"accept": "*/*",
"forwarded": "for=192.168.64.1;host=openwhisk-faas.192.168.64.67.nip.io;proto=https",
"user-agent": "curl/7.54.0",
"x-forwarded-proto": "https",
"host": "controller.faas.svc.cluster.local:8080",
"x-forwarded-host": "openwhisk-faas.192.168.64.67.nip.io",
"x-forwarded-for": "192.168.64.1"
},
"__ow_path": ""
}
} With any JSON request data curl -k -X POST -H 'Content-Type: application/json' -d '{"name": "test"}' $WEB_URL.json {
"response": {
"__ow_method": "post",
"__ow_headers": {
"x-forwarded-port": "443",
"accept": "*/*",
"forwarded": "for=192.168.64.1;host=openwhisk-faas.192.168.64.67.nip.io;proto=https",
"user-agent": "curl/7.54.0",
"x-forwarded-proto": "https",
"host": "controller.faas.svc.cluster.local:8080",
"content-type": "application/json",
"x-forwarded-host": "openwhisk-faas.192.168.64.67.nip.io",
"x-forwarded-for": "192.168.64.1"
},
"__ow_path": "",
"name": "test"
}
} With request data and an invalid content type curl -k -X POST -H 'Content-Type: application/something' -d '{"name": "test"}' $WEB_URL.json Invoke via curl like above , with request data you will see the response like: {
"response": {
"__ow_method": "post",
"__ow_headers": {
"x-forwarded-port": "443",
"accept": "*/*",
"forwarded": "for=192.168.64.1;host=openwhisk-faas.192.168.64.67.nip.io;proto=https",
"user-agent": "curl/7.54.0",
"x-forwarded-proto": "https",
"host": "controller.faas.svc.cluster.local:8080",
"content-type": "application/something",
"x-forwarded-host": "openwhisk-faas.192.168.64.67.nip.io",
"x-forwarded-for": "192.168.64.1"
},
"__ow_path": "",
"__ow_body": "eyJuYW1lIjogInRlc3QifQ==" //(1)
}
}
|
Apache OpenWhisk allows chaining of actions which is called in sequence like how they are defined. We will now create a simple sequence of actions which will split, uppercase and sort a comma separated string.
All the three projects can be co-located in same directory for clarity and easy building:
cd ..
mkdir -p sequence-demo
cd sequence-demo
wsk -i package create redhat-developers-demo # # (1)
-
Create a new package to hold our actions, this gives a better clarity on which actions we add to our sequence. For more details refer Packages
This Action will be first in the sequence which will receive a comma separated string as a parameter and will return a array of string as response.
cd $PROJECT_HOME
mvn archetype:generate \
-DarchetypeGroupId=org.apache.openwhisk.java \
-DarchetypeArtifactId=java-action-archetype \
-DarchetypeVersion=1.0-SNAPSHOT \
-DgroupId=com.example \
-DartifactId=splitter
Update the FunctionApp class with code:
package com.example;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
/**
* Splitter FunctionApp
*/
public class FunctionApp {
public static JsonObject main(JsonObject args) {
JsonObject response = new JsonObject();
String text = null;
if (args.has("text")) {
text = args.getAsJsonPrimitive("text").getAsString();
}
String[] results = new String[] { text };
if (text != null && text.indexOf(",") != -1) {
results = text.split(",");
}
JsonArray splitStrings = new JsonArray();
for (String var : results) {
splitStrings.add(var);
}
response.add("result", splitStrings);
return response;
}
}
Update the FunctionAppTest class with code:
package com.example;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.junit.Test;
/**
* Splitter FunctionAppTest
*/
public class FunctionAppTest {
@Test
public void testFunction() {
JsonObject args = new JsonObject();
args.addProperty("text", "apple,orange,banana");
JsonObject response = FunctionApp.main(args);
assertNotNull(response);
JsonArray results = response.getAsJsonArray("result");
assertNotNull(results);
assertEquals(3, results.size());
ArrayList<String> actuals = new ArrayList<>();
results.forEach(j -> actuals.add(j.getAsString()));
assertTrue(actuals.contains("apple"));
assertTrue(actuals.contains("orange"));
assertTrue(actuals.contains("banana"));
}
}
cd splitter
mvn clean package
wsk -i action update redhat-developers-demo/splitter target/splitter.jar --main com.example.FunctionApp
This Action will take the array of Strings from previous step (Splitter Action) and convert the strings to upper case
cd ..
mvn archetype:generate \
-DarchetypeGroupId=org.apache.openwhisk.java \
-DarchetypeArtifactId=java-action-archetype \
-DarchetypeVersion=1.0-SNAPSHOT \
-DgroupId=com.example \
-DartifactId=uppercase
Update the FunctionApp class with code:
package com.example;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
/**
* UpperCase Function
*/
public class FunctionApp {
public static JsonObject main(JsonObject args) {
JsonObject response = new JsonObject();
JsonArray upperArray = new JsonArray();
if (args.has("result")) { // // (1)
args.getAsJsonArray("result").forEach(e -> upperArray.add(e.getAsString().toUpperCase()));
}
response.add("result", upperArray);
return response;
}
}
-
The function expects the previous action in sequence to send the parameter with JSON attribute called
result
Update the FunctionAppTest class with code:
package com.example;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.junit.Test;
/**
* Unit test for UpperCase Function.
*/
public class FunctionAppTest {
@Test
public void testFunction() {
JsonObject args = new JsonObject();
JsonArray splitStrings = new JsonArray();
splitStrings.add("apple");
splitStrings.add("orange");
splitStrings.add("banana");
args.add("result", splitStrings);
JsonObject response = FunctionApp.main(args);
assertNotNull(response);
JsonArray results = response.getAsJsonArray("result");
assertNotNull(results);
assertEquals(3, results.size());
ArrayList<String> actuals = new ArrayList<>();
results.forEach(j -> actuals.add(j.getAsString()));
assertTrue(actuals.contains("APPLE"));
assertTrue(actuals.contains("ORANGE"));
assertTrue(actuals.contains("BANANA"));
}
}
cd uppercase
mvn clean package
wsk -i action update redhat-developers-demo/uppercase target/uppercase.jar --main com.example.FunctionApp
This Action will take the array of Strings from previous step (Upppercase Action) and sort them
cd ..
mvn archetype:generate \
-DarchetypeGroupId=org.apache.openwhisk.java \
-DarchetypeArtifactId=java-action-archetype \
-DarchetypeVersion=1.0-SNAPSHOT \
-DgroupId=com.example \
-DartifactId=sorter
Update the FunctionApp class with code:
package com.example;
import java.util.ArrayList;
import java.util.Comparator;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
/**
* Sorter FunctionApp
*/
public class FunctionApp {
public static JsonObject main(JsonObject args) {
JsonObject response = new JsonObject();
ArrayList<String> upperStrings = new ArrayList<>();
if (args.has("result")) {
args.getAsJsonArray("result").forEach(e -> upperStrings.add(e.getAsString()));
}
JsonArray sortedArray = new JsonArray();
upperStrings.stream().sorted(Comparator.naturalOrder()).forEach(s -> sortedArray.add(s));
response.add("result", sortedArray);
return response;
}
}
Update the FunctionAppTest class with code:
package com.example;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.junit.Test;
/**
* Unit test for Sorted Function.
*/
public class FunctionAppTest {
@Test
public void testFunction() {
JsonObject args = new JsonObject();
JsonArray splitStrings = new JsonArray();
splitStrings.add("APPLE");
splitStrings.add("ORANGE");
splitStrings.add("BANANA");
args.add("result", splitStrings);
JsonObject response = FunctionApp.main(args);
assertNotNull(response);
JsonArray results = response.getAsJsonArray("result");
assertNotNull(results);
assertEquals(3, results.size());
ArrayList<String> actuals = new ArrayList<>();
results.forEach(j -> actuals.add(j.getAsString()));
assertTrue(actuals.get(0).equals("APPLE"));
assertTrue(actuals.get(1).equals("BANANA"));
assertTrue(actuals.get(2).equals("ORANGE"));
}
}
cd sorter
mvn clean package
wsk -i action update redhat-developers-demo/sorter target/sorter.jar --main com.example.FunctionApp
Having created all the three actions, lets now create OpenWhisk that calls all three function split,uppercase and sort in sequence.
cd ..
wsk -i action update splitUpperAndSort --sequence redhat-developers-demo/splitter,redhat-developers-demo/uppercase,redhat-developers-demo/sorter
If you are on a low bandwidth sometimes the default catalog will not be populated, run the following commands to have them installed
#!/bin/bash
oc delete job install-catalog (1)
cat <<EOF | oc apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: install-catalog
spec:
activeDeadlineSeconds: 600
template:
metadata:
name: install-catalog
spec:
containers:
- name: catalog
image: projectodd/whisk_catalog:openshift-latest
env:
- name: "WHISK_CLI_VERSION"
valueFrom:
configMapKeyRef:
name: whisk.config
key: whisk_cli_version_tag
- name: "WHISK_AUTH"
valueFrom:
secretKeyRef:
name: whisk.auth
key: system
- name: "WHISK_API_HOST_NAME"
value: "http://controller:8080"
initContainers:
- name: wait-for-controller
image: busybox
command: ['sh', '-c', 'until wget -T 5 --spider http://controller:8080/ping; do echo waiting for controller; sleep 2; done;']
restartPolicy: Never
EOF # # (2)
-
Delete the old job
-
Run the install-catalog job again
Now when you run wsk -i action list
you should see output like:
💡
|
|