package com.yammer.breakerbox.service;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.netflix.turbine.discovery.InstanceDiscovery;
import com.netflix.turbine.init.TurbineInit;
import com.netflix.turbine.plugins.PluginsFactory;
import com.netflix.turbine.streaming.servlet.TurbineStreamServlet;
import com.yammer.breakerbox.azure.AzureStore;
import com.yammer.breakerbox.dashboard.bundle.BreakerboxDashboardBundle;
import com.yammer.breakerbox.jdbi.JdbiConfiguration;
import com.yammer.breakerbox.jdbi.JdbiStore;
import com.yammer.breakerbox.service.auth.NullAuthFilter;
import com.yammer.breakerbox.service.auth.NullAuthenticator;
import com.yammer.breakerbox.service.config.BreakerboxServiceConfiguration;
import com.yammer.breakerbox.service.core.SyncComparator;
import com.yammer.breakerbox.service.resources.*;
import com.yammer.breakerbox.service.store.ScheduledTenacityPoller;
import com.yammer.breakerbox.service.store.TenacityPropertyKeysStore;
import com.yammer.breakerbox.service.tenacity.*;
import com.yammer.breakerbox.store.BreakerboxStore;
import com.yammer.breakerbox.turbine.RancherInstanceDiscovery;
import com.yammer.breakerbox.turbine.RegisterClustersInstanceDiscoveryWrapper;
import com.yammer.breakerbox.turbine.YamlInstanceDiscovery;
import com.yammer.breakerbox.turbine.client.DelegatingTenacityClient;
import com.yammer.breakerbox.turbine.managed.ManagedTurbine;
import com.yammer.dropwizard.authenticator.LdapAuthenticator;
import com.yammer.dropwizard.authenticator.LdapConfiguration;
import com.yammer.dropwizard.authenticator.ResourceAuthenticator;
import com.yammer.dropwizard.authenticator.User;
import com.yammer.tenacity.client.TenacityClientBuilder;
import com.yammer.tenacity.core.auth.TenacityAuthenticator;
import com.yammer.tenacity.core.bundle.TenacityBundleConfigurationFactory;
import com.yammer.tenacity.core.config.BreakerboxConfiguration;
import com.yammer.tenacity.core.config.TenacityConfiguration;
import com.yammer.tenacity.core.logging.DefaultExceptionLogger;
import com.yammer.tenacity.core.logging.ExceptionLoggingCommandHook;
import com.yammer.tenacity.core.properties.TenacityPropertyKey;
import com.yammer.tenacity.core.properties.TenacityPropertyKeyFactory;
import com.yammer.tenacity.dbi.DBIExceptionLogger;
import com.yammer.tenacity.dbi.SQLExceptionLogger;
import io.dropwizard.Application;
import io.dropwizard.auth.AuthDynamicFeature;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.auth.CachingAuthenticator;
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
import io.dropwizard.auth.basic.BasicCredentials;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.bundles.DBIExceptionsBundle;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class BreakerboxService extends Application<BreakerboxServiceConfiguration> {
private static final Logger LOGGER = LoggerFactory.getLogger(BreakerboxService.class);
private DelayedTenacityConfiguredBundle tenacityConfiguredBundle;
public static void main(String[] args) throws Exception {
new BreakerboxService().run(args);
}
private BreakerboxService() {}
@Override
public void initialize(Bootstrap<BreakerboxServiceConfiguration> bootstrap) {
bootstrap.addBundle(new DBIExceptionsBundle());
bootstrap.addBundle(new MigrationsBundle<BreakerboxServiceConfiguration>() {
@Override
public DataSourceFactory getDataSourceFactory(BreakerboxServiceConfiguration configuration) {
return configuration.getJdbiConfiguration().or(new JdbiConfiguration()).getDataSourceFactory();
}
});
tenacityConfiguredBundle = ((DelayedTenacityBundleBuilder)DelayedTenacityBundleBuilder
.newBuilder()
.configurationFactory(new TenacityBundleConfigurationFactory<BreakerboxServiceConfiguration>() {
@Override
public Map<TenacityPropertyKey, TenacityConfiguration> getTenacityConfigurations(BreakerboxServiceConfiguration applicationConfiguration) {
return ImmutableMap.<TenacityPropertyKey, TenacityConfiguration>of(
BreakerboxDependencyKey.BRKRBX_SERVICES_PROPERTYKEYS, applicationConfiguration.getBreakerboxServicesPropertyKeys(),
BreakerboxDependencyKey.BRKRBX_SERVICES_CONFIGURATION, applicationConfiguration.getBreakerboxServicesConfiguration(),
BreakerboxDependencyKey.BRKRBX_LDAP_AUTH, new TenacityConfiguration());
}
@Override
public TenacityPropertyKeyFactory getTenacityPropertyKeyFactory(BreakerboxServiceConfiguration applicationConfiguration) {
return new BreakerboxDependencyKeyFactory();
}
@Override
public BreakerboxConfiguration getBreakerboxConfiguration(BreakerboxServiceConfiguration applicationConfiguration) {
return applicationConfiguration.getBreakerboxConfiguration();
}
})
.mapAllHystrixRuntimeExceptionsTo(429)
.commandExecutionHook(new ExceptionLoggingCommandHook(
ImmutableList.of(
new DBIExceptionLogger(bootstrap.getMetricRegistry()),
new SQLExceptionLogger(bootstrap.getMetricRegistry()),
new DefaultExceptionLogger()))))
.build();
bootstrap.addBundle(tenacityConfiguredBundle);
bootstrap.addBundle(new BreakerboxDashboardBundle());
}
@Override
public void run(final BreakerboxServiceConfiguration configuration, final Environment environment) throws Exception {
setupInstanceDiscovery(configuration, environment);
setupAuth(configuration, environment);
final BreakerboxStore breakerboxStore = createBreakerboxStore(configuration, environment);
breakerboxStore.initialize();
final TenacityPropertyKeysStore tenacityPropertyKeysStore = new TenacityPropertyKeysStore(
new TenacityPoller.Factory(new DelegatingTenacityClient(
new TenacityClientBuilder(environment, BreakerboxDependencyKey.BRKRBX_SERVICES_PROPERTYKEYS)
.using(configuration.getTenacityClient())
.build())));
final SyncComparator syncComparator = new SyncComparator(
new TenacityConfigurationFetcher.Factory(new DelegatingTenacityClient(
new TenacityClientBuilder(environment, BreakerboxDependencyKey.BRKRBX_SERVICES_CONFIGURATION)
.using(configuration.getTenacityClient())
.build())),
breakerboxStore);
final Set<String> metaClusters = configuration
.getMetaClusters()
.stream()
.map(String::toUpperCase)
.collect(Collectors.toSet());
environment.servlets().addServlet("turbine.stream", new TurbineStreamServlet()).addMapping("/turbine.stream");
environment.jersey().register(new ArchaiusResource(configuration.getArchaiusOverride(), breakerboxStore));
environment.jersey().register(new ConfigureResource(breakerboxStore));
environment.jersey().register(new DashboardResource(configuration.getDefaultDashboard(), configuration.getBreakerboxHostAndPort(), metaClusters));
environment.jersey().register(new InSyncResource(syncComparator, tenacityPropertyKeysStore));
environment.jersey().register(new ClustersResource(metaClusters, breakerboxStore, tenacityPropertyKeysStore));
final ScheduledExecutorService scheduledExecutorService = environment
.lifecycle()
.scheduledExecutorService("scheduled-tenacity-poller-%d")
.threads(1)
.build();
scheduledExecutorService.scheduleAtFixedRate(
new ScheduledTenacityPoller(tenacityPropertyKeysStore),
30,
60,
TimeUnit.SECONDS);
environment.lifecycle().manage(new ManagedTurbine());
scheduledExecutorService.schedule(() -> {
//TODO: The way properties get registered shouldn't need to depend on this strict of ordering.
//Need to also move off the static properties file for Turbine configuration and move to something more
//dynamic
if (tenacityConfiguredBundle != null) {
tenacityConfiguredBundle.delayedRegisterTenacityProperties(
tenacityConfiguredBundle.getTenacityBundleConfigurationFactory().getTenacityConfigurations(configuration),
configuration);
TurbineInit.init();
tenacityConfiguredBundle = null;
} else {
LOGGER.error("Unable to initialize Tenacity/Turbine.");
throw new RuntimeException("Unable to initialize Tenacity/Turbine.");
}
}, 3, TimeUnit.SECONDS);
}
private static BreakerboxStore createBreakerboxStore(BreakerboxServiceConfiguration configuration, Environment environment) throws Exception {
if (configuration.getJdbiConfiguration().isPresent()) {
return new JdbiStore(configuration.getJdbiConfiguration().get(), environment);
} else if (configuration.getAzure().isPresent()) {
return new AzureStore(configuration.getAzure().get(), environment);
} else {
throw new IllegalStateException("A datastore must be specified: either azure or database");
}
}
private static void setupAuth(BreakerboxServiceConfiguration configuration, Environment environment) {
if (configuration.getLdapConfiguration().isPresent()) {
setupLdapAuth(configuration.getLdapConfiguration().get(), environment);
} else {
setupNullAuth(environment);
}
}
private static void setupNullAuth(Environment environment) {
environment.jersey().register(new AuthDynamicFeature(
new NullAuthFilter.Builder<User>()
.setAuthenticator(new NullAuthenticator())
.setRealm("null")
.buildAuthFilter()));
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
}
private static void setupLdapAuth(LdapConfiguration ldapConfiguration, Environment environment) {
final LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(ldapConfiguration);
final CachingAuthenticator<BasicCredentials, User> cachingAuthenticator =
new CachingAuthenticator<>(
environment.metrics(),
TenacityAuthenticator.wrap(
new ResourceAuthenticator(ldapAuthenticator), BreakerboxDependencyKey.BRKRBX_LDAP_AUTH),
ldapConfiguration.getCachePolicy()
);
environment.jersey().register(new AuthDynamicFeature(
new BasicCredentialAuthFilter.Builder<User>()
.setAuthenticator(cachingAuthenticator)
.setRealm("breakerbox")
.buildAuthFilter()));
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
}
private static void setupInstanceDiscovery(BreakerboxServiceConfiguration configuration,
Environment environment) {
final Optional<InstanceDiscovery> customInstanceDiscovery = createInstanceDiscovery(configuration, environment);
if (customInstanceDiscovery.isPresent()) {
PluginsFactory.setInstanceDiscovery(RegisterClustersInstanceDiscoveryWrapper.wrap(
customInstanceDiscovery.get()));
} else {
final YamlInstanceDiscovery yamlInstanceDiscovery = new YamlInstanceDiscovery(
configuration.getTurbine(), environment.getValidator(), environment.getObjectMapper());
PluginsFactory.setInstanceDiscovery(RegisterClustersInstanceDiscoveryWrapper.wrap(yamlInstanceDiscovery));
}
}
private static Optional<InstanceDiscovery> createInstanceDiscovery(BreakerboxServiceConfiguration configuration,
Environment environment) {
if (configuration.getInstanceDiscoveryClass().isPresent()) {
try {
final Class<InstanceDiscovery> instanceDiscoveryClass =
(Class<InstanceDiscovery>) Class.forName(configuration.getInstanceDiscoveryClass().get());
return Optional.of(createClassInstance(instanceDiscoveryClass, configuration, environment));
} catch (Exception err) {
LOGGER.warn("No default constructor for {}", configuration.getInstanceDiscoveryClass(), err);
}
}
return Optional.empty();
}
private static InstanceDiscovery createClassInstance(Class<InstanceDiscovery> instanceDiscoveryClass,
BreakerboxServiceConfiguration configuration,
Environment environment) throws Exception {
if(instanceDiscoveryClass.equals(RancherInstanceDiscovery.class)
&& configuration.getRancherInstanceConfiguration().isPresent()) {
return new RancherInstanceDiscovery(configuration.getRancherInstanceConfiguration().get(), environment.getObjectMapper());
} else if (instanceDiscoveryClass.equals(YamlInstanceDiscovery.class)) {
return new YamlInstanceDiscovery(configuration.getTurbine(), environment.getValidator(), environment.getObjectMapper());
}
return instanceDiscoveryClass.getConstructor().newInstance();
}
}