Actionloop java (#84)
diff --git a/.gitignore b/.gitignore
index f65f024..1baf0a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,11 @@
results
*.retry
+# Java artifacts
+*.class
+*.jar
+*.zip
+
# Environments
/ansible/environments/*
!/ansible/environments/distributed
@@ -70,3 +75,4 @@
!tests/dat/actions/python_virtualenv_dir.zip
!tests/dat/actions/python_virtualenv_name.zip
!tests/dat/actions/zippedaction.zip
+
diff --git a/.travis.yml b/.travis.yml
index 685e8e8..936b1ef 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,7 +8,11 @@
- 2.12.7
services:
- docker
-
+# required to support multi-stage build
+addons:
+ apt:
+ packages:
+ - docker-ce
notifications:
email: false
webhooks:
diff --git a/core/java8/proxy/src/main/java/org/apache/openwhisk/runtime/java/action/JarLoader.java b/core/java8/proxy/src/main/java/org/apache/openwhisk/runtime/java/action/JarLoader.java
index 03f7c90..e17d0a9 100644
--- a/core/java8/proxy/src/main/java/org/apache/openwhisk/runtime/java/action/JarLoader.java
+++ b/core/java8/proxy/src/main/java/org/apache/openwhisk/runtime/java/action/JarLoader.java
@@ -68,7 +68,6 @@
if (m.getReturnType() != JsonObject.class || !Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {
throw new NoSuchMethodException("main");
}
-
this.mainMethod = m;
}
diff --git a/core/java8actionloop/Dockerfile b/core/java8actionloop/Dockerfile
new file mode 100644
index 0000000..53b4f35
--- /dev/null
+++ b/core/java8actionloop/Dockerfile
@@ -0,0 +1,48 @@
+#
+# 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.
+#
+FROM golang:1.12 as builder
+RUN env CGO_ENABLED=0 go get github.com/apache/incubator-openwhisk-runtime-go/main
+
+FROM adoptopenjdk/openjdk8:x86_64-ubuntu-jdk8u212-b03
+
+RUN rm -rf /var/lib/apt/lists/* && apt-get clean && apt-get update \
+ && apt-get install -y --no-install-recommends locales python vim \
+ && rm -rf /var/lib/apt/lists/* \
+ && locale-gen en_US.UTF-8
+
+ENV LANG="en_US.UTF-8" \
+ LANGUAGE="en_US:en" \
+ LC_ALL="en_US.UTF-8" \
+ VERSION=8 \
+ UPDATE=212 \
+ BUILD=3
+
+RUN locale-gen en_US.UTF-8 ;\
+ mkdir -p /javaAction/action /usr/java/src /usr/java/lib
+
+WORKDIR /javaAction
+COPY --from=builder /go/bin/main /bin/proxy
+
+ADD https://search.maven.org/remotecontent?filepath=com/google/code/gson/gson/2.8.5/gson-2.8.5.jar /usr/java/lib/gson-2.8.5.jar
+ADD lib/src/Launcher.java /usr/java/src/Launcher.java
+RUN cd /usr/java/src ;\
+ javac -cp /usr/java/lib/gson-2.8.5.jar Launcher.java ;\
+ jar cvf /usr/java/lib/launcher.jar *.class
+ADD bin/compile /bin/compile
+ENV OW_COMPILER=/bin/compile
+ENV OW_SAVE_JAR=exec.jar
+ENTRYPOINT /bin/proxy
diff --git a/core/java8actionloop/Makefile b/core/java8actionloop/Makefile
new file mode 100644
index 0000000..61e98d4
--- /dev/null
+++ b/core/java8actionloop/Makefile
@@ -0,0 +1,55 @@
+IMG=actionloop-java-v8:latest
+PREFIX=docker.io/openwhisk
+INVOKE=python ../../tools/invoke.py
+MAIN_JAR=../../example/main.jar
+
+build:
+ docker build -t $(IMG) .
+
+push: build
+ docker login
+ docker tag $(IMG) $(PREFIX)/$(IMG)
+ docker push $(PREFIX)/$(IMG)
+
+clean:
+ docker rmi -f $(IMG)
+
+start: build
+ docker run -p 8080:8080 -ti -v $(PWD):/proxy $(IMG)
+
+debug: build
+ docker run -p 8080:8080 -ti --entrypoint=/bin/bash -v $(PWD):/mnt -e OW_COMPILER=/mnt/bin/compile $(IMG)
+
+.PHONY: build push clean start debug
+
+$(MAIN_JAR):
+ $(MAKE) $< -C ../../example main.jar
+
+## You need to execute make start in another terminal
+
+test-source:
+ $(INVOKE) init ../../example/Main.java
+ $(INVOKE) run '{}'
+ $(INVOKE) run '{"name":"Mike"}'
+
+test-source-hello:
+ $(INVOKE) init action.Hello#hello ../../example/action/Hello.java
+ $(INVOKE) run '{}'
+ $(INVOKE) run '{"name":"Mike"}'
+
+test-jar: $(MAIN_JAR)
+ $(INVOKE) init action.Hello#hello $(MAIN_JAR)
+ $(INVOKE) run '{}'
+ $(INVOKE) run '{"name":"Mike"}'
+
+test-src-zip:
+ $(MAKE) -C ../../example src.zip
+ $(INVOKE) init ../../example/src.zip
+ $(INVOKE) run '{}'
+ $(INVOKE) run '{"name":"Mike"}'
+
+test-bin-zip:
+ $(MAKE) -C ../../example bin.zip
+ $(INVOKE) init ../../example/bin.zip
+ $(INVOKE) run '{}'
+ $(INVOKE) run '{"name":"Mike"}'
diff --git a/core/java8actionloop/bin/compile b/core/java8actionloop/bin/compile
new file mode 100755
index 0000000..6f992b6
--- /dev/null
+++ b/core/java8actionloop/bin/compile
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+"""Java Action Builder
+#
+# 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.
+#
+"""
+
+from __future__ import print_function
+from os.path import abspath, exists, dirname
+import os, sys, codecs, subprocess, shutil, logging
+
+def copy(src, dst):
+ with codecs.open(src, 'r', 'utf-8') as s:
+ body = s.read()
+ with codecs.open(dst, 'w', 'utf-8') as d:
+ d.write(body)
+
+def find_with_ext(basedir, ext):
+ result = []
+ for root, _, files in os.walk(basedir, topdown=False):
+ for name in files:
+ if name.endswith(ext):
+ result.append(os.path.join(root,name))
+ return result
+
+def javac(sources, classpath, target_dir):
+ cmd = [ "javac",
+ "-encoding", "UTF-8",
+ "-cp", ":".join(classpath),
+ "-d", target_dir
+ ]+sources
+ #print(cmd)
+ logging.info(" ".join(cmd))
+ p = subprocess.Popen(cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ (o, e) = p.communicate()
+ if isinstance(o, bytes) and not isinstance(o, str):
+ o = o.decode('utf-8')
+ if isinstance(e, bytes) and not isinstance(e, str):
+ e = e.decode('utf-8')
+ ok = True
+ if o:
+ ok = False
+ sys.stdout.write(o)
+ sys.stdout.flush()
+ if e:
+ ok = False
+ sys.stderr.write(e)
+ sys.stderr.flush()
+ return ok
+
+def build(source_dir, classpath, target_dir, mainClass):
+
+ # copy exec to <main>.java if it is there
+ src = "%s/exec" % source_dir
+ if os.path.isfile(src):
+ main_java = "%s/%s.java" % (source_dir, mainClass.split(".")[-1])
+ copy(src,main_java)
+ logging.info("renamed exec to %s", main_java)
+
+ # look for sources and compile
+ sources = find_with_ext(source_dir, ".java")
+ if len(sources) > 0:
+ jars = find_with_ext(source_dir, ".jar")
+ logging.info("compiling %d sources with %d jars", len(sources),len(jars))
+ return javac(sources, classpath+jars, source_dir)
+
+ return True
+
+def write_exec(target_dir, classpath, main):
+ launcher = "%s/exec" % target_dir
+ jars = find_with_ext(target_dir, ".jar")
+ jars.append(target_dir)
+ cmd = """#!/bin/bash
+cd "%s"
+/opt/java/openjdk/bin/java -Dfile.encoding=UTF-8 -cp "%s" Launcher "%s" "$@"
+""" %( target_dir, ":".join(classpath+jars), main)
+ with codecs.open(launcher, 'w', 'utf-8') as d:
+ d.write(cmd)
+ os.chmod(launcher, 0o755)
+
+def parseMain(main):
+ if main == "main":
+ return "Main", "main"
+ a = main.split("#")
+ if(len(a)==1):
+ return a[0], "main"
+ return a[0], a[1]
+
+def assemble(argv):
+ mainClass, mainMethod = parseMain(argv[1])
+ logging.info("%s %s", mainClass, mainMethod)
+ source_dir = os.path.abspath(argv[2])
+ target_dir = os.path.abspath(argv[3])
+ classpath = ["/usr/java/lib/launcher.jar", "/usr/java/lib/gson-2.8.5.jar"]
+
+ # build
+ if build(source_dir, classpath, target_dir, mainClass):
+ shutil.rmtree(target_dir)
+ shutil.move(source_dir, target_dir)
+ logging.info("moved %s to %s", source_dir, target_dir)
+ # write the launcher is it is there
+ write_exec(target_dir, classpath, "%s#%s" % (mainClass, mainMethod))
+ # launch it to check it can load with immediate exit - it should not produce any output
+ subprocess.call(["%s/exec" % target_dir, "-exit"])
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+if __name__ == '__main__':
+ if len(sys.argv) < 4:
+ sys.stdout.write("usage: <main-class> <source-dir> <target-dir>\n")
+ sys.exit(1)
+ logging.basicConfig(filename="/var/log/compile.log")
+ assemble(sys.argv)
diff --git a/core/java8actionloop/build.gradle b/core/java8actionloop/build.gradle
new file mode 100644
index 0000000..1a7ddcf
--- /dev/null
+++ b/core/java8actionloop/build.gradle
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+ext.dockerImageName = 'actionloop-java-v8'
+apply from: '../../gradle/docker.gradle'
diff --git a/core/java8actionloop/lib/src/Launcher.java b/core/java8actionloop/lib/src/Launcher.java
new file mode 100644
index 0000000..ef571e9
--- /dev/null
+++ b/core/java8actionloop/lib/src/Launcher.java
@@ -0,0 +1,177 @@
+/*
+ * 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.
+ */
+
+import java.io.*;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import com.google.gson.*;
+import java.security.Permission;
+import java.lang.reflect.InvocationTargetException;
+
+class Launcher {
+
+ private static String mainClassName = "Main";
+ private static String mainMethodName = "main";
+ private static Class mainClass = null;
+ private static Method mainMethod = null;
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private static void augmentEnv(Map<String, String> newEnv) {
+ try {
+ for (Class cl : Collections.class.getDeclaredClasses()) {
+ if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
+ Field field = cl.getDeclaredField("m");
+ field.setAccessible(true);
+ Object obj = field.get(System.getenv());
+ Map<String, String> map = (Map<String, String>) obj;
+ map.putAll(newEnv);
+ }
+ }
+ } catch (Exception e) {}
+ }
+
+ private static void initMain(String[] args) throws Exception {
+ if(args.length > 0)
+ mainClassName = args[0];
+ int pos = mainClassName.indexOf("#");
+ if(pos != -1) {
+ if(pos + 1 != mainClassName.length())
+ mainMethodName = args[0].substring(pos+1);
+ mainClassName = args[0].substring(0,pos);
+ }
+
+ mainClass = Class.forName(mainClassName);
+ Method m = mainClass.getMethod(mainMethodName, new Class[] { JsonObject.class });
+ m.setAccessible(true);
+ int modifiers = m.getModifiers();
+ if (m.getReturnType() != JsonObject.class || !Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {
+ throw new NoSuchMethodException(mainMethodName);
+ }
+ mainMethod = m;
+ }
+
+ private static JsonObject invokeMain(JsonObject arg, Map<String, String> env) throws Exception {
+ augmentEnv(env);
+ return (JsonObject) mainMethod.invoke(null, arg);
+ }
+
+ private static SecurityManager defaultSecurityManager = null;
+ private static void installSecurityManager() {
+ defaultSecurityManager = System.getSecurityManager();
+ System.setSecurityManager(new SecurityManager() {
+ @Override
+ public void checkPermission(Permission p) {
+ // Not throwing means accepting anything.
+ }
+
+ @Override
+ public void checkPermission(Permission p, Object ctx) {
+ // Not throwing means accepting anything.
+ }
+
+ @Override
+ public void checkExit(int status) {
+ super.checkExit(status);
+ throw new SecurityException("System.exit(" + status + ") called from within an action.");
+ }
+ });
+ }
+
+ private static void uninstallSecurityManager() {
+ if(defaultSecurityManager != null) {
+ System.setSecurityManager(defaultSecurityManager);
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+
+ initMain(args);
+
+ // exit after main class loading if "exit" specified
+ // used to check healthy launch after init
+ if(args.length >1 && args[1] == "-exit")
+ System.exit(0);
+
+ // install a security manager to prevent exit
+ installSecurityManager();
+
+ BufferedReader in = new BufferedReader(
+ new InputStreamReader(System.in, "UTF-8"));
+ PrintWriter out = new PrintWriter(
+ new OutputStreamWriter(
+ new FileOutputStream("/dev/fd/3"), "UTF-8"));
+ JsonParser json = new JsonParser();
+ JsonObject empty = json.parse("{}").getAsJsonObject();
+ String input = "";
+ while (true) {
+ try {
+ input = in.readLine();
+ if (input == null)
+ break;
+ JsonElement element = json.parse(input);
+ JsonObject payload = empty.deepCopy();
+ HashMap<String, String> env = new HashMap<String, String>();
+ if (element.isJsonObject()) {
+ // collect payload and environment
+ for (Map.Entry<String, JsonElement> entry : element.getAsJsonObject().entrySet()) {
+ if (entry.getKey().equals("value")) {
+ if (entry.getValue().isJsonObject())
+ payload = entry.getValue().getAsJsonObject();
+ } else {
+ env.put(String.format("__OW_%s", entry.getKey().toUpperCase()),
+ entry.getValue().getAsString());
+ }
+ }
+ augmentEnv(env);
+ }
+ JsonElement response = invokeMain(payload, env);
+ out.println(response.toString());
+ } catch(NullPointerException npe) {
+ System.out.println("the action returned null");
+ npe.printStackTrace(System.err);
+ JsonObject error = new JsonObject();
+ error.addProperty("error", "the action returned null");
+ out.println(error.toString());
+ out.flush();
+ } catch(InvocationTargetException ite) {
+ Throwable ex = ite;
+ if(ite.getCause() != null)
+ ex = ite.getCause();
+ ex.printStackTrace(System.err);
+ JsonObject error = new JsonObject();
+ error.addProperty("error", ex.getMessage());
+ out.println(error.toString());
+ out.flush();
+ } catch (Exception ex) {
+ ex.printStackTrace(System.err);
+ JsonObject error = new JsonObject();
+ error.addProperty("error", ex.getMessage());
+ out.println(error.toString());
+ out.flush();
+ }
+ out.flush();
+ System.out.flush();
+ System.err.flush();
+ }
+ uninstallSecurityManager();
+ }
+}
+
diff --git a/settings.gradle b/settings.gradle
index 464ae37..b9557da 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -19,6 +19,7 @@
include 'core:java8'
include 'core:java8:proxy'
+include 'core:java8actionloop'
rootProject.name = 'runtime-java'
diff --git a/tests/src/test/scala/actionContainers/JavaActionContainerTests.scala b/tests/src/test/scala/actionContainers/JavaActionContainerTests.scala
index a990f33..052847c 100644
--- a/tests/src/test/scala/actionContainers/JavaActionContainerTests.scala
+++ b/tests/src/test/scala/actionContainers/JavaActionContainerTests.scala
@@ -24,13 +24,21 @@
import spray.json._
import actionContainers.ResourceHelpers.JarBuilder
import actionContainers.ActionContainer.withContainer
+import org.scalatest.Matchers
@RunWith(classOf[JUnitRunner])
-class JavaActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
+class JavaActionContainerTests extends BasicActionRunnerTests with WskActorSystem with Matchers {
+
+ val image = "java8action"
+ val errPrefix = "\"An error has occurred (see logs for details):"
+ val checkStreamsAtInit = true
+ // this is true for the Java based runtime but not for actionloop
+ // since it does not parse the output and the invoker change the status anyway
+ val runtimeDetectErrors = true
// Helpers specific to java actions
override def withActionContainer(env: Map[String, String] = Map.empty)(
- code: ActionContainer => Unit): (String, String) = withContainer("java8action", env)(code)
+ code: ActionContainer => Unit): (String, String) = withContainer(image, env)(code)
behavior of "Java action"
@@ -48,23 +56,23 @@
JarBuilder.mkBase64Jar(
Seq("example", "HelloWhisk.java") ->
"""
- | package example;
- |
- | import com.google.gson.JsonObject;
- |
- | public class HelloWhisk {
- | public static JsonObject main(JsonObject args) {
- | JsonObject response = new JsonObject();
- | response.addProperty("api_host", System.getenv("__OW_API_HOST"));
- | response.addProperty("api_key", System.getenv("__OW_API_KEY"));
- | response.addProperty("namespace", System.getenv("__OW_NAMESPACE"));
- | response.addProperty("action_name", System.getenv("__OW_ACTION_NAME"));
- | response.addProperty("activation_id", System.getenv("__OW_ACTIVATION_ID"));
- | response.addProperty("deadline", System.getenv("__OW_DEADLINE"));
- | return response;
- | }
- | }
- """.stripMargin.trim),
+ | package example;
+ |
+ | import com.google.gson.JsonObject;
+ |
+ | public class HelloWhisk {
+ | public static JsonObject main(JsonObject args) {
+ | JsonObject response = new JsonObject();
+ | response.addProperty("api_host", System.getenv("__OW_API_HOST"));
+ | response.addProperty("api_key", System.getenv("__OW_API_KEY"));
+ | response.addProperty("namespace", System.getenv("__OW_NAMESPACE"));
+ | response.addProperty("action_name", System.getenv("__OW_ACTION_NAME"));
+ | response.addProperty("activation_id", System.getenv("__OW_ACTIVATION_ID"));
+ | response.addProperty("deadline", System.getenv("__OW_DEADLINE"));
+ | return response;
+ | }
+ | }
+ """.stripMargin.trim),
main = "example.HelloWhisk")
}
@@ -73,19 +81,20 @@
JarBuilder.mkBase64Jar(
Seq("example", "HelloWhisk.java") ->
"""
- | package example;
- |
- | import com.google.gson.JsonObject;
- |
- | public class HelloWhisk {
- | public static JsonObject main(JsonObject args) {
- | System.out.println("hello stdout");
- | System.err.println("hello stderr");
- | return args;
- | }
- | }
- """.stripMargin.trim),
+ | package example;
+ |
+ | import com.google.gson.JsonObject;
+ |
+ | public class HelloWhisk {
+ | public static JsonObject main(JsonObject args) {
+ | System.out.println("hello stdout");
+ | System.err.println("hello stderr");
+ | return args;
+ | }
+ | }
+ """.stripMargin.trim),
"example.HelloWhisk")
+
}
override val testUnicode = {
@@ -93,21 +102,21 @@
JarBuilder.mkBase64Jar(
Seq("example", "HelloWhisk.java") ->
"""
- | package example;
- |
- | import com.google.gson.JsonObject;
- |
- | public class HelloWhisk {
- | public static JsonObject main(JsonObject args) {
- | String delimiter = args.getAsJsonPrimitive("delimiter").getAsString();
- | JsonObject response = new JsonObject();
- | String str = delimiter + " ☃ " + delimiter;
- | System.out.println(str);
- | response.addProperty("winter", str);
- | return response;
- | }
- | }
- """.stripMargin),
+ | package example;
+ |
+ | import com.google.gson.JsonObject;
+ |
+ | public class HelloWhisk {
+ | public static JsonObject main(JsonObject args) {
+ | String delimiter = args.getAsJsonPrimitive("delimiter").getAsString();
+ | JsonObject response = new JsonObject();
+ | String str = delimiter + " ☃ " + delimiter;
+ | System.out.println(str);
+ | response.addProperty("winter", str);
+ | return response;
+ | }
+ | }
+ """.stripMargin),
"example.HelloWhisk")
}
@@ -115,15 +124,15 @@
JarBuilder.mkBase64Jar(
Seq("example", "HelloWhisk.java") ->
s"""
- | package example;
- |
+ | package example;
+ |
| import com.google.gson.JsonObject;
- |
+ |
| public class HelloWhisk {
- | public static JsonObject $main(JsonObject args) {
- | return args;
- | }
- | }
+ | public static JsonObject $main(JsonObject args) {
+ | return args;
+ | }
+ | }
""".stripMargin.trim)
}
@@ -145,21 +154,27 @@
val (initCode, out) = c.init(initPayload(echo("hello"), s"example.HelloWhisk$m"))
initCode shouldBe 502
- out shouldBe {
- val error = m match {
- case c if c == "x" || c == "!" => s"java.lang.ClassNotFoundException: example.HelloWhisk$c"
- case "#bogus" => "java.lang.NoSuchMethodException: example.HelloWhisk.bogus(com.google.gson.JsonObject)"
- case _ => "java.lang.NoSuchMethodException: example.HelloWhisk.main(com.google.gson.JsonObject)"
- }
- Some(JsObject("error" -> s"An error has occurred (see logs for details): $error".toJson))
+ val expected = m match {
+ case c if c == "x" || c == "!" => s"$errPrefix java.lang.ClassNotFoundException: example.HelloWhisk$c"
+ case "#bogus" =>
+ s"$errPrefix java.lang.NoSuchMethodException: example.HelloWhisk.bogus(com.google.gson.JsonObject)"
+ case _ => s"$errPrefix java.lang.NoSuchMethodException: example.HelloWhisk.main(com.google.gson.JsonObject)"
}
+
+ val error = out.get.fields.get("error").get.toString()
+
+ println(error)
+ println(expected)
+
+ error should startWith(expected)
}
- checkStreams(out, err, {
- case (o, e) =>
- o shouldBe empty
- e should not be empty
- })
+ if (checkStreamsAtInit)
+ checkStreams(out, err, {
+ case (o, e) =>
+ o shouldBe empty
+ e should not be empty
+ })
}
}
@@ -178,10 +193,11 @@
}
// Somewhere, the logs should contain an exception.
- checkStreams(out, err, {
- case (o, e) =>
- (o + e).toLowerCase should include("exception")
- })
+ if (checkStreamsAtInit)
+ checkStreams(out, err, {
+ case (o, e) =>
+ (o + e).toLowerCase should include("exception")
+ })
}
it should "return some error on action error" in {
@@ -204,7 +220,10 @@
initCode should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject.empty))
- runCode should not be (200)
+ if (runtimeDetectErrors)
+ runCode should not be (200)
+ else
+ runCode should be(200)
runRes shouldBe defined
runRes.get.fields.get("error") shouldBe defined
@@ -302,7 +321,10 @@
initCode should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject.empty))
- runCode should not be (200)
+ if (runtimeDetectErrors)
+ runCode should not be (200)
+ else
+ runCode should be(200)
runRes shouldBe defined
runRes.get.fields.get("error") shouldBe defined
@@ -334,7 +356,10 @@
initCode should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject.empty))
- runCode should not be (200)
+ if (runtimeDetectErrors)
+ runCode should not be (200)
+ else
+ runCode should be(200)
runRes shouldBe defined
runRes.get.fields.get("error") shouldBe defined
diff --git a/tests/src/test/scala/actionContainers/JavaActionLoopContainerTests.scala b/tests/src/test/scala/actionContainers/JavaActionLoopContainerTests.scala
new file mode 100644
index 0000000..486a742
--- /dev/null
+++ b/tests/src/test/scala/actionContainers/JavaActionLoopContainerTests.scala
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+package actionContainers
+
+import common.WskActorSystem
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class JavaActionLoopContainerTests extends JavaActionContainerTests with WskActorSystem {
+ override val errPrefix = "\"Exception in thread \\\"main\\\""
+ override val checkStreamsAtInit = false
+ override val image = "actionloop-java-v8"
+ override val runtimeDetectErrors = false
+}
diff --git a/tests/src/test/scala/actionContainers/JavaActionLoopSourceTests.scala b/tests/src/test/scala/actionContainers/JavaActionLoopSourceTests.scala
new file mode 100644
index 0000000..ec627b5
--- /dev/null
+++ b/tests/src/test/scala/actionContainers/JavaActionLoopSourceTests.scala
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+package actionContainers
+
+import actionContainers.ActionContainer.withContainer
+import common.WskActorSystem
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class JavaActionLoopSourceTests extends BasicActionRunnerTests with WskActorSystem {
+
+ val image = "actionloop-java-v8"
+
+ // Helpers specific to java actions
+ override def withActionContainer(env: Map[String, String] = Map.empty)(
+ code: ActionContainer => Unit): (String, String) = withContainer(image, env)(code)
+
+ behavior of "Java actionloop"
+
+ override val testNoSourceOrExec = {
+ TestConfig("")
+ }
+
+ override val testNotReturningJson = {
+ // skip this test since and add own below (see Nuller)
+ TestConfig("", skipTest = true)
+ }
+
+ override val testEnv = {
+ TestConfig(
+ """
+ |package example;
+ |
+ |import com.google.gson.JsonObject;
+ |
+ |public class HelloWhisk {
+ | public static JsonObject main(JsonObject args) {
+ | JsonObject response = new JsonObject();
+ | response.addProperty("api_host", System.getenv("__OW_API_HOST"));
+ | response.addProperty("api_key", System.getenv("__OW_API_KEY"));
+ | response.addProperty("namespace", System.getenv("__OW_NAMESPACE"));
+ | response.addProperty("action_name", System.getenv("__OW_ACTION_NAME"));
+ | response.addProperty("activation_id", System.getenv("__OW_ACTIVATION_ID"));
+ | response.addProperty("deadline", System.getenv("__OW_DEADLINE"));
+ | return response;
+ | }
+ |}
+ """.stripMargin.trim,
+ main = "example.HelloWhisk")
+ }
+
+ override val testEcho = {
+ TestConfig(
+ """
+ |package example;
+ |
+ |import com.google.gson.JsonObject;
+ |
+ |public class HelloWhisk {
+ | public static JsonObject main(JsonObject args) {
+ | System.out.println("hello stdout");
+ | System.err.println("hello stderr");
+ | return args;
+ | }
+ |}
+ """.stripMargin.trim,
+ "example.HelloWhisk")
+
+ }
+
+ override val testUnicode = {
+ TestConfig(
+ """
+ |package example;
+ |
+ |import com.google.gson.JsonObject;
+ |
+ |public class HelloWhisk {
+ | public static JsonObject main(JsonObject args) {
+ | String delimiter = args.getAsJsonPrimitive("delimiter").getAsString();
+ | JsonObject response = new JsonObject();
+ | String str = delimiter + " ☃ " + delimiter;
+ | System.out.println(str);
+ | response.addProperty("winter", str);
+ | return response;
+ | }
+ |}
+ """.stripMargin,
+ "example.HelloWhisk")
+ }
+
+ def echo(main: String = "main") = {
+ s"""
+ |package example;
+ |
+ |import com.google.gson.JsonObject;
+ |
+ |public class HelloWhisk {
+ | public static JsonObject $main(JsonObject args) {
+ | return args;
+ | }
+ |}
+ """.stripMargin.trim
+ }
+
+ override val testInitCannotBeCalledMoreThanOnce = {
+ TestConfig(echo(), "example.HelloWhisk")
+ }
+
+ override val testEntryPointOtherThanMain = {
+ TestConfig(echo("naim"), "example.HelloWhisk#naim")
+ }
+
+ override val testLargeInput = {
+ TestConfig(echo(), "example.HelloWhisk")
+ }
+
+}
diff --git a/tools/invoke.py b/tools/invoke.py
new file mode 100755
index 0000000..68b2ca8
--- /dev/null
+++ b/tools/invoke.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python
+"""Executable Python script for testing the action proxy.
+/*
+ * 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.
+ */
+
+ This script is useful for testing the action proxy (or its derivatives)
+ by simulating invoker interactions. Use it in combination with
+ docker run <image> which starts up the action proxy.
+ Example:
+ docker run -i -t -p 8080:8080 dockerskeleton # locally built images may be referenced without a tag
+ ./invoke.py init <action source file>
+ ./invoke.py run '{"some":"json object as a string"}'
+
+ For additional help, try ./invoke.py -h
+"""
+
+import os
+import re
+import sys
+import json
+import base64
+import requests
+import codecs
+import argparse
+try:
+ import argcomplete
+except ImportError:
+ argcomplete = False
+
+def main():
+ try:
+ args = parseArgs()
+ exitCode = {
+ 'init' : init,
+ 'run' : run
+ }[args.cmd](args)
+ except Exception as e:
+ print(e)
+ exitCode = 1
+ sys.exit(exitCode)
+
+def dockerHost():
+ dockerHost = 'localhost'
+ if 'DOCKER_HOST' in os.environ:
+ try:
+ dockerHost = re.compile('tcp://(.*):[\d]+').findall(os.environ['DOCKER_HOST'])[0]
+ except Exception:
+ print('cannot determine docker host from %s' % os.environ['DOCKER_HOST'])
+ sys.exit(-1)
+ return dockerHost
+
+def containerRoute(args, path):
+ return 'http://%s:%s/%s' % (args.host, args.port, path)
+
+def parseArgs():
+ parser = argparse.ArgumentParser(description='initialize and run an OpenWhisk action container')
+ parser.add_argument('-v', '--verbose', help='verbose output', action='store_true')
+ parser.add_argument('--host', help='action container host', default=dockerHost())
+ parser.add_argument('-p', '--port', help='action container port number', default=8080, type=int)
+
+ subparsers = parser.add_subparsers(title='available commands', dest='cmd')
+
+ initmenu = subparsers.add_parser('init', help='initialize container with src or zip/tgz file')
+ initmenu.add_argument('-b', '--binary', help='treat artifact as binary', action='store_true')
+ initmenu.add_argument('main', nargs='?', default='main', help='name of the "main" entry method for the action')
+ initmenu.add_argument('artifact', help='a source file or zip/tgz archive')
+
+ runmenu = subparsers.add_parser('run', help='send arguments to container to run action')
+ runmenu.add_argument('payload', nargs='?', help='the arguments to send to the action, either a reference to a file or an inline JSON object', default=None)
+
+ if argcomplete:
+ argcomplete.autocomplete(parser)
+ return parser.parse_args()
+
+def init(args):
+ main = args.main
+ artifact = args.artifact
+
+ if artifact and (args.binary or artifact.endswith('.zip') or artifact.endswith('tgz') or artifact.endswith('jar')):
+ with open(artifact, 'rb') as fp:
+ contents = fp.read()
+ contents = base64.b64encode(contents)
+ binary = True
+ elif artifact is not '':
+ with(codecs.open(artifact, 'r', 'utf-8')) as fp:
+ contents = fp.read()
+ binary = False
+ else:
+ contents = None
+ binary = False
+
+ r = requests.post(
+ containerRoute(args, 'init'),
+ json = {"value": {"code": contents,
+ "binary": binary,
+ "main": main}})
+ print(r.text)
+
+def run(args):
+ value = processPayload(args.payload)
+ if args.verbose:
+ print('Sending value: %s...' % json.dumps(value)[0:40])
+ r = requests.post(containerRoute(args, 'run'), json = {"value": value})
+ print(r.text)
+
+def processPayload(payload):
+ if payload and os.path.exists(payload):
+ with open(payload) as fp:
+ return json.load(fp)
+ try:
+ d = json.loads(payload if payload else '{}')
+ if isinstance(d, dict):
+ return d
+ else:
+ raise
+ except:
+ print('payload must be a JSON object.')
+ sys.exit(-1)
+
+if __name__ == '__main__':
+ main()