All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.functions.invoker.runner.Invoker Maven / Gradle / Ivy

// Copyright 2020 Google LLC
//
// Licensed 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 com.google.cloud.functions.invoker.runner;

import static java.util.stream.Collectors.toList;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.invoker.BackgroundFunctionExecutor;
import com.google.cloud.functions.invoker.HttpFunctionExecutor;
import com.google.cloud.functions.invoker.gcf.JsonLogHandler;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

/**
 * Java server that runs the user's code (a jar file) on HTTP request and an HTTP response is sent
 * once the user's function is completed. The server accepts HTTP requests at '/' for executing the
 * user's function, handles all HTTP methods.
 *
 * 

This class requires the following environment variables: * *

    *
  • PORT - defines the port on which this server listens to HTTP requests. *
  • FUNCTION_TARGET - defines the name of the class defining the function. *
  • FUNCTION_SIGNATURE_TYPE - determines whether the loaded code defines an HTTP or event * function. *
*/ public class Invoker { private static final Logger rootLogger = Logger.getLogger(""); private static final Logger logger = Logger.getLogger(Invoker.class.getName()); static { if (isGcf()) { // If we're running with Google Cloud Functions, we'll get better-looking logs if we arrange // for them to be formatted using StackDriver's "structured logging" JSON format. Remove the // JDK's standard logger and replace it with the JSON one. for (Handler handler : rootLogger.getHandlers()) { rootLogger.removeHandler(handler); } rootLogger.addHandler(new JsonLogHandler(System.out, false)); } } private static class Options { @Parameter( description = "Port on which to listen for HTTP requests.", names = "--port" ) private String port = System.getenv().getOrDefault("PORT", "8080"); @Parameter( description = "Name of function class to execute when servicing incoming requests.", names = "--target" ) private String target = System.getenv().getOrDefault("FUNCTION_TARGET", "Function"); @Parameter( description = "List of files or directories where the compiled Java classes making up" + " the function will be found. This functions like the -classpath option to the" + " java command. It is a list of filenames separated by '${path.separator}'." + " If an entry in the list names a directory then the class foo.bar.Baz will be looked" + " for in foo${file.separator}bar${file.separator}Baz.class under that" + " directory. If an entry in the list names a file and that file is a jar file then" + " class foo.bar.Baz will be looked for in an entry foo/bar/Baz.class in that jar" + " file. If an entry is a directory followed by '${file.separator}*' then every file" + " in the directory whose name ends with '.jar' will be searched for classes.", names = "--classpath" ) private String classPath = null; @Parameter( names = "--help", help = true ) private boolean help = false; } public static void main(String[] args) throws Exception { Optional invoker = makeInvoker(args); if (invoker.isPresent()) { invoker.get().startServer(); } } static Optional makeInvoker(String... args) { return makeInvoker(System.getenv(), args); } static Optional makeInvoker(Map environment, String... args) { Options options = new Options(); JCommander jCommander = JCommander.newBuilder() .addObject(options) .build(); try { jCommander.parse(args); } catch (ParameterException e) { usage(jCommander); throw e; } if (options.help) { usage(jCommander); return Optional.empty(); } int port; try { port = Integer.parseInt(options.port); } catch (NumberFormatException e) { System.err.println("--port value should be an integer: " + options.port); usage(jCommander); throw e; } String functionTarget = options.target; Path standardFunctionJarPath = Paths.get("function/function.jar"); Optional functionClasspath = Arrays.asList( options.classPath, environment.get("FUNCTION_CLASSPATH"), Files.exists(standardFunctionJarPath) ? standardFunctionJarPath.toString() : null) .stream() .filter(Objects::nonNull) .findFirst(); ClassLoader functionClassLoader = makeClassLoader(functionClasspath); Invoker invoker = new Invoker( port, functionTarget, environment.get("FUNCTION_SIGNATURE_TYPE"), functionClassLoader); return Optional.of(invoker); } private static void usage(JCommander jCommander) { StringBuilder usageBuilder = new StringBuilder(); jCommander.getUsageFormatter().usage(usageBuilder); String usage = usageBuilder.toString() .replace("${file.separator}", File.separator) .replace("${path.separator}", File.pathSeparator); jCommander.getConsole().println(usage); } private static ClassLoader makeClassLoader(Optional functionClasspath) { ClassLoader runtimeLoader = Invoker.class.getClassLoader(); if (functionClasspath.isPresent()) { ClassLoader parent = new OnlyApiClassLoader(runtimeLoader); return new FunctionClassLoader(classpathToUrls(functionClasspath.get()), parent); } return runtimeLoader; } // This is a subclass just so we can identify it from its toString(). private static class FunctionClassLoader extends URLClassLoader { FunctionClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } } private final Integer port; private final String functionTarget; private final String functionSignatureType; private final ClassLoader functionClassLoader; public Invoker( Integer port, String functionTarget, String functionSignatureType, ClassLoader functionClassLoader) { this.port = port; this.functionTarget = functionTarget; this.functionSignatureType = functionSignatureType; this.functionClassLoader = functionClassLoader; } Integer getPort() { return port; } String getFunctionTarget() { return functionTarget; } String getFunctionSignatureType() { return functionSignatureType; } ClassLoader getFunctionClassLoader() { return functionClassLoader; } public void startServer() throws Exception { Server server = new Server(port); ServletContextHandler servletContextHandler = new ServletContextHandler(); servletContextHandler.setContextPath("/"); server.setHandler(NotFoundHandler.forServlet(servletContextHandler)); Class functionClass = loadFunctionClass(); HttpServlet servlet; if (functionSignatureType == null) { servlet = servletForDeducedSignatureType(functionClass); } else { switch (functionSignatureType) { case "http": servlet = HttpFunctionExecutor.forClass(functionClass); break; case "event": case "cloudevent": servlet = BackgroundFunctionExecutor.forClass(functionClass); break; default: String error = String.format( "Function signature type %s is unknown; should be \"http\", \"event\"," + " or \"cloudevent\"", functionSignatureType); throw new RuntimeException(error); } } ServletHolder servletHolder = new ServletHolder(servlet); servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement("")); servletContextHandler.addServlet(servletHolder, "/*"); server.start(); logServerInfo(); server.join(); } private Class loadFunctionClass() throws ClassNotFoundException { String target = functionTarget; ClassNotFoundException firstException = null; while (true) { try { return functionClassLoader.loadClass(target); } catch (ClassNotFoundException e) { if (firstException == null) { firstException = e; } // This might be a nested class like com.example.Foo.Bar. That will actually appear as // com.example.Foo$Bar as far as Class.forName is concerned. So we try to replace every dot // from the last to the first with a $ in the hope of finding a class we can load. int lastDot = target.lastIndexOf('.'); if (lastDot < 0) { throw firstException; } target = target.substring(0, lastDot) + '$' + target.substring(lastDot + 1); } } } private HttpServlet servletForDeducedSignatureType(Class functionClass) { if (HttpFunction.class.isAssignableFrom(functionClass)) { return HttpFunctionExecutor.forClass(functionClass); } Optional maybeExecutor = BackgroundFunctionExecutor.maybeForClass(functionClass); if (maybeExecutor.isPresent()) { return maybeExecutor.get(); } String error = String.format( "Could not determine function signature type from target %s. Either this should be" + " a class implementing one of the interfaces in com.google.cloud.functions, or the" + " environment variable FUNCTION_SIGNATURE_TYPE should be set to \"http\" or" + " \"event\".", functionTarget); throw new RuntimeException(error); } static URL[] classpathToUrls(String classpath) { String[] components = classpath.split(File.pathSeparator); List urls = new ArrayList<>(); for (String component : components) { if (component.endsWith(File.separator + "*")) { urls.addAll(jarsIn(component.substring(0, component.length() - 2))); } else { Path path = Paths.get(component); try { urls.add(path.toUri().toURL()); } catch (MalformedURLException e) { throw new UncheckedIOException(e); } } } return urls.toArray(new URL[0]); } private static List jarsIn(String dir) { Path path = Paths.get(dir); if (!Files.isDirectory(path)) { return Collections.emptyList(); } Stream stream; try { stream = Files.list(path); } catch (IOException e) { throw new UncheckedIOException(e); } return stream .filter(p -> p.getFileName().toString().endsWith(".jar")) .map(p -> { try { return p.toUri().toURL(); } catch (MalformedURLException e) { throw new UncheckedIOException(e); } }) .collect(toList()); } private void logServerInfo() { if (!isGcf()) { logger.log(Level.INFO, "Serving function..."); logger.log(Level.INFO, "Function: {0}", functionTarget); logger.log(Level.INFO, "URL: http://localhost:{0,number,#}/", port); } } private static boolean isGcf() { // This environment variable is set in the GCF environment but won't be set when invoking // the Functions Framework directly. We don't use its value, just whether it is set. return System.getenv("K_SERVICE") != null; } /** * Wrapper that intercepts requests for {@code /favicon.ico} and {@code /robots.txt} and causes * them to produce a 404 status. Otherwise they would be sent to the function code, like any * other URL, meaning that someone testing their function by using a browser as an HTTP client * can see two requests, one for {@code /favicon.ico} and one for {@code /} (or whatever). */ private static class NotFoundHandler extends HandlerWrapper { static NotFoundHandler forServlet(ServletContextHandler servletHandler) { NotFoundHandler handler = new NotFoundHandler(); handler.setHandler(servletHandler); return handler; } private static final Set NOT_FOUND_PATHS = new HashSet<>(Arrays.asList("/favicon.ico", "/robots.txt")); @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (NOT_FOUND_PATHS.contains(request.getRequestURI())) { response.sendError(HttpStatus.NOT_FOUND_404, "Not Found"); } super.handle(target, baseRequest, request, response); } } /** * A loader that only loads GCF API classes. Those are classes whose package is exactly * {@code com.google.cloud.functions}. The package can't be a subpackage, such as * {@code com.google.cloud.functions.invoker}. * *

This loader allows us to load the classes from a user function, without making the * runtime classes visible to them. We will make this loader the parent of the * {@link URLClassLoader} that loads the user code in order to filter out those runtime classes. * *

The reason we do need to share the API classes between the runtime and the user function is * so that the runtime can instantiate the function class and cast it to * {@link com.google.cloud.functions.HttpFunction} or whatever. */ private static class OnlyApiClassLoader extends ClassLoader { private final ClassLoader runtimeClassLoader; OnlyApiClassLoader(ClassLoader runtimeClassLoader) { super(getSystemOrBootstrapClassLoader()); this.runtimeClassLoader = runtimeClassLoader; } @Override protected Class findClass(String name) throws ClassNotFoundException { String prefix = "com.google.cloud.functions."; if ((name.startsWith(prefix) && Character.isUpperCase(name.charAt(prefix.length()))) || name.startsWith("javax.servlet.") || isCloudEventsApiClass(name)) { return runtimeClassLoader.loadClass(name); } return super.findClass(name); // should throw ClassNotFoundException } private static final String CLOUD_EVENTS_API_PREFIX = "io.cloudevents."; private static final int CLOUD_EVENTS_API_PREFIX_LENGTH = CLOUD_EVENTS_API_PREFIX.length(); private static boolean isCloudEventsApiClass(String name) { return name.startsWith(CLOUD_EVENTS_API_PREFIX) && Character.isUpperCase(name.charAt(CLOUD_EVENTS_API_PREFIX_LENGTH)); } private static ClassLoader getSystemOrBootstrapClassLoader() { try { // We're still building against the Java 8 API, so we have to use reflection for now. Method getPlatformClassLoader = ClassLoader.class.getMethod("getPlatformClassLoader"); return (ClassLoader) getPlatformClassLoader.invoke(null); } catch (ReflectiveOperationException e) { return null; } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy