Skip to content

Commit 5990309

Browse files
committed
Import of Cloud Functions JVM from Git-on-Borg.
- b56a91a933b6f438b06ca65fe70b31105fa15fbc Hide the runtime from functions loaded via -jar. by Éamonn McManus <[email protected]> PiperOrigin-RevId: 288496834
1 parent 3f1fc81 commit 5990309

File tree

11 files changed

+300
-35
lines changed

11 files changed

+300
-35
lines changed

invoker-core/core/pom.xml

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
<properties>
2121
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
22-
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
2322
<junit.jupiter.version>5.3.2</junit.jupiter.version>
2423
</properties>
2524

@@ -84,6 +83,13 @@
8483
</dependency>
8584

8685
<!-- Test dependencies -->
86+
<dependency>
87+
<groupId>com.google.cloud.functions.invoker</groupId>
88+
<artifactId>java-function-invoker-core-functionjar</artifactId>
89+
<version>1.0.0-alpha-1</version>
90+
<type>test-jar</type>
91+
<scope>test</scope>
92+
</dependency>
8793
<dependency>
8894
<groupId>org.mockito</groupId>
8995
<artifactId>mockito-core</artifactId>
@@ -124,18 +130,8 @@
124130

125131
<build>
126132
<plugins>
127-
<plugin>
128-
<groupId>org.apache.maven.plugins</groupId>
129-
<artifactId>maven-compiler-plugin</artifactId>
130-
<version>${maven-compiler-plugin.version}</version>
131-
<configuration>
132-
<source>1.8</source>
133-
<target>1.8</target>
134-
</configuration>
135-
</plugin>
136133
<!-- Rename dependencies used by invoker-core so that they don't interfere with user dependencies -->
137134
<plugin>
138-
<groupId>org.apache.maven.plugins</groupId>
139135
<artifactId>maven-shade-plugin</artifactId>
140136
<version>3.2.1</version>
141137
<executions>

invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ private NewBackgroundFunctionExecutor(RawBackgroundFunction function) {
3838
* either the class does not implement {@link RawBackgroundFunction} or we are unable to
3939
* construct an instance using its no-arg constructor.
4040
*/
41-
public static Optional<NewBackgroundFunctionExecutor> forTarget(String target) {
41+
public static Optional<NewBackgroundFunctionExecutor> forTarget(
42+
String target, ClassLoader loader) {
4243
Class<?> c;
4344
try {
44-
c = Class.forName(target);
45+
c = loader.loadClass(target);
4546
} catch (ClassNotFoundException e) {
4647
return Optional.empty();
4748
}

invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ private NewHttpFunctionExecutor(HttpFunction function) {
3131
* either the class does not implement {@link HttpFunction} or we are unable to construct an
3232
* instance using its no-arg constructor.
3333
*/
34-
public static Optional<NewHttpFunctionExecutor> forTarget(String target) {
34+
public static Optional<NewHttpFunctionExecutor> forTarget(String target, ClassLoader loader) {
3535
Class<?> c;
3636
while (true) {
3737
try {
38-
c = Class.forName(target);
38+
c = loader.loadClass(target);
3939
break;
4040
} catch (ClassNotFoundException e) {
4141
// This might be a nested class like com.example.Foo.Bar. That will actually appear as

invoker-core/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.google.cloud.functions.invoker.NewHttpFunctionExecutor;
1212
import java.io.File;
1313
import java.io.IOException;
14+
import java.lang.reflect.Method;
1415
import java.net.URL;
1516
import java.net.URLClassLoader;
1617
import java.util.Arrays;
@@ -139,24 +140,23 @@ public void startServer() throws Exception {
139140
: Optional.empty();
140141
if (functionJarFile.isPresent() && !functionJarFile.get().exists()) {
141142
throw new IllegalArgumentException(
142-
"functionJarPath points to an non-existing file: "
143+
"functionJarPath points to an non-existent file: "
143144
+ functionJarFile.get().getAbsolutePath());
144145
}
145146

147+
ClassLoader runtimeLoader = getClass().getClassLoader();
146148
ClassLoader classLoader;
147149
if (functionJarFile.isPresent()) {
148-
classLoader =
149-
new URLClassLoader(
150-
new URL[]{functionJarFile.get().toURI().toURL()},
151-
Thread.currentThread().getContextClassLoader());
150+
ClassLoader parent = new OnlyApiClassLoader(runtimeLoader);
151+
classLoader = new URLClassLoader(new URL[]{functionJarFile.get().toURI().toURL()}, parent);
152152
} else {
153-
classLoader = Thread.currentThread().getContextClassLoader();
153+
classLoader = runtimeLoader;
154154
}
155155

156156
if ("http".equals(functionSignatureType)) {
157157
HttpServlet servlet;
158158
Optional<NewHttpFunctionExecutor> newExecutor =
159-
NewHttpFunctionExecutor.forTarget(functionTarget);
159+
NewHttpFunctionExecutor.forTarget(functionTarget, classLoader);
160160
if (newExecutor.isPresent()) {
161161
servlet = newExecutor.get();
162162
} else {
@@ -169,7 +169,7 @@ public void startServer() throws Exception {
169169
} else if ("event".equals(functionSignatureType)) {
170170
HttpServlet servlet;
171171
Optional<NewBackgroundFunctionExecutor> newExecutor =
172-
NewBackgroundFunctionExecutor.forTarget(functionTarget);
172+
NewBackgroundFunctionExecutor.forTarget(functionTarget, classLoader);
173173
if (newExecutor.isPresent()) {
174174
servlet = newExecutor.get();
175175
} else {
@@ -196,4 +196,45 @@ private void logServerInfo() {
196196
logger.log(Level.INFO, "URL: http://localhost:{0,number,#}/", port);
197197
}
198198
}
199+
200+
/**
201+
* A loader that only loads GCF API classes. Those are classes whose package is exactly
202+
* {@code com.google.cloud.functions}. The package can't be a subpackage, such as
203+
* {@code com.google.cloud.functions.invoker}.
204+
*
205+
* <p>This loader allows us to load the classes from a user function, without making the
206+
* runtime classes visible to them. We will make this loader the parent of the
207+
* {@link URLClassLoader} that loads the user code in order to filter out those runtime classes.
208+
*
209+
* <p>The reason we do need to share the API classes between the runtime and the user function is
210+
* so that the runtime can instantiate the function class and cast it to
211+
* {@link com.google.cloud.functions.HttpFunction} or whatever.
212+
*/
213+
private static class OnlyApiClassLoader extends ClassLoader {
214+
private final ClassLoader runtimeClassLoader;
215+
216+
OnlyApiClassLoader(ClassLoader runtimeClassLoader) {
217+
super(getSystemOrBootstrapClassLoader());
218+
this.runtimeClassLoader = runtimeClassLoader;
219+
}
220+
221+
@Override
222+
protected Class<?> findClass(String name) throws ClassNotFoundException {
223+
String prefix = "com.google.cloud.functions.";
224+
if (name.startsWith(prefix) && Character.isUpperCase(name.charAt(prefix.length()))) {
225+
return runtimeClassLoader.loadClass(name);
226+
}
227+
return super.findClass(name); // should throw ClassNotFoundException
228+
}
229+
230+
private static ClassLoader getSystemOrBootstrapClassLoader() {
231+
try {
232+
// We're still building against the Java 8 API, so we have to use reflection for now.
233+
Method getPlatformClassLoader = ClassLoader.class.getMethod("getPlatformClassLoader");
234+
return (ClassLoader) getPlatformClassLoader.invoke(null);
235+
} catch (ReflectiveOperationException e) {
236+
return null;
237+
}
238+
}
239+
}
199240
}

invoker-core/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import static com.google.common.truth.Truth.assertThat;
44
import static com.google.common.truth.Truth.assertWithMessage;
5+
import static java.util.stream.Collectors.toList;
56

67
import com.google.auto.value.AutoValue;
78
import com.google.cloud.functions.invoker.runner.Invoker;
9+
import com.google.common.collect.ImmutableList;
810
import com.google.common.collect.ImmutableMap;
9-
import com.google.common.io.Files;
11+
import com.google.common.collect.Iterables;
1012
import com.google.common.io.Resources;
1113
import com.google.gson.Gson;
1214
import com.google.gson.JsonObject;
@@ -19,10 +21,15 @@
1921
import java.net.ServerSocket;
2022
import java.net.URL;
2123
import java.nio.charset.StandardCharsets;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.nio.file.Paths;
2227
import java.util.Arrays;
28+
import java.util.List;
2329
import java.util.Map;
2430
import java.util.concurrent.CountDownLatch;
2531
import java.util.concurrent.TimeUnit;
32+
import java.util.regex.Pattern;
2633
import org.eclipse.jetty.client.HttpClient;
2734
import org.eclipse.jetty.client.api.ContentResponse;
2835
import org.eclipse.jetty.client.api.Request;
@@ -163,6 +170,53 @@ public void packageless() throws Exception {
163170
TestCase.builder().setExpectedResponseText("hello, world\n").build());
164171
}
165172

173+
/** Any runtime class that user code shouldn't be able to see. */
174+
private static final Class<?> INTERNAL_CLASS = CloudFunction.class;
175+
176+
private String functionJarString() throws IOException {
177+
Path functionJarTargetDir = Paths.get("../functionjar/target");
178+
Pattern functionJarPattern = Pattern.compile("java-function-invoker-core-functionjar-.*\\.jar");
179+
List<Path> functionJars = Files.list(functionJarTargetDir)
180+
.map(path -> path.getFileName().toString())
181+
.filter(s -> functionJarPattern.matcher(s).matches())
182+
.map(s -> functionJarTargetDir.resolve(s))
183+
.collect(toList());
184+
assertWithMessage("Number of jars in %s matching %s", functionJarTargetDir, functionJarPattern)
185+
.that(functionJars).hasSize(1);
186+
return Iterables.getOnlyElement(functionJars).toString();
187+
}
188+
189+
/**
190+
* Tests that if we launch an HTTP function with {@code -jar}, then the function code cannot
191+
* see the classes from the runtime. This is allows us to avoid conflicts between versions of
192+
* libraries that we use in the runtime and different versions of the same libraries that the
193+
* function might use.
194+
*/
195+
@Test
196+
public void jarOptionHttp() throws Exception {
197+
testHttpFunction("com.example.functionjar.Foreground",
198+
ImmutableList.of("-jar", functionJarString()),
199+
TestCase.builder()
200+
.setUrl("/?class=" + INTERNAL_CLASS.getName())
201+
.setExpectedResponseText("OK")
202+
.build());
203+
}
204+
205+
/** Like {@link #jarOptionHttp} but for background functions. */
206+
@Test
207+
public void jarOptionBackground() throws Exception {
208+
Gson gson = new Gson();
209+
URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json");
210+
assertThat(resourceUrl).isNotNull();
211+
String originalJson = Resources.toString(resourceUrl, StandardCharsets.UTF_8);
212+
JsonObject json = gson.fromJson(originalJson, JsonObject.class);
213+
JsonObject jsonData = json.getAsJsonObject("data");
214+
jsonData.addProperty("class", INTERNAL_CLASS.getName());
215+
testBackgroundFunction("com.example.functionjar.Background",
216+
ImmutableList.of("-jar", functionJarString()),
217+
TestCase.builder().setRequestText(json.toString()).build());
218+
}
219+
166220
// In these tests, we test a number of different functions that express the same functionality
167221
// in different ways. Each function is invoked with a complete HTTP body that looks like a real
168222
// event. We start with a fixed body and insert into its JSON an extra property that tells the
@@ -180,23 +234,36 @@ private void backgroundTest(String functionTarget) throws Exception {
180234
jsonData.addProperty("targetFile", snoopFile.toString());
181235
testBackgroundFunction(functionTarget,
182236
TestCase.builder().setRequestText(json.toString()).build());
183-
String snooped = Files.asCharSource(snoopFile, StandardCharsets.UTF_8).read();
237+
String snooped = new String(Files.readAllBytes(snoopFile.toPath()), StandardCharsets.UTF_8);
184238
JsonObject snoopedJson = gson.fromJson(snooped, JsonObject.class);
185239
assertThat(snoopedJson).isEqualTo(json);
186240
}
187241

188242
private void testHttpFunction(String target, TestCase... testCases) throws Exception {
189-
testFunction(SignatureType.HTTP, target, testCases);
243+
testHttpFunction(target, ImmutableList.of(), testCases);
244+
}
245+
246+
private void testHttpFunction(
247+
String target, ImmutableList<String> extraArgs, TestCase... testCases) throws Exception {
248+
testFunction(SignatureType.HTTP, target, extraArgs, testCases);
190249
}
191250

192251
private void testBackgroundFunction(String classAndMethod, TestCase... testCases)
193252
throws Exception {
194-
testFunction(SignatureType.BACKGROUND, classAndMethod, testCases);
253+
testBackgroundFunction(classAndMethod, ImmutableList.of(), testCases);
254+
}
255+
private void testBackgroundFunction(
256+
String classAndMethod, ImmutableList<String> extraArgs, TestCase... testCases)
257+
throws Exception {
258+
testFunction(SignatureType.BACKGROUND, classAndMethod, extraArgs, testCases);
195259
}
196260

197261
private void testFunction(
198-
SignatureType signatureType, String target, TestCase... testCases) throws Exception {
199-
Process server = startServer(signatureType, target);
262+
SignatureType signatureType,
263+
String target,
264+
ImmutableList<String> extraArgs,
265+
TestCase... testCases) throws Exception {
266+
Process server = startServer(signatureType, target, extraArgs);
200267
try {
201268
HttpClient httpClient = new HttpClient();
202269
httpClient.start();
@@ -233,7 +300,8 @@ public String toString() {
233300
}
234301
}
235302

236-
private Process startServer(SignatureType signatureType, String target)
303+
private Process startServer(
304+
SignatureType signatureType, String target, ImmutableList<String> extraArgs)
237305
throws IOException, InterruptedException {
238306
File javaHome = new File(System.getProperty("java.home"));
239307
assertThat(javaHome.exists()).isTrue();
@@ -242,9 +310,10 @@ private Process startServer(SignatureType signatureType, String target)
242310
assertThat(javaCommand.exists()).isTrue();
243311
String myClassPath = System.getProperty("java.class.path");
244312
assertThat(myClassPath).isNotNull();
245-
String[] command = {
246-
javaCommand.toString(), "-classpath", myClassPath, Invoker.class.getName(),
247-
};
313+
ImmutableList<String> command = ImmutableList.<String>builder()
314+
.add(javaCommand.toString(), "-classpath", myClassPath, Invoker.class.getName())
315+
.addAll(extraArgs)
316+
.build();
248317
ProcessBuilder processBuilder = new ProcessBuilder()
249318
.command(command)
250319
.redirectErrorStream(true);
@@ -256,7 +325,8 @@ private Process startServer(SignatureType signatureType, String target)
256325
Process serverProcess = processBuilder.start();
257326
CountDownLatch ready = new CountDownLatch(1);
258327
new Thread(() -> monitorOutput(serverProcess.getInputStream(), ready)).start();
259-
ready.await(5, TimeUnit.SECONDS);
328+
boolean serverReady = ready.await(5, TimeUnit.SECONDS);
329+
assertWithMessage("Waiting for server to be ready").that(serverReady).isTrue();
260330
return serverProcess;
261331
}
262332

0 commit comments

Comments
 (0)