Пример #1
0
  @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);
    }
  }
Пример #2
0
    @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());
    }
Пример #3
0
/**
 * 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);
    }
  }
}
Пример #4
0
  /**
   * 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);
  }
Пример #5
0
 public AopModule() {
   this(
       ImmutableMap.of(
           "createJob", ENABLE_JOB_CREATION.get(),
           "acquireLock", ENABLE_UPDATES.get()));
 }
Пример #6
0
/** 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());
    }
  }
}
Пример #7
0
/**
 * 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);
      }
    }
  }
}