Skip to content

Commit 9a85ed0

Browse files
committed
Change the FF --jar option into --classpath.
Instead of requiring the caller to provide a single self-contained jar, we can allow a general classpath. That means for example that we could provide the jar-file or classes directory from the compilation of a user's function, plus the jars containing the function's dependencies. PiperOrigin-RevId: 293704294
1 parent 5c1997c commit 9a85ed0

File tree

3 files changed

+113
-46
lines changed

3 files changed

+113
-46
lines changed

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

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package com.google.cloud.functions.invoker.runner;
1616

17+
import static java.util.stream.Collectors.toList;
18+
1719
import com.beust.jcommander.JCommander;
1820
import com.beust.jcommander.Parameter;
1921
import com.beust.jcommander.ParameterException;
@@ -28,13 +30,18 @@
2830
import com.google.cloud.functions.invoker.NewHttpFunctionExecutor;
2931
import java.io.File;
3032
import java.io.IOException;
33+
import java.io.UncheckedIOException;
3134
import java.lang.reflect.Method;
35+
import java.net.MalformedURLException;
3236
import java.net.URL;
3337
import java.net.URLClassLoader;
3438
import java.nio.file.Files;
3539
import java.nio.file.Path;
3640
import java.nio.file.Paths;
41+
import java.util.ArrayList;
3742
import java.util.Arrays;
43+
import java.util.Collections;
44+
import java.util.List;
3845
import java.util.Map;
3946
import java.util.Objects;
4047
import java.util.Optional;
@@ -91,13 +98,18 @@ private static class Options {
9198
System.getenv().getOrDefault("FUNCTION_TARGET", "TestFunction.function");
9299

93100
@Parameter(
94-
description = "Name of a jar file that contains the function to execute. This must be"
95-
+ " self-contained: either it must be a \"fat jar\" which bundles the dependencies"
96-
+ " of all of the function code, or it must use the Class-Path attribute in the jar"
97-
+ " manifest to point to those dependencies.",
98-
names = "--jar"
101+
description = "List of files or directories where the compiled Java classes making up"
102+
+ " the function will be found. This functions like the -classpath option to the"
103+
+ " java command. It is a list of filenames separated by '${path.separator}'."
104+
+ " If an entry in the list names a directory then the class foo.bar.Baz will be looked"
105+
+ " for in foo${file.separator}bar${file.separator}Baz.class under that"
106+
+ " directory. If an entry in the list names a file and that file is a jar file then"
107+
+ " class foo.bar.Baz will be looked for in an entry foo/bar/Baz.class in that jar"
108+
+ " file. If an entry is a directory followed by '${file.separator}*' then every file"
109+
+ " in the directory whose name ends with '.jar' will be searched for classes.",
110+
names = "--classpath"
99111
)
100-
private String jar = null;
112+
private String classPath = null;
101113

102114
@Parameter(
103115
names = "--help", help = true
@@ -124,12 +136,12 @@ static Optional<Invoker> makeInvoker(Map<String, String> environment, String...
124136
try {
125137
jCommander.parse(args);
126138
} catch (ParameterException e) {
127-
jCommander.usage();
139+
usage(jCommander);
128140
throw e;
129141
}
130142

131143
if (options.help) {
132-
jCommander.usage();
144+
usage(jCommander);
133145
return Optional.empty();
134146
}
135147

@@ -138,15 +150,15 @@ static Optional<Invoker> makeInvoker(Map<String, String> environment, String...
138150
port = Integer.parseInt(options.port);
139151
} catch (NumberFormatException e) {
140152
System.err.println("--port value should be an integer: " + options.port);
141-
jCommander.usage();
153+
usage(jCommander);
142154
throw e;
143155
}
144156
String functionTarget = options.target;
145157
Path standardFunctionJarPath = Paths.get("function/function.jar");
146-
Optional<String> functionJarPath =
158+
Optional<String> functionClasspath =
147159
Arrays.asList(
148-
options.jar,
149-
environment.get("FUNCTION_JAR"),
160+
options.classPath,
161+
environment.get("FUNCTION_CLASSPATH"),
150162
Files.exists(standardFunctionJarPath) ? standardFunctionJarPath.toString() : null)
151163
.stream()
152164
.filter(Objects::nonNull)
@@ -156,28 +168,37 @@ static Optional<Invoker> makeInvoker(Map<String, String> environment, String...
156168
port,
157169
functionTarget,
158170
environment.get("FUNCTION_SIGNATURE_TYPE"),
159-
functionJarPath);
171+
functionClasspath);
160172
return Optional.of(invoker);
161173
}
162174

175+
private static void usage(JCommander jCommander) {
176+
StringBuilder usageBuilder = new StringBuilder();
177+
jCommander.getUsageFormatter().usage(usageBuilder);
178+
String usage = usageBuilder.toString()
179+
.replace("${file.separator}", File.separator)
180+
.replace("${path.separator}", File.pathSeparator);
181+
jCommander.getConsole().println(usage);
182+
}
183+
163184
private static boolean isLocalRun() {
164185
return System.getenv("K_SERVICE") == null;
165186
}
166187

167188
private final Integer port;
168189
private final String functionTarget;
169190
private final String functionSignatureType;
170-
private final Optional<String> functionJarPath;
191+
private final Optional<String> functionClasspath;
171192

172193
public Invoker(
173194
Integer port,
174195
String functionTarget,
175196
String functionSignatureType,
176-
Optional<String> functionJarPath) {
197+
Optional<String> functionClasspath) {
177198
this.port = port;
178199
this.functionTarget = functionTarget;
179200
this.functionSignatureType = functionSignatureType;
180-
this.functionJarPath = functionJarPath;
201+
this.functionClasspath = functionClasspath;
181202
}
182203

183204
Integer getPort() {
@@ -192,8 +213,8 @@ String getFunctionSignatureType() {
192213
return functionSignatureType;
193214
}
194215

195-
Optional<String> getFunctionJarPath() {
196-
return functionJarPath;
216+
Optional<String> getFunctionClasspath() {
217+
return functionClasspath;
197218
}
198219

199220
public void startServer() throws Exception {
@@ -203,21 +224,11 @@ public void startServer() throws Exception {
203224
context.setContextPath("/");
204225
server.setHandler(context);
205226

206-
Optional<File> functionJarFile =
207-
functionJarPath.isPresent()
208-
? Optional.of(new File(functionJarPath.get()))
209-
: Optional.empty();
210-
if (functionJarFile.isPresent() && !functionJarFile.get().exists()) {
211-
throw new IllegalArgumentException(
212-
"functionJarPath points to an non-existent file: "
213-
+ functionJarFile.get().getAbsolutePath());
214-
}
215-
216227
ClassLoader runtimeLoader = getClass().getClassLoader();
217228
ClassLoader classLoader;
218-
if (functionJarFile.isPresent()) {
229+
if (functionClasspath.isPresent()) {
219230
ClassLoader parent = new OnlyApiClassLoader(runtimeLoader);
220-
classLoader = new URLClassLoader(new URL[]{functionJarFile.get().toURI().toURL()}, parent);
231+
classLoader = new URLClassLoader(classpathToUrls(functionClasspath.get()), parent);
221232
} else {
222233
classLoader = runtimeLoader;
223234
}
@@ -279,6 +290,39 @@ public void startServer() throws Exception {
279290
server.join();
280291
}
281292

293+
static URL[] classpathToUrls(String classpath) throws IOException {
294+
String[] components = classpath.split(File.pathSeparator);
295+
List<URL> urls = new ArrayList<>();
296+
for (String component : components) {
297+
if (component.endsWith(File.separator + "*")) {
298+
urls.addAll(jarsIn(component.substring(0, component.length() - 2)));
299+
} else {
300+
Path path = Paths.get(component);
301+
if (Files.exists(path)) {
302+
urls.add(path.toUri().toURL());
303+
}
304+
}
305+
}
306+
return urls.toArray(new URL[0]);
307+
}
308+
309+
private static List<URL> jarsIn(String dir) throws IOException {
310+
Path path = Paths.get(dir);
311+
if (!Files.isDirectory(path)) {
312+
return Collections.emptyList();
313+
}
314+
return Files.list(path)
315+
.filter(p -> p.getFileName().toString().endsWith(".jar"))
316+
.map(p -> {
317+
try {
318+
return p.toUri().toURL();
319+
} catch (MalformedURLException e) {
320+
throw new UncheckedIOException(e);
321+
}
322+
})
323+
.collect(toList());
324+
}
325+
282326
private void logServerInfo() {
283327
if (isLocalRun()) {
284328
logger.log(Level.INFO, "Serving function...");

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -330,24 +330,24 @@ private String functionJarString() throws IOException {
330330
}
331331

332332
/**
333-
* Tests that if we launch an HTTP function with {@code --jar}, then the function code cannot
334-
* see the classes from the runtime. This is allows us to avoid conflicts between versions of
335-
* libraries that we use in the runtime and different versions of the same libraries that the
333+
* Tests that if we launch an HTTP function with {@code --classpath}, then the function code
334+
* cannot see the classes from the runtime. This is allows us to avoid conflicts between versions
335+
* of libraries that we use in the runtime and different versions of the same libraries that the
336336
* function might use.
337337
*/
338338
@Test
339-
public void jarOptionHttp() throws Exception {
339+
public void classpathOptionHttp() throws Exception {
340340
testHttpFunction("com.example.functionjar.Foreground",
341-
ImmutableList.of("--jar", functionJarString()),
341+
ImmutableList.of("--classpath", functionJarString()),
342342
TestCase.builder()
343343
.setUrl("/?class=" + INTERNAL_CLASS.getName())
344344
.setExpectedResponseText("OK")
345345
.build());
346346
}
347347

348-
/** Like {@link #jarOptionHttp} but for background functions. */
348+
/** Like {@link #classpathOptionHttp} but for background functions. */
349349
@Test
350-
public void jarOptionBackground() throws Exception {
350+
public void classpathOptionBackground() throws Exception {
351351
Gson gson = new Gson();
352352
URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json");
353353
assertThat(resourceUrl).isNotNull();
@@ -356,7 +356,7 @@ public void jarOptionBackground() throws Exception {
356356
JsonObject jsonData = json.getAsJsonObject("data");
357357
jsonData.addProperty("class", INTERNAL_CLASS.getName());
358358
testBackgroundFunction("com.example.functionjar.Background",
359-
ImmutableList.of("--jar", functionJarString()),
359+
ImmutableList.of("--classpath", functionJarString()),
360360
TestCase.builder().setRequestText(json.toString()).build());
361361
}
362362

invoker/core/src/test/java/com/google/cloud/functions/invoker/runner/InvokerTest.java

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package com.google.cloud.functions.invoker.runner;
22

33
import static com.google.common.truth.Truth.assertThat;
4+
import static com.google.common.truth.Truth.assertWithMessage;
45
import static com.google.common.truth.Truth8.assertThat;
56

67
import java.io.ByteArrayOutputStream;
8+
import java.io.File;
79
import java.io.IOException;
810
import java.io.PrintStream;
11+
import java.net.URL;
912
import java.nio.charset.StandardCharsets;
13+
import java.util.Arrays;
1014
import java.util.Collections;
1115
import java.util.Map;
1216
import java.util.Optional;
@@ -24,6 +28,7 @@ public void help() throws IOException {
2428
});
2529
assertThat(help).contains("Usage:");
2630
assertThat(help).contains("--target");
31+
assertThat(help).containsMatch("separated\\s+by\\s+'" + File.pathSeparator + "'");
2732
}
2833

2934
@Test
@@ -64,22 +69,40 @@ public void explicitSignatureType() {
6469
}
6570

6671
@Test
67-
public void defaultJar() {
72+
public void defaultClasspath() {
6873
Optional<Invoker> invoker = Invoker.makeInvoker();
69-
assertThat(invoker.get().getFunctionJarPath()).isEmpty();
74+
assertThat(invoker.get().getFunctionClasspath()).isEmpty();
7075
}
7176

77+
private static final String FAKE_CLASSPATH =
78+
"/foo/bar/baz.jar" + File.pathSeparator + "/some/directory";
79+
7280
@Test
73-
public void explicitJarViaEnvironment() {
74-
Map<String, String> env = Collections.singletonMap("FUNCTION_JAR", "/foo/bar/baz.jar");
81+
public void explicitClasspathViaEnvironment() {
82+
Map<String, String> env = Collections.singletonMap("FUNCTION_CLASSPATH", FAKE_CLASSPATH);
7583
Optional<Invoker> invoker = Invoker.makeInvoker(env);
76-
assertThat(invoker.get().getFunctionJarPath()).hasValue("/foo/bar/baz.jar");
84+
assertThat(invoker.get().getFunctionClasspath()).hasValue(FAKE_CLASSPATH);
7785
}
7886

7987
@Test
80-
public void explicitJarViaOption() {
81-
Optional<Invoker> invoker = Invoker.makeInvoker("--jar", "/foo/bar/baz.jar");
82-
assertThat(invoker.get().getFunctionJarPath()).hasValue("/foo/bar/baz.jar");
88+
public void explicitClasspathViaOption() {
89+
Optional<Invoker> invoker = Invoker.makeInvoker("--classpath", FAKE_CLASSPATH);
90+
assertThat(invoker.get().getFunctionClasspath()).hasValue(FAKE_CLASSPATH);
91+
}
92+
93+
@Test
94+
public void classpathToUrls() throws Exception {
95+
String classpath =
96+
"../testfunction/target/test-classes" + File.pathSeparator + "../testfunction/target/lib/*";
97+
URL[] urls = Invoker.classpathToUrls(classpath);
98+
assertWithMessage(Arrays.toString(urls)).that(urls.length).isGreaterThan(2);
99+
File classesDir = new File(urls[0].toURI());
100+
assertWithMessage(classesDir.toString()).that(classesDir.isDirectory()).isTrue();
101+
for (int i = 1; i < urls.length; i++) {
102+
URL url = urls[i];
103+
assertThat(url.toString()).endsWith(".jar");
104+
assertWithMessage(url.toString()).that(new File(url.toURI()).isFile()).isTrue();
105+
}
83106
}
84107

85108
private static String captureOutput(Runnable operation) throws IOException {

0 commit comments

Comments
 (0)