/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.discovery.shared.resolver.aws;
import javax.naming.NamingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import com.netflix.discovery.endpoint.DnsResolver;
import com.netflix.discovery.shared.resolver.ClusterResolver;
import com.netflix.discovery.shared.resolver.ClusterResolverException;
import com.netflix.discovery.shared.resolver.ResolverUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A cluster resolver implementation that assumes that Eureka cluster configuration is provided in two level,
* cascading DNS TXT records. The first level shall point to zone level DNS entries, while at the zone level
* server pools shall be provided (either CNAMEs or A records).
* If no TXT record is found at the provided address, the resolver will add 'txt.' suffix to the address, and try
* to resolve that address.
*
*
* <h3>Example</h3>
* Lets assume we have a service with root domain myservice.net, and a deployment in AWS us-east-1 on all three zones.
* The root discovery domain would be:<br/>
* <table border='1'>
* <thead>
* <th>DNS record</th>
* <th>TXT record content</th>
* </thead>
* <tbody>
* <tr><td>txt.myservice.net</td>
* <td>
* txt.us-east-1a.myservice.net
* txt.us-east-1b.myservice.net
* txt.us-east-1c.myservice.net
* </td>
* </tr>
* <tr><td>txt.us-east-1a.myservice.net</td>
* <td>
* ec2-1-2-3-4.compute-1.amazonaws.com
* ec2-1-2-3-5.compute-1.amazonaws.com
* </td>
* </tr>
* <tr><td>txt.us-east-1b.myservice.net</td>
* <td>
* ec2-1-2-3-6.compute-1.amazonaws.com
* ec2-1-2-3-7.compute-1.amazonaws.com
* </td>
* </tr>
* <tr><td>txt.us-east-1c.myservice.net</td>
* <td>
* ec2-1-2-3-8.compute-1.amazonaws.com
* ec2-1-2-3-9.compute-1.amazonaws.com
* </td>
* </tr>
* </tbody>
* </table>
*
* @author Tomasz Bak
*/
public class DnsTxtRecordClusterResolver implements ClusterResolver<AwsEndpoint> {
private static final Logger logger = LoggerFactory.getLogger(DnsTxtRecordClusterResolver.class);
private final String region;
private final String rootClusterDNS;
private final boolean extractZoneFromDNS;
private final int port;
private final boolean isSecure;
private final String relativeUri;
/**
* @param rootClusterDNS top level domain name, in the two level hierarchy (see {@link DnsTxtRecordClusterResolver} documentation).
* @param extractZoneFromDNS if set to true, zone information will be extract from zone DNS name. It assumed that the zone
* name is the name part immediately followint 'txt.' suffix.
* @param port Eureka sever port number
* @param relativeUri service relative URI that will be appended to server address
*/
public DnsTxtRecordClusterResolver(String region, String rootClusterDNS, boolean extractZoneFromDNS, int port, boolean isSecure, String relativeUri) {
this.region = region;
this.rootClusterDNS = rootClusterDNS;
this.extractZoneFromDNS = extractZoneFromDNS;
this.port = port;
this.isSecure = isSecure;
this.relativeUri = relativeUri;
}
@Override
public String getRegion() {
return region;
}
@Override
public List<AwsEndpoint> getClusterEndpoints() {
List<AwsEndpoint> eurekaEndpoints = resolve(region, rootClusterDNS, extractZoneFromDNS, port, isSecure, relativeUri);
if (logger.isDebugEnabled()) {
logger.debug("Resolved {} to {}", rootClusterDNS, eurekaEndpoints);
}
return eurekaEndpoints;
}
private static List<AwsEndpoint> resolve(String region, String rootClusterDNS, boolean extractZone, int port, boolean isSecure, String relativeUri) {
try {
Set<String> zoneDomainNames = resolve(rootClusterDNS);
if (zoneDomainNames.isEmpty()) {
throw new ClusterResolverException("Cannot resolve Eureka cluster addresses; there are no data in TXT record for DN " + rootClusterDNS);
}
List<AwsEndpoint> endpoints = new ArrayList<>();
for (String zoneDomain : zoneDomainNames) {
String zone = extractZone ? ResolverUtils.extractZoneFromHostName(zoneDomain) : null;
Set<String> zoneAddresses = resolve(zoneDomain);
for (String address : zoneAddresses) {
endpoints.add(new AwsEndpoint(address, port, isSecure, relativeUri, region, zone));
}
}
return endpoints;
} catch (NamingException e) {
throw new ClusterResolverException("Cannot resolve Eureka cluster addresses for root: " + rootClusterDNS, e);
}
}
private static Set<String> resolve(String rootClusterDNS) throws NamingException {
Set<String> result;
try {
result = DnsResolver.getCNamesFromTxtRecord(rootClusterDNS);
if (!rootClusterDNS.startsWith("txt.")) {
result = DnsResolver.getCNamesFromTxtRecord("txt." + rootClusterDNS);
}
} catch (NamingException e) {
if (!rootClusterDNS.startsWith("txt.")) {
result = DnsResolver.getCNamesFromTxtRecord("txt." + rootClusterDNS);
} else {
throw e;
}
}
return result;
}
}