You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by pd...@apache.org on 2019/04/17 20:30:41 UTC

[incubator-openwhisk-runtime-nodejs] branch master updated: An ability to run on Knative along with OpenWhisk (#119)

This is an automated email from the ASF dual-hosted git repository.

pdesai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk-runtime-nodejs.git


The following commit(s) were added to refs/heads/master by this push:
     new 33928f5  An ability to run on Knative along with OpenWhisk (#119)
33928f5 is described below

commit 33928f5bd5cfc4785e73a5f76fba6aeaa59f7e5e
Author: Priti Desai <pd...@us.ibm.com>
AuthorDate: Wed Apr 17 13:30:36 2019 -0700

    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
---
 core/nodejs10Action/build.gradle                   |  26 ++
 core/nodejs8Action/build.gradle                    |  26 ++
 core/nodejsActionBase/app.js                       |  96 ++--
 core/nodejsActionBase/buildtemplate.yaml           |  60 +++
 core/nodejsActionBase/docker-secret.yaml.tmpl      |  12 +
 core/nodejsActionBase/platform/knative.js          | 487 +++++++++++++++++++++
 .../platform/openwhisk.js}                         |  40 +-
 core/nodejsActionBase/platform/platform.js         | 144 ++++++
 core/nodejsActionBase/service-account.yaml         |   9 +
 core/nodejsActionBase/src/service.js               |  25 +-
 10 files changed, 840 insertions(+), 85 deletions(-)

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 @@ apply from: '../../gradle/docker.gradle'
 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 @@ task copyService(type: Copy) {
     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 @@ apply from: '../../gradle/docker.gradle'
 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 @@ task copyService(type: Copy) {
     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 }));
+
+// 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.
+ */
 
-app.post('/init', wrapEndpoint(service.initCode));
-app.post('/run',  wrapEndpoint(service.runCode));
+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/nodejs8Action/build.gradle b/core/nodejsActionBase/platform/openwhisk.js
similarity index 51%
copy from core/nodejs8Action/build.gradle
copy to core/nodejsActionBase/platform/openwhisk.js
index b37befe..eefed16 100644
--- a/core/nodejs8Action/build.gradle
+++ b/core/nodejsActionBase/platform/openwhisk.js
@@ -15,39 +15,15 @@
  * limitations under the License.
  */
 
-apply plugin: 'eclipse'
-eclipse {
-    project {
-        natures 'org.eclipse.wst.jsdt.core.jsNature'
-        buildCommand 'org.eclipse.wst.jsdt.core.javascriptValidator'
-    }
-}
-
-ext.dockerImageName = 'action-nodejs-v8'
-apply from: '../../gradle/docker.gradle'
 
-distDocker.dependsOn 'copyProxy'
-distDocker.dependsOn 'copyRunner'
-distDocker.dependsOn 'copyService'
-distDocker.finalizedBy('cleanup')
-
-task copyProxy(type: Copy) {
-    from '../nodejsActionBase/app.js'
-    into '.'
-}
+function PlatformOpenWhiskImpl(platformFactory) {
+    // Provide access to common runtime services
+    var service = platformFactory.service;
 
-task copyRunner(type: Copy) {
-    from '../nodejsActionBase/runner.js'
-    into '.'
+    this.registerHandlers = function(app, platform) {
+        app.post('/init', platformFactory.wrapEndpoint(service.initCode));
+        app.post('/run', platformFactory.wrapEndpoint(service.runCode));
+    };
 }
 
-task copyService(type: Copy) {
-    from '../nodejsActionBase/src/service.js'
-    into './src'
-}
-
-task cleanup(type: Delete) {
-    delete 'app.js'
-    delete 'runner.js'
-    delete 'src'
-}
+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 @@ function NodeActionService(config) {
         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 @@ function NodeActionService(config) {
      * @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 @@ function NodeActionService(config) {
      *  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 @@ function NodeActionService(config) {
                     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 @@ function NodeActionService(config) {
                 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 NodeActionService(config) {
 
     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];
                 }
             }
         );