Adding Action Language Runtimes

OpenWhisk 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 OpenWhisk platform is extensible and you can add new languages or runtimes (with custom packages and third-party dependencies) following the guide described here.

Runtime general requirements

The unit of execution for all functions is a Docker container which must implement a specific Action interface that, in general performs:

  1. Initialization - accepts an initialization payload (the code) and prepared for execution,
  2. Activation - accepts a runtime payload (the input parameters) and
    • prepares the activation context,
    • runs the function,
    • returns the function result,
  3. 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.

Platform requirements

In order for your language runtime to be properly recognized by the OpenWhisk platform, and officially recognized by the Apache OpenWhisk project, please follow these requirements and best practices:

  1. Implement the runtime in its own repository to permit a management lifecycle independent of the rest of the OpenWhisk platform.
  2. Introduce the runtime specification into the runtimes manifest,
  3. Add the runtime to the Swagger file
  4. Add a new actions-<your runtime>.md file to the docs directory,
  5. Add a link to your new runtime doc to the top level actions index.
  6. Add a standard test action to the tests artifacts directory (as shown below),

The new runtime repository should conform to the Canonical runtime repository layout (as shown below). Further, you should automate and pass the following test suites:

The runtimes manifest

Actions when created specify the desired runtime for the function via a property called “kind”. When using the wsk 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:8 or php:7.4.

The manifest is a map of runtime family names to an array of specific kinds. The details of the schema are found in the Exec Manifest. As an example, the following entry add a new runtime family called nodejs with a single kind nodejs:10.

{
  "nodejs": [{
    "kind": "nodejs:10",
    "default": true,
    "image": {
      "prefix": "openwhisk",
      "name": "nodejs10action",
      "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).

Canonical runtime repository

The runtime repository should follow the canonical structure used by other runtimes.

/path/to/runtime
├── build.gradle         # Gradle build file to integrate with rest of build and test framework
├── core
│   └── <runtime name and version>
│       ├── Dockerfile   # builds the runtime's Docker image
│       └── ...          # your runtimes files which implement the action proxy
└── tests
    └── src              # tests suits...
        └── ...          # ... which extend canonical interface plus additional runtime specific tests

The Docker skeleton repository is an example starting point to fork and modify for your new runtime.

The test action

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:8 becomes nodejs-8.txt.

function main(args) {
    var str = args.delimiter + " ☃ " + args.delimiter;
    console.log(str);
    return { "winter": str };
}

Action Interface

An action consists of the user function (and its dependencies) along with a proxy that implements a canonical protocol to integrate with the OpenWhisk 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.

Initialization

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 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 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 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.

Activation

The proxy is ready to execute a function once it has successfully completed initialization. The OpenWhisk 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 platform.

The route must accept a JSON object and respond with a JSON object, otherwise the OpenWhisk 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 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 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 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 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.

Logging

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.

Testing

Action Interface tests

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.

Runtime proxy tests

There is a canonical test harness for validating a new runtime.

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 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 wsk CLI or other OpenWhisk clients. In which case, appropriate tests should be added as necessary. The OpenWhisk 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.