package com.yammer.breakerbox.turbine;
import com.netflix.turbine.discovery.Instance;
import com.netflix.turbine.discovery.InstanceDiscovery;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Discovers instances in a Kubernetes cluster. The idea is to
* find pods with the 'breakerbox-port' annotation and use their
* base names in combination with the namespace to build Hystrix clusters.
* Example:
* A pod named 'service-5ujh1' in the namespace 'staging' would be in the cluster 'staging-service',
* together with all pods created by the same ReplicationController or ReplicaSet.
*/
public class KubernetesInstanceDiscovery implements InstanceDiscovery {
private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesInstanceDiscovery.class);
public static final String PORT_ANNOTATION_KEY = "breakerbox-port";
public static final String POD_HASH_LABEL_KEY = "pod-template-hash";
private final KubernetesClient client;
public KubernetesInstanceDiscovery() {
this.client = new DefaultKubernetesClient();
}
public KubernetesInstanceDiscovery(KubernetesClient client) {
this.client = client;
}
@Override
public Collection<Instance> getInstanceList() throws Exception {
LOGGER.info("Starting Kubernetes instance discovery using master URL: {}", client.getMasterUrl());
return client.pods().inAnyNamespace()
.list()
.getItems().stream()
.filter(pod -> pod.getMetadata().getAnnotations() != null) // Ignore pods without annotations
.filter(pod -> pod.getMetadata().getAnnotations().containsKey(PORT_ANNOTATION_KEY))
.map(pod -> {
String portString = pod.getMetadata().getAnnotations().get(PORT_ANNOTATION_KEY);
if (!Pattern.compile("^[0-9]{2,5}$").matcher(portString).matches()) {
LOGGER.warn("Invalid port annotation for pod '{}': {}", pod.getMetadata().getName(), portString);
return null;
} else {
String host = String.format("%s:%s", pod.getStatus().getPodIP(), portString);
boolean running = pod.getStatus().getPhase().equals("Running");
LOGGER.info("Found Kubernetes Pod {} at address {}", pod.getMetadata().getName(), host);
return new Instance(host, extractClusterNameFor(pod), running);
}
})
.filter(pod -> pod != null)
.collect(Collectors.toList());
}
private static String extractClusterNameFor(Pod pod) {
String podBaseName = pod.getMetadata().getGenerateName();
// Remove auto-generated hashes, if there are any
if (pod.getMetadata().getLabels() != null && pod.getMetadata().getLabels().containsKey(POD_HASH_LABEL_KEY)) {
String hash = pod.getMetadata().getLabels().get(POD_HASH_LABEL_KEY);
podBaseName = podBaseName.replace(hash + "-", "");
}
// Pod's base names always end with a '-', remove it
podBaseName = podBaseName.substring(0, podBaseName.length()-1);
return String.format("%s-%s", pod.getMetadata().getNamespace(), podBaseName);
}
}