From ac47b7fc3b55026dfd3bcc8dc2ecd527a196f520 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Thu, 26 Mar 2015 09:04:19 -0700 Subject: [PATCH] Adds Zone.email and ttl and updates CLI zone list output --- CHANGELOG.md | 2 + README.md | 3 +- cli/README.md | 21 ++- .../java/denominator/cli/Denominator.java | 17 +- .../java/denominator/cli/DenominatorTest.java | 6 +- .../denominator/clouddns/CloudDNSZoneApi.java | 89 +++++---- .../clouddns/RackspaceAdapters.java | 13 +- .../denominator/clouddns/RackspaceApis.java | 7 +- ...CloudDNSProviderDynamicUpdateMockTest.java | 9 +- .../clouddns/CloudDNSZoneApiMockTest.java | 31 ++-- .../clouddns/RackspaceApisTest.java | 13 +- .../MockAllProfileResourceRecordSetApi.java | 6 +- .../java/denominator/mock/MockZoneApi.java | 25 ++- ...DynamicCredentialsProviderExampleTest.java | 9 +- .../java/denominator/ReadOnlyLiveTest.java | 14 +- .../designate/DesignateAdapters.java | 19 +- .../denominator/designate/DesignateTest.java | 6 +- .../designate/DesignateZoneApiMockTest.java | 16 +- .../main/java/denominator/dynect/DynECT.java | 21 +-- .../denominator/dynect/DynECTAdapters.java | 25 +-- .../denominator/dynect/DynECTProvider.java | 6 +- .../dynect/DynECTResourceRecordSetApi.java | 23 +-- .../denominator/dynect/DynECTZoneApi.java | 31 +++- .../denominator/dynect/SessionTarget.java | 2 +- .../DynECTResourceRecordSetApiMockTest.java | 19 +- .../java/denominator/dynect/DynECTTest.java | 10 +- .../dynect/DynECTZoneApiMockTest.java | 31 ++-- .../java/denominator/ResourceTypeToValue.java | 4 +- .../src/main/java/denominator/model/Zone.java | 170 ++++++++++++++---- .../java/denominator/assertj/ZoneAssert.java | 16 +- .../test/java/denominator/model/ZoneTest.java | 22 +-- .../ListHostedZonesResponseHandler.java | 24 ++- .../java/denominator/route53/Route53.java | 23 +-- .../denominator/route53/Route53ZoneApi.java | 94 +++++++--- .../route53/Route53DecoderTest.java | 20 +-- .../route53/Route53ZoneApiMockTest.java | 76 +++++++- .../java/denominator/ultradns/UltraDNS.java | 18 +- .../ultradns/UltraDNSContentHandlers.java | 11 +- .../ultradns/UltraDNSException.java | 4 + .../ultradns/UltraDNSProvider.java | 4 +- .../denominator/ultradns/UltraDNSZoneApi.java | 34 +++- .../denominator/ultradns/UltraDNSTest.java | 39 ++-- .../ultradns/UltraDNSZoneApiMockTest.java | 58 +++--- 43 files changed, 660 insertions(+), 431 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a0a73c..0e09f6ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ### Version 4.5 +* Adds `Zone.email()`, `Zone.ttl()` and `Zone.builder()` +* Adds email and ttl to CLI zone list output * Adds `ZoneApi.iterateByName()` to support lookups * Adds `-n` parameter to CLI zone list * Refines zone identifiers and handling of zones with the same name diff --git a/README.md b/README.md index 3c7160aa..2f884756 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,9 @@ Advanced usage, including ec2 hooks are covered in the [readme](https://github.c If you just want to fool around, you can use the `mock` provider. ```bash +# first column is the zone id, which isn't always its name! $ denominator -p mock zone list -denominator.io. +denominator.io. denominator.io. admin.denominator.io. 86400 $ denominator -p mock record -z denominator.io. list denominator.io. SOA 3600 ns1.denominator.io. admin.denominator.io. 1 3600 600 604800 60 denominator.io. NS 86400 ns1.denominator.io. diff --git a/cli/README.md b/cli/README.md index 5419a785..fefc3212 100644 --- a/cli/README.md +++ b/cli/README.md @@ -72,8 +72,9 @@ denominator will print out a help statement, but here's the gist. If you just want to fool around, you can use the `mock` provider. ```bash +# first column is the zone id, which isn't always its name! $ denominator -p mock zone list -denominator.io. +denominator.io. denominator.io. admin.denominator.io. 86400 $ denominator -p mock -z denominator.io. record list denominator.io. SOA 3600 ns1.denominator.io. admin.denominator.io. 1 3600 600 604800 60 denominator.io. NS 86400 ns1.denominator.io. @@ -130,18 +131,16 @@ email.netflix.com. A 3600 69.53.237.168 --snip-- ``` -### Zone Identifiers -When the provider doesn't use name-based zone identification, the last column zone list is the `id`. -This can be used to disambiguate zones with the same name, or to improve performance by eliminating a network call. -If you wish to use a zone's identifier, simply pass it instead of the zone name in record commands. +### Zone ID +The first column in the zone list is the `id`. This can be used to disambiguate zones with the same +name, or to improve performance by eliminating a network call. If you wish to use a zone's +identifier, simply pass it instead of the zone name in record commands. ```bash -$ denominator -p route53 -c my_access_key -c my_secret_key zone list -n denominator.io. -[Route53#listHostedZonesByName] ---> GET https://route53.amazonaws.com/2013-04-01/hostedzonesbyname?dnsname=denominator.io. HTTP/1.1 -[Route53#listHostedZonesByName] <--- HTTP/1.1 200 OK (678ms) -denominator.io. 63CEB242-9E3E-327D-9351-2EFD02493E18 Z2ZEEJCUZCVG56 -denominator.io. 022DFF2F-2E0B-F0A2-A581-19339A7BA1F1 Z3OQLQGABCU3T -$ denominator -p route53 -c my_access_key -c my_secret_key record -z Z3OQLQGABCU3T list +$ denominator -q -p route53 -c my_access_key -c my_secret_key zone list -n denominator.io. +Z2ZEEJCUZCVG56 denominator.io. 63CEB242-9E3E-327D-9351-2EFD02493E18 awsdns-hostmaster.amazon.com. 86400 +Z3OQLQGABCU3T denominator.io. 022DFF2F-2E0B-F0A2-A581-19339A7BA1F1 awsdns-hostmaster.amazon.com. 86400 +$ denominator -q -p route53 -c my_access_key -c my_secret_key record -z Z3OQLQGABCU3T list --snip-- denominator.io. NS 172800 ns-1312.awsdns-36.org. --snip-- diff --git a/cli/src/main/java/denominator/cli/Denominator.java b/cli/src/main/java/denominator/cli/Denominator.java index 15b89272..0f9d1806 100644 --- a/cli/src/main/java/denominator/cli/Denominator.java +++ b/cli/src/main/java/denominator/cli/Denominator.java @@ -372,7 +372,7 @@ void overrideFromEnv(Map env) { protected abstract Iterator doRun(DNSApiManager mgr); } - @Command(name = "list", description = "Lists the zones present in this provider. If more than one column is present, the last is the zone id.") + @Command(name = "list", description = "Lists the zones present in this provider. The zone id is the first column.") public static class ZoneList extends DenominatorCommand { @Option(type = OptionType.COMMAND, name = {"-n", @@ -383,20 +383,11 @@ public Iterator doRun(final DNSApiManager mgr) { Iterator zones = name == null ? mgr.api().zones().iterator() : mgr.api().zones().iterateByName(name); return Iterators.transform(zones, new Function() { - @Override public String apply(Zone input) { - Identification zoneId = mgr.provider().zoneIdentification(); - switch (zoneId) { - case NAME: - return input.name(); - case OPAQUE: - return format("%-36s %s", input.name(), input.id()); - case QUALIFIED: - return format("%-36s %-19s %s", input.name(), input.qualifier(), input.id()); - default: - throw new UnsupportedOperationException("unsupported zone identification: " + zoneId); - } + return format("%-24s %-36s %-19s %-36s %d", input.id(), input.name(), + input.qualifier() != null ? input.qualifier() : "", input.email(), + input.ttl()); } }); } diff --git a/cli/src/test/java/denominator/cli/DenominatorTest.java b/cli/src/test/java/denominator/cli/DenominatorTest.java index 930d5cc1..b3fd6bfa 100644 --- a/cli/src/test/java/denominator/cli/DenominatorTest.java +++ b/cli/src/test/java/denominator/cli/DenominatorTest.java @@ -74,7 +74,9 @@ public void listsAllProvidersWithCredentials() { @Test // denominator -p mock zone list public void testZoneList() { - assertThat(new ZoneList().doRun(mgr)).containsExactly("denominator.io."); + assertThat(new ZoneList().doRun(mgr)).containsExactly( + "denominator.io. denominator.io. admin.denominator.io. 86400" + ); } @Test // denominator -p mock zone list -n denominator.com. @@ -224,7 +226,7 @@ public void testResourceRecordSetList() { assertThat(command.doRun(mgr)).containsExactly( "a.denominator.io. A alazona null 192.0.2.1", "denominator.io. NS 86400 ns1.denominator.io.", - "denominator.io. SOA 3600 ns1.denominator.io. admin.denominator.io. 1 3600 600 604800 60", + "denominator.io. SOA 3600 ns1.denominator.io. admin.denominator.io. 1 3600 600 604800 86400", "server1.denominator.io. CERT 3600 12345 1 1 B33F", "server1.denominator.io. SRV 3600 0 1 80 www.denominator.io.", "www.geo.denominator.io. CNAME alazona 86400 a.denominator.io.", diff --git a/clouddns/src/main/java/denominator/clouddns/CloudDNSZoneApi.java b/clouddns/src/main/java/denominator/clouddns/CloudDNSZoneApi.java index f0fa2fd0..0ced9450 100644 --- a/clouddns/src/main/java/denominator/clouddns/CloudDNSZoneApi.java +++ b/clouddns/src/main/java/denominator/clouddns/CloudDNSZoneApi.java @@ -1,28 +1,18 @@ package denominator.clouddns; -import java.net.URI; import java.util.Iterator; import javax.inject.Inject; import denominator.clouddns.RackspaceApis.CloudDNS; import denominator.clouddns.RackspaceApis.ListWithNext; -import denominator.clouddns.RackspaceApis.Pager; import denominator.model.Zone; -import static denominator.clouddns.RackspaceApis.emptyOn404; +import static denominator.common.Util.singletonIterator; class CloudDNSZoneApi implements denominator.ZoneApi { private final CloudDNS api; - private final Pager zonePager = new Pager() { - - @Override - public ListWithNext apply(URI nullOrNext) { - return nullOrNext == null ? api.domains() : api.domains(nullOrNext); - } - - }; @Inject CloudDNSZoneApi(CloudDNS api) { @@ -31,38 +21,59 @@ public ListWithNext apply(URI nullOrNext) { @Override public Iterator iterator() { - final ListWithNext first = emptyOn404(zonePager, null); - if (first.next == null) { - return first.iterator(); + return new ZipWithDomain(api.domains()); + } + + @Override + public Iterator iterateByName(String name) { + ListWithNext zones = api.domainsByName(name); + if (zones.isEmpty()) { + return singletonIterator(null); } - return new Iterator() { - Iterator current = first.iterator(); - URI next = first.next; - - @Override - public boolean hasNext() { - while (!current.hasNext() && next != null) { - ListWithNext nextPage = emptyOn404(zonePager, next); - current = nextPage.iterator(); - next = nextPage.next; - } - return current.hasNext(); - } + return singletonIterator(zipWithDomain(zones.get(0))); + } - @Override - public Zone next() { - return current.next(); - } + /** + * CloudDNS only exposes a domain's ttl in the show api. + */ + private Zone zipWithDomain(Zone next) { + int ttl = api.domain(next.id()).ttl(); + return Zone.builder() + .name(next.name()) + .id(next.id()) + .email(next.email()) + .ttl(ttl).build(); + } + + class ZipWithDomain implements Iterator { - @Override - public void remove() { - throw new UnsupportedOperationException(); + ListWithNext list; + int i = 0; + int length; + + ZipWithDomain(ListWithNext list) { + this.list = list; + this.length = list.size(); + } + + @Override + public boolean hasNext() { + while (i == length && list.next != null) { + list = api.domains(list.next); + length = list.size(); + i = 0; } - }; - } + return i < length; + } - @Override - public Iterator iterateByName(String name) { - return api.domainsByName(name).iterator(); + @Override + public Zone next() { + return zipWithDomain(list.get(i++)); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } } } diff --git a/clouddns/src/main/java/denominator/clouddns/RackspaceAdapters.java b/clouddns/src/main/java/denominator/clouddns/RackspaceAdapters.java index 6cf65f99..ee0fb92e 100644 --- a/clouddns/src/main/java/denominator/clouddns/RackspaceAdapters.java +++ b/clouddns/src/main/java/denominator/clouddns/RackspaceAdapters.java @@ -66,19 +66,22 @@ protected String jsonKey() { } protected Zone build(JsonReader reader) throws IOException { - String name = null; - String id = null; + Zone.Builder result = new Zone.Builder(); while (reader.hasNext()) { String nextName = reader.nextName(); if (nextName.equals("name")) { - name = reader.nextString(); + result.name(reader.nextString()); } else if (nextName.equals("id")) { - id = reader.nextString(); + result.id(reader.nextString()); + } else if (nextName.equals("emailAddress")) { + result.email(reader.nextString()); + } else if (nextName.equals("ttl")) { + result.ttl(reader.nextInt()); } else { reader.skipValue(); } } - return Zone.create(name, id); + return result.build(); } } diff --git a/clouddns/src/main/java/denominator/clouddns/RackspaceApis.java b/clouddns/src/main/java/denominator/clouddns/RackspaceApis.java index 6a8eb7bc..4e9fd1f9 100644 --- a/clouddns/src/main/java/denominator/clouddns/RackspaceApis.java +++ b/clouddns/src/main/java/denominator/clouddns/RackspaceApis.java @@ -25,7 +25,7 @@ static ListWithNext emptyOn404(Pager pagingFunction, URI nullOrNext) { } @Headers("Content-Type: application/json") - static interface CloudIdentity { + interface CloudIdentity { @RequestLine("POST /tokens") @Body("%7B\"auth\":%7B\"RAX-KSKEY:apiKeyCredentials\":%7B\"username\":\"{username}\",\"apiKey\":\"{apiKey}\"%7D%7D%7D") @@ -38,7 +38,7 @@ TokenIdAndPublicURL passwordAuth(URI endpoint, @Param("username") String usernam @Param("password") String password); } - static interface CloudDNS { + interface CloudDNS { @RequestLine("GET /limits") Map limits(); @@ -59,6 +59,9 @@ static interface CloudDNS { @RequestLine("GET /domains") ListWithNext domains(); + @RequestLine("GET /domains/{domainId}?showRecords=false&showSubdomains=false") + Zone domain(@Param("domainId") String id); + @RequestLine("GET") ListWithNext records(URI href); diff --git a/clouddns/src/test/java/denominator/clouddns/CloudDNSProviderDynamicUpdateMockTest.java b/clouddns/src/test/java/denominator/clouddns/CloudDNSProviderDynamicUpdateMockTest.java index 06cc8c51..2194b715 100644 --- a/clouddns/src/test/java/denominator/clouddns/CloudDNSProviderDynamicUpdateMockTest.java +++ b/clouddns/src/test/java/denominator/clouddns/CloudDNSProviderDynamicUpdateMockTest.java @@ -25,8 +25,7 @@ public class CloudDNSProviderDynamicUpdateMockTest { public void dynamicEndpointUpdates() throws Exception { final AtomicReference url = new AtomicReference(server.url()); server.enqueueAuthResponse(); - server.enqueue(new MockResponse().setResponseCode(404).setBody( - "{\"message\":\"Not Found\",\"code\":404,\"details\":\"\"}")); + server.enqueue(new MockResponse().setBody("{ \"domains\": [] }")); DNSApi api = Denominator.create(new CloudDNSProvider() { @Override @@ -54,8 +53,7 @@ public String url() { @Test public void dynamicCredentialUpdates() throws Exception { server.enqueueAuthResponse(); - server.enqueue(new MockResponse().setResponseCode(404).setBody( - "{\"message\":\"Not Found\",\"code\":404,\"details\":\"\"}")); + server.enqueue(new MockResponse().setBody("{ \"domains\": [] }")); AtomicReference dynamicCredentials = @@ -74,8 +72,7 @@ public void dynamicCredentialUpdates() throws Exception { server.credentials("jclouds-bob", "comeon"); server.enqueueAuthResponse(); - server.enqueue(new MockResponse().setResponseCode(404).setBody( - "{\"message\":\"Not Found\",\"code\":404,\"details\":\"\"}")); + server.enqueue(new MockResponse().setBody("{ \"domains\": [] }")); api.zones().iterator(); diff --git a/clouddns/src/test/java/denominator/clouddns/CloudDNSZoneApiMockTest.java b/clouddns/src/test/java/denominator/clouddns/CloudDNSZoneApiMockTest.java index 1d796f46..f5183191 100644 --- a/clouddns/src/test/java/denominator/clouddns/CloudDNSZoneApiMockTest.java +++ b/clouddns/src/test/java/denominator/clouddns/CloudDNSZoneApiMockTest.java @@ -5,15 +5,12 @@ import org.junit.Rule; import org.junit.Test; -import java.util.Iterator; - import denominator.ZoneApi; import denominator.model.Zone; import static denominator.assertj.ModelAssertions.assertThat; -import static denominator.clouddns.RackspaceApisTest.domainId; +import static denominator.clouddns.RackspaceApisTest.domainResponse; import static denominator.clouddns.RackspaceApisTest.domainsResponse; -import static org.junit.Assert.assertFalse; public class CloudDNSZoneApiMockTest { @@ -24,16 +21,19 @@ public class CloudDNSZoneApiMockTest { public void iteratorWhenPresent() throws Exception { server.enqueueAuthResponse(); server.enqueue(new MockResponse().setBody(domainsResponse)); + server.enqueue(new MockResponse().setBody(domainResponse)); ZoneApi api = server.connect().api().zones(); - Iterator domains = api.iterator(); - assertThat(domains.next()) - .hasName("denominator.io") - .hasId(String.valueOf(domainId)); + assertThat(api.iterator()).containsExactly( + Zone.builder().name("denominator.io").id("1234").email("admin@denominator.io").ttl(3600) + .build() + ); server.assertAuthRequest(); server.assertRequest().hasPath("/v1.0/123123/domains"); + server.assertRequest() + .hasPath("/v1.0/123123/domains/1234?showRecords=false&showSubdomains=false"); } @Test @@ -42,7 +42,7 @@ public void iteratorWhenAbsent() throws Exception { server.enqueue(new MockResponse().setBody("{ \"domains\": [] }")); ZoneApi api = server.connect().api().zones(); - assertFalse(api.iterator().hasNext()); + assertThat(api.iterator()).isEmpty(); server.assertAuthRequest(); server.assertRequest().hasPath("/v1.0/123123/domains"); @@ -51,17 +51,20 @@ public void iteratorWhenAbsent() throws Exception { @Test public void iteratorByNameWhenPresent() throws Exception { server.enqueueAuthResponse(); - server.enqueue(new MockResponse().setBody( - "{\"domains\":[{\"name\":\"denominator.io\",\"id\":1234,\"emailAddress\":\"fake@denominator.io\",\"updated\":\"2015-03-22T18:21:33.000+0000\",\"created\":\"2015-03-22T18:21:33.000+0000\"}],\"totalEntries\":1}")); + server.enqueue(new MockResponse().setBody(domainsResponse)); + server.enqueue(new MockResponse().setBody(domainResponse)); ZoneApi api = server.connect().api().zones(); - assertThat(api.iterateByName("denominator.io").next()) - .hasName("denominator.io") - .hasId(String.valueOf(domainId)); + assertThat(api.iterateByName("denominator.io")).containsExactly( + Zone.builder().name("denominator.io").id("1234").email("admin@denominator.io").ttl(3600) + .build() + ); server.assertAuthRequest(); server.assertRequest().hasPath("/v1.0/123123/domains?name=denominator.io"); + server.assertRequest() + .hasPath("/v1.0/123123/domains/1234?showRecords=false&showSubdomains=false"); } @Test diff --git a/clouddns/src/test/java/denominator/clouddns/RackspaceApisTest.java b/clouddns/src/test/java/denominator/clouddns/RackspaceApisTest.java index 05250b6b..937439a8 100644 --- a/clouddns/src/test/java/denominator/clouddns/RackspaceApisTest.java +++ b/clouddns/src/test/java/denominator/clouddns/RackspaceApisTest.java @@ -79,8 +79,10 @@ public void domainsByNamePresent() throws Exception { server.enqueueAuthResponse(); server.enqueue(new MockResponse().setBody(domainsResponse)); - assertThat(mockApi().domainsByName("denominator.io")) - .containsOnly(Zone.create("denominator.io", "1234")); + assertThat(mockApi().domainsByName("denominator.io")).containsExactly( + Zone.builder().name("denominator.io").id("1234").email("admin@denominator.io").ttl(86400) + .build() + ); server.assertAuthRequest(); server.assertRequest() @@ -140,7 +142,7 @@ public void createMXRecord() throws Exception { server.enqueue(new MockResponse().setBody(mxRecordInitialResponse)); Job job = mockApi().createRecordWithPriority(domainId, "www.denominator.io", "MX", - 1800, "mail.denominator.io", 10); + 1800, "mail.denominator.io", 10); assertThat(job.id).isEqualTo("0ade2b3b-07e4-4e68-821a-fcce4f5406f3"); assertThat(job.status).isEqualTo("RUNNING"); @@ -237,7 +239,7 @@ public Credentials get() { }; return feign.newInstance( new CloudDNSTarget(provider, - new InvalidatableAuthProvider(provider, cloudIdentity, credentials))); + new InvalidatableAuthProvider(provider, cloudIdentity, credentials))); } static String limitsResponse = "{\n" @@ -278,6 +280,9 @@ public Credentials get() { static String domainsResponse = "{\"domains\":[{\"name\":\"denominator.io\",\"id\":1234,\"accountId\":123123,\"emailAddress\":\"admin@denominator.io\",\"updated\":\"2013-09-02T19:46:56.000+0000\",\"created\":\"2013-09-02T19:45:51.000+0000\"}],\"totalEntries\":1}"; + static String + domainResponse = + "{\"name\":\"denominator.io\",\"id\":1234,\"accountId\":123123,\"ttl\": 3600,\"emailAddress\":\"admin@denominator.io\"}"; // NOTE records are allowed to be out of order by type static String recordsResponse = diff --git a/core/src/main/java/denominator/mock/MockAllProfileResourceRecordSetApi.java b/core/src/main/java/denominator/mock/MockAllProfileResourceRecordSetApi.java index f9aacf2c..062d5aa0 100644 --- a/core/src/main/java/denominator/mock/MockAllProfileResourceRecordSetApi.java +++ b/core/src/main/java/denominator/mock/MockAllProfileResourceRecordSetApi.java @@ -40,8 +40,7 @@ public Iterator> iterator() { @Override public Iterator> iterateByName(String name) { - Collection> records = records(); - return filter(records.iterator(), and(nameEqualTo(name), filter)); + return filter(records().iterator(), and(nameEqualTo(name), filter)); } protected void put(Filter> valid, ResourceRecordSet rrset) { @@ -67,8 +66,7 @@ public void put(ResourceRecordSet rrset) { @Override public Iterator> iterateByNameAndType(String name, String type) { - Collection> records = records(); - return filter(records.iterator(), and(nameAndTypeEqualTo(name, type), filter)); + return filter(records().iterator(), and(nameAndTypeEqualTo(name, type), filter)); } @Override diff --git a/core/src/main/java/denominator/mock/MockZoneApi.java b/core/src/main/java/denominator/mock/MockZoneApi.java index a840bd52..f27a4bea 100644 --- a/core/src/main/java/denominator/mock/MockZoneApi.java +++ b/core/src/main/java/denominator/mock/MockZoneApi.java @@ -4,6 +4,7 @@ import java.util.Comparator; import java.util.Iterator; import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.ConcurrentSkipListSet; import denominator.model.ResourceRecordSet; @@ -11,7 +12,9 @@ import denominator.model.rdata.SOAData; import static denominator.common.Preconditions.checkArgument; +import static denominator.common.Preconditions.checkState; import static denominator.common.Util.filter; +import static denominator.model.ResourceRecordSets.nameAndTypeEqualTo; import static denominator.model.ResourceRecordSets.ns; import static denominator.model.Zones.nameEqualTo; @@ -34,15 +37,14 @@ public int compare(ResourceRecordSet arg0, ResourceRecordSet arg1) { public void create(String name) { checkArgument(!data.containsKey(name), "zone %s already exists", name); - Collection> - zone = + Collection> zone = new ConcurrentSkipListSet>(TO_STRING); zone.add(ResourceRecordSet.builder() .type("SOA") .name(name) .ttl(3600) .add(SOAData.builder().mname("ns1." + name).rname("admin." + name) - .serial(1).refresh(3600).retry(600).expire(604800).minimum(60).build()) + .serial(1).refresh(3600).retry(600).expire(604800).minimum(86400).build()) .build()); zone.add(ns(name, 86400, "ns1." + name)); data.put(name, zone); @@ -50,7 +52,8 @@ public void create(String name) { @Override public Iterator iterator() { - final Iterator delegate = data.keySet().iterator(); + final Iterator>>> + delegate = data.entrySet().iterator(); return new Iterator() { @Override public boolean hasNext() { @@ -59,7 +62,19 @@ public boolean hasNext() { @Override public Zone next() { - return Zone.create(delegate.next()); + Entry>> next = delegate.next(); + String name = next.getKey(); + Iterator> soa = + filter(next.getValue().iterator(), nameAndTypeEqualTo(name, "SOA")); + + checkState(soa.hasNext(), "SOA record for zone %s was not present", name); + + SOAData soaData = (SOAData) soa.next().records().get(0); + return Zone.builder() + .name(name) + .id(name) + .ttl(soaData.minimum()) + .email(soaData.rname()).build(); } @Override diff --git a/core/src/test/java/denominator/DynamicCredentialsProviderExampleTest.java b/core/src/test/java/denominator/DynamicCredentialsProviderExampleTest.java index 5ec971dd..9075179b 100644 --- a/core/src/test/java/denominator/DynamicCredentialsProviderExampleTest.java +++ b/core/src/test/java/denominator/DynamicCredentialsProviderExampleTest.java @@ -68,9 +68,9 @@ public void testImplicitDynamicCredentialsUpdate() { DNSApiManager mgr = create(new DynamicCredentialsProvider()); ZoneApi zones = mgr.api().zones(); assertThat(zones.iterator()) - .containsExactly(Zone.create("acme"), Zone.create("wily"), Zone.create("coyote")); + .containsExactly(Zone.builder().name("acme").qualifier("wily").email("coyote").build()); assertThat(zones.iterator()) - .containsExactly(Zone.create("acme"), Zone.create("road"), Zone.create("runner")); + .containsExactly(Zone.builder().name("acme").qualifier("road").email("runner").build()); // now, if the supplier doesn't supply a set of credentials, we should // get a correct message @@ -118,8 +118,9 @@ public Iterator iterator() { CustomerUsernamePassword cup = creds.get(); // normally, the credentials object would be used to invoke a remote // command. in this case, we don't and say we did :) - return Arrays.asList(Zone.create(cup.customer), Zone.create(cup.username), - Zone.create(cup.password)).iterator(); + return Arrays.asList( + Zone.builder().name(cup.customer).qualifier(cup.username).email(cup.password).build()) + .iterator(); } @Override diff --git a/core/src/test/java/denominator/ReadOnlyLiveTest.java b/core/src/test/java/denominator/ReadOnlyLiveTest.java index 2145cfb5..9cde6081 100644 --- a/core/src/test/java/denominator/ReadOnlyLiveTest.java +++ b/core/src/test/java/denominator/ReadOnlyLiveTest.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.UUID; import denominator.model.ResourceRecordSet; import denominator.model.Zone; @@ -27,15 +28,20 @@ public void zoneIdentification() { Zone zone = zones.next(); switch (manager.provider().zoneIdentification()) { case NAME: - assertThat(zone).hasNoQualifier().hasId(zone.name()); + assertThat(zone).hasNoQualifier() + .hasId(zone.name()); break; case OPAQUE: assertThat(zone).hasNoQualifier(); - assertThat(zone.id()).isNotEqualTo(zone.name()); + assertThat(zone.id()) + .isNotNull() + .isNotEqualTo(zone.name()); break; case QUALIFIED: assertThat(zone.qualifier()).isNotNull(); - assertThat(zone.id()).isNotEqualTo(zone.name()); + assertThat(zone.id()) + .isNotNull() + .isNotEqualTo(zone.name()); break; default: throw new AssertionError("unknown zone identification"); @@ -52,7 +58,7 @@ public void iterateZonesByName() { @Test public void iterateZonesByNameWhenNotFound() { - assertThat(manager.api().zones().iterateByName("ARGHH")).isEmpty(); + assertThat(manager.api().zones().iterateByName("yesdenominatornodenominator.com")).isEmpty(); } @Test diff --git a/designate/src/main/java/denominator/designate/DesignateAdapters.java b/designate/src/main/java/denominator/designate/DesignateAdapters.java index e2906821..3a5083e2 100644 --- a/designate/src/main/java/denominator/designate/DesignateAdapters.java +++ b/designate/src/main/java/denominator/designate/DesignateAdapters.java @@ -86,19 +86,22 @@ protected String jsonKey() { } protected Zone build(JsonReader reader) throws IOException { - String name = null; - String id = null; + Zone.Builder result = new Zone.Builder(); while (reader.hasNext()) { - String key = reader.nextName(); - if (key.equals("name")) { - name = reader.nextString(); - } else if (key.equals("id")) { - id = reader.nextString(); + String nextName = reader.nextName(); + if (nextName.equals("name")) { + result.name(reader.nextString()); + } else if (nextName.equals("id")) { + result.id(reader.nextString()); + } else if (nextName.equals("email")) { + result.email(reader.nextString()); + } else if (nextName.equals("ttl")) { + result.ttl(reader.nextInt()); } else { reader.skipValue(); } } - return Zone.create(name, id); + return result.build(); } } diff --git a/designate/src/test/java/denominator/designate/DesignateTest.java b/designate/src/test/java/denominator/designate/DesignateTest.java index 396b4784..dfcb49aa 100644 --- a/designate/src/test/java/denominator/designate/DesignateTest.java +++ b/designate/src/test/java/denominator/designate/DesignateTest.java @@ -66,8 +66,10 @@ public void domainsPresent() throws Exception { server.enqueueAuthResponse(); server.enqueue(new MockResponse().setBody(domainsResponse)); - assertThat(mockApi().domains()) - .containsExactly(Zone.create("denominator.io.", domainId)); + assertThat(mockApi().domains()).containsExactly( + Zone.builder().name("denominator.io.").id(domainId).email("admin@denominator.io").ttl(3600) + .build() + ); server.assertAuthRequest(); server.assertRequest() diff --git a/designate/src/test/java/denominator/designate/DesignateZoneApiMockTest.java b/designate/src/test/java/denominator/designate/DesignateZoneApiMockTest.java index 1950f2b2..31fcf98f 100644 --- a/designate/src/test/java/denominator/designate/DesignateZoneApiMockTest.java +++ b/designate/src/test/java/denominator/designate/DesignateZoneApiMockTest.java @@ -5,8 +5,6 @@ import org.junit.Rule; import org.junit.Test; -import java.util.Iterator; - import denominator.ZoneApi; import denominator.model.Zone; @@ -25,11 +23,11 @@ public void iteratorWhenPresent() throws Exception { server.enqueue(new MockResponse().setBody(domainsResponse)); ZoneApi api = server.connect().api().zones(); - Iterator domains = api.iterator(); - assertThat(domains.next()) - .hasName("denominator.io.") - .hasId(domainId); + assertThat(api.iterator()).containsExactly( + Zone.builder().name("denominator.io.").id(domainId).email("admin@denominator.io").ttl(3600) + .build() + ); server.assertAuthRequest(); server.assertRequest().hasPath("/v1/domains"); @@ -54,8 +52,10 @@ public void iteratorByNameWhenPresent() throws Exception { ZoneApi api = server.connect().api().zones(); - assertThat(api.iterateByName("denominator.io.")) - .contains(Zone.create("denominator.io.", domainId)); + assertThat(api.iterateByName("denominator.io.")).containsExactly( + Zone.builder().name("denominator.io.").id(domainId).email("admin@denominator.io").ttl(3600) + .build() + ); server.assertAuthRequest(); server.assertRequest().hasPath("/v1/domains"); diff --git a/dynect/src/main/java/denominator/dynect/DynECT.java b/dynect/src/main/java/denominator/dynect/DynECT.java index 24ef9834..17964476 100644 --- a/dynect/src/main/java/denominator/dynect/DynECT.java +++ b/dynect/src/main/java/denominator/dynect/DynECT.java @@ -15,14 +15,11 @@ import feign.Param; import feign.RequestLine; -@Headers({"API-Version: 3.5.0", "Content-Type: application/json"}) +@Headers({"API-Version: 3.5.10", "Content-Type: application/json"}) public interface DynECT { @RequestLine("GET /Zone") - Data> zones(); - - @RequestLine("GET /Zone/{name}") - void getZone(@Param("name") String name); + Data> zones();; @RequestLine("PUT /Zone/{zone}") @Body("{\"publish\":true}") @@ -47,11 +44,6 @@ Data>> rrsetsInZoneByNameAndType(@Param("zone") St @Param("fqdn") String fqdn, @Param("type") String type); - @RequestLine("GET /{type}Record/{zone}/{fqdn}") - Data> recordIdsInZoneByNameAndType(@Param("zone") String zone, - @Param("fqdn") String fqdn, - @Param("type") String type); - @RequestLine("GET /{type}Record/{zone}/{fqdn}?detail=Y") Data> recordsInZoneByNameAndType(@Param("zone") String zone, @Param("fqdn") String fqdn, @@ -59,12 +51,17 @@ Data> recordsInZoneByNameAndType(@Param("zone") String zone, @RequestLine("POST /{type}Record/{zone}/{fqdn}") void scheduleCreateRecord(@Param("zone") String zone, @Param("fqdn") String fqdn, - @Param("type") String type, - @Param("ttl") int ttl, @Param("rdata") Map rdata); + @Param("type") String type, @Param("ttl") int ttl, + @Param("rdata") Map rdata); @RequestLine("DELETE /{recordId}") void scheduleDeleteRecord(@Param("recordId") String recordId); + @RequestLine("DELETE /{type}Record/{zone}/{fqdn}") + void scheduleDeleteRecordsInZoneByNameAndType(@Param("zone") String zone, + @Param("fqdn") String fqdn, + @Param("type") String type); + /** * DynECT json includes an envelope called "data", which makes it difficult. */ diff --git a/dynect/src/main/java/denominator/dynect/DynECTAdapters.java b/dynect/src/main/java/denominator/dynect/DynECTAdapters.java index b3e5d70e..6a05bc6e 100644 --- a/dynect/src/main/java/denominator/dynect/DynECTAdapters.java +++ b/dynect/src/main/java/denominator/dynect/DynECTAdapters.java @@ -18,7 +18,6 @@ import denominator.dynect.DynECT.Data; import denominator.dynect.DynECT.Record; -import denominator.model.Zone; import feign.RetryableException; import static denominator.common.Preconditions.checkState; @@ -57,25 +56,12 @@ public String build(JsonReader reader) throws IOException { } } - static class ZonesAdapter extends DataAdapter> { - - @Override - public List build(JsonReader reader) throws IOException { - JsonArray data = new JsonParser().parse(reader).getAsJsonArray(); - List zones = new ArrayList(); - for (String name : toFirstGroup("/REST.*/([^/]+)/?$", data)) { - zones.add(Zone.create(name)); - } - return zones; - } - } - - static class RecordIdsAdapter extends DataAdapter> { + static class ZoneNamesAdapter extends DataAdapter> { @Override public List build(JsonReader reader) throws IOException { JsonArray data = new JsonParser().parse(reader).getAsJsonArray(); - return toFirstGroup("/REST/([a-zA-Z]+Record/[^\"]+/[^\"]+/[0-9]+)", data); + return toFirstGroup("/REST.*/([^/]+)/?$", data); } } @@ -83,8 +69,7 @@ static class RecordsByNameAndTypeAdapter extends DataAdapter> { @Override public Iterator build(JsonReader reader) throws IOException { - JsonArray data; - data = new JsonParser().parse(reader).getAsJsonArray(); + JsonArray data = new JsonParser().parse(reader).getAsJsonArray(); List records = new ArrayList(); for (JsonElement datum : data) { records.add(ToRecord.INSTANCE.apply(datum)); @@ -131,8 +116,4 @@ public void write(JsonWriter out, Data value) throws IOException { throw new UnsupportedOperationException(); } } - - private DynECTAdapters() { - // no instances. - } } diff --git a/dynect/src/main/java/denominator/dynect/DynECTProvider.java b/dynect/src/main/java/denominator/dynect/DynECTProvider.java index 4bf1405c..80196402 100644 --- a/dynect/src/main/java/denominator/dynect/DynECTProvider.java +++ b/dynect/src/main/java/denominator/dynect/DynECTProvider.java @@ -24,10 +24,9 @@ import denominator.config.NothingToClose; import denominator.config.WeightedUnsupported; import denominator.dynect.DynECTAdapters.NothingForbiddenAdapter; -import denominator.dynect.DynECTAdapters.RecordIdsAdapter; import denominator.dynect.DynECTAdapters.RecordsByNameAndTypeAdapter; import denominator.dynect.DynECTAdapters.TokenAdapter; -import denominator.dynect.DynECTAdapters.ZonesAdapter; +import denominator.dynect.DynECTAdapters.ZoneNamesAdapter; import denominator.dynect.InvalidatableTokenProvider.Session; import denominator.profile.GeoResourceRecordSetApi; import feign.Feign; @@ -178,8 +177,7 @@ Feign feign(Logger logger, Logger.Level logLevel, DynECTErrorDecoder errorDecode new TokenAdapter(), new NothingForbiddenAdapter(), new ResourceRecordSetsAdapter(), - new ZonesAdapter(), - new RecordIdsAdapter(), + new ZoneNamesAdapter(), new RecordsByNameAndTypeAdapter())) ) .errorDecoder(errorDecoder) diff --git a/dynect/src/main/java/denominator/dynect/DynECTResourceRecordSetApi.java b/dynect/src/main/java/denominator/dynect/DynECTResourceRecordSetApi.java index 537caf51..5f427b9c 100644 --- a/dynect/src/main/java/denominator/dynect/DynECTResourceRecordSetApi.java +++ b/dynect/src/main/java/denominator/dynect/DynECTResourceRecordSetApi.java @@ -113,23 +113,14 @@ public Iterator iterator() { } @Override - public void deleteByNameAndType(final String name, final String type) { - Iterator existingRecords = emptyIteratorOn404(new Iterable() { - public Iterator iterator() { - return api.recordIdsInZoneByNameAndType(zone, name, type).data.iterator(); - } - }); - - if (!existingRecords.hasNext()) { - return; - } - boolean shouldPublish = false; - while (existingRecords.hasNext()) { - shouldPublish = true; - api.scheduleDeleteRecord(existingRecords.next()); - } - if (shouldPublish) { + public void deleteByNameAndType(String name, String type) { + try { + api.scheduleDeleteRecordsInZoneByNameAndType(zone, name, type); api.publish(zone); + } catch (FeignException e) { + if (e.getMessage().indexOf("NOT_FOUND") == -1) { + throw e; + } } } diff --git a/dynect/src/main/java/denominator/dynect/DynECTZoneApi.java b/dynect/src/main/java/denominator/dynect/DynECTZoneApi.java index eca7cf42..b296e495 100644 --- a/dynect/src/main/java/denominator/dynect/DynECTZoneApi.java +++ b/dynect/src/main/java/denominator/dynect/DynECTZoneApi.java @@ -4,8 +4,11 @@ import javax.inject.Inject; +import denominator.model.ResourceRecordSet; import denominator.model.Zone; +import denominator.model.rdata.SOAData; +import static denominator.common.Preconditions.checkState; import static denominator.common.Util.singletonIterator; public final class DynECTZoneApi implements denominator.ZoneApi { @@ -19,15 +22,25 @@ public final class DynECTZoneApi implements denominator.ZoneApi { @Override public Iterator iterator() { - return api.zones().data.iterator(); + final Iterator delegate = api.zones().data.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Zone next() { + return fromSOA(delegate.next()); + } + }; } @Override public Iterator iterateByName(String name) { Zone zone = null; try { - api.getZone(name); - zone = zone.create(name); + zone = fromSOA(name); } catch (DynECTException e) { if (e.getMessage().indexOf("No such zone") == -1) { throw e; @@ -35,4 +48,16 @@ public Iterator iterateByName(String name) { } return singletonIterator(zone); } + + private Zone fromSOA(String name) { + Iterator> soa = api.rrsetsInZoneByNameAndType(name, name, "SOA").data; + checkState(soa.hasNext(), "SOA record for zone %s was not present", name); + + SOAData soaData = (SOAData) soa.next().records().get(0); + return Zone.builder() + .name(name) + .id(name) + .ttl(soaData.minimum()) + .email(soaData.rname()).build(); + } } diff --git a/dynect/src/main/java/denominator/dynect/SessionTarget.java b/dynect/src/main/java/denominator/dynect/SessionTarget.java index a03cf468..5dee10ab 100644 --- a/dynect/src/main/java/denominator/dynect/SessionTarget.java +++ b/dynect/src/main/java/denominator/dynect/SessionTarget.java @@ -34,7 +34,7 @@ public String url() { @Override public Request apply(RequestTemplate input) { - input.header("API-Version", "3.5.2"); + input.header("API-Version", "3.5.10"); input.header("Content-Type", "application/json"); input.insert(0, url()); return input.request(); diff --git a/dynect/src/test/java/denominator/dynect/DynECTResourceRecordSetApiMockTest.java b/dynect/src/test/java/denominator/dynect/DynECTResourceRecordSetApiMockTest.java index 2049292f..d2478c49 100644 --- a/dynect/src/test/java/denominator/dynect/DynECTResourceRecordSetApiMockTest.java +++ b/dynect/src/test/java/denominator/dynect/DynECTResourceRecordSetApiMockTest.java @@ -251,24 +251,17 @@ public void getByNameAndTypeWhenAbsent() throws Exception { @Test public void deleteRRSet() throws Exception { server.enqueueSessionResponse(); - server.enqueue(new MockResponse().setBody(recordIds1And2)); - server.enqueue(new MockResponse().setBody(success)); - server.enqueue(new MockResponse().setBody(success)); + server.enqueue(new MockResponse().setBody( + "{\"status\": \"success\", \"data\": {}, \"job_id\": 1548682166, \"msgs\": [{\"INFO\": \"delete: 1 records deleted\", \"SOURCE\": \"API-B\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}")); server.enqueue(new MockResponse().setBody(success)); ResourceRecordSetApi api = server.connect().api().basicRecordSetsInZone("denominator.io"); api.deleteByNameAndType("www.denominator.io", "A"); server.assertSessionRequest(); - server.assertRequest() - .hasMethod("GET") - .hasPath("/ARecord/denominator.io/www.denominator.io"); - server.assertRequest() - .hasMethod("DELETE") - .hasPath("/ARecord/denominator.io/www.denominator.io/1"); server.assertRequest() .hasMethod("DELETE") - .hasPath("/ARecord/denominator.io/www.denominator.io/2"); + .hasPath("/ARecord/denominator.io/www.denominator.io"); server.assertRequest() .hasMethod("PUT") .hasPath("/Zone/denominator.io") @@ -278,14 +271,16 @@ public void deleteRRSet() throws Exception { @Test public void deleteAbsentRRSDoesNothing() throws Exception { server.enqueueSessionResponse(); - server.enqueue(new MockResponse().setResponseCode(404).setBody(noneWithNameAndType)); + server.enqueue(new MockResponse().setResponseCode(404).setBody( + "{\"status\": \"failure\", \"data\": {}, \"job_id\": 1548708416, \"msgs\": [{\"INFO\": \"node: Not in zone\", \"SOURCE\": \"BLL\", \"ERR_CD\": \"NOT_FOUND\", \"LVL\": \"ERROR\"}, {\"INFO\": \"get: Host not found in this zone\", \"SOURCE\": \"BLL\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}" + )); ResourceRecordSetApi api = server.connect().api().basicRecordSetsInZone("denominator.io"); api.deleteByNameAndType("www.denominator.io", "A"); server.assertSessionRequest(); server.assertRequest() - .hasMethod("GET") + .hasMethod("DELETE") .hasPath("/ARecord/denominator.io/www.denominator.io"); } diff --git a/dynect/src/test/java/denominator/dynect/DynECTTest.java b/dynect/src/test/java/denominator/dynect/DynECTTest.java index 93275f7b..9b7bc446 100644 --- a/dynect/src/test/java/denominator/dynect/DynECTTest.java +++ b/dynect/src/test/java/denominator/dynect/DynECTTest.java @@ -5,12 +5,10 @@ import org.junit.Rule; import org.junit.Test; -import java.util.Iterator; import java.util.concurrent.atomic.AtomicReference; import denominator.Credentials; import denominator.dynect.InvalidatableTokenProvider.Session; -import denominator.model.Zone; import feign.Feign; import static denominator.assertj.ModelAssertions.assertThat; @@ -52,10 +50,8 @@ public void doesntHaveGeoPermissions() throws Exception { public void zonesWhenPresent() throws Exception { server.enqueue(new MockResponse().setBody(zones)); - Iterator iterator = mockApi().zones().data.iterator(); - iterator.next(); - iterator.next(); - assertThat(iterator.next()).hasName("denominator.io"); + assertThat(mockApi().zones().data) + .containsExactly("denominator.io"); server.assertRequest().hasMethod("GET").hasPath("/Zone"); } @@ -239,7 +235,7 @@ public String url() { + "}"; static String zones = - "{\"status\": \"success\", \"data\": [\"/REST/Zone/0.0.0.0.d.6.e.0.0.a.2.ip6.arpa/\", \"/REST/Zone/126.12.44.in-addr.arpa/\", \"/REST/Zone/denominator.io/\"], \"job_id\": 260657587, \"msgs\": [{\"INFO\": \"get: Your 3 zones\", \"SOURCE\": \"BLL\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}"; + "{\"status\": \"success\", \"data\": [\"/REST/Zone/denominator.io/\"], \"job_id\": 260657587, \"msgs\": [{\"INFO\": \"get: Your 1 zone\", \"SOURCE\": \"BLL\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}"; static String noZones = "{\"status\": \"success\", \"data\": [], \"job_id\": 260657587, \"msgs\": [{\"INFO\": \"get: Your 0 zones\", \"SOURCE\": \"BLL\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}"; diff --git a/dynect/src/test/java/denominator/dynect/DynECTZoneApiMockTest.java b/dynect/src/test/java/denominator/dynect/DynECTZoneApiMockTest.java index 74dc36e9..9514689e 100644 --- a/dynect/src/test/java/denominator/dynect/DynECTZoneApiMockTest.java +++ b/dynect/src/test/java/denominator/dynect/DynECTZoneApiMockTest.java @@ -5,8 +5,6 @@ import org.junit.Rule; import org.junit.Test; -import java.util.Iterator; - import denominator.ZoneApi; import denominator.model.Zone; @@ -23,20 +21,20 @@ public class DynECTZoneApiMockTest { public void iteratorWhenPresent() throws Exception { server.enqueueSessionResponse(); server.enqueue(new MockResponse().setBody(zones)); + server.enqueue(new MockResponse().setBody( + "{\"status\": \"success\", \"data\": [{\"zone\": \"denominator.io\", \"ttl\": 3600, \"fqdn\": \"denominator.io\", \"record_type\": \"SOA\", \"rdata\": {\"rname\": \"fake@denominator.io.\", \"retry\": 600, \"mname\": \"ns1.p21.dynect.net.\", \"minimum\": 1800, \"refresh\": 3600, \"expire\": 604800, \"serial\": 478}, \"record_id\": 154671809, \"serial_style\": \"increment\"}], \"job_id\": 1548708326, \"msgs\": [{\"INFO\": \"detail: Found 1 record\", \"SOURCE\": \"BLL\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}" + )); ZoneApi api = server.connect().api().zones(); - Iterator domains = api.iterator(); - assertThat(domains.next()) - .hasName("0.0.0.0.d.6.e.0.0.a.2.ip6.arpa"); - assertThat(domains.next()) - .hasName("126.12.44.in-addr.arpa"); - assertThat(domains.next()) - .hasName("denominator.io"); - assertThat(domains).isEmpty(); + assertThat(api.iterator()).containsExactly( + Zone.builder().name("denominator.io").id("denominator.io.").email("fake@denominator.io.") + .ttl(1800).build() + ); server.assertSessionRequest(); server.assertRequest().hasPath("/Zone"); + server.assertRequest().hasPath("/SOARecord/denominator.io/denominator.io?detail=Y"); } @Test @@ -55,15 +53,18 @@ public void iteratorWhenAbsent() throws Exception { public void iteratorByNameWhenPresent() throws Exception { server.enqueueSessionResponse(); server.enqueue(new MockResponse().setBody( - "{\"status\": \"success\", \"data\": {\"zone_type\": \"Primary\", \"serial_style\": \"increment\", \"serial\": 1, \"zone\": \"denominator.io\"}, \"job_id\": 1536811990, \"msgs\": [{\"INFO\": \"get: Your zone, denominator.io\", \"SOURCE\": \"BLL\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}")); + "{\"status\": \"success\", \"data\": [{\"zone\": \"denominator.io\", \"ttl\": 3600, \"fqdn\": \"denominator.io\", \"record_type\": \"SOA\", \"rdata\": {\"rname\": \"fake@denominator.io.\", \"retry\": 600, \"mname\": \"ns1.p21.dynect.net.\", \"minimum\": 1800, \"refresh\": 3600, \"expire\": 604800, \"serial\": 478}, \"record_id\": 154671809, \"serial_style\": \"increment\"}], \"job_id\": 1548708326, \"msgs\": [{\"INFO\": \"detail: Found 1 record\", \"SOURCE\": \"BLL\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}" + )); ZoneApi api = server.connect().api().zones(); - assertThat(api.iterateByName("denominator.io.")) - .contains(Zone.create("denominator.io.")); + assertThat(api.iterateByName("denominator.io.")).containsExactly( + Zone.builder().name("denominator.io.").id("denominator.io.").email("fake@denominator.io.") + .ttl(1800).build() + ); server.assertSessionRequest(); - server.assertRequest().hasPath("/Zone/denominator.io."); + server.assertRequest().hasPath("/SOARecord/denominator.io./denominator.io.?detail=Y"); } @Test @@ -77,6 +78,6 @@ public void iteratorByNameWhenAbsent() throws Exception { assertThat(api.iterateByName("denominator.io.")).isEmpty(); server.assertSessionRequest(); - server.assertRequest().hasPath("/Zone/denominator.io."); + server.assertRequest().hasPath("/SOARecord/denominator.io./denominator.io.?detail=Y"); } } diff --git a/model/src/main/java/denominator/ResourceTypeToValue.java b/model/src/main/java/denominator/ResourceTypeToValue.java index 9c2605a3..67e294a5 100644 --- a/model/src/main/java/denominator/ResourceTypeToValue.java +++ b/model/src/main/java/denominator/ResourceTypeToValue.java @@ -56,7 +56,7 @@ public static String lookup(Integer type) throws IllegalArgumentException { * >iana types. */ // enum only to look and format prettier than fluent bimap builder calls - static enum ResourceTypes { + enum ResourceTypes { /** * a host address */ @@ -139,7 +139,7 @@ static enum ResourceTypes { private final int value; - private ResourceTypes(int value) { + ResourceTypes(int value) { this.value = value; } } diff --git a/model/src/main/java/denominator/model/Zone.java b/model/src/main/java/denominator/model/Zone.java index 79ebb56b..bfc58180 100644 --- a/model/src/main/java/denominator/model/Zone.java +++ b/model/src/main/java/denominator/model/Zone.java @@ -1,5 +1,8 @@ package denominator.model; +import denominator.model.rdata.SOAData; + +import static denominator.common.Preconditions.checkArgument; import static denominator.common.Preconditions.checkNotNull; import static denominator.common.Util.equal; @@ -50,49 +53,23 @@ public enum Identification { private final String name; private final String qualifier; private final String id; + private final String email; + private final int ttl; - Zone(String name, String qualifier, String id) { + Zone(String name, String qualifier, String id, String email, int ttl) { this.name = checkNotNull(name, "name"); this.qualifier = qualifier; - this.id = checkNotNull(id, "id"); - } - - /** - * Represent a zone without a {@link #qualifier() qualifier} when its {@link #id() id} is its - * name. - * - * @param name corresponds to {@link #name()} and {@link #id()} - */ - public static Zone create(String name) { - return new Zone(name, null, name); - } - - /** - * Represent a zone without a {@link #qualifier() qualifier}. - * - * @param name corresponds to {@link #name()} - * @param id corresponds to {@link #id()} - */ - public static Zone create(String name, String id) { - return new Zone(name, null, id); - } - - /** - * Represent a zone with a {@link #qualifier() qualifier}. - * - * @param name corresponds to {@link #name()} - * @param qualifier corresponds to {@link #qualifier()} - * @param id corresponds to {@link #id()} - * - * @since 4.5 - */ - public static Zone create(String name, String qualifier, String id) { - return new Zone(name, qualifier, id); + this.id = id; + this.email = checkNotNull(email, "email of %s", name); + checkArgument(ttl >= 0, "Invalid ttl value: %s, must be 0-%s", ttl, Integer.MAX_VALUE); + this.ttl = ttl; } /** * The origin or starting point for the zone in the DNS tree. Usually includes a trailing dot, ex. * "{@code netflix.com.}" + * + * @see denominator.model.rdata.SOAData#mname() */ public String name() { return name; @@ -110,18 +87,41 @@ public String qualifier() { } /** - * The potentially transient and opaque string that uniquely identifies the zone. + * The potentially transient and opaque string that uniquely identifies the zone. This may be null + * when used as an input object. * *

