aaaaRrData = new HashSet<>();
+ for (InetAddress ip : host.get().getInetAddresses()) {
+ if (ip instanceof Inet4Address) {
+ aRrData.add(ip.toString());
+ } else {
+ checkArgument(ip instanceof Inet6Address);
+ aaaaRrData.add(ip.toString());
+ }
+ }
+
+ if (!aRrData.isEmpty()) {
+ domainRecords.add(
+ new ResourceRecordSet()
+ .setName(absoluteHostName)
+ .setTtl((int) defaultATtl.getStandardSeconds())
+ .setType("A")
+ .setKind("dns#resourceRecordSet")
+ .setRrdatas(ImmutableList.copyOf(aRrData)));
+ }
+
+ if (!aaaaRrData.isEmpty()) {
+ domainRecords.add(
+ new ResourceRecordSet()
+ .setName(absoluteHostName)
+ .setTtl((int) defaultATtl.getStandardSeconds())
+ .setType("AAAA")
+ .setKind("dns#resourceRecordSet")
+ .setRrdatas(ImmutableList.copyOf(aaaaRrData)));
+ }
+
+ desiredRecords.put(absoluteHostName, domainRecords.build());
+ }
+
+ /**
+ * Publish A/AAAA records to Cloud DNS.
+ *
+ * Cloud DNS has no API for glue -- A/AAAA records are automatically matched to their
+ * corresponding NS records to serve glue.
+ */
+ @Override
+ public void publishHost(String hostName) {
+ // Get the superordinate domain name of the host.
+ InternetDomainName host = InternetDomainName.from(hostName);
+ Optional tld = Registries.findTldForName(host);
+
+ // Host not managed by our registry, no need to update DNS.
+ if (!tld.isPresent()) {
+ logger.severefmt("publishHost called for invalid host %s", hostName);
+ return;
+ }
+
+ // Extract the superordinate domain name. The TLD and host may have several dots so this
+ // must calculate a sublist.
+ ImmutableList hostParts = host.parts();
+ ImmutableList tldParts = tld.get().parts();
+ ImmutableList domainParts =
+ hostParts.subList(hostParts.size() - tldParts.size() - 1, hostParts.size());
+ String domain = Joiner.on(".").join(domainParts);
+
+ // Refresh the superordinate domain, since we shouldn't be publishing glue records if we are not
+ // authoritative for the superordinate domain.
+ publishDomain(domain);
+ }
+
+ /**
+ * Sync changes in a zone requested by publishDomain and publishHost to Cloud DNS.
+ *
+ * The zone for the TLD must exist first in Cloud DNS and must be DNSSEC enabled.
+ *
+ *
The relevant resource records (including those of all subordinate hosts) will be retrieved
+ * and the operation will be retried until the state of the retrieved zone data matches the
+ * representation built via this writer.
+ */
+ @Override
+ protected void commitUnchecked() {
+ ImmutableMap> desiredRecordsCopy =
+ ImmutableMap.copyOf(desiredRecords);
+ retrier.callWithRetry(() -> mutateZone(desiredRecordsCopy), ZoneStateException.class);
+ logger.info("Wrote to Cloud DNS");
+ }
+
+ /**
+ * Returns the glue records for in-bailiwick nameservers for the given domain+records.
+ */
+ private Stream filterGlueRecords(String domainName, Stream records) {
+ return records
+ .filter(record -> record.getType().equals("NS"))
+ .flatMap(record -> record.getRrdatas().stream())
+ .filter(hostName -> hostName.endsWith(domainName) && !hostName.equals(domainName));
+ }
+
+ /**
+ * Mutate the zone with the provided {@code desiredRecords}.
+ */
+ @VisibleForTesting
+ void mutateZone(ImmutableMap> desiredRecords) {
+ // Fetch all existing records for names that this writer is trying to modify
+ ImmutableSet.Builder flattenedExistingRecords = new ImmutableSet.Builder<>();
+
+ // First, fetch the records for the given domains
+ Map> domainRecords =
+ getResourceRecordsForDomains(desiredRecords.keySet());
+
+ // add the records to the list of exiting records
+ domainRecords.values().forEach(flattenedExistingRecords::addAll);
+
+ // Get the glue record host names from the given records
+ ImmutableSet hostsToRead =
+ domainRecords
+ .entrySet()
+ .stream()
+ .flatMap(entry -> filterGlueRecords(entry.getKey(), entry.getValue().stream()))
+ .collect(toImmutableSet());
+
+ // Then fetch and add the records for these hosts
+ getResourceRecordsForDomains(hostsToRead).values().forEach(flattenedExistingRecords::addAll);
+
+ // Flatten the desired records into one set.
+ ImmutableSet.Builder flattenedDesiredRecords = new ImmutableSet.Builder<>();
+ desiredRecords.values().forEach(flattenedDesiredRecords::addAll);
+
+ // Delete all existing records and add back the desired records
+ updateResourceRecords(flattenedDesiredRecords.build(), flattenedExistingRecords.build());
+ }
+
+ /**
+ * Fetch the {@link ResourceRecordSet}s for the given domain names under this zone.
+ *
+ * The provided domain should be in absolute form.
+ */
+ private Map> getResourceRecordsForDomains(
+ Set domainNames) {
+ logger.finefmt("Fetching records for %s", domainNames);
+ // As per Concurrent.transform() - if numThreads or domainNames.size() < 2, it will not use
+ // threading.
+ return ImmutableMap.copyOf(
+ Concurrent.transform(
+ domainNames,
+ numThreads,
+ domainName ->
+ new SimpleImmutableEntry<>(domainName, getResourceRecordsForDomain(domainName))));
+ }
+
+ /**
+ * Fetch the {@link ResourceRecordSet}s for the given domain name under this zone.
+ *
+ * The provided domain should be in absolute form.
+ */
+ private List getResourceRecordsForDomain(String domainName) {
+ // TODO(b/70217860): do we want to use a retrier here?
+ try {
+ Dns.ResourceRecordSets.List listRecordsRequest =
+ dnsConnection.resourceRecordSets().list(projectId, zoneName).setName(domainName);
+
+ rateLimiter.acquire();
+ return listRecordsRequest.execute().getRrsets();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Update {@link ResourceRecordSet}s under this zone.
+ *
+ * This call should be used in conjunction with {@link #getResourceRecordsForDomains} in a
+ * get-and-set retry loop.
+ *
+ *
See {@link "https://cloud.google.com/dns/troubleshooting"} for a list of errors produced by
+ * the Google Cloud DNS API.
+ *
+ * @throws ZoneStateException if the operation could not be completely successfully because the
+ * records to delete do not exist, already exist or have been modified with different
+ * attributes since being queried.
+ */
+ private void updateResourceRecords(
+ ImmutableSet additions, ImmutableSet deletions) {
+ // Find records that are both in additions and deletions, so we can remove them from both before
+ // requesting the change. This is mostly for optimization reasons - not doing so doesn't affect
+ // the result.
+ ImmutableSet intersection =
+ Sets.intersection(additions, deletions).immutableCopy();
+ logger.infofmt(
+ "There are %s common items out of the %s items in 'additions' and %s items in 'deletions'",
+ intersection.size(), additions.size(), deletions.size());
+ // Exit early if we have nothing to update - dnsConnection doesn't work on empty changes
+ if (additions.equals(deletions)) {
+ logger.infofmt("Returning early because additions is the same as deletions");
+ return;
+ }
+ Change change =
+ new Change()
+ .setAdditions(ImmutableList.copyOf(Sets.difference(additions, intersection)))
+ .setDeletions(ImmutableList.copyOf(Sets.difference(deletions, intersection)));
+
+ rateLimiter.acquire();
+ try {
+ dnsConnection.changes().create(projectId, zoneName, change).execute();
+ } catch (GoogleJsonResponseException e) {
+ List errors = e.getDetails().getErrors();
+ // We did something really wrong here, just give up and re-throw
+ if (errors.size() > 1) {
+ throw new RuntimeException(e);
+ }
+ String errorReason = errors.get(0).getReason();
+
+ if (RETRYABLE_EXCEPTION_REASONS.contains(errorReason)) {
+ throw new ZoneStateException(errorReason);
+ } else {
+ throw new RuntimeException(e);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns the presentation format ending in a dot used for an absolute hostname.
+ *
+ * @param hostName the fully qualified hostname
+ */
+ private static String getAbsoluteHostName(String hostName) {
+ return hostName.endsWith(".") ? hostName : hostName + ".";
+ }
+
+ private static ImmutableSet multiplyAbsoluteName(String absoluteName) {
+ return IntStream.range(0, 10)
+ .mapToObj(i -> i == 0 ? absoluteName : String.format("%d-%s", i, absoluteName))
+ .collect(ImmutableSet.toImmutableSet());
+ }
+
+ /** Zone state on Cloud DNS does not match the expected state. */
+ static class ZoneStateException extends RuntimeException {
+ public ZoneStateException(String reason) {
+ super("Zone state on Cloud DNS does not match the expected state: " + reason);
+ }
+ }
+}