package com.hubspot.blazar.guice;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nonnull;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.ws.rs.container.ContainerRequestFilter;
import org.apache.curator.framework.recipes.leader.LeaderLatchListener;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.github.RateLimitHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.inject.Binder;
import com.google.inject.Provides;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
import com.hubspot.blazar.GitHubNamingFilter;
import com.hubspot.blazar.config.BlazarConfiguration;
import com.hubspot.blazar.config.BlazarConfigurationWrapper;
import com.hubspot.blazar.config.GitHubConfiguration;
import com.hubspot.blazar.data.BlazarDataModule;
import com.hubspot.blazar.exception.IllegalArgumentExceptionMapper;
import com.hubspot.blazar.exception.IllegalStateExceptionMapper;
import com.hubspot.blazar.resources.BranchResource;
import com.hubspot.blazar.resources.BranchStateResource;
import com.hubspot.blazar.resources.BuildHistoryResource;
import com.hubspot.blazar.resources.GitHubWebhookResource;
import com.hubspot.blazar.resources.InstantMessageResource;
import com.hubspot.blazar.resources.InterProjectBuildResource;
import com.hubspot.blazar.resources.ModuleBuildResource;
import com.hubspot.blazar.resources.RepositoryBuildResource;
import com.hubspot.blazar.util.GitHubWebhookHandler;
import com.hubspot.blazar.externalservice.LostBuildCleaner;
import com.hubspot.blazar.util.ManagedScheduledExecutorServiceProvider;
import com.hubspot.dropwizard.guicier.DropwizardAwareModule;
import com.hubspot.horizon.AsyncHttpClient;
import com.hubspot.horizon.HttpClient;
import com.hubspot.horizon.HttpConfig;
import com.hubspot.horizon.HttpRequest;
import com.hubspot.horizon.HttpResponse;
import com.hubspot.horizon.RetryStrategy;
import com.hubspot.horizon.ning.NingAsyncHttpClient;
import com.hubspot.horizon.ning.NingHttpClient;
import com.hubspot.jackson.jaxrs.PropertyFilteringMessageBodyWriter;
import com.hubspot.singularity.client.SingularityClient;
import com.hubspot.singularity.client.SingularityClientModule;
import io.dropwizard.db.DataSourceFactory;
public class BlazarServiceModule extends DropwizardAwareModule<BlazarConfigurationWrapper> {
private static final Logger LOG = LoggerFactory.getLogger(BlazarServiceModule.class);
/**
* Blazar has an option to enable "webhook only" mode. This is because you may wish to have
* a public facing Blazar instance that does not have the ability to start builds etc. But can still
* receive and process webhooks from external GitHub installations (like GitHub.Com).
*
* In webhook only mode we configure only the bare minimum required resources for Blazar to be able
* to accept web-hook events and send them into our SQL backed event bus. Other Blazar instances running
* against the same database are then able to process those events using the SQL backed event bus. These
* instances are also configured to accept webhooks, but also have the rest of Blazar's API enabled.
*/
@Override
public void configure(Binder binder) {
BlazarConfiguration blazarConfiguration = getConfiguration().getBlazarConfiguration();
configureWebhooks(binder, blazarConfiguration);
// Stop here for webhook instances so that they only have the Webhook API enabled.
if (blazarConfiguration.isWebhookOnly()) {
return;
}
configureRemaining(binder, blazarConfiguration);
}
private void configureWebhooks(Binder binder, BlazarConfiguration blazarConfiguration) {
// Bind GitHub configurations
MapBinder<String, GitHub> mapBinder = MapBinder.newMapBinder(binder, String.class, GitHub.class);
for (Map.Entry<String, GitHubConfiguration> entry : blazarConfiguration.getGitHubConfiguration().entrySet()) {
String host = entry.getKey();
mapBinder.addBinding(host).toInstance(toGitHub(host, entry.getValue()));
}
binder.install(new BlazarEventBusModule());
binder.install(new BlazarDataModule());
binder.bind(DataSourceFactory.class).toInstance(blazarConfiguration.getDatabaseConfiguration());
binder.bind(MetricRegistry.class).toInstance(getEnvironment().metrics());
binder.bind(ObjectMapper.class).toInstance(getEnvironment().getObjectMapper());
binder.bind(IllegalArgumentExceptionMapper.class);
binder.bind(IllegalStateExceptionMapper.class);
Multibinder.newSetBinder(binder, ContainerRequestFilter.class).addBinding().to(GitHubNamingFilter.class).in(Scopes.SINGLETON);
// the webhook resource that lets you post webhooks
binder.bind(GitHubWebhookResource.class);
}
private void configureRemaining(Binder binder, BlazarConfiguration blazarConfiguration) {
binder.install(new DiscoveryModule());
binder.install(new BlazarSlackModule(blazarConfiguration));
binder.bind(PropertyFilteringMessageBodyWriter.class)
.toConstructor(defaultConstructor(PropertyFilteringMessageBodyWriter.class))
.in(Scopes.SINGLETON);
binder.bind(YAMLFactory.class).toInstance(new YAMLFactory());
binder.bind(XmlFactory.class).toInstance(new XmlFactory());
// Bind resources
binder.bind(BranchResource.class);
binder.bind(BranchStateResource.class);
binder.bind(ModuleBuildResource.class);
binder.bind(RepositoryBuildResource.class);
binder.bind(BuildHistoryResource.class);
binder.bind(InstantMessageResource.class);
binder.bind(InterProjectBuildResource.class);
// Only configure leader-based activities like processing events etc. if you are connected to zookeeper
if (blazarConfiguration.getZooKeeperConfiguration().isPresent()) {
binder.bind(GitHubWebhookHandler.class); // Event processing for GitHub webhook events.
binder.install(new BuildVisitorModule()); // Configures event processors for all build events.
binder.install(new BlazarQueueProcessorModule());
binder.install(new BlazarZooKeeperModule());
Multibinder.newSetBinder(binder, LeaderLatchListener.class).addBinding().to(LostBuildCleaner.class);
binder.bind(ScheduledExecutorService.class)
.annotatedWith(Names.named("LostBuildCleaner"))
.toProvider(new ManagedScheduledExecutorServiceProvider(1, "LostBuildCleaner"))
.in(Scopes.SINGLETON);
// Bind and configure Singularity clients for the available clusters
binder.install(new SingularityClientModule());
binder.bind(new TypeLiteral<Map<String, SingularityClient>>() {}).toProvider(SingularityClusterClientsProvider.class);
} else {
LOG.info("Not enabling queue-processing or build event handlers because no zookeeper configuration is specified. We need to elect a leader to process events.");
}
}
@Provides
@Singleton
public BlazarConfiguration getBlazarConfiguration() {
return getConfiguration().getBlazarConfiguration();
}
@Provides
@Singleton
@Named("whitelist")
public Set<String> providesWhitelist() {
return getConfiguration().getBlazarConfiguration().getWhitelist();
}
@Provides
@Singleton
@Named("blacklist")
public Set<String> providesBlacklist() {
return getConfiguration().getBlazarConfiguration().getBlacklist();
}
@Provides
@Singleton
public AsyncHttpClient providesAsyncHttpClient(ObjectMapper mapper) {
HttpConfig config = HttpConfig.newBuilder().setObjectMapper(mapper).setRetryStrategy(new RetryStrategy() {
@Override
public boolean shouldRetry(@Nonnull HttpRequest request, @Nonnull HttpResponse response) {
return response.getStatusCode() == 409 || RetryStrategy.DEFAULT.shouldRetry(request, response);
}
@Override
public boolean shouldRetry(@Nonnull HttpRequest request, @Nonnull IOException exception) {
return RetryStrategy.DEFAULT.shouldRetry(request, exception);
}
}).build();
return new NingAsyncHttpClient(config);
}
@Provides
@Singleton
public HttpClient provideHttpClient(ObjectMapper objectMapper) {
return new NingHttpClient(HttpConfig.newBuilder().setMaxRetries(5).setObjectMapper(objectMapper).build());
}
public static GitHub toGitHub(String host, GitHubConfiguration gitHubConfig) {
final String endpoint;
if ("github.com".equals(host) || "api.github.com".equals(host)) {
endpoint = "https://api.github.com";
} else {
endpoint = "https://" + host + "/api/v3";
}
GitHubBuilder builder = new GitHubBuilder().withEndpoint(endpoint).withRateLimitHandler(RateLimitHandler.FAIL);
if (gitHubConfig.getOauthToken().isPresent()) {
builder.withOAuthToken(gitHubConfig.getOauthToken().get(), gitHubConfig.getUser().orNull());
} else if (gitHubConfig.getPassword().isPresent()) {
builder.withPassword(gitHubConfig.getUser().orNull(), gitHubConfig.getPassword().get());
}
try {
return builder.build();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static <T> Constructor<T> defaultConstructor(Class<T> type) {
try {
return type.getConstructor();
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}