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()