org.apache.hadoop.yarn.webapp.WebApps Maven / Gradle / Ivy
The newest version!
/**
* 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 org.apache.hadoop.yarn.webapp;
import static org.apache.hadoop.util.Preconditions.checkNotNull;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configuration.IntegerRanges;
import org.apache.hadoop.http.HttpConfig.Policy;
import org.apache.hadoop.http.HttpServer2;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authorize.AccessControlList;
import org.apache.hadoop.security.http.RestCsrfPreventionFilter;
import org.apache.hadoop.yarn.api.ApplicationClientProtocol;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.webapp.util.WebAppUtils;
import org.eclipse.jetty.webapp.WebAppContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceFilter;
/**
* Helpers to create an embedded webapp.
*
* Quick start:
*
* WebApp wa = WebApps.$for(myApp).start();
* Starts a webapp with default routes binds to 0.0.0.0 (all network interfaces)
* on an ephemeral port, which can be obtained with:
* int port = wa.port();
* With more options:
*
* WebApp wa = WebApps.$for(myApp).at(address, port).
* with(configuration).
* start(new WebApp() {
* @Override public void setup() {
* route("/foo/action", FooController.class);
* route("/foo/:id", FooController.class, "show");
* }
* });
*/
@InterfaceAudience.LimitedPrivate({"YARN", "MapReduce"})
public class WebApps {
static final Logger LOG = LoggerFactory.getLogger(WebApps.class);
public static class Builder {
static class ServletStruct {
public Class extends HttpServlet> clazz;
public String name;
public String spec;
public Map params;
public boolean loadExistingFilters = true;
}
final String name;
final String wsName;
final Class api;
final T application;
String bindAddress = "0.0.0.0";
int port = 0;
boolean findPort = false;
Configuration conf;
Policy httpPolicy = null;
boolean needsClientAuth = false;
String portRangeConfigKey = null;
boolean devMode = false;
private String spnegoPrincipalKey;
private String spnegoKeytabKey;
private String csrfConfigPrefix;
private String xfsConfigPrefix;
private final HashSet servlets = new HashSet();
private final HashMap attributes = new HashMap();
private ApplicationClientProtocol appClientProtocol;
Builder(String name, Class api, T application, String wsName) {
this.name = name;
this.api = api;
this.application = application;
this.wsName = wsName;
}
Builder(String name, Class api, T application) {
this(name, api, application, null);
}
public Builder at(String bindAddress) {
String[] parts = StringUtils.split(bindAddress, ':');
if (parts.length == 2) {
int port = Integer.parseInt(parts[1]);
return at(parts[0], port, port == 0);
}
return at(bindAddress, 0, true);
}
public Builder at(int port) {
return at("0.0.0.0", port, port == 0);
}
public Builder at(String address, int port, boolean findPort) {
this.bindAddress = checkNotNull(address, "bind address");
this.port = port;
this.findPort = findPort;
return this;
}
public Builder withAttribute(String key, Object value) {
attributes.put(key, value);
return this;
}
public Builder withServlet(String name, String pathSpec,
Class extends HttpServlet> servlet) {
ServletStruct struct = new ServletStruct();
struct.clazz = servlet;
struct.name = name;
struct.spec = pathSpec;
servlets.add(struct);
return this;
}
public Builder withServlet(String name, String pathSpec,
Class extends HttpServlet> servlet,
Map params,boolean loadExistingFilters) {
ServletStruct struct = new ServletStruct();
struct.clazz = servlet;
struct.name = name;
struct.spec = pathSpec;
struct.params = params;
struct.loadExistingFilters = loadExistingFilters;
servlets.add(struct);
return this;
}
public Builder with(Configuration conf) {
this.conf = conf;
return this;
}
public Builder withHttpPolicy(Configuration conf, Policy httpPolicy) {
this.conf = conf;
this.httpPolicy = httpPolicy;
return this;
}
public Builder needsClientAuth(boolean needsClientAuth) {
this.needsClientAuth = needsClientAuth;
return this;
}
/**
* Set port range config key and associated configuration object.
* @param config configuration.
* @param portRangeConfKey port range config key.
* @return builder object.
*/
public Builder withPortRange(Configuration config,
String portRangeConfKey) {
this.conf = config;
this.portRangeConfigKey = portRangeConfKey;
return this;
}
public Builder withHttpSpnegoPrincipalKey(String spnegoPrincipalKey) {
this.spnegoPrincipalKey = spnegoPrincipalKey;
return this;
}
public Builder withHttpSpnegoKeytabKey(String spnegoKeytabKey) {
this.spnegoKeytabKey = spnegoKeytabKey;
return this;
}
/**
* Enable the CSRF filter.
* @param prefix The config prefix that identifies the
* CSRF parameters applicable for this filter
* instance.
* @return the Builder instance
*/
public Builder withCSRFProtection(String prefix) {
this.csrfConfigPrefix = prefix;
return this;
}
/**
* Enable the XFS filter.
* @param prefix The config prefix that identifies the
* XFS parameters applicable for this filter
* instance.
* @return the Builder instance
*/
public Builder withXFSProtection(String prefix) {
this.xfsConfigPrefix = prefix;
return this;
}
public Builder inDevMode() {
devMode = true;
return this;
}
public Builder withAppClientProtocol(
ApplicationClientProtocol appClientProto) {
this.appClientProtocol = appClientProto;
return this;
}
public WebApp build(WebApp webapp) {
if (webapp == null) {
webapp = new WebApp() {
@Override
public void setup() {
// Defaults should be fine in usual cases
}
};
}
webapp.setName(name);
webapp.setWebServices(wsName);
String basePath = "/" + name;
webapp.setRedirectPath(basePath);
List pathList = new ArrayList();
if (basePath.equals("/")) {
webapp.addServePathSpec("/*");
pathList.add("/*");
} else {
webapp.addServePathSpec(basePath);
webapp.addServePathSpec(basePath + "/*");
pathList.add(basePath + "/*");
}
if (wsName != null && !wsName.equals(basePath)) {
if (wsName.equals("/")) {
webapp.addServePathSpec("/*");
pathList.add("/*");
} else {
webapp.addServePathSpec("/" + wsName);
webapp.addServePathSpec("/" + wsName + "/*");
pathList.add("/" + wsName + "/*");
}
}
for (ServletStruct s : servlets) {
if (!pathList.contains(s.spec)) {
// The servlet told us to not load-existing filters, but we still want
// to add the default authentication filter always, so add it to the
// pathList
if (!s.loadExistingFilters) {
pathList.add(s.spec);
}
}
}
if (conf == null) {
conf = new Configuration();
}
try {
if (application != null) {
webapp.setHostClass(application.getClass());
} else {
String cls = inferHostClass();
LOG.debug("setting webapp host class to {}", cls);
webapp.setHostClass(Class.forName(cls));
}
if (devMode) {
if (port > 0) {
try {
new URL("http://localhost:"+ port +"/__stop").getContent();
LOG.info("stopping existing webapp instance");
Thread.sleep(100);
} catch (ConnectException e) {
LOG.info("no existing webapp instance found: {}", e.toString());
} catch (Exception e) {
// should not be fatal
LOG.warn("error stopping existing instance: {}", e.toString());
}
} else {
LOG.error("dev mode does NOT work with ephemeral port!");
System.exit(1);
}
}
String httpScheme;
if (this.httpPolicy == null) {
httpScheme = WebAppUtils.getHttpSchemePrefix(conf);
} else {
httpScheme =
(httpPolicy == Policy.HTTPS_ONLY) ? WebAppUtils.HTTPS_PREFIX
: WebAppUtils.HTTP_PREFIX;
}
HttpServer2.Builder builder = new HttpServer2.Builder()
.setName(name).setConf(conf).setFindPort(findPort)
.setACL(new AccessControlList(conf.get(
YarnConfiguration.YARN_ADMIN_ACL,
YarnConfiguration.DEFAULT_YARN_ADMIN_ACL)))
.setPathSpec(pathList.toArray(new String[0]));
// Set the X-FRAME-OPTIONS header, use the HttpServer2 default if
// the header value is not specified
Map xfsParameters =
getConfigParameters(xfsConfigPrefix);
if (xfsParameters != null) {
String xFrameOptions = xfsParameters.get("xframe-options");
if (xFrameOptions != null) {
builder.configureXFrame(hasXFSEnabled())
.setXFrameOption(xFrameOptions);
}
}
// Get port ranges from config.
IntegerRanges ranges = null;
if (portRangeConfigKey != null) {
ranges = conf.getRange(portRangeConfigKey, "");
}
int startPort = port;
if (ranges != null && !ranges.isEmpty()) {
// Set port ranges if its configured.
startPort = ranges.getRangeStart();
builder.setPortRanges(ranges);
}
builder.addEndpoint(URI.create(httpScheme + bindAddress +
":" + startPort));
boolean hasSpnegoConf = spnegoPrincipalKey != null
&& conf.get(spnegoPrincipalKey) != null && spnegoKeytabKey != null
&& conf.get(spnegoKeytabKey) != null;
if (hasSpnegoConf) {
builder.setUsernameConfKey(spnegoPrincipalKey)
.setKeytabConfKey(spnegoKeytabKey)
.setSecurityEnabled(UserGroupInformation.isSecurityEnabled());
}
if (httpScheme.equals(WebAppUtils.HTTPS_PREFIX)) {
String amKeystoreLoc = System.getenv("KEYSTORE_FILE_LOCATION");
if (amKeystoreLoc != null) {
LOG.info("Setting keystore location to " + amKeystoreLoc);
String password = System.getenv("KEYSTORE_PASSWORD");
builder.keyStore(amKeystoreLoc, password, "jks");
} else {
LOG.info("Loading standard ssl config");
WebAppUtils.loadSslConfiguration(builder, conf);
}
builder.needsClientAuth(needsClientAuth);
if (needsClientAuth) {
String amTruststoreLoc = System.getenv("TRUSTSTORE_FILE_LOCATION");
if (amTruststoreLoc != null) {
LOG.info("Setting truststore location to " + amTruststoreLoc);
String password = System.getenv("TRUSTSTORE_PASSWORD");
builder.trustStore(amTruststoreLoc, password, "jks");
}
}
}
HttpServer2 server = builder.build();
for(ServletStruct struct: servlets) {
if (!struct.loadExistingFilters) {
server.addInternalServlet(struct.name, struct.spec,
struct.clazz, struct.params);
} else {
server.addServlet(struct.name, struct.spec, struct.clazz);
}
}
for(Map.Entry entry : attributes.entrySet()) {
server.setAttribute(entry.getKey(), entry.getValue());
}
Map params = getConfigParameters(csrfConfigPrefix);
if (hasCSRFEnabled(params)) {
LOG.info("CSRF Protection has been enabled for the {} application. "
+ "Please ensure that there is an authentication mechanism "
+ "enabled (kerberos, custom, etc).",
name);
String restCsrfClassName = RestCsrfPreventionFilter.class.getName();
HttpServer2.defineFilter(server.getWebAppContext(), restCsrfClassName,
restCsrfClassName, params,
new String[] {"/*"});
}
HttpServer2.defineFilter(server.getWebAppContext(), "guice",
GuiceFilter.class.getName(), null, new String[] { "/*" });
webapp.setConf(conf);
webapp.setHttpServer(server);
} catch (ClassNotFoundException e) {
throw new WebAppException("Error starting http server", e);
} catch (IOException e) {
throw new WebAppException("Error starting http server", e);
}
Injector injector = Guice.createInjector(webapp, new AbstractModule() {
@Override
protected void configure() {
if (api != null) {
bind(api).toInstance(application);
}
if (appClientProtocol != null) {
bind(ApplicationClientProtocol.class).toInstance(appClientProtocol);
}
}
});
LOG.info("Registered webapp guice modules");
// save a guice filter instance for webapp stop (mostly for unit tests)
webapp.setGuiceFilter(injector.getInstance(GuiceFilter.class));
if (devMode) {
injector.getInstance(Dispatcher.class).setDevMode(devMode);
LOG.info("in dev mode!");
}
return webapp;
}
private boolean hasCSRFEnabled(Map params) {
return params != null && Boolean.valueOf(params.get("enabled"));
}
/**
* XFS filter is enabled by default. If the enabled flag is not explicitly
* specified and set to "false", this method returns true.
* @return true if XFS is enabled, false otherwise.
*/
private boolean hasXFSEnabled() {
return conf.getBoolean(YarnConfiguration.YARN_XFS_ENABLED, true);
}
private Map getConfigParameters(String configPrefix) {
return configPrefix != null ? conf.getPropsWithPrefix(configPrefix) :
null;
}
public WebApp start() {
return start(null);
}
public WebApp start(WebApp webapp) {
return start(webapp, null);
}
public WebApp start(WebApp webapp, WebAppContext ui2Context) {
WebApp webApp = build(webapp);
HttpServer2 httpServer = webApp.httpServer();
if (ui2Context != null) {
addFiltersForNewContext(ui2Context);
httpServer.addHandlerAtFront(ui2Context);
}
try {
httpServer.start();
LOG.info("Web app " + name + " started at "
+ httpServer.getConnectorAddress(0).getPort());
} catch (IOException e) {
throw new WebAppException("Error starting http server", e, webApp);
}
return webApp;
}
private void addFiltersForNewContext(WebAppContext ui2Context) {
Map params = getConfigParameters(csrfConfigPrefix);
if (hasCSRFEnabled(params)) {
LOG.info("CSRF Protection has been enabled for the {} application. "
+ "Please ensure that there is an authentication mechanism "
+ "enabled (kerberos, custom, etc).", name);
String restCsrfClassName = RestCsrfPreventionFilter.class.getName();
HttpServer2.defineFilter(ui2Context, restCsrfClassName,
restCsrfClassName, params, new String[]{"/*"});
}
}
private String inferHostClass() {
String thisClass = this.getClass().getName();
Throwable t = new Throwable();
for (StackTraceElement e : t.getStackTrace()) {
if (e.getClassName().equals(thisClass)) continue;
return e.getClassName();
}
LOG.warn("could not infer host class from", t);
return thisClass;
}
}
/**
* Create a new webapp builder.
* @see WebApps for a complete example
* @param application (holding the embedded webapp) type
* @param prefix of the webapp
* @param api the api class for the application
* @param app the application instance
* @param wsPrefix the prefix for the webservice api for this app
* @return a webapp builder
*/
public static Builder $for(String prefix, Class api, T app, String wsPrefix) {
return new Builder(prefix, api, app, wsPrefix);
}
/**
* Create a new webapp builder.
* @see WebApps for a complete example
* @param application (holding the embedded webapp) type
* @param prefix of the webapp
* @param api the api class for the application
* @param app the application instance
* @return a webapp builder
*/
public static Builder $for(String prefix, Class api, T app) {
return new Builder(prefix, api, app);
}
// Short cut mostly for tests/demos
@SuppressWarnings("unchecked")
public static Builder $for(String prefix, T app) {
return $for(prefix, (Class)app.getClass(), app);
}
// Ditto
public static Builder $for(T app) {
return $for("", app);
}
public static Builder $for(String prefix) {
return $for(prefix, null, null);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy