Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instance discovery for rancher managed cluster #29

Merged
merged 1 commit into from
Oct 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ turbine:
clusters:
- breakerbox

rancherDiscovery:
serviceApiUrl: http://localhost:8080/v1/projects/1a5/services
accessKey: 3E0F7DB0A2B601981F1B
secretKey: fWWKGNvmuWpSngyVYHXFMSnE5cDhZWKNkVmQS8zn

server:
applicationConnectors:
- type: http
Expand Down Expand Up @@ -166,8 +171,23 @@ production:

Instance Discovery Class
------------------------
Specifies the `Java` canonical class name. It defaults to the `YamlInstanceDiscovery` implemention. You can also leverage the
`com.yammer.breakerbox.turbine.KubernetesInstanceDiscovery` class.
Specifies the `Java` canonical class name. It defaults to the `YamlInstanceDiscovery` implementation. You can also leverage the
`com.yammer.breakerbox.turbine.KubernetesInstanceDiscovery` and `com.yammer.breakerbox.turbine.RancherInstanceDiscovery` classes.

To integrate with RancherInstanceDiscovery,

1. specify rancher services Api url, accessKey and secret key.
rancherDiscovery:
serviceApiUrl: http://localhost:8080/v1/projects/1a5/services
accessKey: 3E0F7DB0A2B601981F1B
secretKey: fWWKGNvmuWpSngyVYHXFMSnE5cDhZWKNkVmQS8zn

2. add labels in rancher service containers:
a. tenacity.metrics.stream.enabled: true
b. tenacity.metrics.stream.port: 8080
c. service.cluster.name: clusterName

3. RancherInstanceDiscovery will create dashboards per service-cluster with service.cluster.name label and one aggregated production dashboard. Dashboards can be created, enabled, disabled by updating labels at runtime.

