An ability to run on Knative along with OpenWhisk (#119)
* changes to app.js to support knative and openwhisk runtimes
* changes to service.js
* adding debug and utitilies
* adding platform files
* adding build template
* updating nodejs 10 runtime to include platform
* updating nodejs 10 runtime to include platform
* updating nodejs 10 runtime to include platform
* adding docker secret and service account yaml
* adding knative README
* adding presentation deck and demo gif
* combining READMEs
* adding tests
* fixing link to gif
* fixing links to build and service yaml
* updating git repo
* commenting log in app.js
* adding platform to nodejs 8
* init once and run multiple times
* fixing init once and run many
* updating README
* dropping tests/doc additions, will create a seperate PR
* dropping debug utility
* dropping duplicate functions
* addressing rr's comments - dropping cfg, console errors
* adding console errors which was deleted
* addressing dragos's suggestion, avoid using else block
diff --git a/core/nodejs10Action/build.gradle b/core/nodejs10Action/build.gradle
index 3d9314a..c1b302b 100644
--- a/core/nodejs10Action/build.gradle
+++ b/core/nodejs10Action/build.gradle
@@ -29,6 +29,10 @@
distDocker.dependsOn 'copyProxy'
distDocker.dependsOn 'copyRunner'
distDocker.dependsOn 'copyService'
+distDocker.dependsOn 'copyPlatform'
+distDocker.dependsOn 'copyOpenWhisk'
+distDocker.dependsOn 'copyKnative'
+distDocker.dependsOn 'copyBuildTemplate'
distDocker.finalizedBy('cleanup')
task copyProxy(type: Copy) {
@@ -46,8 +50,30 @@
into './src'
}
+task copyPlatform(type: Copy) {
+ from '../nodejsActionBase/platform/platform.js'
+ into './platform'
+}
+
+task copyOpenWhisk(type: Copy) {
+ from '../nodejsActionBase/platform/openwhisk.js'
+ into './platform'
+}
+
+task copyKnative(type: Copy) {
+ from '../nodejsActionBase/platform/knative.js'
+ into './platform'
+}
+
+task copyBuildTemplate(type: Copy) {
+ from '../nodejsActionBase/buildtemplate.yaml'
+ into '.'
+}
+
task cleanup(type: Delete) {
delete 'app.js'
delete 'runner.js'
delete 'src'
+ delete 'platform'
+ delete 'buildtemplate.yaml'
}
diff --git a/core/nodejs8Action/build.gradle b/core/nodejs8Action/build.gradle
index b37befe..a671ef8 100644
--- a/core/nodejs8Action/build.gradle
+++ b/core/nodejs8Action/build.gradle
@@ -29,6 +29,10 @@
distDocker.dependsOn 'copyProxy'
distDocker.dependsOn 'copyRunner'
distDocker.dependsOn 'copyService'
+distDocker.dependsOn 'copyPlatform'
+distDocker.dependsOn 'copyOpenWhisk'
+distDocker.dependsOn 'copyKnative'
+distDocker.dependsOn 'copyBuildTemplate'
distDocker.finalizedBy('cleanup')
task copyProxy(type: Copy) {
@@ -46,8 +50,30 @@
into './src'
}
+task copyPlatform(type: Copy) {
+ from '../nodejsActionBase/platform/platform.js'
+ into './platform'
+}
+
+task copyOpenWhisk(type: Copy) {
+ from '../nodejsActionBase/platform/openwhisk.js'
+ into './platform'
+}
+
+task copyKnative(type: Copy) {
+ from '../nodejsActionBase/platform/knative.js'
+ into './platform'
+}
+
+task copyBuildTemplate(type: Copy) {
+ from '../nodejsActionBase/buildtemplate.yaml'
+ into '.'
+}
+
task cleanup(type: Delete) {
delete 'app.js'
delete 'runner.js'
delete 'src'
+ delete 'platform'
+ delete 'buildtemplate.yaml'
}
diff --git a/core/nodejsActionBase/app.js b/core/nodejsActionBase/app.js
index 9e28fa6..f45ee4b 100644
--- a/core/nodejsActionBase/app.js
+++ b/core/nodejsActionBase/app.js
@@ -15,74 +15,80 @@
* limitations under the License.
*/
+// __OW_ALLOW_CONCURRENT: see docs/concurrency.md
var config = {
- 'port': 8080,
- 'apiHost': process.env.__OW_API_HOST,
- 'allowConcurrent': process.env.__OW_ALLOW_CONCURRENT
+ 'port': 8080,
+ 'apiHost': process.env.__OW_API_HOST,
+ 'allowConcurrent': process.env.__OW_ALLOW_CONCURRENT,
+ 'requestBodyLimit': "48mb"
};
var bodyParser = require('body-parser');
var express = require('express');
+/**
+ * instantiate app as an instance of Express
+ * i.e. app starts the server
+ */
var app = express();
-
/**
* instantiate an object which handles REST calls from the Invoker
*/
var service = require('./src/service').getService(config);
-app.set('port', config.port);
-app.use(bodyParser.json({ limit: "48mb" }));
+/**
+ * setup a middleware layer to restrict the request body size
+ * this middleware is called every time a request is sent to the server
+ */
+app.use(bodyParser.json({ limit: config.requestBodyLimit }));
-app.post('/init', wrapEndpoint(service.initCode));
-app.post('/run', wrapEndpoint(service.runCode));
+// identify the target Serverless platform
+const platformFactory = require('./platform/platform.js');
+const factory = new platformFactory(app, config, service);
+var targetPlatform = process.env.__OW_RUNTIME_PLATFORM;
+
+// default to "openwhisk" platform initialization if not defined
+// TODO export isvalid() from platform, if undefined this is OK to default, but if not valid value then error out
+if(typeof targetPlatform === "undefined") {
+ targetPlatform = platformFactory.PLATFORM_OPENWHISK;
+ // console.log("__OW_RUNTIME_PLATFORM is undefined; defaulting to 'openwhisk' ...");
+}
+
+if(!platformFactory.isSupportedPlatform(targetPlatform)){
+ console.error("__OW_RUNTIME_PLATFORM ("+targetPlatform+") is not supported by the runtime.");
+ process.exit(9);
+}
+
+/**
+ * Register different endpoint handlers depending on target PLATFORM and its expected behavior.
+ * In addition, register request pre-processors and/or response post-processors as needed
+ * to move data where the platform and function author expects it to be.
+ */
+
+var platformImpl = factory.createPlatformImpl(targetPlatform);
+
+if(typeof platformImpl == "undefined") {
+ console.error("Failed to initialize __OW_RUNTIME_PLATFORM ("+targetPlatform+").");
+ process.exit(10);
+}
+
+// Call platform impl. to register platform-specific endpoints (routes)
+platformImpl.registerHandlers(app, platformImpl);
// short-circuit any requests to invalid routes (endpoints) that we have no handlers for.
app.use(function (req, res, next) {
res.status(500).json({error: "Bad request."});
});
-// register a default error handler. This effectively only gets called when invalid JSON is received (JSON Parser)
-// and we do not wish the default handler to error with a 400 and send back HTML in the body of the response.
+/**
+ * Register a default error handler. This effectively only gets called when invalid JSON is received
+ * (JSON Parser) and we do not wish the default handler to error with a 400 and send back HTML in the
+ * body of the response.
+ */
app.use(function (err, req, res, next) {
console.log(err.stackTrace);
res.status(500).json({error: "Bad request."});
});
service.start(app);
-
-/**
- * Wraps an endpoint written to return a Promise into an express endpoint,
- * producing the appropriate HTTP response and closing it for all controllable
- * failure modes.
- *
- * The expected signature for the promise value (both completed and failed)
- * is { code: int, response: object }.
- *
- * @param ep a request=>promise function
- * @returns an express endpoint handler
- */
-function wrapEndpoint(ep) {
- return function (req, res) {
- try {
- ep(req).then(function (result) {
- res.status(result.code).json(result.response);
- }).catch(function (error) {
- if (typeof error.code === "number" && typeof error.response !== "undefined") {
- res.status(error.code).json(error.response);
- } else {
- console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error));
- res.status(500).json({ error: "Internal error." });
- }
- });
- } catch (e) {
- // This should not happen, as the contract for the endpoints is to
- // never (externally) throw, and wrap failures in the promise instead,
- // but, as they say, better safe than sorry.
- console.error("[wrapEndpoint]", "exception caught", e.message);
-
- res.status(500).json({ error: "Internal error (exception)." });
- }
- }
-}
diff --git a/core/nodejsActionBase/buildtemplate.yaml b/core/nodejsActionBase/buildtemplate.yaml
new file mode 100644
index 0000000..ab80f99
--- /dev/null
+++ b/core/nodejsActionBase/buildtemplate.yaml
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more contributor
+# license agreements; and to You under the Apache License, Version 2.0.
+apiVersion: build.knative.dev/v1alpha1
+kind: BuildTemplate
+metadata:
+ name: openwhisk-nodejs-runtime
+spec:
+ parameters:
+ - name: TARGET_IMAGE_NAME
+ description: name of the image to be tagged and pushed
+ - name: TARGET_IMAGE_TAG
+ description: tag the image before pushing
+ default: "latest"
+ - name: DOCKERFILE
+ description: name of the dockerfile
+ - name: OW_RUNTIME_DEBUG
+ description: flag to indicate debug mode should be on/off
+ default: "false"
+ - name: OW_RUNTIME_PLATFORM
+ description: flag to indicate the platform, one of ["openwhisk", "knative", ... ]
+ default: "knative"
+ - name: OW_ACTION_NAME
+ description: name of the action
+ default: ""
+ - name: OW_ACTION_CODE
+ description: JavaScript source code to be evaluated
+ default: ""
+ - name: OW_ACTION_MAIN
+ description: name of the function in the "__OW_ACTION_CODE" to call as the action handler
+ default: "main"
+ - name: OW_ACTION_BINARY
+ description: flag to indicate zip function, for zip actions, "__OW_ACTION_CODE" must be base64 encoded string
+ default: "false"
+ - name: OW_HTTP_METHODS
+ description: list of HTTP methods, any combination of [GET, POST, PUT, and DELETE], default is [POST]
+ default: "[POST]"
+ - name: OW_ACTION_RAW
+ description: flag to indicate raw HTTP handling, interpret and process an incoming HTTP body directly
+ default: "false"
+ steps:
+ - name: add-ow-env-to-dockerfile
+ image: "gcr.io/kaniko-project/executor:debug"
+ command:
+ - /busybox/sh
+ args:
+ - -c
+ - |
+ cat <<EOF >> ${DOCKERFILE}
+ ENV __OW_RUNTIME_DEBUG "${OW_RUNTIME_DEBUG}"
+ ENV __OW_RUNTIME_PLATFORM "${OW_RUNTIME_PLATFORM}"
+ ENV __OW_ACTION_NAME "${OW_ACTION_NAME}"
+ ENV __OW_ACTION_CODE "${OW_ACTION_CODE}"
+ ENV __OW_ACTION_MAIN "${OW_ACTION_MAIN}"
+ ENV __OW_ACTION_BINARY "${OW_ACTION_BINARY}"
+ ENV __OW_HTTP_METHODS "${OW_HTTP_METHODS}"
+ ENV __OW_ACTION_RAW "${OW_ACTION_RAW}"
+ EOF
+ - name: build-openwhisk-nodejs-runtime
+ image: "gcr.io/kaniko-project/executor:latest"
+ args: ["--destination=${TARGET_IMAGE_NAME}:${TARGET_IMAGE_TAG}", "--dockerfile=${DOCKERFILE}"]
diff --git a/core/nodejsActionBase/docker-secret.yaml.tmpl b/core/nodejsActionBase/docker-secret.yaml.tmpl
new file mode 100644
index 0000000..ea2ec2c
--- /dev/null
+++ b/core/nodejsActionBase/docker-secret.yaml.tmpl
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: dockerhub-user-pass
+ annotations:
+ build.knative.dev/docker-0: https://index.docker.io/v1/
+type: kubernetes.io/basic-auth
+data:
+ # use `echo -n "username" | base64 -b 0` to generate this value
+ username: ${DOCKERHUB_USERNAME_BASE64_ENCODED}
+ # use `echo -n "password" | base64 -b 0` to generate this value
+ password: ${DOCKERHUB_PASSWORD_BASE64_ENCODED}
diff --git a/core/nodejsActionBase/platform/knative.js b/core/nodejsActionBase/platform/knative.js
new file mode 100644
index 0000000..cbc9b8c
--- /dev/null
+++ b/core/nodejsActionBase/platform/knative.js
@@ -0,0 +1,487 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+const OW_ENV_PREFIX = "__OW_";
+const CONTENT_TYPE = "Content-Type";
+
+/**
+ * Determine if runtime is a "stem" cell, i.e., can be initialized with request init. data
+ * @param env
+ * @returns {boolean}
+ */
+function isStemCell(env) {
+ let actionCode = env.__OW_ACTION_CODE;
+ // It is a stem cell if valid code is "built into" the runtime's process environment.
+ return (typeof actionCode === 'undefined' || actionCode.length === 0);
+}
+
+/**
+ * Determine if the request (body) contains valid activation data.
+ * @param req
+ * @returns {boolean}
+ */
+function hasActivationData(req) {
+ // it is a valid activation if the body contains an activation and value keys with data.
+ if (typeof req.body !== "undefined" &&
+ typeof req.body.activation !== "undefined" &&
+ typeof req.body.value !== "undefined") {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Determine if the request (body) contains valid init data.
+ * @param req
+ * @returns {boolean}
+ */
+function hasInitData(req) {
+ // it is a valid init. if the body contains an init key with data.
+ if (typeof req.body !== "undefined" &&
+ typeof req.body.init !== "undefined") {
+ return true;
+ }
+ return false;
+}
+
+
+/**
+ * Remove all INIT data from the value data that will be passed to the user function.
+ * @param body
+ */
+function removeInitData(body) {
+
+ if (typeof body !== "undefined" &&
+ typeof body.value !== "undefined") {
+ delete body.value.code;
+ delete body.value.main;
+ delete body.value.binary;
+ delete body.value.raw;
+ }
+}
+
+/**
+ * Pre-process the incoming
+ */
+function preProcessInitData(env, initdata, valuedata, activationdata) {
+ try {
+ // Set defaults to use INIT data not provided on the request
+ // Look first to the process (i.e., Container's) environment variables.
+ var main = (typeof env.__OW_ACTION_MAIN === 'undefined') ? "main" : env.__OW_ACTION_MAIN;
+ // TODO: Throw error if CODE is NOT defined!
+ var code = (typeof env.__OW_ACTION_CODE === 'undefined') ? "" : env.__OW_ACTION_CODE;
+ var binary = (typeof env.__OW_ACTION_BINARY === 'undefined') ? false : env.__OW_ACTION_BINARY.toLowerCase() === "true";
+ // TODO: default to empty?
+ var actionName = (typeof env.__OW_ACTION_NAME === 'undefined') ? "" : env.__OW_ACTION_NAME;
+ var raw = (typeof env.__OW_ACTION_RAW === 'undefined') ? false : env.__OW_ACTION_RAW.toLowerCase() === "true";
+
+
+ // Look for init data within the request (i.e., "stem cell" runtime, where code is injected by request)
+ if (typeof(initdata) !== "undefined") {
+ if (initdata.name && typeof initdata.name === 'string') {
+ actionName = initdata.name;
+ }
+ if (initdata.main && typeof initdata.main === 'string') {
+ main = initdata.main;
+ }
+ if (initdata.code && typeof initdata.code === 'string') {
+ code = initdata.code;
+ }
+ if (initdata.binary) {
+ if (typeof initdata.binary === 'boolean') {
+ binary = initdata.binary;
+ } else {
+ throw ("Invalid Init. data; expected boolean for key 'binary'.");
+ }
+ }
+ if (initdata.raw ) {
+ if (typeof initdata.raw === 'boolean') {
+ raw = initdata.raw;
+ } else {
+ throw ("Invalid Init. data; expected boolean for key 'raw'.");
+ }
+ }
+ }
+
+ // Move the init data to the request body under the "value" key.
+ // This will allow us to reuse the "openwhisk" /init route handler function
+ valuedata.main = main;
+ valuedata.code = code;
+ valuedata.binary = binary;
+ valuedata.raw = raw;
+
+ // Action name is a special case, as we have a key collision on "name" between init. data and request
+ // param. data (as they both appear within "body.value") so we must save it to its final location
+ // as the default Action name as part of the activation data
+ // NOTE: if action name is not present in the action data, we will set it regardless even if an empty string
+ if( typeof(activationdata) !== "undefined" ) {
+ if ( typeof(activationdata.action_name) === "undefined" ||
+ (typeof(activationdata.action_name) === "string" && activationdata.action_name.length == 0 )){
+ activationdata.action_name = actionName;
+ }
+ }
+
+ } catch(e){
+ console.error(e);
+ throw("Unable to initialize the runtime: " + e.message);
+ }
+}
+
+/**
+ * Pre-process the incoming http request data, moving it to where the
+ * route handlers expect it to be for an openwhisk runtime.
+ */
+function preProcessActivationData(env, activationdata) {
+ try {
+ // Note: we move the values here so that the "run()" handler does not have
+ // to move them again.
+ Object.keys(activationdata).forEach(
+ function (k) {
+ if (typeof activationdata[k] === 'string') {
+ var envVariable = OW_ENV_PREFIX + k.toUpperCase();
+ process.env[envVariable] = activationdata[k];
+ }
+ }
+ );
+ } catch(e){
+ console.error(e);
+ throw("Unable to initialize the runtime: " + e.message);
+ }
+}
+
+/**
+ * Pre-process HTTP request information and make it available as parameter data to the action function
+ * by moving it to where the route handlers expect it to be (i.e., in the JSON value data map).
+ *
+ * See: https://github.com/apache/incubator-openwhisk/blob/master/docs/webactions.md#http-context
+ *
+ * HTTP Context
+ * ============
+ * All web actions, when invoked, receives additional HTTP request details as parameters to the action
+ * input argument. These include:
+ *
+ * __ow_method (type: string): the HTTP method of the request.
+ * __ow_headers (type: map string to string): the request headers.
+ * __ow_path (type: string): the unmatched path of the request (matching stops after consuming the action extension).
+ * __ow_user (type: string): the namespace identifying the OpenWhisk authenticated subject.
+ * __ow_body (type: string): the request body entity, as a base64 encoded string when content is
+ * binary or JSON object/array, or plain string otherwise.
+ * __ow_query (type: string): the query parameters from the request as an unparsed string.
+ *
+ * TODO:
+ * A request may not override any of the named __ow_ parameters above; doing so will result in a
+ * failed request with status equal to 400 Bad Request.
+ */
+function preProcessHTTPContext(req, valueData) {
+ try {
+ if (valueData.raw) {
+ // __ow_body is a base64 encoded string when content is binary or JSON object/array,
+ // or plain string otherwise.
+ if (typeof req.body.value === "string" && req.body.value !== undefined) {
+ valueData.__ow_body = req.body.value;
+ } else {
+ // make value data available as __ow_body
+ const tmpBody = Object.assign({}, req.body.value);
+ // delete main, binary, raw, and code from the body before sending it as an action argument
+ delete tmpBody.main;
+ delete tmpBody.code;
+ delete tmpBody.binary;
+ delete tmpBody.raw;
+ var bodyStr = JSON.stringify(tmpBody);
+ // note: we produce an empty map if there are no query parms/
+ valueData.__ow_body = Buffer.from(bodyStr).toString("base64");;
+ }
+ valueData.__ow_query = req.query;
+ }
+
+ var namespace = "";
+ if (process.env[OW_ENV_PREFIX + "NAMESPACE"] !== undefined) {
+ namespace = process.env[OW_ENV_PREFIX + "NAMESPACE"];
+ }
+ valueData.__ow_user = namespace;
+ valueData.__ow_method = req.method;
+ valueData.__ow_headers = req.headers;
+ valueData.__ow_path = "";
+ } catch (e) {
+ console.error(e);
+ throw ("Unable to initialize the runtime: " + e.message)
+ }
+}
+
+
+/**
+ * Pre-process the incoming http request data, moving it to where the
+ * route handlers expect it to be for an openwhisk runtime.
+ */
+function preProcessRequest(req){
+ try {
+ // Get or create valid references to the various data we might encounter
+ // in a request such as Init., Activation and function parameter data.
+ let body = req.body || {};
+ let valueData = body.value || {};
+ let initData = body.init || {};
+ let activationData = body.activation || {};
+ let env = process.env || {};
+
+ // Fix up pointers in case we had to allocate new maps
+ req.body = body;
+ req.body.value = valueData;
+ req.body.init = initData;
+ req.body.activation = activationData;
+
+ // process initialization (i.e., "init") data
+ preProcessInitData(env, initData, valueData, activationData);
+
+ // process HTTP request header and body to make it available to function as parameter data
+ preProcessHTTPContext(req, valueData);
+
+ // process per-activation (i.e, "run") data
+ preProcessActivationData(env, activationData);
+
+ } catch(e){
+ console.error(e);
+ // TODO: test this error is handled properly and results in an HTTP error response
+ throw("Unable to initialize the runtime: " + e.message);
+ }
+}
+
+function postProcessResponse(req, result, res) {
+
+ var content_types = {
+ json: 'application/json',
+ html: 'text/html',
+ png: 'image/png',
+ svg: 'image/svg+xml',
+ };
+
+ // After getting the result back from an action, update the HTTP headers,
+ // status code, and body based on its result if it includes one or more of the
+ // following as top level JSON properties: headers, statusCode, body
+ let statusCode = result.code;
+ let headers = {};
+ let body = result.response;
+ let contentTypeInHeader = false;
+
+ // statusCode: default is 200 OK if body is not empty otherwise 204 No Content
+ if (result.response.statusCode !== undefined) {
+ statusCode = result.response.statusCode;
+ delete body['statusCode'];
+ }
+
+ // the default content-type for an HTTP response is application/json
+ // this default are overwritten with the action specified headers
+ if (result.response.headers !== undefined) {
+ headers = result.response.headers;
+ delete body['headers'];
+ }
+
+ // addressing content-type v/s Content-Type
+ // marking 'Content-Type' as standard inside header
+ if (headers.hasOwnProperty(CONTENT_TYPE.toLowerCase())) {
+ headers[CONTENT_TYPE] = headers[CONTENT_TYPE.toLowerCase()];
+ delete headers[CONTENT_TYPE.toLowerCase()];
+ }
+
+ // If a content-type header is not declared in the action result’s headers,
+ // the body is interpreted as application/json for non-string values,
+ // and text/html otherwise.
+ if (!headers.hasOwnProperty(CONTENT_TYPE)) {
+ if (result.response.body !== undefined && typeof result.response.body == "string") {
+ headers[CONTENT_TYPE] = content_types.html;
+ } else {
+ headers[CONTENT_TYPE] = content_types.json;
+ }
+ } else {
+ contentTypeInHeader = true;
+ }
+
+
+ // body: a string which is either a plain text, JSON object, or a base64 encoded string for binary data (default is "")
+ // body is considered empty if it is null, "", or undefined
+ if (result.response.body !== undefined) {
+ body = result.response.body;
+ delete body['main'];
+ delete body['code'];
+ delete body['binary'];
+ }
+
+ //When the content-type is defined, check if the response is binary data or
+ // plain text and decode the plain text using a base64 decoder whenever needed.
+ // Should the body fail to decoded correctly, return an error to the caller.
+ if (contentTypeInHeader && headers[CONTENT_TYPE].lastIndexOf("image", 0) === 0) {
+ if (typeof body === "string") {
+ body = Buffer.from(body, 'base64')
+ headers["Content-Transfer-Encoding"] = "binary";
+ }
+ // TODO: throw an error if body can not be decoded
+ }
+
+
+ // statusCode: set it to 204 No Content if body is empty
+ if (statusCode === 200 && body === "") {
+ statusCode = 204;
+ }
+
+ if (!headers.hasOwnProperty('Access-Control-Allow-Origin')) {
+ headers['Access-Control-Allow-Origin'] = '*';
+ }
+ if (!headers.hasOwnProperty('Access-Control-Allow-Methods')) {
+ headers['Access-Control-Allow-Methods'] = 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH';
+ }
+ // the header Access-Control-Request-Headers is echoed back as the header Access-Control-Allow-Headers if it is present in the HTTP request.
+ // Otherwise, a default value is generated.
+ if (!headers.hasOwnProperty['Access-Control-Allow-Headers']) {
+ headers['Access-Control-Allow-Headers'] = 'Authorization, Origin, X - Requested - With, Content - Type, Accept, User - Agent';
+ if (typeof req.headers['Access-Control-Request-Headers'] !== "undefined") {
+ headers['Access-Control-Allow-Headers'] = req.headers['Access-Control-Request-Headers'];
+ }
+ }
+
+ res.header(headers).status(statusCode).send(body);
+}
+
+function PlatformKnativeImpl(platformFactory) {
+
+ var http_method = {
+ get: 'GET',
+ post: 'POST',
+ put: 'PUT',
+ delete: 'DELETE',
+ options: 'OPTIONS',
+ };
+
+ const DEFAULT_METHOD = [ 'POST' ];
+
+ // Provide access to common runtime services
+ var service = platformFactory.service;
+
+ // TODO: Should we use app.WrapEndpoint()?
+ this.run = function(req, res) {
+
+ try {
+
+ // Do not process requests with init. data if this is not a "stem" cell
+ if (hasInitData(req) && !isStemCell(process.env))
+ throw ("Cannot initialize a runtime with a dedicated function.");
+
+ if(hasInitData(req) && hasActivationData(req)){
+
+ // Process request and process env. variables to provide them in the manner
+ // an OpenWhisk Action expects them, as well as enable additional Http features.
+ preProcessRequest(req);
+
+ service.initCode(req).then(function () {
+ // delete any INIT data (e.g., code, raw, etc.) from the 'value' data before calling run().
+ removeInitData(req.body);
+ service.runCode(req).then(function (result) {
+ postProcessResponse(req, result, res)
+ });
+ }).catch(function (error) {
+ console.error(error);
+ if (typeof error.code === "number" && typeof error.response !== "undefined") {
+ res.status(error.code).json(error.response);
+ } else {
+ console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error));
+ res.status(500).json({ error: "Internal error during function execution." });
+ }
+ });
+ } else if(hasInitData(req)){
+
+ // Process request and process env. variables to provide them in the manner
+ // an OpenWhisk Action expects them, as well as enable additional Http features.
+ preProcessRequest(req);
+
+ service.initCode(req).then(function (result) {
+ res.status(result.code).send(result.response);
+ }).catch(function (error) {
+ console.error(error);
+ if (typeof error.code === "number" && typeof error.response !== "undefined") {
+ res.status(error.code).json(error.response);
+ } else {
+ console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error));
+ res.status(500).json({ error: "Internal error during function execution." });
+ }
+ });
+ } else if(hasActivationData(req)){
+ // Process request and process env. variables to provide them in the manner
+ // an OpenWhisk Action expects them, as well as enable additional Http features.
+ preProcessRequest(req);
+
+ service.runCode(req).then(function (result) {
+ postProcessResponse(req, result, res)
+ }).catch(function (error) {
+ console.error(error);
+ if (typeof error.code === "number" && typeof error.response !== "undefined") {
+ res.status(error.code).json(error.response);
+ } else {
+ console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error));
+ res.status(500).json({ error: "Internal error during function execution." });
+ }
+ });
+ }
+
+ } catch (e) {
+ res.status(500).json({error: "internal error during function initialization."})
+ }
+ };
+
+ // TODO: the 'httpMethods' var should not alternatively store string and string[] types
+ this.registerHandlers = function(app, platform) {
+ var httpMethods = process.env.__OW_HTTP_METHODS;
+ // default to "[post]" HTTP method if not defined
+ if (typeof httpMethods === "undefined") {
+ console.error("__OW_HTTP_METHODS is undefined; defaulting to '[post]' ...");
+ httpMethods = DEFAULT_METHOD;
+ } else {
+ if (httpMethods.startsWith('[') && httpMethods.endsWith(']')) {
+ httpMethods = httpMethods.substr(1, httpMethods.length);
+ httpMethods = httpMethods.substr(0, httpMethods.length -1);
+ httpMethods = httpMethods.split(',');
+ }
+ }
+ // default to "[post]" HTTP method if specified methods are not valid
+ if (!Array.isArray(httpMethods) || !Array.length) {
+ console.error("__OW_HTTP_METHODS is undefined; defaulting to '[post]' ...");
+ httpMethods = DEFAULT_METHOD;
+ }
+
+ httpMethods.forEach(function (method) {
+ switch (method.toUpperCase()) {
+ case http_method.get:
+ app.get('/', platform.run);
+ break;
+ case http_method.post:
+ app.post('/', platform.run);
+ break;
+ case http_method.put:
+ app.put('/', platform.run);
+ break;
+ case http_method.delete:
+ app.delete('/', platform.run);
+ break;
+ case http_method.options:
+ app.options('/', platform.run);
+ break;
+ default:
+ console.error("Environment variable '__OW_HTTP_METHODS' has an unrecognized value (" + method + ").");
+ }
+ });
+ };
+}
+
+module.exports = PlatformKnativeImpl;
diff --git a/core/nodejsActionBase/platform/openwhisk.js b/core/nodejsActionBase/platform/openwhisk.js
new file mode 100644
index 0000000..eefed16
--- /dev/null
+++ b/core/nodejsActionBase/platform/openwhisk.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+function PlatformOpenWhiskImpl(platformFactory) {
+ // Provide access to common runtime services
+ var service = platformFactory.service;
+
+ this.registerHandlers = function(app, platform) {
+ app.post('/init', platformFactory.wrapEndpoint(service.initCode));
+ app.post('/run', platformFactory.wrapEndpoint(service.runCode));
+ };
+}
+
+module.exports = PlatformOpenWhiskImpl;
diff --git a/core/nodejsActionBase/platform/platform.js b/core/nodejsActionBase/platform/platform.js
new file mode 100644
index 0000000..1196ccc
--- /dev/null
+++ b/core/nodejsActionBase/platform/platform.js
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Runtime platform factory
+ *
+ * This module is a NodeJS compatible version of a factory that will
+ * produce an implementation module provides OpenWhisk Language
+ * Runtime functionality and is able to register endpoints/handlers
+ * allowing to host OpenWhisk Actions and process OpenWhisk Activations.
+ */
+
+
+// Export supported platform impls.
+const PLATFORM_OPENWHISK = 'openwhisk';
+const PLATFORM_KNATIVE = 'knative';
+
+const SUPPORTED_PLATFORMS = [
+ PLATFORM_OPENWHISK,
+ PLATFORM_KNATIVE
+];
+
+module.exports = class PlatformFactory {
+
+ /**
+ * Object constructor
+ * @param app NodeJS express application instance
+ * @param cfg Runtime configuration
+ * @@param svc Runtime services (default handlers)
+ */
+ constructor (app, cfg, svc) {
+ this._app = app;
+ this._service = svc;
+ this._config = cfg;
+ }
+
+ /**
+ * @returns {string[]} List of supported platforms by their string ID
+ */
+ static get SUPPORTED_PLATFORMS() {
+ return SUPPORTED_PLATFORMS;
+ }
+
+ static get PLATFORM_OPENWHISK() {
+ return PLATFORM_OPENWHISK;
+ }
+
+ static get PLATFORM_KNATIVE() {
+ return PLATFORM_KNATIVE;
+ }
+
+ get app(){
+ return this._app;
+ }
+
+ get service(){
+ return this._service;
+ }
+
+ get config(){
+ return this._config;
+ }
+
+ /**
+ * validate if a platform ID is a known, supported value
+ * @param id Platform Id
+ */
+ static isSupportedPlatform(id) {
+ if (SUPPORTED_PLATFORMS.indexOf(id) > -1) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Instantiate a platform implementation
+ * @param id Platform ID
+ * @returns {PlatformImpl} Platform instance (interface), as best can be done with NodeJS
+ */
+ createPlatformImpl(id) {
+ // Load the appropriate implementation module and return reference to it
+ switch (id.toLowerCase()) {
+ case PLATFORM_KNATIVE:
+ const knPlatformImpl = require('./knative.js');
+ this._platformImpl = new knPlatformImpl(this);
+ break;
+ case PLATFORM_OPENWHISK:
+ const owPlatformImpl = require('./openwhisk.js');
+ this._platformImpl = new owPlatformImpl(this);
+ break;
+ default:
+ console.error("Platform ID is not a known value (" + id + ").");
+ }
+ return this._platformImpl;
+ }
+
+ /**
+ * Wraps an endpoint written to return a Promise into an express endpoint,
+ * producing the appropriate HTTP response and closing it for all controllable
+ * failure modes.
+ *
+ * The expected signature for the promise value (both completed and failed)
+ * is { code: int, response: object }.
+ *
+ * @param ep a request=>promise function
+ * @returns an express endpoint handler
+ */
+ wrapEndpoint(ep) {
+ return function (req, res) {
+ try {
+ ep(req).then(function (result) {
+ res.status(result.code).json(result.response);
+ }).catch(function (error) {
+ if (typeof error.code === "number" && typeof error.response !== "undefined") {
+ res.status(error.code).json(error.response);
+ } else {
+ console.error("[wrapEndpoint]", "invalid errored promise", JSON.stringify(error));
+ res.status(500).json({ error: "Internal error." });
+ }
+ });
+ } catch (e) {
+ // This should not happen, as the contract for the endpoints is to
+ // never (externally) throw, and wrap failures in the promise instead,
+ // but, as they say, better safe than sorry.
+ console.error("[wrapEndpoint]", "exception caught", e.message);
+ res.status(500).json({ error: "Internal error (exception)." });
+ }
+ }
+ }
+};
diff --git a/core/nodejsActionBase/service-account.yaml b/core/nodejsActionBase/service-account.yaml
new file mode 100644
index 0000000..bbe72c3
--- /dev/null
+++ b/core/nodejsActionBase/service-account.yaml
@@ -0,0 +1,9 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more contributor
+# license agreements; and to You under the Apache License, Version 2.0.
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: openwhisk-runtime-builder
+secrets:
+ - name: dockerhub-user-pass
diff --git a/core/nodejsActionBase/src/service.js b/core/nodejsActionBase/src/service.js
index d124ae4..eca3102 100644
--- a/core/nodejsActionBase/src/service.js
+++ b/core/nodejsActionBase/src/service.js
@@ -15,9 +15,11 @@
* limitations under the License.
*/
+
var NodeActionRunner = require('../runner');
function NodeActionService(config) {
+
var Status = {
ready: 'ready',
starting: 'starting',
@@ -25,6 +27,7 @@
stopped: 'stopped'
};
+ // TODO: save the entire configuration for use by any of the route handlers
var status = Status.ready;
var ignoreRunStatus = config.allowConcurrent === undefined ? false : config.allowConcurrent.toLowerCase() === "true";
var server = undefined;
@@ -58,11 +61,12 @@
* @param app express app
*/
this.start = function start(app) {
- server = app.listen(app.get('port'), function() {
+ server = app.listen(config.port, function() {
var host = server.address().address;
var port = server.address().port;
});
- //This is required as http server will auto disconnect in 2 minutes, this to not auto disconnect at all
+
+ // This is required as http server will auto disconnect in 2 minutes, this to not auto disconnect at all
server.timeout = 0;
};
@@ -71,7 +75,9 @@
* req.body = { main: String, code: String, binary: Boolean }
*/
this.initCode = function initCode(req) {
+
if (status === Status.ready && userCodeRunner === undefined) {
+
setStatus(Status.starting);
var body = req.body || {};
@@ -82,13 +88,14 @@
setStatus(Status.ready);
return responseMessage(200, { OK: true });
}).catch(function (error) {
- var errStr = error.stack ? String(error.stack) : error;
setStatus(Status.stopped);
- return Promise.reject(errorMessage(502, "Initialization has failed due to: " + errStr));
+ var errStr = "Initialization has failed due to: " + error.stack ? String(error.stack) : error;
+ return Promise.reject(errorMessage(502, errStr));
});
} else {
setStatus(Status.ready);
- return Promise.reject(errorMessage(403, "Missing main/no code to execute."));
+ var msg = "Missing main/no code to execute.";
+ return Promise.reject(errorMessage(403, msg));
}
} else if (userCodeRunner !== undefined) {
var msg = "Cannot initialize the action more than once.";
@@ -119,15 +126,15 @@
if (!ignoreRunStatus) {
setStatus(Status.ready);
}
-
if (typeof result !== "object") {
return errorMessage(502, "The action did not return a dictionary.");
} else {
return responseMessage(200, result);
}
}).catch(function (error) {
+ var msg = "An error has occurred: " + error;
setStatus(Status.stopped);
- return Promise.reject(errorMessage(502, "An error has occurred: " + error));
+ return Promise.reject(errorMessage(502, msg));
});
} else {
var msg = "System not ready, status is " + status + ".";
@@ -155,10 +162,12 @@
function doRun(req) {
var msg = req && req.body || {};
+ // Move per-activation keys to process env. vars with __OW_ (reserved) prefix)
Object.keys(msg).forEach(
function (k) {
if(typeof msg[k] === 'string' && k !== 'value'){
- process.env['__OW_' + k.toUpperCase()] = msg[k];
+ var envVariable = '__OW_' + k.toUpperCase();
+ process.env[envVariable] = msg[k];
}
}
);