From 65fd1d9fbb6a16ecce1bdb31e3056ffa56317191 Mon Sep 17 00:00:00 2001 From: Vishal Date: Wed, 27 Jun 2018 21:39:24 +0530 Subject: [PATCH] Java env alpha (#656) This change adds support for JVM based environment for running Java functions. This is alpha release of JVM environment and might undergo changes. --- environments/jvm/.gitignore | 7 + environments/jvm/Dockerfile | 10 ++ environments/jvm/pom.xml | 46 +++++++ .../java/io/fission/FunctionLoadRequest.java | 32 +++++ .../jvm/src/main/java/io/fission/Server.java | 123 ++++++++++++++++++ examples/jvm/java/.gitignore | 4 + examples/jvm/java/build.sh | 5 + examples/jvm/java/pom.xml | 64 +++++++++ .../src/main/java/io/fission/HelloWorld.java | 16 +++ .../test/java/io/fission/HelloWorldTest.java | 24 ++++ test/build_and_test.sh | 1 + test/tests/test_environments/test_java_env.sh | 55 ++++++++ 12 files changed, 387 insertions(+) create mode 100644 environments/jvm/.gitignore create mode 100644 environments/jvm/Dockerfile create mode 100644 environments/jvm/pom.xml create mode 100644 environments/jvm/src/main/java/io/fission/FunctionLoadRequest.java create mode 100644 environments/jvm/src/main/java/io/fission/Server.java create mode 100644 examples/jvm/java/.gitignore create mode 100755 examples/jvm/java/build.sh create mode 100644 examples/jvm/java/pom.xml create mode 100644 examples/jvm/java/src/main/java/io/fission/HelloWorld.java create mode 100644 examples/jvm/java/src/test/java/io/fission/HelloWorldTest.java create mode 100755 test/tests/test_environments/test_java_env.sh diff --git a/environments/jvm/.gitignore b/environments/jvm/.gitignore new file mode 100644 index 00000000..e0a3199d --- /dev/null +++ b/environments/jvm/.gitignore @@ -0,0 +1,7 @@ +.springBeans +.project +.mvn +.settings +.classpath +target/ +bin/ diff --git a/environments/jvm/Dockerfile b/environments/jvm/Dockerfile new file mode 100644 index 00000000..d9f7985b --- /dev/null +++ b/environments/jvm/Dockerfile @@ -0,0 +1,10 @@ +FROM maven:3.5-jdk-8 as BUILD +COPY src /usr/src/myapp/src +COPY pom.xml /usr/src/myapp +RUN mvn -f /usr/src/myapp/pom.xml clean package + +FROM openjdk:8-jdk-alpine +VOLUME /tmp +COPY --from=BUILD /usr/src/myapp/target/env-java-0.0.1-SNAPSHOT.jar /app.jar +ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar","--server.port=8888"] +EXPOSE 8888 \ No newline at end of file diff --git a/environments/jvm/pom.xml b/environments/jvm/pom.xml new file mode 100644 index 00000000..9834a447 --- /dev/null +++ b/environments/jvm/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + io.fission + env-java + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.0.1.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + io.fission + fission-java-core + 0.0.2-SNAPSHOT + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + fission-java-core + fission-java-core-snapshot + https://oss.sonatype.org/content/repositories/snapshots/ + + + \ No newline at end of file diff --git a/environments/jvm/src/main/java/io/fission/FunctionLoadRequest.java b/environments/jvm/src/main/java/io/fission/FunctionLoadRequest.java new file mode 100644 index 00000000..f5276840 --- /dev/null +++ b/environments/jvm/src/main/java/io/fission/FunctionLoadRequest.java @@ -0,0 +1,32 @@ +package io.fission; + +public class FunctionLoadRequest { + + private String filepath; + private String functionName; + private String url; + + String getFilepath() { + return filepath; + } + + void setFilepath(String filepath) { + this.filepath = filepath; + } + + String getUrl() { + return url; + } + + void setUrl(String url) { + this.url = url; + } + + public String getFunctionName() { + return functionName; + } + + public void setFunctionName(String functionName) { + this.functionName = functionName; + } +} diff --git a/environments/jvm/src/main/java/io/fission/Server.java b/environments/jvm/src/main/java/io/fission/Server.java new file mode 100644 index 00000000..3d752279 --- /dev/null +++ b/environments/jvm/src/main/java/io/fission/Server.java @@ -0,0 +1,123 @@ +package io.fission; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.springframework.boot.*; +import org.springframework.boot.autoconfigure.*; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import io.fission.Function; + +@RestController +@EnableAutoConfiguration +public class Server { + + private Function fn; + + private static final int CLASS_LENGTH = 6; + + private static Logger logger = Logger.getGlobal(); + + @RequestMapping(value = "/", method = { RequestMethod.GET, RequestMethod.POST, RequestMethod.DELETE, + RequestMethod.PUT }) + ResponseEntity home(RequestEntity req) { + if (fn == null) { + return ResponseEntity.badRequest().body("Container not specialized"); + } else { + return ((ResponseEntity) ((Function) fn).call(req, null)); + } + } + + @PostMapping(path = "/v2/specialize", consumes = "application/json") + ResponseEntity specialize(@RequestBody FunctionLoadRequest req) { + long startTime = System.nanoTime(); + File file = new File(req.getFilepath()); + if (!file.exists()) { + return ResponseEntity.badRequest().body("/userfunc/user not found"); + } + + String entryPoint = req.getFunctionName(); + logger.log(Level.INFO, "Entrypoint class:" + entryPoint); + if (entryPoint == null) { + return ResponseEntity.badRequest().body("Entrypoint class is missing in the function"); + } + + JarFile jarFile = null; + ClassLoader cl = null; + try { + + jarFile = new JarFile(file); + Enumeration e = jarFile.entries(); + URL[] urls = { new URL("jar:file:" + file + "!/") }; + + // TODO Check if the classloading can be improved for ex. use something like: + // Thread.currentThread().setContextClassLoader(cl); + if (this.getClass().getClassLoader() == null) { + cl = URLClassLoader.newInstance(urls); + } else { + cl = URLClassLoader.newInstance(urls, this.getClass().getClassLoader()); + } + + if (cl == null) { + return ResponseEntity.status(500).body("Failed to initialize the classloader"); + } + + // Load all dependent classes from libraries etc. + while (e.hasMoreElements()) { + JarEntry je = e.nextElement(); + if (je.isDirectory() || !je.getName().endsWith(".class")) { + continue; + } + String className = je.getName().substring(0, je.getName().length() - CLASS_LENGTH); + className = className.replace('/', '.'); + cl.loadClass(className); + } + + // Instantiate the function class + fn = (Function) cl.loadClass(entryPoint).newInstance(); + + } catch (MalformedURLException e) { + e.printStackTrace(); + return ResponseEntity.badRequest().body("Error loading the Function class file"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + return ResponseEntity.badRequest().body("Error loading Function or dependent class"); + } catch (InstantiationException e) { + e.printStackTrace(); + return ResponseEntity.badRequest().body("Error creating a new instance of function class"); + } catch (IllegalAccessException e) { + e.printStackTrace(); + return ResponseEntity.badRequest().body("Error creating a new instance of function class"); + } catch (IOException e) { + e.printStackTrace(); + return ResponseEntity.badRequest().body("Error reading the JAR file"); + } finally { + try { + // cl.close(); + jarFile.close(); + } catch (IOException e) { + e.printStackTrace(); + return ResponseEntity.badRequest().body("Error closing the file while loading the class"); + } + } + long elapsedTime = System.nanoTime() - startTime; + logger.log(Level.INFO, "Specialize call done in: " + elapsedTime / 1000000 + " ms"); + return ResponseEntity.ok("Done"); + } + + public static void main(String[] args) throws Exception { + SpringApplication.run(Server.class, args); + } + +} \ No newline at end of file diff --git a/examples/jvm/java/.gitignore b/examples/jvm/java/.gitignore new file mode 100644 index 00000000..4c3f8757 --- /dev/null +++ b/examples/jvm/java/.gitignore @@ -0,0 +1,4 @@ +.project +.settings +.classpath +target/ diff --git a/examples/jvm/java/build.sh b/examples/jvm/java/build.sh new file mode 100755 index 00000000..37a11f01 --- /dev/null +++ b/examples/jvm/java/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh +# This script allows you to build the jar without needing Maven & JDK installed locally. +# You need docker, as it uses a Docker image to build source code + +docker run -it --rm -v "$(pwd)":/usr/src/mymaven -w /usr/src/mymaven maven:3.5-jdk-8 mvn clean package diff --git a/examples/jvm/java/pom.xml b/examples/jvm/java/pom.xml new file mode 100644 index 00000000..8504d856 --- /dev/null +++ b/examples/jvm/java/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + io.fission + hello-world + 1.0-SNAPSHOT + jar + + hello-world + http://maven.apache.org + + + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + 2.0.1.RELEASE + provided + + + io.fission + fission-java-core + 0.0.2-SNAPSHOT + provided + + + + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + + + + fission-java-core + fission-java-core-snapshot + https://oss.sonatype.org/content/repositories/snapshots/ + + + + \ No newline at end of file diff --git a/examples/jvm/java/src/main/java/io/fission/HelloWorld.java b/examples/jvm/java/src/main/java/io/fission/HelloWorld.java new file mode 100644 index 00000000..89e3d067 --- /dev/null +++ b/examples/jvm/java/src/main/java/io/fission/HelloWorld.java @@ -0,0 +1,16 @@ +package io.fission; + +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; + +import io.fission.Function; +import io.fission.Context; + +public class HelloWorld implements Function { + + @Override + public ResponseEntity call(RequestEntity req, Context context) { + return ResponseEntity.ok("Hello World!"); + } + +} diff --git a/examples/jvm/java/src/test/java/io/fission/HelloWorldTest.java b/examples/jvm/java/src/test/java/io/fission/HelloWorldTest.java new file mode 100644 index 00000000..8133c35c --- /dev/null +++ b/examples/jvm/java/src/test/java/io/fission/HelloWorldTest.java @@ -0,0 +1,24 @@ +package io.fission; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; + +public class HelloWorldTest { + + public void testResponse() { + HelloWorld hw = new HelloWorld(); + RequestEntity request = null; + try { + request = RequestEntity.get(new URI("http://example.com/bar")).build(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + ResponseEntity resp = hw.call(request, null); + Assert.hasText(resp.getBody().toString(), "Hello World!"); + } +} \ No newline at end of file diff --git a/test/build_and_test.sh b/test/build_and_test.sh index ef8ab338..9663a6d3 100755 --- a/test/build_and_test.sh +++ b/test/build_and_test.sh @@ -41,6 +41,7 @@ build_and_push_builder $BUILDER_IMAGE:$TAG ENV='python' build_and_push_env_runtime $ENV $REPO/$ENV-env:$TAG +build_and_push_env_runtime jvm $REPO/jvm-env:$TAG build_and_push_env_builder $ENV $REPO/$ENV-env-builder:$TAG $BUILDER_IMAGE:$TAG diff --git a/test/tests/test_environments/test_java_env.sh b/test/tests/test_environments/test_java_env.sh new file mode 100755 index 00000000..2193307a --- /dev/null +++ b/test/tests/test_environments/test_java_env.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -euo pipefail + +ROOT=$(dirname $0)/../../.. + +cleanup() { + fission fn delete --name hellon + fission fn delete --name hellop + fission env delete --name jvm +} + +test_fn() { + echo "Checking for valid response" + + while true; do + response0=$(curl http://$FISSION_ROUTER/$1) + echo $response0 | grep -i $2 + if [[ $? -eq 0 ]]; then + break + fi + sleep 1 + done +} + +export -f test_fn + +cd $ROOT/examples/jvm/java + +log "Creating the jar from application" +#Using Docker to build Jar so that maven & other Java dependencies are not needed on CI server +docker run -it --rm -v "$(pwd)":/usr/src/mymaven -w /usr/src/mymaven maven:3.5-jdk-8 mvn clean package + +log "Creating environment for Java" +fission env create --name jvm --image gcr.io/fission-ci/jvm-env:test --version 2 --extract=false + +log "Creating pool manager & new deployment function for Java" +fission fn create --name hellop --deploy target/hello-world-1.0-SNAPSHOT-jar-with-dependencies.jar --env jvm --entrypoint io.fission.HelloWorld +fission fn create --name hellon --deploy target/hello-world-1.0-SNAPSHOT-jar-with-dependencies.jar --env jvm --executortype newdeploy --entrypoint io.fission.HelloWorld +trap cleanup EXIT + +log "Creating route for pool manager function" +fission route create --function hellop --url /hellop --method GET + +log "Creating route for new deployment function" +fission route create --function hellon --url /hellon --method GET + +log "Waiting for router & pools to catch up" +sleep 5 + +log "Testing pool manager function" +timeout 60 bash -c "test_fn hellop 'Hello'" + +log "Testing new deployment function" +timeout 60 bash -c "test_fn hellon 'Hello'" \ No newline at end of file