Meta Clusters
-------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand Down Expand Up @@ -221,8 +222,7 @@ private static void setupLdapAuth(LdapConfiguration ldapConfiguration, Environme

private static void setupInstanceDiscovery(BreakerboxServiceConfiguration configuration,
Environment environment) {

final Optional<InstanceDiscovery> customInstanceDiscovery = createInstanceDiscovery(configuration);
final Optional<InstanceDiscovery> customInstanceDiscovery = createInstanceDiscovery(configuration, environment);
if (customInstanceDiscovery.isPresent()) {
PluginsFactory.setInstanceDiscovery(RegisterClustersInstanceDiscoveryWrapper.wrap(
customInstanceDiscovery.get()));
Expand All @@ -233,15 +233,29 @@ private static void setupInstanceDiscovery(BreakerboxServiceConfiguration config
}
}

private static Optional<InstanceDiscovery> createInstanceDiscovery(BreakerboxServiceConfiguration configuration) {
private static Optional<InstanceDiscovery> createInstanceDiscovery(BreakerboxServiceConfiguration configuration,
Environment environment) {
if (configuration.getInstanceDiscoveryClass().isPresent()) {
try {
final Class<?> instanceDiscoveryClass = Class.forName(configuration.getInstanceDiscoveryClass().get());
return Optional.of((InstanceDiscovery) instanceDiscoveryClass.newInstance());
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.newInstance();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.google.common.net.HostAndPort;
import com.yammer.breakerbox.azure.AzureTableConfiguration;
import com.yammer.breakerbox.jdbi.JdbiConfiguration;
import com.yammer.breakerbox.turbine.config.RancherInstanceConfiguration;
import com.yammer.dropwizard.authenticator.LdapConfiguration;
import com.yammer.tenacity.core.config.BreakerboxConfiguration;
import com.yammer.tenacity.core.config.TenacityConfiguration;
Expand Down Expand Up @@ -61,6 +62,9 @@ public class BreakerboxServiceConfiguration extends Configuration {
@NotNull @UnwrapValidatedValue(false)
private Optional<String> instanceDiscoveryClass;

@NotNull @UnwrapValidatedValue(false)
private final Optional<RancherInstanceConfiguration> rancherInstanceConfiguration;

@JsonCreator
public BreakerboxServiceConfiguration(@JsonProperty("azure") AzureTableConfiguration azure,
@JsonProperty("tenacityClient") JerseyClientConfiguration tenacityClientConfiguration,
Expand All @@ -73,7 +77,8 @@ public BreakerboxServiceConfiguration(@JsonProperty("azure") AzureTableConfigura
@JsonProperty("breakerboxHostAndPort") HostAndPort breakerboxHostAndPort,
@JsonProperty("defaultDashboard") String defaultDashboard,
@JsonProperty("turbine") Path turbine,
@JsonProperty("instanceDiscoveryClass") String instanceDiscoveryClass) {
@JsonProperty("instanceDiscoveryClass") String instanceDiscoveryClass,
@JsonProperty("rancherDiscovery") RancherInstanceConfiguration rancherInstanceConfiguration) {
this.azure = Optional.fromNullable(azure);
this.tenacityClient = tenacityClientConfiguration;
this.breakerboxServicesPropertyKeys = Optional.fromNullable(breakerboxServicesPropertyKeys).or(new TenacityConfiguration());
Expand All @@ -87,6 +92,7 @@ public BreakerboxServiceConfiguration(@JsonProperty("azure") AzureTableConfigura
this.turbine = Optional.fromNullable(turbine).or(Paths.get("breakerbox-instances.yml"));
this.instanceDiscoveryClass = Optional.fromNullable(instanceDiscoveryClass)
.or(Optional.fromNullable(System.getProperty("InstanceDiscovery.impl")));
this.rancherInstanceConfiguration = Optional.fromNullable(rancherInstanceConfiguration);
}

public Optional<AzureTableConfiguration> getAzure() {
Expand Down Expand Up @@ -159,9 +165,13 @@ public void setInstanceDiscoveryClass(String instanceDiscoveryClass) {
this.instanceDiscoveryClass = Optional.fromNullable(instanceDiscoveryClass);
}

public Optional<RancherInstanceConfiguration> getRancherInstanceConfiguration() {
return rancherInstanceConfiguration;
}

@Override
public int hashCode() {
return Objects.hash(azure, tenacityClient, breakerboxServicesPropertyKeys, breakerboxServicesConfiguration, breakerboxConfiguration, turbine, ldapConfiguration, archaiusOverride, jdbiConfiguration, metaClusters, breakerboxHostAndPort, defaultDashboard, instanceDiscoveryClass);
return Objects.hash(azure, tenacityClient, breakerboxServicesPropertyKeys, breakerboxServicesConfiguration, breakerboxConfiguration, turbine, ldapConfiguration, archaiusOverride, jdbiConfiguration, metaClusters, breakerboxHostAndPort, defaultDashboard, instanceDiscoveryClass, rancherInstanceConfiguration);
}

@Override
Expand All @@ -185,6 +195,7 @@ public boolean equals(Object obj) {
&& Objects.equals(this.metaClusters, other.metaClusters)
&& Objects.equals(this.breakerboxHostAndPort, other.breakerboxHostAndPort)
&& Objects.equals(this.defaultDashboard, other.defaultDashboard)
&& Objects.equals(this.instanceDiscoveryClass, other.instanceDiscoveryClass);
&& Objects.equals(this.instanceDiscoveryClass, other.instanceDiscoveryClass)
&& Objects.equals(this.rancherInstanceConfiguration, other.rancherInstanceConfiguration);
}
}
}
23 changes: 22 additions & 1 deletion breakerbox-turbine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>mockito-core</artifactId>
<groupId>org.mockito</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.yammer.tenacity</groupId>
Expand All @@ -36,5 +42,20 @@
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-api</artifactId>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.yammer.breakerbox.turbine;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.ws.rs.core.Response;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.netflix.turbine.discovery.Instance;
import com.netflix.turbine.discovery.InstanceDiscovery;
import com.yammer.breakerbox.turbine.client.RancherClient;
import com.yammer.breakerbox.turbine.config.RancherInstanceConfiguration;

public class RancherInstanceDiscovery implements InstanceDiscovery {
private static final Logger LOGGER = LoggerFactory.getLogger(RancherInstanceDiscovery.class);
private RancherClient rancherClient;
private ObjectMapper mapper;

public RancherInstanceDiscovery(RancherInstanceConfiguration instanceConfiguration,
ObjectMapper mapper) {
this.rancherClient = new RancherClient(instanceConfiguration);
this.mapper = mapper;
}

@Override
public Collection<Instance> getInstanceList() throws Exception {
Response response = rancherClient.getServiceInstanceDetails();
return response.getStatus() == 200
? createServiceInstanceList(response.readEntity(String.class))
: Collections.emptyList();
}

private Collection<Instance> createServiceInstanceList(String rancherServiceApiResponse) throws IOException {
JsonNode serviceJsonResponseNode = mapper.readValue(rancherServiceApiResponse, JsonNode.class);
List<JsonNode> dataList = convertJsonArrayToList((ArrayNode) serviceJsonResponseNode.get("data"));
Collection<Instance> instances = dataList.stream()
.filter(this::isServiceDashboardEnabled)
.map(this::createInstanceList)
.flatMap(Collection::stream)
.collect(Collectors.toList());
instances.addAll(createProductionDashboard(instances));
return instances;
}

private Collection<? extends Instance> createProductionDashboard(Collection<Instance> instances) {
return instances.stream()
.map(instance -> new Instance(instance.getHostname(), "production", true))
.collect(Collectors.toList());
}

private boolean isServiceDashboardEnabled(JsonNode dataNode) {
JsonNode serviceStatus = getRancherLabels(dataNode).get("tenacity.metrics.stream.enabled");
return !Objects.isNull(serviceStatus) ? serviceStatus.asBoolean() : Boolean.FALSE;
}

private List<Instance> createInstanceList(JsonNode dataNode) {
String clusterName = getServiceClusterName(dataNode);
int metricsStreamPort = getServiceMetricsStreamPort(dataNode);
List<JsonNode> publicEndpoints = getPublicEndpoints(dataNode);
return publicEndpoints.stream()
.filter(jsonNodes -> jsonNodes.get("port").asInt() == metricsStreamPort)
.map(jsonNode -> createTurbineInstance(jsonNode, clusterName))
.collect(Collectors.toList());
}

private String getServiceClusterName(JsonNode dataNode) {
JsonNode clusterNameLabel = getRancherLabels(dataNode).get("service.cluster.name");
return !Objects.isNull(clusterNameLabel) ? clusterNameLabel.asText() : dataNode.get("name").asText();
}

private int getServiceMetricsStreamPort(JsonNode dataNode) {
JsonNode portLabel = getRancherLabels(dataNode).get("tenacity.metrics.stream.port");
return !Objects.isNull(portLabel) ? portLabel.asInt() : 8080;
}

private List<JsonNode> getPublicEndpoints(JsonNode objectNode) {
return objectNode.get("publicEndpoints").getNodeType().equals(JsonNodeType.ARRAY)
? convertJsonArrayToList((ArrayNode) objectNode.get("publicEndpoints"))
: Collections.emptyList();
}

private Instance createTurbineInstance(JsonNode jsonNode, String clusterName) {
String hostAndPort = jsonNode.get("ipAddress").asText() + ":" + jsonNode.get("port").asText();
return new Instance(hostAndPort, clusterName, true);
}

private JsonNode getRancherLabels(JsonNode dataNode) {
return dataNode.get("launchConfig").get("labels");
}

private List<JsonNode> convertJsonArrayToList(ArrayNode arrayNode) {
try {
return mapper.readValue(arrayNode.toString(), TypeFactory.defaultInstance()
.constructCollectionType(List.class, JsonNode.class));
} catch (IOException e) {
LOGGER.error("Failed to convert ArrayNode to List<JsonNode>", e);
return Collections.emptyList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.turbine.discovery.Instance;
import com.netflix.turbine.discovery.InstanceDiscovery;
import com.yammer.breakerbox.turbine.config.YamlInstanceConfiguration;
import io.dropwizard.configuration.ConfigurationFactory;
import io.dropwizard.configuration.YamlConfigurationFactory;
import org.slf4j.Logger;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.yammer.breakerbox.turbine.client;

import java.io.UnsupportedEncodingException;
import java.util.Objects;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import javax.xml.bind.DatatypeConverter;

import com.yammer.breakerbox.turbine.config.RancherInstanceConfiguration;

public class RancherClient {
private final Invocation.Builder builder;
private final RancherInstanceConfiguration instanceConfiguration;

public RancherClient(final RancherInstanceConfiguration instanceConfiguration) {
Objects.requireNonNull(instanceConfiguration);
this.instanceConfiguration = instanceConfiguration;
this.builder = createInvocationBuilder();
}

private Invocation.Builder createInvocationBuilder() {
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target(createRancherServiceUrl());
return webTarget.request().header("Authorization", getBasicAuthentication());
}

private String createRancherServiceUrl() {
String filterParameters = "state=active&kind=service";
String apiUrl = instanceConfiguration.getServiceApiUrl();
String serviceUrl = apiUrl.charAt(apiUrl.length() - 1) == '?' ? apiUrl : apiUrl + "?";
return serviceUrl.replaceAll("\\s", "") + filterParameters;
}

private String getBasicAuthentication() {
String token = instanceConfiguration.getAccessKey() + ":" + instanceConfiguration.getSecretKey();
try {
return "BASIC " + DatatypeConverter.printBase64Binary(token.getBytes("UTF-8"));
} catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("Cannot encode with UTF-8", ex);
}
}

public Response getServiceInstanceDetails() {
return builder.get();
}
}
Loading