package com.netflix.eureka.aws;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.InstanceProfileCredentialsProvider;
import com.amazonaws.services.route53.AmazonRoute53Client;
import com.amazonaws.services.route53.model.*;
import com.netflix.appinfo.AmazonInfo;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.eureka.EurekaServerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
/**
* Route53 binder implementation. Will look for a free domain in the list of service url to bind itself to via Route53.
*/
@Singleton
public class Route53Binder implements AwsBinder {
private static final Logger logger = LoggerFactory
.getLogger(Route53Binder.class);
public static final String NULL_DOMAIN = "null";
private final EurekaServerConfig serverConfig;
private final EurekaClientConfig clientConfig;
private final ApplicationInfoManager applicationInfoManager;
/**
* the hostname to register under the Route53 CNAME
*/
private final String registrationHostname;
private final Timer timer;
private final AmazonRoute53Client amazonRoute53Client;
@Inject
public Route53Binder(EurekaServerConfig serverConfig,
EurekaClientConfig clientConfig,
ApplicationInfoManager applicationInfoManager) {
this(getRegistrationHostnameFromAmazonDataCenterInfo(applicationInfoManager),
serverConfig,
clientConfig,
applicationInfoManager);
}
/**
* @param registrationHostname the hostname to register under the Route53 CNAME
*/
public Route53Binder(String registrationHostname, EurekaServerConfig serverConfig,
EurekaClientConfig clientConfig, ApplicationInfoManager applicationInfoManager) {
this.registrationHostname = registrationHostname;
this.serverConfig = serverConfig;
this.clientConfig = clientConfig;
this.applicationInfoManager = applicationInfoManager;
this.timer = new Timer("Eureka-Route53Binder", true);
this.amazonRoute53Client = getAmazonRoute53Client(serverConfig);
}
private static String getRegistrationHostnameFromAmazonDataCenterInfo(ApplicationInfoManager applicationInfoManager) {
InstanceInfo myInfo = applicationInfoManager.getInfo();
AmazonInfo dataCenterInfo = (AmazonInfo) myInfo.getDataCenterInfo();
String ip = dataCenterInfo.get(AmazonInfo.MetaDataKey.publicHostname);
if (ip == null || ip.length() == 0) {
return dataCenterInfo.get(AmazonInfo.MetaDataKey.localHostname);
}
return ip;
}
@Override
@PostConstruct
public void start() throws InterruptedException {
doBind();
timer.schedule(
new TimerTask() {
@Override
public void run() {
try {
doBind();
} catch (Throwable e) {
logger.error("Could not bind to Route53", e);
}
}
},
serverConfig.getRoute53BindingRetryIntervalMs(),
serverConfig.getRoute53BindingRetryIntervalMs());
}
private void doBind() throws InterruptedException {
List<ResourceRecordSetWithHostedZone> freeDomains = new ArrayList<>();
List<String> domains = getDeclaredDomains();
for(String domain : domains) {
ResourceRecordSetWithHostedZone rrs = getResourceRecordSetWithHostedZone(domain);
if (rrs != null) {
if (rrs.getResourceRecordSet() == null) {
ResourceRecordSet resourceRecordSet = new ResourceRecordSet();
resourceRecordSet.setName(domain);
resourceRecordSet.setType(RRType.CNAME);
resourceRecordSet.setTTL(serverConfig.getRoute53DomainTTL());
freeDomains.add(new ResourceRecordSetWithHostedZone(rrs.getHostedZone(), resourceRecordSet));
} else if (NULL_DOMAIN.equals(rrs.getResourceRecordSet().getResourceRecords().get(0).getValue())) {
freeDomains.add(rrs);
}
// already registered
if (hasValue(rrs, registrationHostname)) {
return;
}
}
}
for(ResourceRecordSetWithHostedZone rrs : freeDomains) {
if (createResourceRecordSet(rrs)) {
logger.info("Bind {} to {}" , registrationHostname, rrs.getResourceRecordSet().getName());
return;
}
}
logger.warn("Unable to find free domain in {}", domains);
}
private boolean createResourceRecordSet(ResourceRecordSetWithHostedZone rrs) throws InterruptedException {
rrs.getResourceRecordSet().setResourceRecords(Arrays.asList(new ResourceRecord(registrationHostname)));
Change change = new Change(ChangeAction.UPSERT, rrs.getResourceRecordSet());
if (executeChangeWithRetry(change, rrs.getHostedZone())) {
Thread.sleep(1000);
// check change not overwritten
ResourceRecordSet resourceRecordSet = getResourceRecordSet(rrs.getResourceRecordSet().getName(), rrs.getHostedZone());
return resourceRecordSet.getResourceRecords().equals(rrs.getResourceRecordSet().getResourceRecords());
}
return false;
}
private List<String> toDomains(List<String> ec2Urls) {
List<String> domains = new ArrayList<>(ec2Urls.size());
for(String url : ec2Urls) {
try {
domains.add(extractDomain(url));
} catch(MalformedURLException e) {
logger.error("Invalid url " + url, e);
}
}
return domains;
}
private String getMyZone() {
InstanceInfo info = applicationInfoManager.getInfo();
AmazonInfo amazonInfo = info != null ? (AmazonInfo) info.getDataCenterInfo() : null;
String zone = amazonInfo != null ? amazonInfo.get(AmazonInfo.MetaDataKey.availabilityZone) : null;
if (zone == null) {
throw new RuntimeException("Cannot extract availabilityZone");
}
return zone;
}
private List<String> getDeclaredDomains() {
final String myZone = getMyZone();
List<String> ec2Urls = clientConfig.getEurekaServerServiceUrls(myZone);
return toDomains(ec2Urls);
}
private boolean executeChangeWithRetry(Change change, HostedZone hostedZone) throws InterruptedException {
Throwable firstError = null;
for (int i = 0; i < serverConfig.getRoute53BindRebindRetries(); i++) {
try {
executeChange(change, hostedZone);
return true;
} catch (Throwable e) {
if (firstError == null) {
firstError = e;
}
Thread.sleep(1000);
}
}
if (firstError != null) {
logger.error("Cannot execute change " + change + " " + firstError, firstError);
}
return false;
}
private void executeChange(Change change, HostedZone hostedZone) {
logger.info("Execute change {} ", change);
ChangeResourceRecordSetsRequest changeResourceRecordSetsRequest = new ChangeResourceRecordSetsRequest();
changeResourceRecordSetsRequest.setHostedZoneId(hostedZone.getId());
ChangeBatch changeBatch = new ChangeBatch();
changeBatch.withChanges(change);
changeResourceRecordSetsRequest.setChangeBatch(changeBatch);
amazonRoute53Client.changeResourceRecordSets(changeResourceRecordSetsRequest);
}
private ResourceRecordSetWithHostedZone getResourceRecordSetWithHostedZone(String domain) {
HostedZone hostedZone = getHostedZone(domain);
if (hostedZone != null) {
return new ResourceRecordSetWithHostedZone(hostedZone, getResourceRecordSet(domain, hostedZone));
}
return null;
}
private ResourceRecordSet getResourceRecordSet(String domain, HostedZone hostedZone) {
ListResourceRecordSetsRequest request = new ListResourceRecordSetsRequest();
request.setMaxItems(String.valueOf(Integer.MAX_VALUE));
request.setHostedZoneId(hostedZone.getId());
ListResourceRecordSetsResult listResourceRecordSetsResult = amazonRoute53Client.listResourceRecordSets(request);
for(ResourceRecordSet rrs : listResourceRecordSetsResult.getResourceRecordSets()) {
if (rrs.getName().equals(domain)) {
return rrs;
}
}
return null;
}
private HostedZone getHostedZone(String domain) {
ListHostedZonesRequest listHostedZoneRequest = new ListHostedZonesRequest();
listHostedZoneRequest.setMaxItems(String.valueOf(Integer.MAX_VALUE));
ListHostedZonesResult listHostedZonesResult = amazonRoute53Client.listHostedZones(listHostedZoneRequest);
for(HostedZone hostedZone : listHostedZonesResult.getHostedZones()) {
if (domain.endsWith(hostedZone.getName())) {
return hostedZone;
}
}
return null;
}
private void unbindFromDomain(String domain) throws InterruptedException {
ResourceRecordSetWithHostedZone resourceRecordSetWithHostedZone = getResourceRecordSetWithHostedZone(domain);
if (hasValue(resourceRecordSetWithHostedZone, registrationHostname)) {
resourceRecordSetWithHostedZone.getResourceRecordSet().getResourceRecords().get(0).setValue(NULL_DOMAIN);
executeChangeWithRetry(new Change(ChangeAction.UPSERT, resourceRecordSetWithHostedZone.getResourceRecordSet()), resourceRecordSetWithHostedZone.getHostedZone());
}
}
private String extractDomain(String url) throws MalformedURLException {
return new URL(url).getHost() + ".";
}
@Override
@PreDestroy
public void shutdown() throws InterruptedException {
timer.cancel();
for(String domain : getDeclaredDomains()) {
unbindFromDomain(domain);
}
amazonRoute53Client.shutdown();
}
private AmazonRoute53Client getAmazonRoute53Client(EurekaServerConfig serverConfig) {
String aWSAccessId = serverConfig.getAWSAccessId();
String aWSSecretKey = serverConfig.getAWSSecretKey();
ClientConfiguration clientConfiguration = new ClientConfiguration()
.withConnectionTimeout(serverConfig.getASGQueryTimeoutMs());
if (null != aWSAccessId && !"".equals(aWSAccessId)
&& null != aWSSecretKey && !"".equals(aWSSecretKey)) {
return new AmazonRoute53Client(
new BasicAWSCredentials(aWSAccessId, aWSSecretKey),
clientConfiguration);
} else {
return new AmazonRoute53Client(
new InstanceProfileCredentialsProvider(),
clientConfiguration);
}
}
private boolean hasValue(ResourceRecordSetWithHostedZone resourceRecordSetWithHostedZone, String ip) {
if (resourceRecordSetWithHostedZone != null && resourceRecordSetWithHostedZone.getResourceRecordSet() != null) {
for (ResourceRecord rr : resourceRecordSetWithHostedZone.getResourceRecordSet().getResourceRecords()) {
if (ip.equals(rr.getValue())) {
return true;
}
}
}
return false;
}
private class ResourceRecordSetWithHostedZone {
private final HostedZone hostedZone;
private final ResourceRecordSet resourceRecordSet;
public ResourceRecordSetWithHostedZone(HostedZone hostedZone, ResourceRecordSet resourceRecordSet) {
this.hostedZone = hostedZone;
this.resourceRecordSet = resourceRecordSet;
}
public HostedZone getHostedZone() {
return hostedZone;
}
public ResourceRecordSet getResourceRecordSet() {
return resourceRecordSet;
}
}
}