package io.fathom.cloud.dns.backend.aws;
import io.fathom.cloud.CloudException;
import io.fathom.cloud.dns.backend.DnsBackendBase;
import io.fathom.cloud.dns.model.DnsZone;
import io.fathom.cloud.dns.services.DnsSecrets;
import io.fathom.cloud.openstack.client.dns.model.Record;
import io.fathom.cloud.openstack.client.dns.model.Recordset;
import io.fathom.cloud.protobuf.DnsModel.BackendData;
import io.fathom.cloud.protobuf.DnsModel.BackendSecretData;
import io.fathom.cloud.protobuf.DnsModel.DnsBackendProviderType;
import io.fathom.cloud.protobuf.DnsModel.DnsSuffixData;
import io.fathom.cloud.server.model.Project;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amazonaws.services.route53.model.HostedZone;
import com.amazonaws.services.route53.model.ResourceRecord;
import com.amazonaws.services.route53.model.ResourceRecordSet;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@Singleton
public class AwsDnsBackend extends DnsBackendBase {
private static final Logger log = LoggerFactory.getLogger(AwsDnsBackend.class);
@Inject
DnsSecrets dnsSecrets;
@Inject
AwsExecutor awsExecutor;
AwsRoute53Client client;
public void init(BackendData backendData) {
BackendSecretData secretData = dnsSecrets.getSecretData(backendData);
String accessKey = secretData.getUsername();
String secretKey = secretData.getPassword();
client = new AwsRoute53Client(accessKey, secretKey);
}
@Override
public void updateDomain(Project project, DnsZone zone) {
UpdateRoute53 job = new UpdateRoute53(project, zone, client);
awsExecutor.execute(job);
}
@Override
public String createZone(Project project, String zone, String topZone, DnsSuffixData suffixData) {
if (suffixData.getSharedDomain()) {
log.info("Request to create {} under shared AWS Route 53 domain {}; will reuse", zone, suffixData.getKey());
return null;
} else {
HostedZone hostedZone = client.createHostedZone(topZone);
return hostedZone.getId();
}
}
@Override
public DnsBackendProviderType getType() {
return DnsBackendProviderType.AWS_ROUTE53;
}
public class UpdateRoute53 extends UpdateDnsDomainBase {
protected final AwsRoute53Client client;
public UpdateRoute53(Project project, DnsZone zone, AwsRoute53Client client) {
super(project, zone);
this.client = client;
}
@Override
public Void call() throws CloudException, IOException {
log.debug("Updating domain: {}", zone.getName());
List<Recordset> requested = readFromDatabase(false);
HostedZone hostedZone = getHostedZone();
List<Recordset> aws = readFromAws(hostedZone);
Changes changes = computeChanges(aws, requested);
List<ResourceRecordSet> create = mapToAws(hostedZone.getName(), changes.create);
List<ResourceRecordSet> remove = mapToAws(hostedZone.getName(), changes.remove);
client.changeRecords(hostedZone.getId(), create, remove);
return null;
}
private List<ResourceRecordSet> mapToAws(String hostedZoneName, List<Recordset> list) {
List<ResourceRecordSet> ret = Lists.newArrayList();
for (Recordset r : list) {
String name = r.name;
if (!name.endsWith(".")) {
name += ".";
}
if ("SOA".equals(r.type)) {
// ignore if not at the root
if (!name.equals(hostedZoneName)) {
log.info("Ignoring SOA record not at root of AWS hosted zone: {}", name);
continue;
}
}
ResourceRecordSet rrs = new ResourceRecordSet();
rrs.setName(name);
rrs.setWeight(r.weight);
Long ttl = r.ttl;
if (ttl == null) {
ttl = 600L;
}
rrs.setTTL(ttl);
rrs.setType(r.type);
// rrs.setSetIdentifier("openstack:" + r.zone_id + ":" + r.id);
List<ResourceRecord> resourceRecords = Lists.newArrayList();
Set<String> values = Sets.newHashSet();
for (Record record : r.records) {
String value = record.value;
// These get encoded into a string for route 53
if (record.port != null) {
throw new IllegalStateException();
}
if (record.priority != null) {
throw new IllegalStateException();
}
if (record.weight != null) {
throw new IllegalStateException();
}
if (values.contains(value)) {
log.debug("Skipping duplicate value: {}", value);
continue;
}
values.add(value);
ResourceRecord rr = new ResourceRecord();
rr.setValue(record.value);
resourceRecords.add(rr);
}
rrs.setResourceRecords(resourceRecords);
ret.add(rrs);
}
return ret;
}
HostedZone getHostedZone() {
Map<String, HostedZone> hostedZones = client.getHostedZones();
String zoneName = zone.getName();
if (!zoneName.endsWith(".")) {
zoneName += ".";
}
String awsZoneName = client.getAwsZoneName(zoneName);
HostedZone hostedZone = hostedZones.get(awsZoneName);
if (hostedZone == null) {
hostedZone = client.createHostedZone(awsZoneName);
}
return hostedZone;
}
private List<Recordset> readFromAws(HostedZone hostedZone) {
log.debug("Reading AWS zone: {}", hostedZone.getName());
List<Recordset> recordsets = Lists.newArrayList();
String domainName = CharMatcher.is('.').trimTrailingFrom(hostedZone.getName());
String awsZoneId = hostedZone.getId();
// String tagPrefix = "openstack:" + domain.getId() + ":";
List<ResourceRecordSet> awsRecordsets = client.getResourceRecords(awsZoneId);
for (ResourceRecordSet awsRecordset : awsRecordsets) {
boolean sameDomain = false;
String awsRecordsetName = CharMatcher.is('.').trimTrailingFrom(awsRecordset.getName());
if (awsRecordsetName.equals(domainName)) {
sameDomain = true;
} else if (awsRecordsetName.endsWith("." + domainName)) {
sameDomain = true;
// // Don't match www.sub.domain.com against domain.com
// String prefix = rrsDomainName.substring(0,
// rrsDomainName.length() - (domainName.length() + 1));
// int dotIndex = prefix.indexOf('.');
// if (dotIndex == -1) {
// sameDomain = true;
// }
}
// String tag = awsRecordset.getSetIdentifier();
// if (Strings.isNullOrEmpty(tag)) {
// log.warn("Record did not have tag: {}",
// awsRecordset.getName());
// } else {
// if (tag.startsWith(tagPrefix)) {
// sameDomain = true;
// }
// }
if (!sameDomain) {
log.debug("Domain name not part of domain: {}", awsRecordsetName);
continue;
}
Recordset recordset = new Recordset();
recordset.type = awsRecordset.getType();
recordset.name = awsRecordsetName;
recordset.ttl = awsRecordset.getTTL();
recordset.weight = awsRecordset.getWeight();
recordset.records = Lists.newArrayList();
for (ResourceRecord awsRR : awsRecordset.getResourceRecords()) {
Record rr = new Record();
rr.value = awsRR.getValue();
if (rr.value.contains(" ")) {
List<String> tokens = Splitter.on(' ').splitToList(rr.value);
if (recordset.type.equals("SOA") && tokens.size() == 7) {
// Ignore for now
// String ns
// ns-310.awsdns-38.com.
// awsdns-hostmaster.amazon.com. 1
// 7200 900 1209600 86400
} else {
throw new IllegalStateException("Cannot decode route 53 value: " + awsRR);
}
}
recordset.records.add(rr);
}
recordsets.add(recordset);
}
return recordsets;
}
@Override
protected List<Recordset> readFromDatabase(boolean createSoa) throws CloudException {
List<Recordset> recordsets = super.readFromDatabase(createSoa);
Map<String, Recordset> unique = Maps.newHashMap();
for (Recordset recordset : recordsets) {
String key = recordset.type + ":" + recordset.name;
if (recordset.weight != null) {
key += ":" + recordset.id;
}
Recordset existing = unique.get(key);
if (existing == null) {
unique.put(key, recordset);
} else {
Recordset merged = merge(existing, recordset);
unique.put(key, merged);
}
}
return Lists.newArrayList(unique.values());
}
Recordset merge(Recordset a, Recordset b) {
if (a.isDeleted()) {
return b;
}
if (b.isDeleted()) {
return a;
}
Recordset merged = new Recordset();
merged.name = a.name;
merged.type = a.type;
merged.records = Lists.newArrayList();
merged.records.addAll(a.records);
merged.records.addAll(b.records);
merged.ttl = a.ttl;
merged.weight = a.weight;
merged.zone_id = a.zone_id;
merged.version = a.version;
return merged;
}
}
}