@Override protected void configure() { bind(Runnable.class) .annotatedWith(Names.named(AbortHandler.ABORT_HANDLER_KEY)) .to(AbortCallback.class); bind(AbortCallback.class).in(Singleton.class); bind(Runnable.class) .annotatedWith(Names.named(QuitHandler.QUIT_HANDLER_KEY)) .to(QuitCallback.class); bind(QuitCallback.class).in(Singleton.class); bind(new TypeLiteral<Supplier<Boolean>>() {}) .annotatedWith(Names.named(HealthHandler.HEALTH_CHECKER_KEY)) .toInstance(Suppliers.ofInstance(true)); final Optional<String> hostnameOverride = Optional.fromNullable(HOSTNAME_OVERRIDE.get()); if (hostnameOverride.isPresent()) { try { InetAddress.getByName(hostnameOverride.get()); } catch (UnknownHostException e) { /* Possible misconfiguration, so warn the user. */ LOG.warning( "Unable to resolve name specified in -hostname. " + "Depending on your environment, this may be valid."); } } install( new PrivateModule() { @Override protected void configure() { bind(new TypeLiteral<Optional<String>>() {}).toInstance(hostnameOverride); bind(HttpService.class).to(HttpServerLauncher.class); bind(HttpServerLauncher.class).in(Singleton.class); expose(HttpServerLauncher.class); expose(HttpService.class); } }); SchedulerServicesModule.addAppStartupServiceBinding(binder()).to(HttpServerLauncher.class); bind(LeaderRedirect.class).in(Singleton.class); SchedulerServicesModule.addAppStartupServiceBinding(binder()).to(RedirectMonitor.class); if (production) { install(PRODUCTION_SERVLET_CONTEXT_LISTENER); } }
@Override protected void startUp() { server = new Server(); ServletContextHandler servletHandler = new ServletContextHandler(server, "/", ServletContextHandler.NO_SESSIONS); servletHandler.addServlet(DefaultServlet.class, "/"); servletHandler.addFilter(GuiceFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); servletHandler.addEventListener(servletContextListener); HandlerCollection rootHandler = new HandlerList(); RequestLogHandler logHandler = new RequestLogHandler(); logHandler.setRequestLog(new Slf4jRequestLog()); rootHandler.addHandler(logHandler); rootHandler.addHandler(servletHandler); ServerConnector connector = new ServerConnector(server); connector.setPort(HTTP_PORT.get()); server.addConnector(connector); server.setHandler(getGzipHandler(getRewriteHandler(rootHandler))); try { connector.open(); server.start(); } catch (Exception e) { throw Throwables.propagate(e); } String host; if (connector.getHost() == null) { // Resolve the local host name. try { host = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { throw new RuntimeException("Failed to resolve local host address: " + e, e); } } else { // If jetty was configured with a specific host to bind to, use that. host = connector.getHost(); } serverAddress = HostAndPort.fromParts(host, connector.getLocalPort()); }
/** * Manages translation from a string-mapped configuration to a concrete configuration type, and * defaults for optional values. * * <p>TODO(William Farner): Add input validation to all fields (strings not empty, positive ints, * etc). */ public final class ConfigurationManager { @CmdLine( name = "allowed_container_types", help = "Container types that are allowed to be used by jobs.") private static final Arg<List<Container._Fields>> ALLOWED_CONTAINER_TYPES = Arg.create(ImmutableList.of(Container._Fields.MESOS)); @CmdLine( name = "allow_docker_parameters", help = "Allow to pass docker container parameters in the job.") private static final Arg<Boolean> ENABLE_DOCKER_PARAMETERS = Arg.create(false); public static final String DEDICATED_ATTRIBUTE = "dedicated"; private static final Pattern GOOD_IDENTIFIER = Pattern.compile(GOOD_IDENTIFIER_PATTERN_JVM); private static final int MAX_IDENTIFIER_LENGTH = 255; private interface Validator<T> { void validate(T value) throws TaskDescriptionException; } private static class GreaterThan implements Validator<Number> { private final double min; private final String label; GreaterThan(double min, String label) { this.min = min; this.label = label; } @Override public void validate(Number value) throws TaskDescriptionException { if (this.min >= value.doubleValue()) { throw new TaskDescriptionException(label + " must be greater than " + this.min); } } } private static class RequiredFieldValidator<T> implements Validator<TaskConfig> { private final _Fields field; private final Validator<T> validator; RequiredFieldValidator(_Fields field, Validator<T> validator) { this.field = field; this.validator = validator; } public void validate(TaskConfig task) throws TaskDescriptionException { if (!task.isSet(field)) { throw new TaskDescriptionException("Field " + field.getFieldName() + " is required."); } @SuppressWarnings("unchecked") T value = (T) task.getFieldValue(field); validator.validate(value); } } private static final Iterable<RequiredFieldValidator<?>> REQUIRED_FIELDS_VALIDATORS = ImmutableList.of( new RequiredFieldValidator<>(_Fields.NUM_CPUS, new GreaterThan(0.0, "num_cpus")), new RequiredFieldValidator<>(_Fields.RAM_MB, new GreaterThan(0.0, "ram_mb")), new RequiredFieldValidator<>(_Fields.DISK_MB, new GreaterThan(0.0, "disk_mb"))); private ConfigurationManager() { // Utility class. } /** * Verifies that an identifier is an acceptable name component. * * @param identifier Identifier to check. * @return false if the identifier is null or invalid. */ public static boolean isGoodIdentifier(@Nullable String identifier) { return identifier != null && GOOD_IDENTIFIER.matcher(identifier).matches() && identifier.length() <= MAX_IDENTIFIER_LENGTH; } private static void requireNonNull(Object value, String error) throws TaskDescriptionException { if (value == null) { throw new TaskDescriptionException(error); } } private static void assertOwnerValidity(IIdentity jobOwner) throws TaskDescriptionException { requireNonNull(jobOwner, "No job owner specified!"); requireNonNull(jobOwner.getRole(), "No job role specified!"); requireNonNull(jobOwner.getUser(), "No job user specified!"); if (!isGoodIdentifier(jobOwner.getRole())) { throw new TaskDescriptionException( "Job role contains illegal characters: " + jobOwner.getRole()); } if (!isGoodIdentifier(jobOwner.getUser())) { throw new TaskDescriptionException( "Job user contains illegal characters: " + jobOwner.getUser()); } } private static String getRole(IValueConstraint constraint) { return Iterables.getOnlyElement(constraint.getValues()).split("/")[0]; } private static boolean isValueConstraint(ITaskConstraint taskConstraint) { return taskConstraint.getSetField() == TaskConstraint._Fields.VALUE; } public static boolean isDedicated(Iterable<IConstraint> taskConstraints) { return Iterables.any(taskConstraints, getConstraintByName(DEDICATED_ATTRIBUTE)); } @Nullable private static IConstraint getDedicatedConstraint(ITaskConfig task) { return Iterables.find(task.getConstraints(), getConstraintByName(DEDICATED_ATTRIBUTE), null); } /** * Check validity of and populates defaults in a job configuration. This will return a deep copy * of the provided job configuration with default configuration values applied, and configuration * map values sanitized and applied to their respective struct fields. * * @param job Job to validate and populate. * @return A deep copy of {@code job} that has been populated. * @throws TaskDescriptionException If the job configuration is invalid. */ public static IJobConfiguration validateAndPopulate(IJobConfiguration job) throws TaskDescriptionException { Objects.requireNonNull(job); if (!job.isSetTaskConfig()) { throw new TaskDescriptionException("Job configuration must have taskConfig set."); } if (job.getInstanceCount() <= 0) { throw new TaskDescriptionException("Instance count must be positive."); } JobConfiguration builder = job.newBuilder(); if (!JobKeys.isValid(job.getKey())) { throw new TaskDescriptionException("Job key " + job.getKey() + " is invalid."); } if (job.isSetOwner()) { assertOwnerValidity(job.getOwner()); if (!job.getKey().getRole().equals(job.getOwner().getRole())) { throw new TaskDescriptionException("Role in job key must match job owner."); } } builder.setTaskConfig( validateAndPopulate(ITaskConfig.build(builder.getTaskConfig())).newBuilder()); // Only one of [service=true, cron_schedule] may be set. if (!Strings.isNullOrEmpty(job.getCronSchedule()) && builder.getTaskConfig().isIsService()) { throw new TaskDescriptionException( "A service task may not be run on a cron schedule: " + builder); } return IJobConfiguration.build(builder); } /** * Check validity of and populates defaults in a task configuration. This will return a deep copy * of the provided task configuration with default configuration values applied, and configuration * map values sanitized and applied to their respective struct fields. * * @param config Task config to validate and populate. * @return A reference to the modified {@code config} (for chaining). * @throws TaskDescriptionException If the task is invalid. */ public static ITaskConfig validateAndPopulate(ITaskConfig config) throws TaskDescriptionException { TaskConfig builder = config.newBuilder(); if (!builder.isSetRequestedPorts()) { builder.setRequestedPorts(ImmutableSet.of()); } maybeFillLinks(builder); if (!isGoodIdentifier(config.getJobName())) { throw new TaskDescriptionException( "Job name contains illegal characters: " + config.getJobName()); } if (!isGoodIdentifier(config.getEnvironment())) { throw new TaskDescriptionException( "Environment contains illegal characters: " + config.getEnvironment()); } if (config.isSetTier() && !isGoodIdentifier(config.getTier())) { throw new TaskDescriptionException("Tier contains illegal characters: " + config.getTier()); } if (config.isSetJob()) { if (!JobKeys.isValid(config.getJob())) { // Job key is set but invalid throw new TaskDescriptionException("Job key " + config.getJob() + " is invalid."); } if (!config.getJob().getRole().equals(config.getOwner().getRole())) { // Both owner and job key are set but don't match throw new TaskDescriptionException("Role must match job owner."); } } else { // TODO(maxim): Make sure both key and owner are populated to support older clients. // Remove in 0.7.0. (AURORA-749). // Job key is not set -> populate from owner, environment and name assertOwnerValidity(config.getOwner()); builder.setJob( JobKeys.from(config.getOwner().getRole(), config.getEnvironment(), config.getJobName()) .newBuilder()); } if (!builder.isSetExecutorConfig()) { throw new TaskDescriptionException("Configuration may not be null"); } // Maximize the usefulness of any thrown error message by checking required fields first. for (RequiredFieldValidator<?> validator : REQUIRED_FIELDS_VALIDATORS) { validator.validate(builder); } IConstraint constraint = getDedicatedConstraint(config); if (constraint != null) { if (!isValueConstraint(constraint.getConstraint())) { throw new TaskDescriptionException("A dedicated constraint must be of value type."); } IValueConstraint valueConstraint = constraint.getConstraint().getValue(); if (valueConstraint.getValues().size() != 1) { throw new TaskDescriptionException("A dedicated constraint must have exactly one value"); } String dedicatedRole = getRole(valueConstraint); if (!config.getOwner().getRole().equals(dedicatedRole)) { throw new TaskDescriptionException( "Only " + dedicatedRole + " may use hosts dedicated for that role."); } } Optional<Container._Fields> containerType; if (config.isSetContainer()) { IContainer containerConfig = config.getContainer(); containerType = Optional.of(containerConfig.getSetField()); if (containerConfig.isSetDocker()) { if (!containerConfig.getDocker().isSetImage()) { throw new TaskDescriptionException("A container must specify an image"); } if (containerConfig.getDocker().isSetParameters() && !containerConfig.getDocker().getParameters().isEmpty() && !ENABLE_DOCKER_PARAMETERS.get()) { throw new TaskDescriptionException("Docker parameters not allowed."); } } } else { // Default to mesos container type if unset. containerType = Optional.of(Container._Fields.MESOS); } if (!containerType.isPresent()) { throw new TaskDescriptionException("A job must have a container type."); } if (!ALLOWED_CONTAINER_TYPES.get().contains(containerType.get())) { throw new TaskDescriptionException( "The container type " + containerType.get().toString() + " is not allowed"); } return ITaskConfig.build(builder); } /** * Provides a filter for the given constraint name. * * @param name The name of the constraint. * @return A filter that matches the constraint. */ public static Predicate<IConstraint> getConstraintByName(final String name) { return constraint -> constraint.getName().equals(name); } private static void maybeFillLinks(TaskConfig task) { if (task.getTaskLinksSize() == 0) { ImmutableMap.Builder<String, String> links = ImmutableMap.builder(); if (task.getRequestedPorts().contains("health")) { links.put("health", "http://%host%:%port:health%"); } if (task.getRequestedPorts().contains("http")) { links.put("http", "http://%host%:%port:http%"); } task.setTaskLinks(links.build()); } } /** Thrown when an invalid task or job configuration is encountered. */ public static class TaskDescriptionException extends Exception { public TaskDescriptionException(String msg) { super(msg); } } }
/** * Check validity of and populates defaults in a task configuration. This will return a deep copy * of the provided task configuration with default configuration values applied, and configuration * map values sanitized and applied to their respective struct fields. * * @param config Task config to validate and populate. * @return A reference to the modified {@code config} (for chaining). * @throws TaskDescriptionException If the task is invalid. */ public static ITaskConfig validateAndPopulate(ITaskConfig config) throws TaskDescriptionException { TaskConfig builder = config.newBuilder(); if (!builder.isSetRequestedPorts()) { builder.setRequestedPorts(ImmutableSet.of()); } maybeFillLinks(builder); if (!isGoodIdentifier(config.getJobName())) { throw new TaskDescriptionException( "Job name contains illegal characters: " + config.getJobName()); } if (!isGoodIdentifier(config.getEnvironment())) { throw new TaskDescriptionException( "Environment contains illegal characters: " + config.getEnvironment()); } if (config.isSetTier() && !isGoodIdentifier(config.getTier())) { throw new TaskDescriptionException("Tier contains illegal characters: " + config.getTier()); } if (config.isSetJob()) { if (!JobKeys.isValid(config.getJob())) { // Job key is set but invalid throw new TaskDescriptionException("Job key " + config.getJob() + " is invalid."); } if (!config.getJob().getRole().equals(config.getOwner().getRole())) { // Both owner and job key are set but don't match throw new TaskDescriptionException("Role must match job owner."); } } else { // TODO(maxim): Make sure both key and owner are populated to support older clients. // Remove in 0.7.0. (AURORA-749). // Job key is not set -> populate from owner, environment and name assertOwnerValidity(config.getOwner()); builder.setJob( JobKeys.from(config.getOwner().getRole(), config.getEnvironment(), config.getJobName()) .newBuilder()); } if (!builder.isSetExecutorConfig()) { throw new TaskDescriptionException("Configuration may not be null"); } // Maximize the usefulness of any thrown error message by checking required fields first. for (RequiredFieldValidator<?> validator : REQUIRED_FIELDS_VALIDATORS) { validator.validate(builder); } IConstraint constraint = getDedicatedConstraint(config); if (constraint != null) { if (!isValueConstraint(constraint.getConstraint())) { throw new TaskDescriptionException("A dedicated constraint must be of value type."); } IValueConstraint valueConstraint = constraint.getConstraint().getValue(); if (valueConstraint.getValues().size() != 1) { throw new TaskDescriptionException("A dedicated constraint must have exactly one value"); } String dedicatedRole = getRole(valueConstraint); if (!config.getOwner().getRole().equals(dedicatedRole)) { throw new TaskDescriptionException( "Only " + dedicatedRole + " may use hosts dedicated for that role."); } } Optional<Container._Fields> containerType; if (config.isSetContainer()) { IContainer containerConfig = config.getContainer(); containerType = Optional.of(containerConfig.getSetField()); if (containerConfig.isSetDocker()) { if (!containerConfig.getDocker().isSetImage()) { throw new TaskDescriptionException("A container must specify an image"); } if (containerConfig.getDocker().isSetParameters() && !containerConfig.getDocker().getParameters().isEmpty() && !ENABLE_DOCKER_PARAMETERS.get()) { throw new TaskDescriptionException("Docker parameters not allowed."); } } } else { // Default to mesos container type if unset. containerType = Optional.of(Container._Fields.MESOS); } if (!containerType.isPresent()) { throw new TaskDescriptionException("A job must have a container type."); } if (!ALLOWED_CONTAINER_TYPES.get().contains(containerType.get())) { throw new TaskDescriptionException( "The container type " + containerType.get().toString() + " is not allowed"); } return ITaskConfig.build(builder); }
public AopModule() { this( ImmutableMap.of( "createJob", ENABLE_JOB_CREATION.get(), "acquireLock", ENABLE_UPDATES.get())); }
/** Binding module for AOP-style decorations of the thrift API. */ public class AopModule extends AbstractModule { @CmdLine(name = "enable_job_updates", help = "Whether new job updates should be accepted.") private static final Arg<Boolean> ENABLE_UPDATES = Arg.create(true); @CmdLine( name = "enable_job_creation", help = "Allow new jobs to be created, if false all job creation requests will be denied.") private static final Arg<Boolean> ENABLE_JOB_CREATION = Arg.create(true); private static final Matcher<? super Class<?>> THRIFT_IFACE_MATCHER = Matchers.subclassesOf(AnnotatedAuroraAdmin.class) .and(Matchers.annotatedWith(DecoratedThrift.class)); private final Map<String, Boolean> toggledMethods; public AopModule() { this( ImmutableMap.of( "createJob", ENABLE_JOB_CREATION.get(), "acquireLock", ENABLE_UPDATES.get())); } @VisibleForTesting AopModule(Map<String, Boolean> toggledMethods) { this.toggledMethods = ImmutableMap.copyOf(toggledMethods); } private static final Function<Method, String> GET_NAME = new Function<Method, String>() { @Override public String apply(Method method) { return method.getName(); } }; @Override protected void configure() { requireBinding(CapabilityValidator.class); // Layer ordering: // APIVersion -> Log -> CapabilityValidator -> FeatureToggle -> StatsExporter -> // SchedulerThriftInterface // It's important for this interceptor to be registered first to ensure it's at the 'top' of // the stack and the standard message is always applied. bindThriftDecorator(new ServerInfoInterceptor()); // TODO(Sathya): Consider using provider pattern for constructing interceptors to facilitate // unit testing without the creation of Guice injectors. bindThriftDecorator(new LoggingInterceptor()); // Note: it's important that the capability interceptor is only applied to AuroraAdmin.Iface // methods, and does not pick up methods on AuroraSchedulerManager.Iface. MethodInterceptor authInterceptor = new UserCapabilityInterceptor(); requestInjection(authInterceptor); bindInterceptor( THRIFT_IFACE_MATCHER, GuiceUtils.interfaceMatcher(AuroraAdmin.Iface.class, true), authInterceptor); install( new PrivateModule() { @Override protected void configure() { // Ensure that the provided methods exist on the decorated interface. List<Method> methods = ImmutableList.copyOf(AuroraSchedulerManager.Iface.class.getMethods()); for (String toggledMethod : toggledMethods.keySet()) { Preconditions.checkArgument( Iterables.any( methods, Predicates.compose(Predicates.equalTo(toggledMethod), GET_NAME)), String.format( "Method %s was not found in class %s", toggledMethod, AuroraSchedulerManager.Iface.class)); } bind(new TypeLiteral<Map<String, Boolean>>() {}).toInstance(toggledMethods); bind(IsFeatureEnabled.class).in(Singleton.class); Key<Predicate<Method>> predicateKey = Key.get(new TypeLiteral<Predicate<Method>>() {}); bind(predicateKey).to(IsFeatureEnabled.class); expose(predicateKey); } }); bindThriftDecorator(new FeatureToggleInterceptor()); bindThriftDecorator(new ThriftStatsExporterInterceptor()); } private void bindThriftDecorator(MethodInterceptor interceptor) { bindThriftDecorator(binder(), THRIFT_IFACE_MATCHER, interceptor); } @VisibleForTesting static void bindThriftDecorator( Binder binder, Matcher<? super Class<?>> classMatcher, MethodInterceptor interceptor) { binder.bindInterceptor( classMatcher, Matchers.returns(Matchers.subclassesOf(Response.class)), interceptor); binder.requestInjection(interceptor); } private static class IsFeatureEnabled implements Predicate<Method> { private final Predicate<String> methodEnabled; @Inject IsFeatureEnabled(Map<String, Boolean> toggleMethods) { Predicate<String> builder = Predicates.alwaysTrue(); for (Map.Entry<String, Boolean> toggleMethod : toggleMethods.entrySet()) { Predicate<String> enableMethod = Predicates.or( toggleMethod.getValue() ? Predicates.alwaysTrue() : Predicates.alwaysFalse(), Predicates.not(Predicates.equalTo(toggleMethod.getKey()))); builder = Predicates.and(builder, enableMethod); } methodEnabled = builder; } @Override public boolean apply(Method method) { return methodEnabled.apply(method.getName()); } } }
/** * Binding module for scheduler HTTP servlets. * * <p>TODO(wfarner): Continue improvements here by simplifying serving of static assets. Jetty's * DefaultServlet can take over this responsibility, and jetty-rewite can be used to rewrite * requests (for static assets) similar to what we currently do with path specs. */ public class JettyServerModule extends AbstractModule { private static final Logger LOG = Logger.getLogger(JettyServerModule.class.getName()); // The name of the request attribute where the path for the current request before it was // rewritten is stored. static final String ORIGINAL_PATH_ATTRIBUTE_NAME = "originalPath"; @CmdLine( name = "hostname", help = "The hostname to advertise in ZooKeeper instead of the locally-resolved hostname.") private static final Arg<String> HOSTNAME_OVERRIDE = Arg.create(null); @Nonnegative @CmdLine( name = "http_port", help = "The port to start an HTTP server on. Default value will choose a random port.") protected static final Arg<Integer> HTTP_PORT = Arg.create(0); public static final Map<String, String> GUICE_CONTAINER_PARAMS = ImmutableMap.of( FEATURE_POJO_MAPPING, Boolean.TRUE.toString(), PROPERTY_CONTAINER_REQUEST_FILTERS, GZIPContentEncodingFilter.class.getName(), PROPERTY_CONTAINER_RESPONSE_FILTERS, GZIPContentEncodingFilter.class.getName()); private static final String STATIC_ASSETS_ROOT = Resource.newClassPathResource("scheduler/assets/index.html") .toString() .replace("assets/index.html", ""); private final boolean production; public JettyServerModule() { this(true); } @VisibleForTesting JettyServerModule(boolean production) { this.production = production; } @Override protected void configure() { bind(Runnable.class) .annotatedWith(Names.named(AbortHandler.ABORT_HANDLER_KEY)) .to(AbortCallback.class); bind(AbortCallback.class).in(Singleton.class); bind(Runnable.class) .annotatedWith(Names.named(QuitHandler.QUIT_HANDLER_KEY)) .to(QuitCallback.class); bind(QuitCallback.class).in(Singleton.class); bind(new TypeLiteral<Supplier<Boolean>>() {}) .annotatedWith(Names.named(HealthHandler.HEALTH_CHECKER_KEY)) .toInstance(Suppliers.ofInstance(true)); final Optional<String> hostnameOverride = Optional.fromNullable(HOSTNAME_OVERRIDE.get()); if (hostnameOverride.isPresent()) { try { InetAddress.getByName(hostnameOverride.get()); } catch (UnknownHostException e) { /* Possible misconfiguration, so warn the user. */ LOG.warning( "Unable to resolve name specified in -hostname. " + "Depending on your environment, this may be valid."); } } install( new PrivateModule() { @Override protected void configure() { bind(new TypeLiteral<Optional<String>>() {}).toInstance(hostnameOverride); bind(HttpService.class).to(HttpServerLauncher.class); bind(HttpServerLauncher.class).in(Singleton.class); expose(HttpServerLauncher.class); expose(HttpService.class); } }); SchedulerServicesModule.addAppStartupServiceBinding(binder()).to(HttpServerLauncher.class); bind(LeaderRedirect.class).in(Singleton.class); SchedulerServicesModule.addAppStartupServiceBinding(binder()).to(RedirectMonitor.class); if (production) { install(PRODUCTION_SERVLET_CONTEXT_LISTENER); } } private static final Module PRODUCTION_SERVLET_CONTEXT_LISTENER = new AbstractModule() { @Override protected void configure() { // Provider binding only. } @Provides @Singleton ServletContextListener provideServletContextListener(Injector parentInjector) { return makeServletContextListener( parentInjector, Modules.combine( new ApiModule(), new H2ConsoleModule(), new HttpSecurityModule(), new ThriftModule())); } }; private static final Set<String> LEADER_ENDPOINTS = ImmutableSet.of( "api", "cron", "locks", "maintenance", "mname", "offers", "pendingtasks", "quotas", "slaves", "utilization"); private static final Multimap<Class<?>, String> JAX_RS_ENDPOINTS = ImmutableMultimap.<Class<?>, String>builder() .put(AbortHandler.class, "abortabortabort") .put(ContentionPrinter.class, "contention") .put(Cron.class, "cron") .put(HealthHandler.class, "health") .put(Locks.class, "locks") .put(LogConfig.class, "logconfig") .put(Maintenance.class, "maintenance") .put(Mname.class, "mname") .put(Offers.class, "offers") .put(PendingTasks.class, "pendingtasks") .put(QuitHandler.class, "quitquitquit") .put(Quotas.class, "quotas") .put(Services.class, "services") .put(Slaves.class, "slaves") .put(StructDump.class, "structdump") .put(ThreadStackPrinter.class, "threads") .put(TimeSeriesDataSource.class, "graphdata") .put(Utilization.class, "utilization") .put(VarsHandler.class, "vars") .put(VarsJsonHandler.class, "vars.json") .build(); private static String allOf(Set<String> paths) { return "^(?:" + Joiner.on("|").join(Iterables.transform(paths, path -> "/" + path)) + ").*$"; } // TODO(ksweeney): Factor individual servlet configurations to their own ServletModules. @VisibleForTesting static ServletContextListener makeServletContextListener( final Injector parentInjector, final Module childModule) { return new GuiceServletContextListener() { @Override protected Injector getInjector() { return parentInjector.createChildInjector( childModule, new JerseyServletModule() { @Override protected void configureServlets() { bind(HttpStatsFilter.class).in(Singleton.class); filter("*").through(HttpStatsFilter.class); bind(LeaderRedirectFilter.class).in(Singleton.class); filterRegex(allOf(LEADER_ENDPOINTS)).through(LeaderRedirectFilter.class); bind(GuiceContainer.class).in(Singleton.class); filterRegex(allOf(ImmutableSet.copyOf(JAX_RS_ENDPOINTS.values()))) .through(GuiceContainer.class, GUICE_CONTAINER_PARAMS); filterRegex("/assets/scheduler(?:/.*)?").through(LeaderRedirectFilter.class); serve("/assets", "/assets/*") .with( new DefaultServlet(), ImmutableMap.of("resourceBase", STATIC_ASSETS_ROOT, "dirAllowed", "false")); for (Class<?> jaxRsHandler : JAX_RS_ENDPOINTS.keySet()) { bind(jaxRsHandler); } } }); } }; } static class RedirectMonitor extends AbstractIdleService { private final LeaderRedirect redirector; @Inject RedirectMonitor(LeaderRedirect redirector) { this.redirector = requireNonNull(redirector); } @Override public void startUp() throws MonitorException { redirector.monitor(); } @Override protected void shutDown() { // Nothing to do here - we await VM shutdown. } } public static final class HttpServerLauncher extends AbstractIdleService implements HttpService { private final ServletContextListener servletContextListener; private final Optional<String> advertisedHostOverride; private volatile Server server; private volatile HostAndPort serverAddress = null; @Inject HttpServerLauncher( ServletContextListener servletContextListener, Optional<String> advertisedHostOverride) { this.servletContextListener = requireNonNull(servletContextListener); this.advertisedHostOverride = requireNonNull(advertisedHostOverride); } private static final Map<String, String> REGEX_REWRITE_RULES = ImmutableMap.<String, String>builder() .put("/(?:index.html)?", "/assets/index.html") .put("/graphview(?:/index.html)?", "/assets/graphview/graphview.html") .put("/graphview/(.*)", "/assets/graphview/$1") .put("/(?:scheduler|updates)(?:/.*)?", "/assets/scheduler/index.html") .build(); private static Handler getRewriteHandler(Handler wrapped) { RewriteHandler rewrites = new RewriteHandler(); rewrites.setOriginalPathAttribute(ORIGINAL_PATH_ATTRIBUTE_NAME); rewrites.setRewriteRequestURI(true); rewrites.setRewritePathInfo(true); for (Map.Entry<String, String> entry : REGEX_REWRITE_RULES.entrySet()) { RewriteRegexRule rule = new RewriteRegexRule(); rule.setRegex(entry.getKey()); rule.setReplacement(entry.getValue()); rewrites.addRule(rule); } rewrites.setHandler(wrapped); return rewrites; } private static Handler getGzipHandler(Handler wrapped) { GzipHandler gzip = new GzipHandler(); gzip.addIncludedMethods(HttpMethod.POST); gzip.setHandler(wrapped); return gzip; } @Override public HostAndPort getAddress() { Preconditions.checkState(state() == State.RUNNING); return HostAndPort.fromParts( advertisedHostOverride.or(serverAddress.getHostText()), serverAddress.getPort()); } @Override protected void startUp() { server = new Server(); ServletContextHandler servletHandler = new ServletContextHandler(server, "/", ServletContextHandler.NO_SESSIONS); servletHandler.addServlet(DefaultServlet.class, "/"); servletHandler.addFilter(GuiceFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); servletHandler.addEventListener(servletContextListener); HandlerCollection rootHandler = new HandlerList(); RequestLogHandler logHandler = new RequestLogHandler(); logHandler.setRequestLog(new Slf4jRequestLog()); rootHandler.addHandler(logHandler); rootHandler.addHandler(servletHandler); ServerConnector connector = new ServerConnector(server); connector.setPort(HTTP_PORT.get()); server.addConnector(connector); server.setHandler(getGzipHandler(getRewriteHandler(rootHandler))); try { connector.open(); server.start(); } catch (Exception e) { throw Throwables.propagate(e); } String host; if (connector.getHost() == null) { // Resolve the local host name. try { host = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { throw new RuntimeException("Failed to resolve local host address: " + e, e); } } else { // If jetty was configured with a specific host to bind to, use that. host = connector.getHost(); } serverAddress = HostAndPort.fromParts(host, connector.getLocalPort()); } @Override protected void shutDown() { LOG.info("Shutting down embedded http server"); try { server.stop(); } catch (Exception e) { LOG.log(Level.INFO, "Failed to stop jetty server: " + e, e); } } } }