Expand support for entry point handler to allow module.nested.main. (#132)
diff --git a/core/nodejsActionBase/runner.js b/core/nodejsActionBase/runner.js
index ff6fcf3..42cce76 100644
--- a/core/nodejsActionBase/runner.js
+++ b/core/nodejsActionBase/runner.js
@@ -25,6 +25,7 @@
var child_process = require('child_process');
var fs = require('fs');
var path = require('path');
+
const serializeError = require('serialize-error');
function NodeActionRunner() {
@@ -33,7 +34,7 @@
this.userScriptMain = undefined;
- this.init = function(message) {
+ this.init = function (message) {
function assertMainIsFunction() {
if (typeof thisRunner.userScriptMain !== 'function') {
throw "Action entrypoint '" + message.main + "' is not a function.";
@@ -44,19 +45,31 @@
if (message.binary) {
// The code is a base64-encoded zip file.
return unzipInTmpDir(message.code).then(function (moduleDir) {
- if(!fs.existsSync(path.join(moduleDir, 'package.json')) &&
- !fs.existsSync(path.join(moduleDir, 'index.js'))) {
- return Promise.reject('Zipped actions must contain either package.json or index.js at the root.')
+ let parts = splitMainHandler(message.main);
+ if (parts === undefined) {
+ // message.main is guaranteed to not be empty but be defensive anyway
+ return Promise.reject('Name of main function is not valid.');
}
+ // if there is only one property in the "main" handler, it is the function name
+ // and the module name is specified either from package.json or assumed to be index.js
+ let [index, main] = parts;
+
try {
// Set the executable directory to the project dir
process.chdir(moduleDir);
- thisRunner.userScriptMain = eval('require("' + moduleDir + '").' + message.main);
+
+ if (index === undefined && !fs.existsSync('package.json') && !fs.existsSync('index.js')) {
+ return Promise.reject('Zipped actions must contain either package.json or index.js at the root.');
+ }
+
+ // The module to require
+ let whatToRequire = index !== undefined ? path.join(moduleDir, index) : moduleDir;
+ thisRunner.userScriptMain = eval('require("' + whatToRequire + '").' + main);
assertMainIsFunction();
- // The value 'true' has no special meaning here;
- // the successful state is fully reflected in the
- // successful resolution of the promise.
+
+ // The value 'true' has no special meaning here; the successful state is
+ // fully reflected in the successful resolution of the promise.
return true;
} catch (e) {
return Promise.reject(e);
@@ -79,7 +92,7 @@
// Returns a Promise with the result of the user code invocation.
// The Promise is rejected iff the user code throws.
- this.run = function(args) {
+ this.run = function (args) {
return new Promise(
function (resolve, reject) {
try {
@@ -101,9 +114,9 @@
// Special case if the user just called `reject()`.
if (!error) {
- resolve({ error: {}});
+ resolve({error: {}});
} else {
- resolve({ error: serializeError(error) });
+ resolve({error: serializeError(error)});
}
});
}
@@ -132,9 +145,9 @@
}).then(function (zipFile) {
return exec(mkTempCmd).then(function (tmpDir2) {
return exec("unzip -qq " + zipFile + " -d " + tmpDir2).then(function (res) {
- return path.resolve(tmpDir2);
+ return path.resolve(tmpDir2);
}).catch(function (error) {
- return Promise.reject("There was an error uncompressing the action archive.");
+ return Promise.reject("There was an error uncompressing the action archive.");
});
});
});
@@ -154,6 +167,21 @@
}
);
}
+
+ /**
+ * Splits handler into module name and path to main.
+ * If the string contains no '.', return [ undefined, the string ].
+ * If the string contains one or more '.', return [ string up to first period, rest of the string after ].
+ */
+ function splitMainHandler(handler) {
+ let matches = handler.match(/^([^.]+)$|^([^.]+)\.(.+)$/);
+ if (matches && matches.length == 4) {
+ let index = matches[2];
+ let main = matches[3] || matches[1];
+ return [index, main]
+ } else return undefined
+ }
+
}
module.exports = NodeActionRunner;
diff --git a/tests/dat/actions/nodejs-test.zip b/tests/dat/actions/nodejs-test.zip
index a0bfa23..c7483e3 100644
--- a/tests/dat/actions/nodejs-test.zip
+++ b/tests/dat/actions/nodejs-test.zip
Binary files differ
diff --git a/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala b/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
index 49c900b..20a5fdb 100644
--- a/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
+++ b/tests/src/test/scala/runtime/actionContainers/NodeJsActionContainerTests.scala
@@ -54,54 +54,54 @@
override val testEcho = {
TestConfig("""
- |function main(args) {
- | console.log('hello stdout')
- | console.error('hello stderr')
- | return args
- |}
- """.stripMargin)
+ |function main(args) {
+ | console.log('hello stdout')
+ | console.error('hello stderr')
+ | return args
+ |}
+ """.stripMargin)
}
override val testUnicode = {
TestConfig("""
- |function main(args) {
- | var str = args.delimiter + " ☃ " + args.delimiter;
- | console.log(str);
- | return { "winter": str };
- |}
- """.stripMargin.trim)
+ |function main(args) {
+ | var str = args.delimiter + " ☃ " + args.delimiter;
+ | console.log(str);
+ | return { "winter": str };
+ |}
+ """.stripMargin.trim)
}
override val testEnv = {
TestConfig("""
- |function main(args) {
- | return {
- | "api_host": process.env['__OW_API_HOST'],
- | "api_key": process.env['__OW_API_KEY'],
- | "namespace": process.env['__OW_NAMESPACE'],
- | "action_name": process.env['__OW_ACTION_NAME'],
- | "activation_id": process.env['__OW_ACTIVATION_ID'],
- | "deadline": process.env['__OW_DEADLINE']
- | }
- |}
- """.stripMargin.trim)
+ |function main(args) {
+ | return {
+ | "api_host": process.env['__OW_API_HOST'],
+ | "api_key": process.env['__OW_API_KEY'],
+ | "namespace": process.env['__OW_NAMESPACE'],
+ | "action_name": process.env['__OW_ACTION_NAME'],
+ | "activation_id": process.env['__OW_ACTIVATION_ID'],
+ | "deadline": process.env['__OW_DEADLINE']
+ | }
+ |}
+ """.stripMargin.trim)
}
override val testInitCannotBeCalledMoreThanOnce = {
TestConfig("""
- |function main(args) {
- | return args
- |}
- """.stripMargin)
+ |function main(args) {
+ | return args
+ |}
+ """.stripMargin)
}
override val testEntryPointOtherThanMain = {
TestConfig(
"""
- | function niam(args) {
- | return args;
- | }
- """.stripMargin,
+ | function niam(args) {
+ | return args;
+ | }
+ """.stripMargin,
main = "niam")
}
@@ -115,10 +115,11 @@
it should "fail to initialize with bad code" in {
val (out, err) = withNodeJsContainer { c =>
- val code = """
- | 10 PRINT "Hello world!"
- | 20 GOTO 10
- """.stripMargin
+ val code =
+ """
+ | 10 PRINT "Hello world!"
+ | 20 GOTO 10
+ """.stripMargin
val (initCode, _) = c.init(initPayload(code))
@@ -148,11 +149,12 @@
it should "return some error on action error" in {
withNodeJsContainer { c =>
- val code = """
- | function main(args) {
- | throw "nooooo";
- | }
- """.stripMargin
+ val code =
+ """
+ | function main(args) {
+ | throw "nooooo";
+ | }
+ """.stripMargin
val (initCode, _) = c.init(initPayload(code))
initCode should be(200)
@@ -168,11 +170,12 @@
it should "support application errors" in {
withNodeJsContainer { c =>
- val code = """
- | function main(args) {
- | return { "error" : "sorry" };
- | }
- """.stripMargin;
+ val code =
+ """
+ | function main(args) {
+ | return { "error" : "sorry" };
+ | }
+ """.stripMargin;
val (initCode, _) = c.init(initPayload(code))
initCode should be(200)
@@ -187,18 +190,19 @@
it should "support the documentation examples (1)" in {
val (out, err) = withNodeJsContainer { c =>
- val code = """
- | // an action in which each path results in a synchronous activation
- | function main(params) {
- | if (params.payload == 0) {
- | return;
- | } else if (params.payload == 1) {
- | return {payload: 'Hello, World!'} // indicates normal completion
- | } else if (params.payload == 2) {
- | return {error: 'payload must be 0 or 1'} // indicates abnormal completion
- | }
- | }
- """.stripMargin
+ val code =
+ """
+ | // an action in which each path results in a synchronous activation
+ | function main(params) {
+ | if (params.payload == 0) {
+ | return;
+ | } else if (params.payload == 1) {
+ | return {payload: 'Hello, World!'} // indicates normal completion
+ | } else if (params.payload == 2) {
+ | return {error: 'payload must be 0 or 1'} // indicates abnormal completion
+ | }
+ | }
+ """.stripMargin
c.init(initPayload(code))._1 should be(200)
@@ -226,21 +230,22 @@
it should "support the documentation examples (2)" in {
val (out, err) = withNodeJsContainer { c =>
- val code = """
- | function main(params) {
- | if (params.payload) {
- | // asynchronous activation
- | return new Promise(function(resolve, reject) {
- | setTimeout(function() {
- | resolve({ done: true });
- | }, 100);
- | })
- | } else {
- | // synchronous activation
- | return {done: true};
- | }
- | }
- """.stripMargin
+ val code =
+ """
+ | function main(params) {
+ | if (params.payload) {
+ | // asynchronous activation
+ | return new Promise(function(resolve, reject) {
+ | setTimeout(function() {
+ | resolve({ done: true });
+ | }, 100);
+ | })
+ | } else {
+ | // synchronous activation
+ | return {done: true};
+ | }
+ | }
+ """.stripMargin
c.init(initPayload(code))._1 should be(200)
@@ -266,11 +271,12 @@
// of the package below ever being valid.
// https://docs.npmjs.com/files/package.json
val (out, err) = withNodeJsContainer { c =>
- val code = """
- | function main(args) {
- | require('.mildlyinvalidnameofanonexistentpackage');
- | }
- """.stripMargin
+ val code =
+ """
+ | function main(args) {
+ | require('.mildlyinvalidnameofanonexistentpackage');
+ | }
+ """.stripMargin
val (initCode, _) = c.init(initPayload(code))
@@ -290,11 +296,12 @@
it should "have openwhisk package available" in {
// GIVEN that it should "error when requiring a non-existent package" (see test above for this)
val (out, err) = withNodeJsContainer { c =>
- val code = """
- | function main(args) {
- | require('openwhisk');
- | }
- """.stripMargin
+ val code =
+ """
+ | function main(args) {
+ | require('openwhisk');
+ | }
+ """.stripMargin
val (initCode, _) = c.init(initPayload(code))
@@ -316,15 +323,16 @@
it should "support resolved promises" in {
val (out, err) = withNodeJsContainer { c =>
- val code = """
- | function main(args) {
- | return new Promise(function(resolve, reject) {
- | setTimeout(function() {
- | resolve({ done: true });
- | }, 100);
- | })
- | }
- """.stripMargin
+ val code =
+ """
+ | function main(args) {
+ | return new Promise(function(resolve, reject) {
+ | setTimeout(function() {
+ | resolve({ done: true });
+ | }, 100);
+ | })
+ | }
+ """.stripMargin
c.init(initPayload(code))._1 should be(200)
@@ -342,15 +350,16 @@
it should "support rejected promises" in {
val (out, err) = withNodeJsContainer { c =>
- val code = """
- | function main(args) {
- | return new Promise(function(resolve, reject) {
- | setTimeout(function() {
- | reject({ done: true });
- | }, 100);
- | })
- | }
- """.stripMargin
+ val code =
+ """
+ | function main(args) {
+ | return new Promise(function(resolve, reject) {
+ | setTimeout(function() {
+ | reject({ done: true });
+ | }, 100);
+ | })
+ | }
+ """.stripMargin
c.init(initPayload(code))._1 should be(200)
@@ -369,12 +378,13 @@
it should "support rejected promises with no message" in {
val (out, err) = withNodeJsContainer { c =>
- val code = """
- | function main(args) {
- | return new Promise(function (resolve, reject) {
- | reject();
- | });
- | }""".stripMargin
+ val code =
+ """
+ | function main(args) {
+ | return new Promise(function (resolve, reject) {
+ | reject();
+ | });
+ | }""".stripMargin
c.init(initPayload(code))._1 should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject()))
@@ -386,14 +396,16 @@
val thought = " I took the one less traveled by, and that has made all the difference."
val assignment = " x = \"" + thought + "\";\n"
- val code = """
- | function main(args) {
- | var x = "hello";
- """.stripMargin + (assignment * 7000) + """
- | x = "world";
- | return { "message" : x };
- | }
- """.stripMargin
+ val code =
+ """
+ | function main(args) {
+ | var x = "hello";
+ """.stripMargin + (assignment * 7000) +
+ """
+ | x = "world";
+ | return { "message" : x };
+ | }
+ """.stripMargin
// Lest someone should make it too easy.
code.length should be >= 500000
@@ -414,29 +426,31 @@
})
}
- val examplePackageDotJson: String = """
- | {
- | "name": "wskaction",
- | "version": "1.0.0",
- | "description": "An OpenWhisk action as an npm package.",
- | "main": "index.js",
- | "author": "info@openwhisk.org",
- | "license": "Apache-2.0"
- | }
+ val examplePackageDotJson: String =
+ """
+ | {
+ | "name": "wskaction",
+ | "version": "1.0.0",
+ | "description": "An OpenWhisk action as an npm package.",
+ | "main": "index.js",
+ | "author": "info@openwhisk.org",
+ | "license": "Apache-2.0"
+ | }
""".stripMargin
it should "support zip-encoded npm package actions" in {
val srcs = Seq(
Seq("package.json") -> examplePackageDotJson,
- Seq("index.js") -> """
- | exports.main = function (args) {
- | var name = typeof args["name"] === "string" ? args["name"] : "stranger";
- |
- | return {
- | greeting: "Hello " + name + ", from an npm package action."
- | };
- | }
- """.stripMargin)
+ Seq("index.js") ->
+ """
+ | exports.main = function (args) {
+ | var name = typeof args["name"] === "string" ? args["name"] : "stranger";
+ |
+ | return {
+ | greeting: "Hello " + name + ", from an npm package action."
+ | };
+ | }
+ """.stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
@@ -458,15 +472,16 @@
it should "support zip-encoded npm package actions without a package.json file" in {
val srcs = Seq(
- Seq("index.js") -> """
- | exports.main = function (args) {
- | var name = typeof args["name"] === "string" ? args["name"] : "stranger";
- |
- | return {
- | greeting: "Hello " + name + ", from an npm package action without a package.json."
- | };
- | }
- """.stripMargin)
+ Seq("index.js") ->
+ """
+ | exports.main = function (args) {
+ | var name = typeof args["name"] === "string" ? args["name"] : "stranger";
+ |
+ | return {
+ | greeting: "Hello " + name + ", from an npm package action without a package.json."
+ | };
+ | }
+ """.stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
@@ -487,6 +502,99 @@
})
}
+ it should "support zip-encoded npm package with main from file other than index.js" in {
+ val srcs = Seq(
+ Seq("other.js") ->
+ """
+ | exports.niam = function (args) {
+ | var name = typeof args["name"] === "string" ? args["name"] : "stranger";
+ |
+ | return {
+ | greeting: "Hello " + name + ", from other.niam."
+ | };
+ | }
+ """.stripMargin)
+
+ val code = ZipBuilder.mkBase64Zip(srcs)
+
+ val (out, err) = withNodeJsContainer { c =>
+ c.init(initPayload(code, "other.niam"))._1 should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runCode should be(200)
+ runRes.get.fields.get("greeting") shouldBe Some(JsString("Hello stranger, from other.niam."))
+ }
+
+ checkStreams(out, err, {
+ case (o, e) =>
+ o shouldBe empty
+ e shouldBe empty
+ })
+ }
+
+ it should "support nested main" in {
+ val srcs = Seq(
+ Seq("other.js") ->
+ """
+ | exports.niam = { xyz: function (args) {
+ | var name = typeof args["name"] === "string" ? args["name"] : "stranger";
+ |
+ | return {
+ | greeting: "Hello " + name + ", from nested main."
+ | };
+ | } }
+ """.stripMargin)
+
+ val code = ZipBuilder.mkBase64Zip(srcs)
+
+ val (out, err) = withNodeJsContainer { c =>
+ c.init(initPayload(code, "other.niam.xyz"))._1 should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runCode should be(200)
+ runRes.get.fields.get("greeting") shouldBe Some(JsString("Hello stranger, from nested main."))
+ }
+
+ checkStreams(out, err, {
+ case (o, e) =>
+ o shouldBe empty
+ e shouldBe empty
+ })
+ }
+
+ it should "allow zip-encoded npm package containing package.json and overriding entry point" in {
+ val srcs = Seq(
+ Seq("package.json") -> examplePackageDotJson,
+ Seq("index.js") ->
+ """
+ | exports.main = function (args) {
+ | return { result: "it works" };
+ | }
+ """,
+ Seq("other.js") ->
+ """
+ | exports.niam = function (args) {
+ | return { result: "it should also work" };
+ | }
+ """.stripMargin)
+
+ val code = ZipBuilder.mkBase64Zip(srcs)
+
+ val (out, err) = withNodeJsContainer { c =>
+ c.init(initPayload(code, "other.niam"))._1 should be(200)
+
+ val (runCode, runRes) = c.run(runPayload(JsObject()))
+ runCode should be(200)
+ runRes.get.fields.get("result") shouldBe Some(JsString("it should also work"))
+ }
+
+ checkStreams(out, err, {
+ case (o, e) =>
+ o shouldBe empty
+ e shouldBe empty
+ })
+ }
+
it should "fail gracefully on invalid zip files" in {
// Some text-file encoded to base64.
val code = "Q2VjaSBuJ2VzdCBwYXMgdW4gemlwLgo="
@@ -504,9 +612,11 @@
}
it should "fail gracefully on valid zip files that are not actions" in {
- val srcs = Seq(Seq("hello") -> """
- | Hello world!
- """.stripMargin)
+ val srcs = Seq(
+ Seq("hello") ->
+ """
+ | Hello world!
+ """.stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
@@ -524,11 +634,12 @@
it should "support zipped actions using non-default entry point" in {
val srcs = Seq(
Seq("package.json") -> examplePackageDotJson,
- Seq("index.js") -> """
- | exports.niam = function (args) {
- | return { result: "it works" };
- | }
- """.stripMargin)
+ Seq("index.js") ->
+ """
+ | exports.niam = function (args) {
+ | return { result: "it works" };
+ | }
+ """.stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
@@ -544,13 +655,14 @@
val srcs = Seq(
Seq("package.json") -> examplePackageDotJson,
Seq("test.txt") -> "test text",
- Seq("index.js") -> s"""
- | const fs = require('fs');
- | exports.main = function (args) {
- | const fileData = fs.readFileSync('./test.txt').toString();
- | return { result1: fileData,
- | result2: __dirname === process.cwd() };
- | }
+ Seq("index.js") ->
+ s"""
+ | const fs = require('fs');
+ | exports.main = function (args) {
+ | const fileData = fs.readFileSync('./test.txt').toString();
+ | return { result1: fileData,
+ | result2: __dirname === process.cwd() };
+ | }
""".stripMargin)
val code = ZipBuilder.mkBase64Zip(srcs)
@@ -566,12 +678,13 @@
it should "support default function parameters" in {
val (out, err) = withNodeJsContainer { c =>
- val code = """
- | function main(args) {
- | let foo = 3;
- | return {isValid: (function (a, b = 2) {return a === 3 && b === 2;}(foo))};
- | }
- """.stripMargin
+ val code =
+ """
+ | function main(args) {
+ | let foo = 3;
+ | return {isValid: (function (a, b = 2) {return a === 3 && b === 2;}(foo))};
+ | }
+ """.stripMargin
val (initCode, _) = c.init(initPayload(code))
initCode should be(200)
@@ -596,7 +709,7 @@
c.init(initPayload(code))._1 should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject()))
- runRes.get.fields.get("message") shouldBe Some(JsString("success"))
+ runRes.get.fields.get("message") shouldBe Some(JsString("hello local library"))
}
}
@@ -604,14 +717,14 @@
withContainer(nodejsTestDockerImageName) { c =>
val code =
"""
- | function main(args) {
- | var ow = require('openwhisk');
- | // actions only exists on 2.* versions of openwhisk, not 3.*, so if this was 3.* it would throw an error,
- | var actions = ow().actions;
- |
- | return { "message": "success" };
- |}
- """.stripMargin
+ | function main(args) {
+ | var ow = require('openwhisk');
+ | // actions only exists on 2.* versions of openwhisk, not 3.*, so if this was 3.* it would throw an error,
+ | var actions = ow().actions;
+ |
+ | return { "message": "success" };
+ |}
+ """.stripMargin
// Initialization of the code should be successful
val (initCode, err) = c.init(initPayload(code))
initCode should be(200)