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.atSevere().log("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.atInfo().log("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.atFine().log("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.atInfo().log(
- "There are %d common items out of the %d items in 'additions' and %d 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.atInfo().log("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);
- }
- }
-}