In this section, you can find advanced reference documentations here.
Please follow the links below.
This is the multi-page printable view of this section. Click here to print.
In this section, you can find advanced reference documentations here.
Please follow the links below.
OpenServerless offers a powerful command line interface named ops
which
extends and embeds the OpenWhisk wsk
.
Download instructions are available here.
Let’s see some advanced uses of ops
.
OpenServerless access is usually configured logging into the platform with the ops -login
command.
You can also configure access directly using the ops -wsk
command.
There are two required properties to configure:
API host (name or IP address) for the OpenWhisk and OpenServerless deployment you want to use.
Authorization key (username and password) which grants you access to the OpenWhisk and OpenServerless API.
The API host is the installation host, the one you configure in
installation with ops config apihost
ops -wsk property set --apihost <openwhisk_baseurl>
If you know your authorization key, you can configure the CLI to use it.
Otherwise, you will need to obtain an authorization key for most CLI
operations. The API key is visible in the file ~/.wskprops
after you
perform a ops -login
. This file can be sourced to be read as
environment variables.
source ~/.wskprops
ops -wsk property set --auth $AUTH
Tip: The OpenWhisk and OpenServerless CLI stores properties in the
~/.wskprops
configuration file by default. The location of this file
can be altered by setting the WSK_CONFIG_FILE
environment variable.
The required properties described above have the following keys in the
.wskprops
file:
APIHOST - Required key for the API host value.
AUTH - Required key for the Authorization key.
To verify your CLI setup, try ops action list
.
The CLI can be setup to use an HTTPS proxy. To setup an HTTPS proxy, an
environment variable called HTTPS_PROXY
must be created. The variable
must be set to the address of the HTTPS proxy, and its port using the
following format: {PROXY IP}:{PROXY PORT}
.
The CLI has an extra level of security from client to apihost, system provides default client certificate configuration which deployment process generated, then you can refer to below steps to use client certificate:
ops -wsk property set --cert <client_cert_path> --key <client_key_path>
The following sections provide more details about the OpenWhisk and OpenServerless system.
OpenWhisk and OpenServerless actions, triggers, and rules belong in a namespace, and optionally a package.
Packages can contain actions and feeds. A package cannot contain another package, so package nesting is not allowed. Also, entities do not have to be contained in a package.
In OpenServerless a namespace corresponds to an user. You can create users with the admin subcommand of the CLI.
The fully qualified name of an entity is
/namespaceName[/packageName]/entityName
. Notice that /
is used to
delimit namespaces, packages, and entities.
If the fully qualified name has three parts:
/namespaceName/packageName/entityName
, then the namespace can be
entered without a prefixed /
; otherwise, namespaces must be prefixed
with a /
.
For convenience, the namespace can be left off if it is the user’s default namespace.
For example, consider a user whose default namespace is /myOrg
.
Following are examples of the fully qualified names of a number of
entities and their aliases.
Fully qualified name | Alias | Namespace | Package | Name |
---|---|---|---|---|
|
|
|
| |
|
|
|
|
|
|
|
|
|
You will be using this naming scheme when you use the OpenWhisk and OpenServerless CLI, among other places.
The names of all entities, including actions, triggers, rules, packages, and namespaces, are a sequence of characters that follow the following format:
The first character must be an alphanumeric character, or an underscore.
The subsequent characters can be alphanumeric, spaces, or any of the
following: _
, @
, .
, -
.
The last character can’t be a space.
More precisely, a name must match the following regular expression
(expressed with Java metacharacter syntax):
\A([\w]|[\w][\w@ .-]*[\w@.-]+)\z
.
OpenWhisk and OpenServerless has a few system limits, including how much memory an action can use and how many action invocations are allowed per minute.
Note: On Openwhisk 2.0 with the scheduler service, concurrent in the table below really means the max containers that can be provisioned at once for a namespace. The api may be able to accept more activations than this number at once depending on a number of factors.
The following table lists the default limits for actions.
limit | description | configurable | unit | default |
---|---|---|---|---|
timeout | a container is not allowed to run longer than N milliseconds | per action | milliseconds | 60000 |
memory | a container is not allowed to allocate more than N MB of memory | per action | MB | 256 |
logs | a container is not allowed to write more than N MB to stdout | per action | MB | 10 |
instances | an action is not allowed to have more containers than this value (new scheduler only) | per action | number | namespace concurrency limit |
concurrent | no more than N activations may be submitted per namespace either executing or queued for execution | per namespace | number | 100 |
minuteRate | no more than N activations may be submitted per namespace per minute | per namespace | number | 120 |
codeSize | the maximum size of the action code | configurable, limit per action | MB | 48 |
parameters | the maximum size of the parameters that can be attached | not configurable, limit per action/package/trigger | MB | 1 |
result | the maximum size of the action result | not configurable, limit per action | MB | 1 |
The timeout limit N is in the range [100ms..300000ms] and is set per action in milliseconds.
A user can change the limit when creating the action.
A container that runs longer than N milliseconds is terminated.
The memory limit M is in the range from [128MB..512MB] and is set per action in MB.
A user can change the limit when creating the action.
A container cannot have more memory allocated than the limit.
The max containers that will be created for an action before throttling in the range from [1..concurrentInvocations limit for namespace]
By default the max allowed containers / server instances for an action is equal to the namespace limit.
A user can change the limit when creating the action.
Defining a lower limit than the namespace limit means your max container concurrency will be the action defined limit.
If using actionConcurrency > 1 such that your action can handle multiple requests per instance, your true concurrency limit is actionContainerConcurrency * actionConcurrency.
The actions within a namespaces containerConcurrency total do not have to add up to the namespace limit though you can configure it that way to guarantee an action will get exactly the action container concurrency.
For example with a namespace limit of 30 with 2 actions each with a container limit of 20; if the first action is using 20, there will still be space for 10 for the other.
The log limit N is in the range [0MB..10MB] and is set per action.
A user can change the limit when creating or updating the action.
Logs that exceed the set limit are truncated and a warning is added as the last output of the activation to indicate that the activation exceeded the set log limit.
The maximum code size for the action is 48MB.
It is recommended for a JavaScript action to use a tool to concatenate all source code including dependencies into a single bundled file.
The number of activations that are either executing or queued for execution for a namespace cannot exceed 100.
A user is currently not able to change the limits.
The rate limit N is set to 120 and limits the number of action invocations in one minute windows.
A user cannot change this limit when creating the action.
A CLI or API call that exceeds this limit receives an error code
corresponding to HTTP status code 429: TOO MANY REQUESTS
.
The size limit for the parameters on creating or updating of an action/package/trigger is 1MB.
The limit cannot be changed by the user.
An entity with too big parameters will be rejected on trying to create or update it.
The maximum number of open files is 1024 (for both hard and soft limits).
The docker run command use the argument --ulimit nofile=1024:1024
.
For more information about the ulimit for open files see the docker run documentation.
The maximum number of processes available to the action container is 1024.
The docker run command use the argument --pids-limit 1024
.
For more information about the ulimit for maximum number of processes see the docker run documentation.
Triggers are subject to a firing rate per minute as documented in the table below.
limit | description | configurable | unit | default |
---|---|---|---|---|
minuteRate | no more than N triggers may be fired per namespace per minute | per user | number | 60 |
The rate limit N is set to 60 and limits the number of triggers that may be fired in one minute windows.
A user cannot change this limit when creating the trigger.
A CLI or API call that exceeds this limit receives an error code
corresponding to HTTP status code 429: TOO MANY REQUESTS
.
After your OpenWhisk and OpenServerlesss environment is enabled, you can use it with your web apps or mobile apps with REST API calls.
For more details about the APIs for actions, activations, packages, rules, and triggers, see the OpenWhisk and OpenServerless API documentation.
All the capabilities in the system are available through a REST API. There are collection and entity endpoints for actions, triggers, rules, packages, activations, and namespaces.
These are the collection endpoints:
https://$APIHOST/api/v1/namespaces
https://$APIHOST/api/v1/namespaces/{namespace}/actions
https://$APIHOST/api/v1/namespaces/{namespace}/triggers
https://$APIHOST/api/v1/namespaces/{namespace}/rules
https://$APIHOST/api/v1/namespaces/{namespace}/packages
https://$APIHOST/api/v1/namespaces/{namespace}/activations
https://$APIHOST/api/v1/namespaces/{namespace}/limits
The $APIHOST
is the OpenWhisk and OpenServerless API hostname (for example,
localhost, 172.17.0.1, and so on). For the {namespace}
, the character
_
can be used to specify the user’s default namespace.
You can perform a GET request on the collection endpoints to fetch a list of entities in the collection.
There are entity endpoints for each type of entity:
https://$APIHOST/api/v1/namespaces/{namespace}
https://$APIHOST/api/v1/namespaces/{namespace}/actions/[{packageName}/]{actionName}
https://$APIHOST/api/v1/namespaces/{namespace}/triggers/{triggerName}
https://$APIHOST/api/v1/namespaces/{namespace}/rules/{ruleName}
https://$APIHOST/api/v1/namespaces/{namespace}/packages/{packageName}
https://$APIHOST/api/v1/namespaces/{namespace}/activations/{activationName}
The namespace and activation endpoints support only GET requests. The actions, triggers, rules, and packages endpoints support GET, PUT, and DELETE requests. The endpoints of actions, triggers, and rules also support POST requests, which are used to invoke actions and triggers and enable or disable rules.
All APIs are protected with HTTP Basic authentication. You can use the
ops admin tool to generate a new namespace and
authentication. The Basic authentication credentials are in the AUTH
property in your ~/.nuvprops
file, delimited by a colon. You can also
retrieve these credentials using the CLI running
ops property get --auth
.
The following is an example that uses the cURL
command tool to get the list of all packages in the whisk.system
namespace:
curl -u USERNAME:PASSWORD https://$APIHOST/api/v1/namespaces/whisk.system/packages
[
{
"name": "slack",
"binding": false,
"publish": true,
"annotations": [
{
"key": "description",
"value": "Package that contains actions to interact with the Slack messaging service"
}
],
"version": "0.0.1",
"namespace": "whisk.system"
}
]
In this example the authentication was passed using the -u
flag, you
can pass this value also as part of the URL as
https://USERNAME:PASSWORD@$APIHOST
.
The OpenWhisk API supports request-response calls from web clients.
OpenWhisk responds to OPTIONS
requests with Cross-Origin Resource
Sharing headers. Currently, all origins are allowed (that is,
Access-Control-Allow-Origin is “*
”), the standard set of methods are
allowed (that is, Access-Control-Allow-Methods is
GET, DELETE, POST, PUT, HEAD
), and Access-Control-Allow-Headers yields
Authorization, Origin, X-Requested-With, Content-Type, Accept, User-Agent
.
Attention: Because OpenWhisk and OpenServerless currently supports only one key per namespace, it is not recommended to use CORS beyond simple experiments. Use Web Actions to expose your actions to the public and not use the OpenWhisk and OpenServerless authorization key for client applications that require CORS.
The OpenWhisk and OpenServerless CLI is an interface to the OpenWhisk and
OpenServerless REST API. You can run the CLI in verbose mode with the flag
-v
, this will print truncated information about the HTTP request and
response. To print all information use the flag -d
for debug.
Note: HTTP request and response bodies will only be truncated if they exceed 1000 bytes.
Let’s try getting the namespace value for the current user.
ops namespace list -v
REQUEST:
[GET] https://$APIHOST/api/v1/namespaces
Req Headers
{
"Authorization": [
"Basic XXXYYYY"
],
"User-Agent": [
"OpenWhisk and OpenServerless-CLI/1.0 (2017-08-10T20:09:30+00:00)"
]
}
RESPONSE:Got response with code 200
Resp Headers
{
"Content-Type": [
"application/json; charset=UTF-8"
]
}
Response body size is 28 bytes
Response body received:
["john@example.com_dev"]
As you can see you the printed information provides the properties of
the HTTP request, it performs a HTTP method GET
on the URL
https://$APIHOST/api/v1/namespaces
using a User-Agent header
OpenWhisk and OpenServerless-CLI/1.0 (<CLI-Build-version>)
and Basic
Authorization header Basic XXXYYYY
. Notice that the authorization
value is your base64-encoded OpenWhisk and OpenServerless authorization
string. The response is of content type application/json
.
Note: In the examples that follow, $AUTH
and $APIHOST
represent
environment variables set respectively to your OpenWhisk and OpenServerless
authorization key and API host.
To create or update an action send a HTTP request with method PUT
on
the the actions collection. For example, to create a nodejs:6
action
with the name hello
using a single file content use the following:
curl -u $AUTH -d '{"namespace":"_","name":"hello","exec":{"kind":"nodejs:6","code":"function main(params) { return {payload:\"Hello \"+params.name}}"}}' -X PUT -H "Content-Type: application/json" https://$APIHOST/api/v1/namespaces/_/actions/hello?overwrite=true
To perform a blocking invocation on an action, send a HTTP request with
a method POST
and body containing the input parameter name
use the
following:
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/actions/hello?blocking=true \
-X POST -H "Content-Type: application/json" \
-d '{"name":"John"}'
You get the following response:
{
"duration": 2,
"name": "hello",
"subject": "john@example.com_dev",
"activationId": "c7bb1339cb4f40e3a6ccead6c99f804e",
"publish": false,
"annotations": [{
"key": "limits",
"value": {
"timeout": 60000,
"memory": 256,
"logs": 10
}
}, {
"key": "path",
"value": "john@example.com_dev/hello"
}],
"version": "0.0.1",
"response": {
"result": {
"payload": "Hello John"
},
"success": true,
"status": "success"
},
"end": 1493327653769,
"logs": [],
"start": 1493327653767,
"namespace": "john@example.com_dev"
}
If you just want to get the response.result
, run the command again
with the query parameter result=true
curl -u $AUTH "https://$APIHOST/api/v1/namespaces/_/actions/hello?blocking=true&result=true" \
-X POST -H "Content-Type: application/json" \
-d '{"name":"John"}'
You get the following response:
{
"payload": "hello John"
}
To create an action as a web action, you need to add an
annotation of web-export=true
for web actions.
Since web-actions are publicly accessible, you should protect
pre-defined parameters (i.e., treat them as final) using the annotation
final=true
. If you create or update an action using the CLI flag
--web true
this command will add both annotations web-export=true
and final=true
.
Run the curl command providing the complete list of annotations to set on the action
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/actions/hello?overwrite=true \
-X PUT -H "Content-Type: application/json" \
-d '{"namespace":"_","name":"hello","exec":{"kind":"nodejs:6","code":"function main(params) { return {payload:\"Hello \"+params.name}}"},"annotations":[{"key":"web-export","value":true},{"key":"raw-http","value":false},{"key":"final","value":true}]}'
You can now invoke this action as a public URL with no OpenWhisk and
OpenServerless authorization. Try invoking using the web action public URL
including an optional extension such as .json
or .http
for example
at the end of the URL.
curl https://$APIHOST/api/v1/web/john@example.com_dev/default/hello.json?name=John
{
"payload": "Hello John"
}
Note that this example source code will not work with .http
, see web
actions documentation on how to modify.
To create an action sequence, you need to create it by providing the names of the actions that compose the sequence in the desired order, so the output from the first action is passed as input to the next action.
$ ops action create sequenceAction –sequence /whisk-system/utils/split,/whisk-system/utils/sort
Create a sequence with the actions /whisk-system/utils/split
and
/whisk-system/utils/sort
.
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/actions/sequenceAction?overwrite=true \
-X PUT -H "Content-Type: application/json" \
-d '{"namespace":"_","name":"sequenceAction","exec":{"kind":"sequence","components":["/whisk.system/utils/split","/whisk.system/utils/sort"]},"annotations":[{"key":"web-export","value":true},{"key":"raw-http","value":false},{"key":"final","value":true}]}'
Take into account when specifying the names of the actions, they have to be full qualified.
To create a trigger, the minimum information you need is a name for the trigger. You could also include default parameters that get passed to the action through a rule when the trigger gets fired.
Create a trigger with name events
with a default parameter type
with
value webhook
set.
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/triggers/events?overwrite=true \
-X PUT -H "Content-Type: application/json" \
-d '{"name":"events","parameters":[{"key":"type","value":"webhook"}]}'
Now whenever you have an event that needs to fire this trigger it just
takes an HTTP request with a method POST
using the OpenWhisk and
OpenServerless Authorization key.
To fire the trigger events
with a parameter temperature
, send the
following HTTP request.
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/triggers/events \
-X POST -H "Content-Type: application/json" \
-d '{"temperature":60}'
To create a rule that associates a trigger with an action, send a HTTP
request with a PUT
method providing the trigger and action in the body
of the request.
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/rules/t2a?overwrite=true \
-X PUT -H "Content-Type: application/json" \
-d '{"name":"t2a","status":"","trigger":"/_/events","action":"/_/hello"}'
Rules can be enabled or disabled, and you can change the status of the
rule by updating its status property. For example, to disable the rule
t2a
send in the body of the request status: "inactive"
with a POST
method.
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/rules/t2a?overwrite=true \
-X POST -H "Content-Type: application/json" \
-d '{"status":"inactive","trigger":null,"action":null}'
To create an action in a package you have to create a package first, to
create a package with name iot
send an HTTP request with a PUT
method
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/packages/iot?overwrite=true \
-X PUT -H "Content-Type: application/json" \
-d '{"namespace":"_","name":"iot"}'
To force delete a package that contains entities, set the force parameter to true. Failure will return an error either for failure to delete an action within the package or the package itself. The package will not be attempted to be deleted until all actions are successfully deleted.
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/packages/iot?force=true \
-X DELETE
To get the list of the last 3 activations use a HTTP request with a
GET
method, passing the query parameter limit=3
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/activations?limit=3
To get all the details of an activation including results and logs, send
a HTTP request with a GET
method passing the activation identifier as
a path parameter
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/activations/f81dfddd7156401a8a6497f2724fec7b
To get the limits set for a namespace (i.e. invocationsPerMinute, concurrentInvocations, firesPerMinute, actionMemoryMax, actionLogsMax…)
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/limits
Note that the default system values are returned if no specific limits are set for the user corresponding to the authenticated identity.
OpenServerless Operator offers the possibility to deploy a simple “scheduler” to invoke repetitive or one-shot OpenWhisk actions. For example, an action executing a SQL script to create a PostgreSQL Database or inserting reference data, or simply an action that sends notifications with an API call every day at the same time.
Using the ops
CLI, you can enable the scheduler with the following command:
ops config enable --cron
# if OpenServerless is not yet deployed
ops setup devcluster
# alternatively if OpenServerless is already deployed
ops update apply
By default, the internal scheduler executes a job every minute that starts searching for OpenWhisk actions with special annotations.
Let’s assume we want to deploy an OpenWhisk action to be executed every 30 minutes. Suppose it’s an action that simply prints something, like this:
def main(args):
print('Hello from a repeated action')
return {
'body': 'action invoked'
}
abd save it to a file called scheduled-action.py
To deploy the action and instruct OpenServerless to execute it every 30 minutes, issue the following command:
ops action create scheduled-action scheduled-action.py -a cron "*/30 * * * *"
So you can create the action in the usual way and at the end add -a cron yourCronExpression.
Now suppose we want to execute the same action scheduled-action.py
only once.
To deploy an action and request a single execution automatically via the OpenServerless Scheduler, issue the following command:
ops action create scheduled-action scheduled-action.py -a autoexec true
If we now print activation logs with ops activation poll
, we will see our action execution log:
Activation: 'scheduled' (ebd532139a464e9d9532139a46ae9d8a)
[
"2024-03-08T07:28:02.050739962Z stdout: Hello from a scheduled action"
]
The Scheduler executes the action according to the following rules:
Actions are called in a non-blocking fashion. To verify execution and logs, use the command ops activation list
.
Actions are invoked without any parameters. It is advised to deploy actions with self-contained parameters.
OpenWhisk and OpenServerless supports several languages and runtimes but there may be other languages or runtimes that are important for your organization, and for which you want tighter integration with the platform.
The platform is extensible and you can add new languages or runtimes (with custom packages and third-party dependencies)
💡 NOTE
This guide describes the contract a runtime must satisfy. However all the OpenServerless runtimes are implemented the using the ActionLoop Proxy. This proxy is implemented in Go, already satifies the semantic of a runtime ands makes very easy to build a new runtime. You just need to provide “launcher code” in your favorite programming language and a compilation script (generally written in python) for the initialization of an action. You are advised to use it for your own runtimes and use the material of this document as reference for the behaviour of a runtime.
The unit of execution for all functions is a Docker container which must implement a specific Action interface that, in general performs:
Initialization - accepts an initialization payload (the code) and prepared for execution,
Activation - accepts a runtime payload (the input parameters) and
prepares the activation context,
runs the function,
returns the function result,
Logging - flushes all stdout
and stderr
logs
and adds a frame marker at the end of the activation.
The specifics of the Action interface and its functions are shown below.
Actions when created specify the desired runtime for the function via a
property called kind
. When using the nuv
CLI, this is specified as
--kind <runtime-kind>
. The value is typically a string describing the
language (e.g., nodejs
) followed by a colon and the version for the
runtime as in nodejs:20
or php:8.1
.
The manifest is a map of runtime family names to an array of specific
kinds. As an example, the following entry add a new runtime family
called nodejs
with a single kind nodejs:20
.
{
"nodejs": [{
"kind": "nodejs:20",
"default": true,
"image": {
"prefix": "openwhisk",
"name": "action-nodejs-v20",
"tag": "latest"
}
}]
}
The default
property indicates if the corresponding kind should be
treated as the default for the runtime family. The JSON image
structure defines the Docker image name that is used for actions of this
kind (e.g., openwhisk/nodejs10action:latest
for the JSON example
above).
The standard test action is shown below in JavaScript. It should be
adapted for the new language and added to the test artifacts
directory with the name
<runtime-kind>.txt
for plain text file or <runtime-kind>.bin
for a a
binary file. The <runtime-kind>
must match the value used for kind
in the corresponding runtime manifest entry, replacing :
in the kind
with a -
. For example, a plain text function for nodejs:20
becomes
nodejs-20.txt
.
function main(args) {
var str = args.delimiter + " ☃ " + args.delimiter;
console.log(str);
return { "winter": str };
}
An action consists of the user function (and its dependencies) along with a proxy that implements a canonical protocol to integrate with the OpenWhisk and OpenServerless platform.
The proxy is a web server with two endpoints.
It listens on port 8080
.
It implements /init
to initialize the container.
It also implements /run
to activate the function.
The proxy also prepares the execution context, and flushes the logs produced by the function to stdout and stderr.
The initialization route is /init
. It must accept a POST
request
with a JSON object as follows:
{
"value": {
"name" : String,
"main" : String,
"code" : String,
"binary": Boolean,
"env": Map[String, String]
}
}
name
is the name of the action.
main
is the name of the function to execute.
code
is either plain text or a base64 encoded string for binary
functions (i.e., a compiled executable).
binary
is false if code
is in plain text, and true if code
is
base64 encoded.
env
is a map of key-value pairs of properties to export to the
environment. And contains several properties starting with the
__OW_
prefix that are specific to the running action.
__OW_API_KEY
the API key for the subject invoking the action,
this key may be a restricted API key. This property is absent
unless explicitly
requested.
__OW_NAMESPACE
the namespace for the activation (this may
not be the same as the namespace for the action).
__OW_ACTION_NAME
the fully qualified name of the running
action.
__OW_ACTION_VERSION
the internal version number of the running
action.
__OW_ACTIVATION_ID
the activation id for this running action
instance.
__OW_DEADLINE
the approximate time when this initializer will
have consumed its entire duration quota (measured in epoch
milliseconds).
The initialization route is called exactly once by the OpenWhisk and
OpenServerless platform, before executing a function. The route should report
an error if called more than once. It is possible however that a single
initialization will be followed by many activations (via /run
). If an
env
property is provided, the corresponding environment variables
should be defined before the action code is initialized.
Successful initialization: The route should respond with 200 OK
if
the initialization is successful and the function is ready to execute.
Any content provided in the response is ignored.
Failures to initialize: Any response other than 200 OK
is treated
as an error to initialize. The response from the handler if provided
must be a JSON object with a single field called error
describing the
failure. The value of the error field may be any valid JSON value. The
proxy should make sure to generate meaningful log message on failure to
aid the end user in understanding the failure.
Time limit: Every action in OpenWhisk and OpenServerless has a defined time limit (e.g., 60 seconds). The initialization must complete within the allowed duration. Failure to complete initialization within the allowed time frame will destroy the container.
Limitation: The proxy does not currently receive any of the activation context at initialization time. There are scenarios where the context is convenient if present during initialization. This will require a change in the OpenWhisk and OpenServerless platform itself. Note that even if the context is available during initialization, it must be reset with every new activation since the information will change with every execution.
The proxy is ready to execute a function once it has successfully
completed initialization. The OpenWhisk and OpenServerless platform will
invoke the function by posting an HTTP request to /run
with a JSON
object providing a new activation context and the input parameters for
the function. There may be many activations of the same function against
the same proxy (viz. container). Currently, the activations are
guaranteed not to overlap — that is, at any given time, there is at most
one request to /run
from the OpenWhisk and OpenServerless platform.
The route must accept a JSON object and respond with a JSON object, otherwise the OpenWhisk and OpenServerless platform will treat the activation as a failure and proceed to destroy the container. The JSON object provided by the platform follows the following schema:
{
"value": JSON,
"namespace": String,
"action_name": String,
"api_host": String,
"api_key": String,
"activation_id": String,
"transaction_id": String,
"deadline": Number
}
value
is a JSON object and contains all the parameters for the
function activation.
namespace
is the OpenWhisk and OpenServerless namespace for the action
(e.g., whisk-system
).
action_name
is the fully qualified
name of the action.
activation_id
is a unique ID for this activation.
transaction_id
is a unique ID for the request of which this
activation is part of.
deadline
is the deadline for the function.
api_key
is the API key used to invoke the action.
The value
is the function parameters. The rest of the properties
become part of the activation context which is a set of environment
variables constructed by capitalizing each of the property names, and
prefixing the result with __OW_
. Additionally, the context must define
__OW_API_HOST
whose value is the OpenWhisk and OpenServerless API host.
This value is currently provided as an environment variable defined at
container startup time and hence already available in the context.
Successful activation: The route must respond with 200 OK
if the
activation is successful and the function has produced a JSON object as
its result. The response body is recorded as the result of the
activation.
Failed activation: Any response other than 200 OK
is treated as an
activation error. The response from the handler must be a JSON object
with a single field called error
describing the failure. The value of
the error field may be any valid JSON value. Should the proxy fail to
respond with a JSON object, the OpenWhisk and OpenServerless platform will
treat the failure as an uncaught exception. These two failures modes are
distinguished by the value of the response.status
in the activation
record which is application error
if the proxy returned an error
object, and action developer error
otherwise.
Time limit: Every action in OpenWhisk and OpenServerless has a defined time limit (e.g., 60 seconds). The activation must complete within the allowed duration. Failure to complete activation within the allowed time frame will destroy the container.
The proxy must flush all the logs produced during initialization and
execution and add a frame marker to denote the end of the log stream for
an activation. This is done by emitting the token
XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
as the last log line for the
stdout
and stderr
streams. Failure to emit this marker will cause
delayed or truncated activation logs.
The Action interface is enforced via a canonical test suite which validates the initialization protocol, the runtime protocol, ensures the activation context is correctly prepared, and that the logs are properly framed. Your runtime should extend this test suite, and of course include additional tests as needed.
The tests verify that the proxy can handle the following scenarios:
Test the proxy can handle the identity functions (initialize and run).
Test the proxy can handle pre-defined environment variables as well as initialization parameters.
Test the proxy properly constructs the activation context.
Test the proxy can properly handle functions with Unicode characters.
Test the proxy can handle large payloads (more than 1MB).
Test the proxy can handle an entry point other than main
.
Test the proxy does not permit re-initialization.
Test the error handling for an action returning an invalid response.
Test the proxy when initialized with no content.
The canonical test suite should be extended by the new runtime tests. Additional tests will be required depending on the feature set provided by the runtime.
Since the OpenWhisk and OpenServerless platform is language and runtime
agnostic, it is generally not necessary to add integration tests. That
is the unit tests verifying the protocol are sufficient. However, it may
be necessary in some cases to modify the ops
CLI or other OpenWhisk
and OpenServerless clients. In which case, appropriate tests should be added
as necessary. The OpenWhisk and OpenServerless platform will perform a
generic integration test as part of its basic system tests. This
integration test will require a test function to be
available so that the test harness can create, invoke, and delete the
action.
The OpenWhisk and OpenServerless runtime specification defines the expected behavior of an OpenWhisk and OpenServerless runtime; you can choose to implement a new runtime from scratch by just following this specification. However, the fastest way to develop a new, compliant runtime is by reusing the ActionLoop proxy which already implements most of the specification and requires you to write code for just a few hooks to get a fully functional (and fast) runtime in a few hours or less.
The ActionLoop proxy
is a runtime “engine”, written in the Go
programming language, originally developed
specifically to support the OpenWhisk and OpenServerless Go language
runtime. However, it
was written in a generic way such that it has since been adopted to
implement OpenWhisk and OpenServerless runtimes for Swift, PHP, Python, Rust,
Java, Ruby and Crystal. Even though it was developed with compiled
languages in mind it works equally well with scripting languages.
Using it, you can develop a new runtime in a fraction of the time needed for authoring a full-fledged runtime from scratch. This is due to the fact that you have only to write a command line protocol and not a fully-featured web server (with a small amount of corner cases to consider). The results should also produce a runtime that is fairly fast and responsive. In fact, the ActionLoop proxy has also been adopted to improve the performance of existing runtimes like Python, Ruby, PHP, and Java where performance has improved by a factor between 2x to 20x.
In addition to being the basis for new runtime development, ActionLoop runtimes can also support offline “precompilation” of OpenWhisk and OpenServerless Action source files into a ZIP file that contains only the compiled binaries which are very fast to start once deployed. More information on this approach can be found here: Precompiling Go Sources Offline which describes how to do this for the Go language, but the approach applies to any language supported by ActionLoop.
This section contains a stepwise tutorial which will take you through the process of developing a new ActionLoop runtime using the Ruby language as the example.
The general procedure for authoring a runtime with the
ActionLoop proxy
requires the following steps:
building a docker image containing your target language compiler and the ActionLoop runtime.
writing a simple line-oriented protocol in your target language.
writing a compilation script for your target language.
writing some mandatory tests for your language.
To facilitate the process, there is an actionloop-starter-kit
in the
openwhisk-devtools
GitHub repository, that implements a fully working runtime for Python.
It contains a stripped-down version of the real Python runtime (with
some advanced features removed) along with guided, step-by-step
instructions on how to translate it to a different target runtime
language using Ruby as an example.
In short, the starter kit provides templates you can adapt in creating an ActionLoop runtime for each of the steps listed above, these include :
-checking out the actionloop-starter-kit
from the openwhisk-devtools
repository -editing the Dockerfile
to create the target environment
for your target language. -converting (rewrite) the launcher.py
script
to an equivalent for script for your target language. -editing the
compile
script to compile your action in your target language.
-writing the mandatory tests for your target language, by adapting the
ActionLoopPythonBasicTests.scala
file.
As a starting language, we chose Python since it is one of the more
human-readable languages (can be treated as pseudo-code
). Do not
worry, you should only need just enough Python knowledge to be able to
rewrite launcher.py
and edit the compile
script for your target
language.
Finally, you will need to update the ActionLoopPythonBasicTests.scala
test file which, although written in the Scala language, only serves as
a wrapper that you will use to embed your target language tests into.
In each step of this tutorial, we typically show snippets of either terminal transcripts (i.e., commands and results) or “diffs” of changes to existing code files.
Within terminal transcript snippets, comments are prefixed with #
character and commands are prefixed by the $
character. Lines that
follow commands may include sample output (from their execution) which
can be used to verify against results in your local environment.
When snippets show changes to existing source files, lines without a
prefix should be left “as is”, lines with -
should be removed and
lines with +
should be added.
# Verify docker version
$ docker --version
Docker version 18.09.3
# Verify docker is running
$ docker ps
# The result should be a valid response listing running processes
So let’s start to create our own actionloop-demo-ruby-2.6
runtime.
First, check out the devtools
repository to access the starter kit,
then move it in your home directory to work on it.
git clone https://github.com/apache/openwhisk-devtools
mv openwhisk-devtools/actionloop-starter-kit ~/actionloop-demo-ruby-v2.6
Now, take the directory python3.7
and rename it to ruby2.6
and use
sed
to fix the directory name references in the Gradle build files.
cd ~/actionloop-demo-ruby-v2.6
mv python3.7 ruby2.6
sed -i.bak -e 's/python3.7/ruby2.6/' settings.gradle
sed -i.bak -e 's/actionloop-demo-python-v3.7/actionloop-demo-ruby-v2.6/' ruby2.6/build.gradle
Let’s check everything is fine building the image.
# building the image
$ ./gradlew distDocker
# ... intermediate output omitted ...
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
# checking the image is available
$ docker images actionloop-demo-ruby-v2.6
REPOSITORY TAG IMAGE ID CREATED SIZE
actionloop-demo-ruby-v2.6 latest df3e77c9cd8f 2 minutes ago 94.3MB
At this point, we have built a new image named
actionloop-demo-ruby-v2.6
. However, despite having Ruby
in the name,
internally it still is a Python
language runtime which we will need to
change to one supporting Ruby
as we continue in this tutorial.
Our language runtime’s Dockerfile
has the task of preparing an
environment for executing OpenWhisk and OpenServerless Actions. Using the
ActionLoop approach, we use a multistage Docker build to
derive our OpenWhisk and OpenServerless language runtime from an existing Docker image that has all the target language’s tools and libraries for running functions authored in that language.
ruby:2.6.2-alpine3.9
image
from the Official Docker Images for
Ruby on Docker Hub.leverage the existing openwhisk/actionlooop-v2
image on Docker Hub
from which we will “extract” the ActionLoop proxy (i.e. copy
/bin/proxy
binary) our runtime will use to process Activation
requests from the OpenWhisk and OpenServerless platform and execute
Actions by using the language’s tools and libraries from step #1.
Let’s edit the ruby2.6/Dockerfile
to use the official Ruby image on
Docker Hub as our base image, instead of a Python image, and add our our
Ruby launcher script:
FROM openwhisk/actionloop-v2:latest as builder
-FROM python:3.7-alpine
+FROM ruby:2.6.2-alpine3.9
RUN mkdir -p /proxy/bin /proxy/lib /proxy/action
WORKDIR /proxy
COPY --from=builder /bin/proxy /bin/proxy
-ADD lib/launcher.py /proxy/lib/launcher.py
+ADD lib/launcher.rb /proxy/lib/launcher.rb
ADD bin/compile /proxy/bin/compile
+RUN apk update && apk add python3
ENV OW_COMPILER=/proxy/bin/compile
ENTRYPOINT ["/bin/proxy"]
Next, let’s rename the launcher.py
(a Python script) to one that
indicates it is a Ruby script named launcher.rb
.
mv ruby2.6/lib/launcher.py ruby2.6/lib/launcher.rb
Note that:
You changed the base Docker image to use a Ruby
language image.
You changed the launcher script from Python
to Ruby
.
We had to add a python3
package to our Ruby image since our
compile
script will be written in Python for this tutorial. Of
course, you may choose to rewrite the compile
script in Ruby
if
you wish to as your own exercise.
This section will take you through how to convert the contents of
launcher.rb
(formerly launcher.py
) to the target Ruby programming
language and implement the ActionLoop protocol
.
Let’s recap the steps the launcher must accomplish to implement the
ActionLoop protocol
:
import the Action function’s main
method for execution.
compile
script will make the function available to
the launcher.open the system’s file descriptor 3
which will be used to output
the functions response.
read the system’s standard input, stdin
, line-by-line. Each line
is parsed as a JSON string and produces a JSON object (not an array
nor a scalar) to be passed as the input arg
to the function.
value
key contains the user
parameter data to be passed to your functions. All the other
keys are made available as process environment variables to the
function; these need to be uppercased and prefixed with
"__OW_"
.invoke the main
function with the JSON object payload.
encode the result of the function in JSON (ensuring it is only one
line and it is terminated with one newline) and write it to
file descriptor 3
.
Once the function returns the result, flush the contents of
stdout
, stderr
and file descriptor 3
(FD 3).
Finally, include the above steps in a loop so that it continually looks for Activations. That’s it.
Now, let’s look at the protocol described above, codified within the
launcher script launcher.rb
, and work to convert its contents from
Python to Ruby.
Skipping the first few library import statements within launcer.rb
,
which we will have to resolve later after we determine which ones Ruby
may need, we see the first significant line of code importing the actual
Action function.
# now import the action as process input/output
from main__ import main as main
In Ruby, this can be rewritten as:
# requiring user's action code
require "./main__"
Note that you are free to decide the path and filename for the
function’s source code. In our examples, we chose a base filename that
includes the word "main"
(since it is OpenWhisk and OpenServerless
default function name) and append two underscores to better assure
uniqueness.
The ActionLoop
proxy expects to read the results of invoking the
Action function from File Descriptor (FD) 3.
The existing Python:
out = fdopen(3, "wb")
would be rewritten in Ruby as:
out = IO.new(3)
Each time the function is invoked via an HTTP request, the ActionLoop
proxy passes the message contents to the launcher via STDIN. The
launcher must read STDIN line-by-line and parse it as JSON.
The launcher
’s existing Python code reads STDIN line-by-line as
follows:
while True:
line = stdin.readline()
if not line: break
# ...continue...
would be translated to Ruby as follows:
while true
# JSON arguments get passed via STDIN
line = STDIN.gets()
break unless line
# ...continue...
end
Each line is parsed in JSON, where the payload
is extracted from
contents of the "value"
key. Other keys and their values are as
uppercased, "__OW_"
prefixed environment variables:
The existing Python code for this is:
# ... continuing ...
args = json.loads(line)
payload = {}
for key in args:
if key == "value":
payload = args["value"]
else:
os.environ["__OW_%s" % key.upper()]= args[key]
# ... continue ...
would be translated to Ruby:
# ... continuing ...
args = JSON.parse(line)
payload = {}
args.each do |key, value|
if key == "value"
payload = value
else
# set environment variables for other keys
ENV["__OW_#{key.upcase}"] = value
end
end
# ... continue ...
We are now at the point of invoking the Action function and producing
its result. Note we must also capture exceptions and produce an
{"error": <result> }
if anything goes wrong during execution.
The existing Python code for this is:
# ... continuing ...
res = {}
try:
res = main(payload)
except Exception as ex:
print(traceback.format_exc(), file=stderr)
res = {"error": str(ex)}
# ... continue ...
would be translated to Ruby:
# ... continuing ...
res = {}
begin
res = main(payload)
rescue Exception => e
puts "exception: #{e}"
res ["error"] = "#{e}"
end
# ... continue ...
Finally, we need to write the function’s result to File Descriptor (FD) 3 and “flush” standard out (stdout), standard error (stderr) and FD 3.
The existing Python code for this is:
out.write(json.dumps(res, ensure_ascii=False).encode('utf-8'))
out.write(b'\n')
stdout.flush()
stderr.flush()
out.flush()
would be translated to Ruby:
STDOUT.flush()
STDERR.flush()
out.puts(res.to_json)
out.flush()
Congratulations! You just completed your ActionLoop
request handler.
Now, we need to write the compilation script
. It is basically a script
that will prepare the uploaded sources for execution, adding the
launcher
code and generate the final executable.
For interpreted languages, the compilation script will only “prepare” the sources for execution. The executable is simply a shell script to invoke the interpreter.
For compiled languages, like Go it will actually invoke a compiler in order to produce the final executable. There are also cases like Java where we still need to execute the compilation step that produces intermediate code, but the executable is just a shell script that will launch the Java runtime.
The OpenWhisk and OpenServerless user can upload actions with the ops
Command Line Interface (CLI) tool as a single file.
This single file can be:
a source file
an executable file
a ZIP file containing sources
a ZIP file containing an executable and other support files
Important: an executable for ActionLoop is either a Linux binary (an
ELF executable) or a script. A script is, using Linux conventions, is
anything starting with #!
. The first line is interpreted as the
command to use to launch the script: #!/bin/bash
, #!/usr/bin/python
etc.
The ActionLoop proxy accepts any file, prepares a work folder, with two
folders in it named "src"
and "bin"
. Then it detects the format of
the uploaded file. For each case, the behavior is different.
If the uploaded file is an executable, it is stored as bin/exec
and executed.
If the uploaded file is not an executable and not a zip file, it is
stored as src/exec
then the compilation script is invoked.
If the uploaded file is a zip file, it is unzipped in the src
folder, then the src/exec
file is checked.
If it exists and it is an executable, the folder src
is renamed to
bin
and then again the bin/exec
is executed.
If the src/exec
is missing or is not an executable, then the
compiler script is invoked.
The compilation script is invoked only when the upload contains sources.
According to the description in the past paragraph, if the upload is a
single file, we can expect the file is in src/exec
, without any
prefix. Otherwise, sources are spread the src
folder and it is the
task of the compiler script to find the sources. A runtime may impose
that when a zip file is uploaded, then there should be a fixed file with
the main function. For example, the Python runtime expects the file
__main__.py
. However, it is not a rule: the Go runtime does not
require any specific file as it compiles everything. It only requires a
function with the name specified.
The compiler script goal is ultimately to leave in bin/exec
an
executable (implementing the ActionLoop protocol) that the proxy can
launch. Also, if the executable is not standalone, other files must be
stored in this folder, since the proxy can also zip all of them and send
to the user when using the pre-compilation feature.
The compilation script is a script pointed by the OW_COMPILER
environment variable (you may have noticed it in the Dockerfile) that
will be invoked with 3 parameters:
<main>
is the name of the main function specified by the user on
the ops
command line
<src>
is the absolute directory with the sources already unzipped
an empty <bin>
directory where we are expected to place our final
executables
Note that both the <src>
and <bin>
are disposable, so we can do
things like removing the <bin>
folder and rename the <src>
.
Since the user generally only sends a function specified by the <main>
parameter, we have to add the launcher we wrote and adapt it to execute
the function.
compile
for RubyThis is the algorithm that the compile
script in the kit follows for
Python:
if there is a <src>/exec
it must rename to the main file; I use
the name main__.py
if there is a <src>/__main__.py
it will rename to the main file
main__.py
copy the launcher.py
to exec__.py
, replacing the main(arg)
with <main>(arg)
; this file imports the main__.py
and invokes
the function <main>
add a launcher script <src>/exec
finally it removes the <bin>
folder and rename <src>
to <bin>
We can adapt this algorithm easily to Ruby with just a few changes.
The script defines the functions sources
and build
then starts the
execution, at the end of the script.
Start from the end of the script, where the script collect parameters
from the command line. Instead of launcher.py
, use launcher.rb
:
- launcher = "%s/lib/launcher.py" % dirname(dirname(sys.argv[0]))
+ launcher = "%s/lib/launcher.rb" % dirname(dirname(sys.argv[0]))
Then the script invokes the source
function. This function renames the
exec
file to main__.py
, you will rename it instead to main__.rb
:
- copy_replace(src_file, "%s/main__.py" % src_dir)
+ copy_replace(src_file, "%s/main__.rb" % src_dir)
If instead there is a __main__.py
the function will rename to
main__.py
(the launcher invokes this file always). The Ruby runtime
will use a main.rb
as starting point. So the next change is:
- # move __main__ in the right place if it exists
- src_file = "%s/__main__.py" % src_dir
+ # move main.rb in the right place if it exists
+ src_file = "%s/main.rb" % src_dir
Now, the source
function copies the launcher as exec__.py
, replacing
the line from main__ import main as main
(invoking the main function)
with from main__ import <main> as main
. In Ruby you may want to
replace the line res = main(payload)
with res = <main>(payload)
. In
code it is:
- copy_replace(launcher, "%s/exec__.py" % src_dir,
- "from main__ import main as main",
- "from main__ import %s as main" % main )
+ copy_replace(launcher, "%s/exec__.rb" % src_dir,
+ "res = main(payload)",
+ "res = %s(payload)" % main )
We are almost done. We just need the startup script that instead of
invoking python will invoke Ruby. So in the build
function do this
change:
write_file("%s/exec" % tgt_dir, """#!/bin/sh
cd "$(dirname $0)"
-exec /usr/local/bin/python exec__.py
+exec ruby exec__.rb
""")
For an interpreted language that is all. We move the src
folder in the
bin
. For a compiled language instead, we may want to actually invoke
the compiler to produce the executable.
Now that we have completed both the launcher
and compile
scripts, it
is time to test them.
Here we will learn how to:
enter in a test environment
simple smoke tests to check things work
writing the validation tests
testing the image in an actual OpenWhisk and OpenServerless environment
In the starter kit, there is a Makefile
that can help with our
development efforts.
We can build the Dockerfile using the provided Makefile. Since it has a reference to the image we are building, let’s change it:
sed -i.bak -e 's/actionloop-demo-python-v3.7/actionloop-demo-ruby-v2.6/' ruby2.6/Makefile
We should be now able to build the image and enter in it with
make debug
. It will rebuild the image for us and put us into a shell
so we can enter access the image environment for testing and debugging:
$ cd ruby2.6
$ make debug
# results omitted for brevity ...
Let’s start with a couple of notes about this test environment.
First, use --entrypoint=/bin/sh
when starting the image to have a
shell available at our image entrypoint. Generally, this is true by
default; however, in some stripped down base images a shell may not be
available.
Second, the /proxy
folder is mounted in our local directory, so that
we can edit the bin/compile
and the lib/launcher.rb
using our editor
outside the Docker image
NOTE It is not necessary to rebuild the Docker image with every change
when using make debug
since directories and environment variables used
by the proxy indicate where the code outside the Docker container is
located.
Once at the shell prompt that we will use for development, we will have to start and stop the proxy. The shell will help us to inspect what happened inside the container.
It is time to test. Let’s write a very simple test first, converting the
example\hello.py
in example\hello.rb
to appear as follows:
def hello(args)
name = args["name"] || "stranger"
greeting = "Hello #{name}!"
puts greeting
{ "greeting" => greeting }
end
Now change into the ruby2.6
subdirectory of our runtime project and in
one terminal type:
$ cd <projectdir>/ruby2.6
$ make debug
# results omitted for brevity ...
# (you should see a shell prompt of your image)
$ /bin/proxy -debug
2019/04/08 07:47:36 OpenWhisk and OpenServerless ActionLoop Proxy 2: starting
Now the runtime is started in debug mode, listening on port 8080, and ready to accept Action deployments.
Open another terminal (while leaving the first one running the proxy)
and go into the top-level directory of our project to test the Action
by executing an init
and then a couple of run
requests using the
tools/invoke.py
test script.
These steps should look something like this in the second terminal:
$ cd <projectdir>
$ python tools/invoke.py init hello example/hello.rb
{"ok":true}
$ python tools/invoke.py run '{}'
{"greeting":"Hello stranger!"}
$ python tools/invoke.py run '{"name":"Mike"}'
{"greeting":"Hello Mike!"}
We should also see debug output from the first terminal running the
proxy (with the debug
flag) which should have successfully processed
the init
and run
requests above.
The proxy’s debug output should appear something like:
/proxy # /bin/proxy -debug
2019/04/08 07:54:57 OpenWhisk and OpenServerless ActionLoop Proxy 2: starting
2019/04/08 07:58:00 compiler: /proxy/bin/compile
2019/04/08 07:58:00 it is source code
2019/04/08 07:58:00 compiling: ./action/16/src/exec main: hello
2019/04/08 07:58:00 compiling: /proxy/bin/compile hello action/16/src action/16/bin
2019/04/08 07:58:00 compiler out: , <nil>
2019/04/08 07:58:00 env: [__OW_API_HOST=]
2019/04/08 07:58:00 starting ./action/16/bin/exec
2019/04/08 07:58:00 Start:
2019/04/08 07:58:00 pid: 13
2019/04/08 07:58:24 done reading 13 bytes
Hello stranger!
XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
2019/04/08 07:58:24 received::{"greeting":"Hello stranger!"}
2019/04/08 07:58:54 done reading 27 bytes
Hello Mike!
XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
2019/04/08 07:58:54 received::{"greeting":"Hello Mike!"}
Of course, it is very possible something went wrong. Here a few debugging suggestions:
The ActionLoop runtime (proxy) can only be initialized once using the
init
command from the invoke.py
script. If we need to re-initialize
the runtime, we need to stop the runtime (i.e., with Control-C) and
restart it.
We can also check what is in the action folder. The proxy creates a
numbered folder under action
and then a src
and bin
folder.
For example, using a terminal window, we would would see a directory and file structure created by a single action:
$ find
action/
action/1
action/1/bin
action/1/bin/exec__.rb
action/1/bin/exec
action/1/bin/main__.rb
Note that the exec
starter, exec__.rb
launcher and main__.rb
action code are have all been copied under a directory numbered`1`.
In addition, we can try to run the action directly and see if it behaves properly:
$ cd action/1/bin
$ ./exec 3>&1
$ {"value":{"name":"Mike"}}
Hello Mike!
{"greeting":"Hello Mike!"}
Note we redirected the file descriptor 3 in stdout to check what is happening, and note that logs appear in stdout too.
Also, we can test the compiler invoking it directly.
First let’s prepare the environment as it appears when we just uploaded the action:
$ cd /proxy
$ mkdir -p action/2/src action/2/bin
$ cp action/1/bin/main__.rb action/2/src/exec
$ find action/2
action/2
action/2/bin
action/2/src
action/2/src/exec
Now compile and examine the results again:
$ /proxy/bin/compile main action/2/src action/2/bin
$ find action/2
action/2/
action/2/bin
action/2/bin/exec__.rb
action/2/bin/exec
action/2/bin/main__.rb
If we have reached this point in the tutorial, the runtime is able to run and execute a simple test action. Now we need to validate the runtime against a set of mandatory tests both locally and within an OpenWhisk and OpenServerless staging environment. Additionally, we should author and automate additional tests for language specific features and styles.
The starter kit
includes two handy makefiles
that we can leverage
for some additional tests. In the next sections, we will show how to
update them for testing our Ruby runtime.
So far we tested a only an Action comprised of a single file. We should also test multi-file Actions (i.e., those with relative imports) sent to the runtime in both source and binary formats.
First, let’s try a multi-file Action by creating a Ruby Action script
named example/main.rb
that invokes our hello.rb
as follows:
require "./hello"
def main(args)
hello(args)
end
Within the example/Makefile
makefile:
update the name of the image to ruby-v2.6"
as well as the name of
the main
action.
update the PREFIX with your DockerHub username.
-IMG=actionloop-demo-python-v3.7:latest
-ACT=hello-demo-python
-PREFIX=docker.io/openwhisk
+IMG=actionloop-demo-ruby-v2.6:latest
+ACT=hello-demo-ruby
+PREFIX=docker.io/<docker username>
Now, we are ready to test the various cases. Again, start the runtime proxy in debug mode:
cd ruby2.6
make debug
/bin/proxy -debug
On another terminal, try to deploy a single file:
$ make test-single
python ../tools/invoke.py init hello ../example/hello.rb
{"ok":true}
python ../tools/invoke.py run '{}'
{"greeting":"Hello stranger!"}
python ../tools/invoke.py run '{"name":"Mike"}'
{"greeting":"Hello Mike!"}
Now, stop and restart the proxy and try to send a ZIP file with the sources:
$ make test-src-zip
zip src.zip main.rb hello.rb
adding: main.rb (deflated 42%)
adding: hello.rb (deflated 42%)
python ../tools/invoke.py init ../example/src.zip
{"ok":true}
python ../tools/invoke.py run '{}'
{"greeting":"Hello stranger!"}
python ../tools/invoke.py run '{"name":"Mike"}'
{"greeting":"Hello Mike!"}
Finally, test the pre-compilation: the runtime builds a zip file with the sources ready to be deployed. Again, stop and restart the proxy then:
$ make test-bin-zip
docker run -i actionloop-demo-ruby-v2.6:latest -compile main <src.zip >bin.zip
python ../tools/invoke.py init ../example/bin.zip
{"ok":true}
python ../tools/invoke.py run '{}'
{"greeting":"Hello stranger!"}
python ../tools/invoke.py run '{"name":"Mike"}'
{"greeting":"Hello Mike!"}
Congratulations! The runtime works locally! Time to test it on the
public cloud. So as the last step before moving forward, let’s push the
image to Docker Hub with make push
.
To run this test you need to configure access to OpenWhisk and OpenServerless
with ops
. A simple way is to get access is to register a free account
in the IBM Cloud but this works also with our own deployment of
OpenWhisk and OpenServerless.
Edit the Makefile as we did previously:
IMG=actionloop-demo-ruby-v2.6:latest
ACT=hello-demo-ruby
PREFIX=docker.io/<docker username>
Also, change any reference to hello.py
and main.py
to hello.rb
and
main.rb
.
Once this is done, we can re-run the tests we executed locally on “the real thing”.
Test single:
$ make test-single
ops action update hello-demo-ruby hello.rb --docker docker.io/linus/actionloop-demo-ruby-v2.6:latest --main hello
ok: updated action hello-demo-ruby
ops action invoke hello-demo-ruby -r
{
"greeting": "Hello stranger!"
}
ops action invoke hello-demo-ruby -p name Mike -r
{
"greeting": "Hello Mike!"
}
Test source zip:
$ make test-src-zip
zip src.zip main.rb hello.rb
adding: main.rb (deflated 42%)
adding: hello.rb (deflated 42%)
ops action update hello-demo-ruby src.zip --docker docker.io/linus/actionloop-demo-ruby-v2.6:latest
ok: updated action hello-demo-ruby
ops action invoke hello-demo-ruby -r
{
"greeting": "Hello stranger!"
}
ops action invoke hello-demo-ruby -p name Mike -r
{
"greeting": "Hello Mike!"
}
Test binary ZIP:
$ make test-bin-zip
docker run -i actionloop-demo-ruby-v2.6:latest -compile main <src.zip >bin.zip
ops action update hello-demo-ruby bin.zip --docker docker.io/actionloop/actionloop-demo-ruby-v2.6:latest
ok: updated action hello-demo-ruby
ops action invoke hello-demo-ruby -r
{
"greeting": "Hello stranger!"
}
ops action invoke hello-demo-ruby -p name Mike -r
{
"greeting": "Hello Mike!"
}
Congratulations! Your runtime works also in the real world.
Before you can submit your runtime you should ensure your runtime pass the validation tests.
Under
tests/src/test/scala/runtime/actionContainers/ActionLoopPythonBasicTests.scala
there is the template for the test.
Rename to
tests/src/test/scala/runtime/actionContainers/ActionLoopRubyBasicTests.scala
,
change internally the class name to class ActionLoopRubyBasicTests
and
implement the following test cases:
testNotReturningJson
testUnicode
testEnv
testInitCannotBeCalledMoreThanOnce
testEntryPointOtherThanMain
testLargeInput
You should convert Python code to Ruby code. We do not do go into the details of each test, as they are pretty simple and obvious. You can check the source code for the real test here.
You can verify tests are running properly with:
$ ./gradlew test
Starting a Gradle Daemon, 1 busy Daemon could not be reused, use --status for details
> Task :tests:test
runtime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should handle initialization with no code PASSED
runtime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should handle initialization with no content PASSED
runtime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should run and report an error for function not returning a json object PASSED
runtime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should fail to initialize a second time PASSED
runtime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should invoke non-standard entry point PASSED
runtime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should echo arguments and print message to stdout/stderr PASSED
runtime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should handle unicode in source, input params, logs, and result PASSED
runtime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should confirm expected environment variables PASSED
runtime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should echo a large input PASSED
BUILD SUCCESSFUL in 55s
Big congratulations are in order having reached this point successfully. At this point, our runtime should be ready to run on any OpenWhisk and OpenServerless platform and also can be submitted for consideration to be included in the Apache OpenWhisk and OpenServerless project.