Note that this is not used in {@link #hashCode()} or {@link #equals(Object)}, as it may * change over time. * * @return identifier based on {@code denominator.Provider#zoneIdentification()}. * @see denominator.model.Zone.Identification + * @since 4.5 */ public String id() { return id; } + /** + * Email contact for the zone. The {@literal @} in the email will be converted to a {@literal .} + * in the {@link denominator.model.rdata.SOAData#rname() SOA rname field}. + * + * @see denominator.model.rdata.SOAData#rname() + */ + public String email() { + return email; + } + + /** + * Default cache expiration of all resource records that don't declare an {@link + * denominator.model.Zone#ttl() override}. + * + * @see denominator.model.rdata.SOAData#minimum() + * @since 4.5 + */ + public int ttl() { + return ttl; + } + /** * @deprecated only use {@link #id()} when performing operations against a zone. This will be * removed in version 5. @@ -135,8 +135,10 @@ public String idOrName() { public boolean equals(Object obj) { if (obj instanceof Zone) { Zone other = (Zone) obj; - return equal(name(), other.name()) - && equal(qualifier(), other.qualifier()); + return name().equals(other.name()) + && equal(qualifier(), other.qualifier()) + && email().equals(other.email()) + && ttl() == other.ttl(); } return false; } @@ -146,6 +148,8 @@ public int hashCode() { int result = 17; result = 31 * result + name().hashCode(); result = 31 * result + (qualifier() != null ? qualifier().hashCode() : 0); + result = 31 * result + email().hashCode(); + result = 31 * result + ttl(); return result; } @@ -160,7 +164,97 @@ public String toString() { if (!name().equals(id())) { builder.append(", ").append("id=").append(id()); } + builder.append(", ").append("email=").append(email()); + builder.append(", ").append("ttl=").append(ttl()); builder.append("]"); return builder.toString(); } + + /** + * Represent a zone without a {@link #qualifier() qualifier} when its {@link #id() id} is its + * name. + * + * @param name corresponds to {@link #name()} and {@link #id()} + * @deprecated Use {@link #builder()}. This will be removed in version 5. + */ + @Deprecated + public static Zone create(String name) { + return create(name, name); + } + + /** + * Represent a zone without a {@link #qualifier() qualifier}. + * + * @param name corresponds to {@link #name()} + * @param id corresponds to {@link #id()} + * @deprecated Use {@link #builder()}. This will be removed in version 5. + */ + @Deprecated + public static Zone create(String name, String id) { + return new Zone(name, null, id, "fake@" + name, 86400); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * @since 4.5 + */ + public static final class Builder { + + private String name; + private String qualifier; + private String id; + private int ttl = 86400; + private String email; + + /** + * @see Zone#name() + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * @see Zone#qualifier() + */ + public Builder qualifier(String qualifier) { + this.qualifier = qualifier; + return this; + } + + /** + * Can be null for input objects. + * + * @see Zone#id() + */ + public Builder id(String id) { + this.id = id; + return this; + } + + /** + * @see Zone#email() + */ + public Builder email(String email) { + this.email = email; + return this; + } + + /** + * Overrides default of {@code 86400}. + * + * @see Zone#ttl() + */ + public Builder ttl(Integer ttl) { + this.ttl = ttl; + return this; + } + + public Zone build() { + return new Zone(name, qualifier, id, email, ttl); + } + } } diff --git a/model/src/test/java/denominator/assertj/ZoneAssert.java b/model/src/test/java/denominator/assertj/ZoneAssert.java index 34e9ecde..34e1498a 100644 --- a/model/src/test/java/denominator/assertj/ZoneAssert.java +++ b/model/src/test/java/denominator/assertj/ZoneAssert.java @@ -19,15 +19,27 @@ public ZoneAssert hasName(String expected) { return this; } + public ZoneAssert hasQualifier(String expected) { + isNotNull(); + objects.assertEqual(info, actual.qualifier(), expected); + return this; + } + public ZoneAssert hasId(String expected) { isNotNull(); objects.assertEqual(info, actual.id(), expected); return this; } - public ZoneAssert hasQualifier(String expected) { + public ZoneAssert hasEmail(String expected) { isNotNull(); - objects.assertEqual(info, actual.qualifier(), expected); + objects.assertEqual(info, actual.email(), expected); + return this; + } + + public ZoneAssert hasTtl(Integer expected) { + isNotNull(); + objects.assertEqual(info, actual.ttl(), expected); return this; } diff --git a/model/src/test/java/denominator/model/ZoneTest.java b/model/src/test/java/denominator/model/ZoneTest.java index 6914d4dd..7d17eb3b 100644 --- a/model/src/test/java/denominator/model/ZoneTest.java +++ b/model/src/test/java/denominator/model/ZoneTest.java @@ -15,7 +15,11 @@ public class ZoneTest { public void factoryMethodsWork() { Zone name = Zone.create("denominator.io."); Zone id = Zone.create("denominator.io.", "ABCD"); - Zone qualifier = Zone.create("denominator.io.", "Test-Zone", "ABCD"); + Zone qualifier = Zone.builder() + .name("denominator.io.") + .qualifier("Test-Zone") + .id("ABCD") + .email("fake@denominator.io.").build(); assertThat(name) .hasName("denominator.io.") @@ -28,7 +32,7 @@ public void factoryMethodsWork() { assertThat(name.hashCode()) .isEqualTo(id.hashCode()) .isNotEqualTo(qualifier.hashCode()); - assertThat(name.toString()).isEqualTo("Zone [name=denominator.io.]"); + assertThat(name.toString()).isEqualTo("Zone [name=denominator.io., email=fake@denominator.io., ttl=86400]"); assertThat(id) .hasName("denominator.io.") @@ -42,7 +46,7 @@ public void factoryMethodsWork() { .isEqualTo(name.hashCode()) .isNotEqualTo(qualifier.hashCode()); - assertThat(id.toString()).isEqualTo("Zone [name=denominator.io., id=ABCD]"); + assertThat(id.toString()).isEqualTo("Zone [name=denominator.io., id=ABCD, email=fake@denominator.io., ttl=86400]"); assertThat(qualifier) .hasName("denominator.io.") @@ -57,7 +61,7 @@ public void factoryMethodsWork() { .isNotEqualTo(id.hashCode()); assertThat(qualifier.toString()) - .isEqualTo("Zone [name=denominator.io., qualifier=Test-Zone, id=ABCD]"); + .isEqualTo("Zone [name=denominator.io., qualifier=Test-Zone, id=ABCD, email=fake@denominator.io., ttl=86400]"); } @Test @@ -65,14 +69,6 @@ public void nullNameNPEMessage() { thrown.expect(NullPointerException.class); thrown.expectMessage("name"); - new Zone(null, null, "id"); - } - - @Test - public void nullIdNPEMessage() { - thrown.expect(NullPointerException.class); - thrown.expectMessage("id"); - - new Zone("name", null, null); + Zone.builder().build(); } } diff --git a/route53/src/main/java/denominator/route53/ListHostedZonesResponseHandler.java b/route53/src/main/java/denominator/route53/ListHostedZonesResponseHandler.java index 1a30d1c7..1fe7bf17 100644 --- a/route53/src/main/java/denominator/route53/ListHostedZonesResponseHandler.java +++ b/route53/src/main/java/denominator/route53/ListHostedZonesResponseHandler.java @@ -2,8 +2,8 @@ import org.xml.sax.helpers.DefaultHandler; -import denominator.model.Zone; -import denominator.route53.Route53.ZoneList; +import denominator.route53.Route53.HostedZone; +import denominator.route53.Route53.HostedZoneList; import feign.sax.SAXDecoder.ContentHandlerWithResult; /** @@ -11,30 +11,28 @@ * >docs */ class ListHostedZonesResponseHandler extends DefaultHandler - implements ContentHandlerWithResult { + implements ContentHandlerWithResult { private final StringBuilder currentText = new StringBuilder(); - private final ZoneList zones = new ZoneList(); - private String name; - private String qualifier; - private String id; + private final HostedZoneList zones = new HostedZoneList(); + private HostedZone zone = new HostedZone(); @Override - public ZoneList result() { + public HostedZoneList result() { return zones; } @Override public void endElement(String uri, String name, String qName) { if (qName.equals("Name")) { - this.name = currentText.toString().trim(); + zone.name = currentText.toString().trim(); } else if (qName.equals("Id")) { - id = currentText.toString().trim().replace("/hostedzone/", ""); + zone.id = currentText.toString().trim().replace("/hostedzone/", ""); } else if (qName.equals("CallerReference")) { - qualifier = currentText.toString().trim(); + zone.callerReference = currentText.toString().trim(); } else if (qName.equals("HostedZone")) { - zones.add(Zone.create(this.name, qualifier, id)); - this.name = this.id = null; + zones.add(zone); + zone = new HostedZone(); } else if (qName.equals("NextMarker")) { zones.next = currentText.toString().trim(); } diff --git a/route53/src/main/java/denominator/route53/Route53.java b/route53/src/main/java/denominator/route53/Route53.java index 05adbdd9..5b2905dd 100644 --- a/route53/src/main/java/denominator/route53/Route53.java +++ b/route53/src/main/java/denominator/route53/Route53.java @@ -4,7 +4,6 @@ import java.util.List; import denominator.model.ResourceRecordSet; -import denominator.model.Zone; import feign.Headers; import feign.Param; import feign.RequestLine; @@ -12,13 +11,13 @@ interface Route53 { @RequestLine("GET /2012-12-12/hostedzone") - ZoneList listHostedZones(); + HostedZoneList listHostedZones(); @RequestLine("GET /2012-12-12/hostedzone?marker={marker}") - ZoneList listHostedZones(@Param("marker") String marker); + HostedZoneList listHostedZones(@Param("marker") String marker); @RequestLine("GET /2013-04-01/hostedzonesbyname?dnsname={dnsname}") - ZoneList listHostedZonesByName(@Param("dnsname") String dnsname); + HostedZoneList listHostedZonesByName(@Param("dnsname") String dnsname); @RequestLine("GET /2012-12-12/hostedzone/{zoneId}/rrset") ResourceRecordSetList listResourceRecordSets(@Param("zoneId") String zoneId); @@ -44,15 +43,19 @@ void changeResourceRecordSets(@Param("zoneId") String zoneId, List changes) throws InvalidChangeBatchException; - static class ZoneList extends ArrayList { + class HostedZone { - private static final long serialVersionUID = 1L; - String next; + String id; + String name; + String callerReference; } - static class ResourceRecordSetList extends ArrayList> { + class HostedZoneList extends ArrayList { - private static final long serialVersionUID = 1L; + public String next; + } + + class ResourceRecordSetList extends ArrayList> { NextRecord next; static class NextRecord { @@ -67,7 +70,7 @@ static class NextRecord { } } - static class ActionOnResourceRecordSet { + class ActionOnResourceRecordSet { final String action; final ResourceRecordSet rrs; diff --git a/route53/src/main/java/denominator/route53/Route53ZoneApi.java b/route53/src/main/java/denominator/route53/Route53ZoneApi.java index 8858eb71..58c20457 100644 --- a/route53/src/main/java/denominator/route53/Route53ZoneApi.java +++ b/route53/src/main/java/denominator/route53/Route53ZoneApi.java @@ -2,11 +2,14 @@ import java.util.Iterator; +import denominator.common.PeekingIterator; import denominator.model.Zone; -import denominator.route53.Route53.ZoneList; +import denominator.model.rdata.SOAData; +import denominator.route53.Route53.HostedZone; +import denominator.route53.Route53.HostedZoneList; +import denominator.route53.Route53.ResourceRecordSetList; -import static denominator.common.Util.filter; -import static denominator.model.Zones.nameEqualTo; +import static denominator.common.Preconditions.checkState; public final class Route53ZoneApi implements denominator.ZoneApi { @@ -18,41 +21,74 @@ public final class Route53ZoneApi implements denominator.ZoneApi { @Override public Iterator iterator() { - final ZoneList first = api.listHostedZones(); - if (first.next == null) { - return first.iterator(); - } - return new Iterator() { - Iterator current = first.iterator(); - String next = first.next; + return new ZipWithSOA(api.listHostedZones()); + } + /** + * This implementation assumes that there isn't more than one page of zones with the same name. + */ + @Override + public Iterator iterateByName(final String name) { + final Iterator delegate = api.listHostedZonesByName(name).iterator(); + return new PeekingIterator() { @Override - public boolean hasNext() { - while (!current.hasNext() && next != null) { - ZoneList nextPage = api.listHostedZones(next); - current = nextPage.iterator(); - next = nextPage.next; + protected Zone computeNext() { + if (delegate.hasNext()) { + HostedZone next = delegate.next(); + if (next.name.equals(name)) { + return zipWithSOA(next); + } } - return current.hasNext(); + return endOfData(); } + }; + } - @Override - public Zone next() { - return current.next(); - } + private Zone zipWithSOA(HostedZone next) { + ResourceRecordSetList soa = api.listResourceRecordSets(next.id, next.name, "SOA"); + checkState(!soa.isEmpty(), "SOA record for zone %s %s was not present", next.id, next.name); - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; + SOAData soaData = (SOAData) soa.get(0).records().get(0); + return Zone.builder() + .name(next.name) + .qualifier(next.callerReference) + .id(next.id) + .ttl(soaData.minimum()) + .email(soaData.rname()).build(); } /** - * This implementation assumes that there isn't more than one page of zones with the same name. + * For each hosted zone, lazy fetch the corresponding SOA record and zip into a Zone object. */ - @Override - public Iterator iterateByName(String name) { - return filter(api.listHostedZonesByName(name).iterator(), nameEqualTo(name)); + class ZipWithSOA implements Iterator { + + HostedZoneList list; + int i = 0; + int length; + + ZipWithSOA(HostedZoneList list) { + this.list = list; + this.length = list.size(); + } + + @Override + public boolean hasNext() { + while (i == length && list.next != null) { + list = api.listHostedZones(list.next); + length = list.size(); + i = 0; + } + return i < length; + } + + @Override + public Zone next() { + return zipWithSOA(list.get(i++)); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } } } diff --git a/route53/src/test/java/denominator/route53/Route53DecoderTest.java b/route53/src/test/java/denominator/route53/Route53DecoderTest.java index e7e91073..cb49a43b 100644 --- a/route53/src/test/java/denominator/route53/Route53DecoderTest.java +++ b/route53/src/test/java/denominator/route53/Route53DecoderTest.java @@ -9,13 +9,14 @@ import denominator.model.rdata.AData; import denominator.model.rdata.NSData; import denominator.model.rdata.SOAData; +import denominator.route53.Route53.HostedZoneList; import denominator.route53.Route53.ResourceRecordSetList; -import denominator.route53.Route53.ZoneList; import feign.Response; import feign.codec.Decoder; import static denominator.assertj.ModelAssertions.assertThat; import static feign.Util.UTF_8; +import static org.assertj.core.api.Assertions.tuple; public class Route53DecoderTest { @@ -23,7 +24,7 @@ public class Route53DecoderTest { @Test public void decodeZoneListWithNext() throws Exception { - ZoneList result = (ZoneList) decoder.decode(response( + HostedZoneList result = (HostedZoneList) decoder.decode(response( "\n" + " \n" + " \n" @@ -49,17 +50,12 @@ public void decodeZoneListWithNext() throws Exception { + " true\n" + " Z333333YYYYYYY\n" + " 10\n" - + ""), ZoneList.class); + + ""), HostedZoneList.class); - assertThat(result.get(0)) - .hasName("example.com.") - .hasQualifier("a_unique_reference") - .hasId("Z21DW1QVGID6NG"); - - assertThat(result.get(1)) - .hasName("example2.com.") - .hasQualifier("a_unique_reference2") - .hasId("Z2682N5HXP0BZ4"); + assertThat(result).extracting("name", "callerReference", "id").containsExactly( + tuple("example.com.", "a_unique_reference", "Z21DW1QVGID6NG"), + tuple("example2.com.", "a_unique_reference2", "Z2682N5HXP0BZ4") + ); assertThat(result.next).isEqualTo("Z333333YYYYYYY"); } diff --git a/route53/src/test/java/denominator/route53/Route53ZoneApiMockTest.java b/route53/src/test/java/denominator/route53/Route53ZoneApiMockTest.java index 3c0d015f..d3ad9993 100644 --- a/route53/src/test/java/denominator/route53/Route53ZoneApiMockTest.java +++ b/route53/src/test/java/denominator/route53/Route53ZoneApiMockTest.java @@ -33,6 +33,25 @@ public void iteratorWhenPresent() throws Exception { + " \n" + " \n" + "")); + server.enqueue(new MockResponse().setBody( + "\n" + + "\n" + + " \n" + + " \n" + + " denominator.io.\n" + + " SOA\n" + + " 900\n" + + " \n" + + " \n" + + " ns-273.awsdns-34.com. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " false\n" + + " 100\n" + + "" + )); ZoneApi api = server.connect().api().zones(); Iterator domains = api.iterator(); @@ -40,11 +59,16 @@ public void iteratorWhenPresent() throws Exception { assertThat(domains.next()) .hasName("denominator.io.") .hasQualifier("denomination") - .hasId("Z1PA6795UKMFR9"); + .hasId("Z1PA6795UKMFR9") + .hasEmail("awsdns-hostmaster.amazon.com.") + .hasTtl(86400); server.assertRequest() .hasMethod("GET") .hasPath("/2012-12-12/hostedzone"); + server.assertRequest() + .hasMethod("GET") + .hasPath("/2012-12-12/hostedzone/Z1PA6795UKMFR9/rrset?name=denominator.io.&type=SOA"); } @Test @@ -88,16 +112,62 @@ public void iterateByNameWhenPresent() throws Exception { + " false\n" + " 100\n" + "")); + server.enqueue(new MockResponse().setBody( + "\n" + + "\n" + + " \n" + + " \n" + + " denominator.io.\n" + + " SOA\n" + + " 900\n" + + " \n" + + " \n" + + " ns-273.awsdns-34.com. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " false\n" + + " 100\n" + + "" + )); + server.enqueue(new MockResponse().setBody( + "\n" + + "\n" + + " \n" + + " \n" + + " denominator.io.\n" + + " SOA\n" + + " 900\n" + + " \n" + + " \n" + + " ns-273.awsdns-35.com. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " false\n" + + " 100\n" + + "" + )); ZoneApi api = server.connect().api().zones(); assertThat(api.iterateByName("denominator.io.")).containsExactly( - Zone.create("denominator.io.", "Foo", "Z2ZEEJCUZCVG56"), - Zone.create("denominator.io.", "Bar", "Z3OQLQGABCU3T") + Zone.builder().name("denominator.io.").qualifier("Foo").id("Z2ZEEJCUZCVG56") + .email("awsdns-hostmaster.amazon.com.").ttl(86400).build(), + Zone.builder().name("denominator.io.").qualifier("Bar").id("Z3OQLQGABCU3T") + .email("awsdns-hostmaster.amazon.com.").ttl(86400).build() ); server.assertRequest() .hasMethod("GET") .hasPath("/2013-04-01/hostedzonesbyname?dnsname=denominator.io."); + server.assertRequest() + .hasMethod("GET") + .hasPath("/2012-12-12/hostedzone/Z2ZEEJCUZCVG56/rrset?name=denominator.io.&type=SOA"); + server.assertRequest() + .hasMethod("GET") + .hasPath("/2012-12-12/hostedzone/Z3OQLQGABCU3T/rrset?name=denominator.io.&type=SOA"); } /** diff --git a/ultradns/src/main/java/denominator/ultradns/UltraDNS.java b/ultradns/src/main/java/denominator/ultradns/UltraDNS.java index 7b3f305a..86f9e29c 100644 --- a/ultradns/src/main/java/denominator/ultradns/UltraDNS.java +++ b/ultradns/src/main/java/denominator/ultradns/UltraDNS.java @@ -6,7 +6,6 @@ import java.util.Map; import java.util.TreeMap; -import denominator.model.Zone; import feign.Body; import feign.Headers; import feign.Param; @@ -25,12 +24,7 @@ interface UltraDNS { @RequestLine("POST") @Body("{accountId}all") - List getZonesOfAccount(@Param("accountId") String accountId); - - /** Returns the account id of the zone. */ - @RequestLine("POST") - @Body("{zoneName}") - String getZoneInfo(@Param("zoneName") String idOrName); + List getZonesOfAccount(@Param("accountId") String accountId); @RequestLine("POST") @Body("{zoneName}0") @@ -142,11 +136,11 @@ String addDirectionalPool(@Param("zoneName") String zoneName, @Param("hostName") @Body("{dirPoolID}") void deleteDirectionalPool(@Param("dirPoolID") String dirPoolID); - static enum NetworkStatus { + enum NetworkStatus { GOOD, FAILED; } - static class Record { + class Record { String id; Long created; @@ -156,7 +150,7 @@ static class Record { List rdata = new ArrayList(); } - static class NameAndType { + class NameAndType { String name; String type; @@ -184,13 +178,13 @@ public String toString() { } } - static class DirectionalGroup { + class DirectionalGroup { String name; Map> regionToTerritories = new TreeMap>(); } - static class DirectionalRecord extends Record { + class DirectionalRecord extends Record { String geoGroupId; String geoGroupName; diff --git a/ultradns/src/main/java/denominator/ultradns/UltraDNSContentHandlers.java b/ultradns/src/main/java/denominator/ultradns/UltraDNSContentHandlers.java index 4ac42f5f..f67cac07 100644 --- a/ultradns/src/main/java/denominator/ultradns/UltraDNSContentHandlers.java +++ b/ultradns/src/main/java/denominator/ultradns/UltraDNSContentHandlers.java @@ -15,7 +15,6 @@ import java.util.TimeZone; import java.util.TreeMap; -import denominator.model.Zone; import denominator.ultradns.UltraDNS.DirectionalGroup; import denominator.ultradns.UltraDNS.DirectionalRecord; import denominator.ultradns.UltraDNS.NameAndType; @@ -143,20 +142,20 @@ public void characters(char ch[], int start, int length) { } } - static class ZoneListHandler extends DefaultHandler - implements ContentHandlerWithResult> { + static class ZoneNamesHandler extends DefaultHandler + implements ContentHandlerWithResult> { - private final List zones = new ArrayList(); + private final List zones = new ArrayList(); @Override - public List result() { + public List result() { return zones; } @Override public void startElement(String uri, String localName, String qName, Attributes attrs) { if (attrs.getValue("zoneName") != null) { - zones.add(Zone.create(attrs.getValue("zoneName"))); + zones.add(attrs.getValue("zoneName")); } } } diff --git a/ultradns/src/main/java/denominator/ultradns/UltraDNSException.java b/ultradns/src/main/java/denominator/ultradns/UltraDNSException.java index 6648eabc..dc615dea 100644 --- a/ultradns/src/main/java/denominator/ultradns/UltraDNSException.java +++ b/ultradns/src/main/java/denominator/ultradns/UltraDNSException.java @@ -26,6 +26,10 @@ class UltraDNSException extends FeignException { * No Pool or Multiple pools of same type exists for the PoolName */ static final int DIRECTIONALPOOL_NOT_FOUND = 2142; + /** + * Invalid zone name + */ + static final int INVALID_ZONE_NAME = 2507; /** * Directional Pool Record does not exist in the system */ diff --git a/ultradns/src/main/java/denominator/ultradns/UltraDNSProvider.java b/ultradns/src/main/java/denominator/ultradns/UltraDNSProvider.java index fe943bee..50cd4c6a 100644 --- a/ultradns/src/main/java/denominator/ultradns/UltraDNSProvider.java +++ b/ultradns/src/main/java/denominator/ultradns/UltraDNSProvider.java @@ -31,7 +31,7 @@ import denominator.ultradns.UltraDNSContentHandlers.RRPoolListHandler; import denominator.ultradns.UltraDNSContentHandlers.RecordListHandler; import denominator.ultradns.UltraDNSContentHandlers.RegionTableHandler; -import denominator.ultradns.UltraDNSContentHandlers.ZoneListHandler; +import denominator.ultradns.UltraDNSContentHandlers.ZoneNamesHandler; import denominator.ultradns.UltraDNSErrorDecoder.UltraDNSError; import feign.Feign; import feign.Logger; @@ -191,7 +191,7 @@ static Decoder decoder() { return SAXDecoder.builder() .registerContentHandler(NetworkStatusHandler.class) .registerContentHandler(IDHandler.class) - .registerContentHandler(ZoneListHandler.class) + .registerContentHandler(ZoneNamesHandler.class) .registerContentHandler(RecordListHandler.class) .registerContentHandler(DirectionalRecordListHandler.class) .registerContentHandler(DirectionalPoolListHandler.class) diff --git a/ultradns/src/main/java/denominator/ultradns/UltraDNSZoneApi.java b/ultradns/src/main/java/denominator/ultradns/UltraDNSZoneApi.java index 2609978a..8879ae9e 100644 --- a/ultradns/src/main/java/denominator/ultradns/UltraDNSZoneApi.java +++ b/ultradns/src/main/java/denominator/ultradns/UltraDNSZoneApi.java @@ -1,13 +1,16 @@ package denominator.ultradns; import java.util.Iterator; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import denominator.model.Zone; +import denominator.ultradns.UltraDNS.Record; +import static denominator.common.Preconditions.checkState; import static denominator.common.Util.singletonIterator; public final class UltraDNSZoneApi implements denominator.ZoneApi { @@ -26,21 +29,42 @@ public final class UltraDNSZoneApi implements denominator.ZoneApi { */ @Override public Iterator iterator() { - return api.getZonesOfAccount(account.get()).iterator(); + final Iterator delegate = api.getZonesOfAccount(account.get()).iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Zone next() { + return fromSOA(delegate.next()); + } + }; } @Override public Iterator iterateByName(String name) { Zone zone = null; try { - if (api.getZoneInfo(name).equals(account.get())) { - zone = Zone.create(name); - } + zone = fromSOA(name); } catch (UltraDNSException e) { - if (e.code() != UltraDNSException.ZONE_NOT_FOUND) { + if (e.code() != UltraDNSException.INVALID_ZONE_NAME) { throw e; } } return singletonIterator(zone); } + + private Zone fromSOA(String name) { + List soa = api.getResourceRecordsOfDNameByType(name, name, 6); + checkState(!soa.isEmpty(), "SOA record for zone %s was not present", name); + List rdata = soa.get(0).rdata; + return Zone.builder() + .name(name) + .id(name) + .email(rdata.get(1)) + .ttl(Integer.valueOf(rdata.get(6))) + .build(); + } } diff --git a/ultradns/src/test/java/denominator/ultradns/UltraDNSTest.java b/ultradns/src/test/java/denominator/ultradns/UltraDNSTest.java index 452ffe4b..13909c02 100644 --- a/ultradns/src/test/java/denominator/ultradns/UltraDNSTest.java +++ b/ultradns/src/test/java/denominator/ultradns/UltraDNSTest.java @@ -11,7 +11,6 @@ import java.util.Map; import denominator.Credentials; -import denominator.model.Zone; import denominator.ultradns.UltraDNS.DirectionalGroup; import denominator.ultradns.UltraDNS.DirectionalRecord; import denominator.ultradns.UltraDNS.NameAndType; @@ -24,6 +23,7 @@ import static denominator.ultradns.UltraDNSException.DIRECTIONALPOOL_NOT_FOUND; import static denominator.ultradns.UltraDNSException.DIRECTIONALPOOL_RECORD_NOT_FOUND; import static denominator.ultradns.UltraDNSException.GROUP_NOT_FOUND; +import static denominator.ultradns.UltraDNSException.INVALID_ZONE_NAME; import static denominator.ultradns.UltraDNSException.POOL_ALREADY_EXISTS; import static denominator.ultradns.UltraDNSException.POOL_NOT_FOUND; import static denominator.ultradns.UltraDNSException.POOL_RECORD_ALREADY_EXISTS; @@ -99,24 +99,12 @@ public void accountId() throws Exception { server.assertSoapBody(getAccountsListOfUser); } - @Test - public void getZoneInfo() throws Exception { - server.enqueue( - new MockResponse().setBody(format(getZoneInfoResponseTemplate, "AAAAAAAAAAAAAAAA"))); - - assertThat(mockApi().getZoneInfo("denominator.io.")).isEqualTo("AAAAAAAAAAAAAAAA"); - - server.assertSoapBody(getZoneInfo); - } - @Test public void zonesOfAccountPresent() throws Exception { server.enqueue(new MockResponse().setBody(getZonesOfAccountResponsePresent)); - List zones = mockApi().getZonesOfAccount("AAAAAAAAAAAAAAAA"); - - assertThat(zones.get(0)).hasName("denominator.io."); - assertThat(zones.get(1)).hasName("0.1.2.3.4.5.6.7.ip6.arpa."); + assertThat(mockApi().getZonesOfAccount("AAAAAAAAAAAAAAAA")) + .containsExactly("denominator.io."); server.assertSoapBody(getZonesOfAccount); } @@ -188,6 +176,16 @@ public void recordsInZoneByNameAndTypeAbsent() throws Exception { server.assertSoapBody(getResourceRecordsOfDNameByType); } + @Test + public void recordsInZoneByNameAndTypeInvalidZone() throws Exception { + thrown.expect(UltraDNSException.class); + thrown.expectMessage("2507: Invalid zone name."); + + server.enqueueError(INVALID_ZONE_NAME, "Invalid zone name."); + + mockApi().getResourceRecordsOfDNameByType("ARGHH", "ARGHH", 6); + } + @Test public void createRecordInZone() throws Exception { server.enqueue(new MockResponse().setBody(createResourceRecordResponse)); @@ -742,20 +740,9 @@ public Credentials get() { + ""; static String getZonesOfAccountResponsePresent = getZonesOfAccountResponseHeader + " \n" - + " \n" + getZonesOfAccountResponseFooter; static String getZonesOfAccountResponseAbsent = getZonesOfAccountResponseHeader + getZonesOfAccountResponseFooter; - static String getZoneInfo = - "denominator.io."; - static String getZoneInfoResponseTemplate = - "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + ""; static String getResourceRecordsOfZone = "denominator.io.0"; diff --git a/ultradns/src/test/java/denominator/ultradns/UltraDNSZoneApiMockTest.java b/ultradns/src/test/java/denominator/ultradns/UltraDNSZoneApiMockTest.java index 4bbf6081..9175cd1b 100644 --- a/ultradns/src/test/java/denominator/ultradns/UltraDNSZoneApiMockTest.java +++ b/ultradns/src/test/java/denominator/ultradns/UltraDNSZoneApiMockTest.java @@ -6,17 +6,17 @@ import org.junit.Test; import denominator.ZoneApi; +import denominator.model.Zone; import static denominator.assertj.ModelAssertions.assertThat; -import static denominator.ultradns.UltraDNSException.ZONE_NOT_FOUND; +import static denominator.ultradns.UltraDNSException.INVALID_ZONE_NAME; import static denominator.ultradns.UltraDNSTest.getAccountsListOfUser; import static denominator.ultradns.UltraDNSTest.getAccountsListOfUserResponse; -import static denominator.ultradns.UltraDNSTest.getZoneInfo; -import static denominator.ultradns.UltraDNSTest.getZoneInfoResponseTemplate; +import static denominator.ultradns.UltraDNSTest.getResourceRecordsOfDNameByType; +import static denominator.ultradns.UltraDNSTest.getResourceRecordsOfDNameByTypeResponsePresent; import static denominator.ultradns.UltraDNSTest.getZonesOfAccount; import static denominator.ultradns.UltraDNSTest.getZonesOfAccountResponseAbsent; import static denominator.ultradns.UltraDNSTest.getZonesOfAccountResponsePresent; -import static java.lang.String.format; public class UltraDNSZoneApiMockTest { @@ -27,66 +27,56 @@ public class UltraDNSZoneApiMockTest { public void iteratorWhenPresent() throws Exception { server.enqueue(new MockResponse().setBody(getAccountsListOfUserResponse)); server.enqueue(new MockResponse().setBody(getZonesOfAccountResponsePresent)); + server.enqueue(new MockResponse().setBody(getResourceRecordsOfDNameByTypeResponsePresent)); ZoneApi api = server.connect().api().zones(); - assertThat(api.iterator().next()).hasName("denominator.io."); + assertThat(api.iterator()).containsExactly( + Zone.builder().name("denominator.io.").id("denominator.io.").email("adrianc.netflix.com.") + .ttl(86400).build() + ); server.assertSoapBody(getAccountsListOfUser); server.assertSoapBody(getZonesOfAccount); + server.assertSoapBody(getResourceRecordsOfDNameByType); } @Test - public void iteratorByNameWhenPresentAndSameAccount() throws Exception { + public void iteratorWhenAbsent() throws Exception { server.enqueue(new MockResponse().setBody(getAccountsListOfUserResponse)); - server.enqueue( - new MockResponse().setBody(format(getZoneInfoResponseTemplate, "AAAAAAAAAAAAAAAA"))); + server.enqueue(new MockResponse().setBody(getZonesOfAccountResponseAbsent)); + server.enqueue(new MockResponse().setBody(getResourceRecordsOfDNameByTypeResponsePresent)); ZoneApi api = server.connect().api().zones(); - assertThat(api.iterateByName("denominator.io.").next()).hasName("denominator.io."); + assertThat(api.iterator()).isEmpty(); - server.assertSoapBody(getZoneInfo); server.assertSoapBody(getAccountsListOfUser); + server.assertSoapBody(getZonesOfAccount); } @Test - public void iteratorByNameWhenPresentAndDifferentAccount() throws Exception { - server.enqueue(new MockResponse().setBody(getAccountsListOfUserResponse)); - server.enqueue( - new MockResponse().setBody(format(getZoneInfoResponseTemplate, "BBBBBBBBBBBBBBBB"))); + public void iteratorByName() throws Exception { + server.enqueue(new MockResponse().setBody(getResourceRecordsOfDNameByTypeResponsePresent)); ZoneApi api = server.connect().api().zones(); - assertThat(api.iterateByName("denominator.io.")).isEmpty(); + assertThat(api.iterateByName("denominator.io.")).containsExactly( + Zone.builder().name("denominator.io.").id("denominator.io.").email("adrianc.netflix.com.") + .ttl(86400).build() + ); - server.assertSoapBody(getZoneInfo); - server.assertSoapBody(getAccountsListOfUser); + server.assertSoapBody(getResourceRecordsOfDNameByType); } @Test public void iteratorByNameWhenNotFound() throws Exception { - server.enqueue(new MockResponse().setBody(getAccountsListOfUserResponse)); - server.enqueueError(ZONE_NOT_FOUND, "Zone does not exist in the system."); + server.enqueueError(INVALID_ZONE_NAME, "Invalid zone name."); ZoneApi api = server.connect().api().zones(); assertThat(api.iterateByName("denominator.io.")).isEmpty(); - server.assertSoapBody(getZoneInfo); - server.assertSoapBody(getAccountsListOfUser); - } - - @Test - public void iteratorWhenAbsent() throws Exception { - server.enqueue(new MockResponse().setBody(getAccountsListOfUserResponse)); - server.enqueue(new MockResponse().setBody(getZonesOfAccountResponseAbsent)); - - ZoneApi api = server.connect().api().zones(); - - assertThat(api.iterator()).isEmpty(); - - server.assertSoapBody(getAccountsListOfUser); - server.assertSoapBody(getZonesOfAccount); + server.assertSoapBody(getResourceRecordsOfDNameByType); } }