Skip to content

Commit

Permalink
Refines zone identifiers and handling of zones with the same name
Browse files Browse the repository at this point in the history
Before this change, `Zone.id()` implied that the provider supported
multiple zones with the same name. This turned out to be false, as for
example both CloudDNS and Designate use ids, but enforce zone names
must be unique.

This change introduces a series of changes to support both determining
how zones are identified, as well if the provider supports zones that
have the same name.

One goal was to not add complexity. The overall approach to this is to
only add a concept while removing one.

`Zone.qualifier()` is added to disambiguate zones with the same name.
This is paid for by deprecating `Zone.idOrName()`.

`Provider.zoneIdentification()` is added to that callers can understand
if zone names can be substituted for ids or if qualifiers are supported.
This is paid for by deprecating `Provider.supportsDuplicateZoneNames()`.

Here are the details of this change:
  * Adds `Zone.qualifier()` in support duplicate zones
  * Deprecates `supportsDuplicateZoneNames()` in favor of `Provider.zoneIdentification()` in `NAME`, `OPAQUE` and `QUALIFIED`
  * Deprecates `Zone.idOrName()` as `Zone.id()` cannot be null
  * Adds `ZoneApi.iterateByName()` to support lookups
  * Adds `-n` parameter to CLI zone list
  • Loading branch information
Adrian Cole committed Mar 24, 2015
1 parent 51a5e31 commit 6ad96a4
Show file tree
Hide file tree
Showing 80 changed files with 926 additions and 266 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
### Version 4.5
* Refines zone identifiers and handling of zones with the same name
* Adds `Zone.qualifier()` in support duplicate zones
* Deprecates `supportsDuplicateZoneNames()` in favor of `Provider.zoneIdentification()` in `NAME`, `OPAQUE` and `QUALIFIED`
* Deprecates `Zone.idOrName()` as `Zone.id()` cannot be null
* Adds `ZoneApi.iterateByName()` to support lookups
* Adds `-n` parameter to CLI zone list
* Documents third-party provider process
* Publishes model and core test jars
* Adds example server
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ DNSApiManager manager = Denominator.create("ultradns", credentials(username, pas
The credentials are variable length, as certain providers require more that 2 parts. The above returns an instance of `DNSApiManager` where apis such as `ZoneApis` are found. Here's how to list zones:
```java
for (Zone zone : manager.api().zones()) {
for (ResourceRecordSet<?> rrs : manager.api().recordSetsInZone(zone.idOrName())) {
for (ResourceRecordSet<?> rrs : manager.api().recordSetsInZone(zone.id())) {
processRRS(rrs);
}
}
Expand Down
43 changes: 32 additions & 11 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,21 @@ Different providers connect to different urls and need different credentials. F

```bash
$ denominator providers
provider url duplicateZones credentialType credentialArgs
mock mem:mock false
clouddns https://identity.api.rackspacecloud.com/v2.0/ true apiKey username apiKey
designate http://localhost:5000/v2.0 true password tenantId username password
dynect https://api2.dynect.net/REST false password customer username password
route53 https://route53.amazonaws.com true accessKey accessKey secretKey
route53 https://route53.amazonaws.com true session accessKey secretKey sessionToken
ultradns https://ultra-api.ultradns.com:8443/UltraDNS_WS/v01 false password username password
provider url zoneIds credentialType credentialArgs
clouddns https://identity.api.rackspacecloud.com/v2.0 opaque password username password
clouddns https://identity.api.rackspacecloud.com/v2.0 opaque apiKey username apiKey
designate http://localhost:5000/v2.0 opaque password tenantId username password
dynect https://api2.dynect.net/REST name password customer username password
mock mem:mock name
route53 https://route53.amazonaws.com qualified accessKey accessKey secretKey
route53 https://route53.amazonaws.com qualified session accessKey secretKey sessionToken
ultradns https://ultra-api.ultradns.com:8443/UltraDNS_WS/v01 name password username password
```

The first field says the type, if any. If there's no type listed, it needs no credentials. If there is a type listed, the following fields are credential args. Say for example, you were using `ultradns`.
If the credentialType column is blank it needs no credentials. Otherwise, credentialArgs describes what you need to pass. Say for example, you were using `ultradns`.

```
ultradns https://ultra-api.ultradns.com:8443/UltraDNS_WS/v01 password username password
ultradns https://ultra-api.ultradns.com:8443/UltraDNS_WS/v01 name password username password
```
This says the provider `ultradns` connects by default to `https://ultra-api.ultradns.com:8443/UltraDNS_WS/v01` and supports `password` authentication, which needs two `-c` parameters: `username` and `password`. To put it together, you'd specify the following to do a zone list:
```bash
Expand All @@ -108,8 +109,11 @@ If you need to connect to an alternate url, pass the `-u` parameter:
./denominator -p ultradns -u https://ultra-api.ultradns.com:8443/UltraDNS_WS/v01-BETA -c myusername -c mypassword zone list
```

The `zoneIds` column indicates how the provider addresses zones. For example, `name` means you simply
pass the zone name (like `denominator.io.`) to commands that need it. This topic is further described in the `Zone Identifiers` section to follow.

### Zone
`zone list` returns the zones names in your account. Ex.
`zone list` returns the zones in your account. Ex.
```bash
$ denominator -p ultradns -c my_user -c my_password zone list
--snip--
Expand All @@ -126,6 +130,23 @@ 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.

```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
--snip--
denominator.io. NS 172800 ns-1312.awsdns-36.org.
--snip--
```

### Geo
#### Show Supported Geo Regions
`denominator geo regions` returns all supported regions for the zone specified.
Expand Down
56 changes: 31 additions & 25 deletions cli/src/main/java/denominator/cli/Denominator.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
Expand Down Expand Up @@ -63,6 +62,7 @@
import denominator.cli.ResourceRecordSetCommands.ResourceRecordSetRemove;
import denominator.cli.ResourceRecordSetCommands.ResourceRecordSetReplace;
import denominator.model.Zone;
import denominator.model.Zone.Identification;
import feign.Logger;
import feign.Logger.Level;
import io.airlift.airline.Cli;
Expand Down Expand Up @@ -172,20 +172,13 @@ static Object logModule(boolean quiet, boolean verbose) {
return new LogModule(logLevel);
}

static String idOrName(DNSApiManager mgr, String zoneIdOrName) {
if (!InternetDomainName.isValid(zoneIdOrName) || !InternetDomainName.from(zoneIdOrName)
.hasParent()) {
static String id(DNSApiManager mgr, String zoneIdOrName) {
if (mgr.provider().zoneIdentification() == Identification.NAME) {
return zoneIdOrName;
} else if (InternetDomainName.isValid(zoneIdOrName) && mgr.provider()
.supportsDuplicateZoneNames()) {
List<Zone> currentZones = new ArrayList<Zone>();
for (Zone zone : mgr.api().zones()) {
if (zoneIdOrName.equals(zone.name())) {
return zone.id();
}
currentZones.add(zone);
}
checkArgument(false, "zone %s not found in %s", zoneIdOrName, currentZones);
} else if (zoneIdOrName.indexOf('.') != -1 && InternetDomainName.isValid(zoneIdOrName)) {
Iterator<Zone> result = mgr.api().zones().iterateByName(zoneIdOrName);
checkArgument(result.hasNext(), "zone %s not found", zoneIdOrName);
return result.next().id();
}
return zoneIdOrName;
}
Expand All @@ -202,24 +195,25 @@ public void run() {
@Command(name = "providers", description = "List the providers and their metadata ")
public static class ListProviders implements Runnable {

final static String table = "%-10s %-51s %-14s %-14s %s%n";
final static String table = "%-10s %-51s %-9s %-14s %s%n";

public static String providerAndCredentialsTable() {
StringBuilder builder = new StringBuilder();

builder.append(
format(table, "provider", "url", "duplicateZones", "credentialType", "credentialArgs"));
format(table, "provider", "url", "zoneIds", "credentialType", "credentialArgs"));
for (Provider provider : ImmutableSortedSet
.copyOf(Ordering.usingToString(), Providers.list())) {
if (provider.credentialTypeToParameterNames().isEmpty()) {
builder.append(format("%-10s %-51s %-14s %n", provider.name(), provider.url(),
provider.supportsDuplicateZoneNames()));
provider.zoneIdentification().toString().toLowerCase()));
}
for (Entry<String, Collection<String>> entry : provider.credentialTypeToParameterNames()
.entrySet()) {
String parameters = Joiner.on(' ').join(entry.getValue());
builder.append(format(table, provider.name(), provider.url(),
provider.supportsDuplicateZoneNames(), entry.getKey(), parameters));
provider.zoneIdentification().toString().toLowerCase(),
entry.getKey(), parameters));
}
}
return builder.toString();
Expand Down Expand Up @@ -378,20 +372,32 @@ void overrideFromEnv(Map<String, String> env) {
protected abstract Iterator<String> doRun(DNSApiManager mgr);
}

@Command(name = "list", description = "Lists the zones present in this provider. If the second column is present, it is the zone id.")
@Command(name = "list", description = "Lists the zones present in this provider. If more than one column is present, the last is the zone id.")
public static class ZoneList extends DenominatorCommand {

public Iterator<String> doRun(DNSApiManager mgr) {
return Iterators.transform(mgr.api().zones().iterator(), new Function<Zone, String>() {
@Option(type = OptionType.COMMAND, name = {"-n",
"--name"}, description = "name of the zone. ex. denominator.io.")
public String name;

public Iterator<String> doRun(final DNSApiManager mgr) {
Iterator<Zone> zones =
name == null ? mgr.api().zones().iterator() : mgr.api().zones().iterateByName(name);
return Iterators.transform(zones, new Function<Zone, String>() {

@Override
public String apply(Zone input) {
if (input.id() != null) {
return input.name() + " " + input.id();
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 input.name();
}

});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import static com.google.common.collect.Iterators.forArray;
import static com.google.common.collect.Iterators.singletonIterator;
import static com.google.common.collect.Iterators.transform;
import static denominator.cli.Denominator.idOrName;
import static denominator.cli.Denominator.id;
import static denominator.cli.Denominator.json;
import static denominator.common.Util.join;
import static denominator.model.profile.Geos.withAdditionalRegions;
Expand Down Expand Up @@ -99,7 +99,7 @@ public static class GeoRegionList extends GeoResourceRecordSetCommand {
@Override
protected Iterator<String> doRun(DNSApiManager mgr) {
return FluentIterable
.from(mgr.api().geoRecordSetsInZone(idOrName(mgr, zoneIdOrName)).supportedRegions()
.from(mgr.api().geoRecordSetsInZone(id(mgr, zoneIdOrName)).supportedRegions()
.entrySet())
.transform(new Function<Map.Entry<String, Collection<String>>, String>() {
@Override
Expand All @@ -125,13 +125,13 @@ public Iterator<String> doRun(DNSApiManager mgr) {
Iterator<ResourceRecordSet<?>> iterator;
if (name != null && type != null) {
iterator =
mgr.api().geoRecordSetsInZone(idOrName(mgr, zoneIdOrName))
mgr.api().geoRecordSetsInZone(id(mgr, zoneIdOrName))
.iterateByNameAndType(name, type);
}
if (name != null) {
iterator = mgr.api().geoRecordSetsInZone(idOrName(mgr, zoneIdOrName)).iterateByName(name);
iterator = mgr.api().geoRecordSetsInZone(id(mgr, zoneIdOrName)).iterateByName(name);
} else {
iterator = mgr.api().geoRecordSetsInZone(idOrName(mgr, zoneIdOrName)).iterator();
iterator = mgr.api().geoRecordSetsInZone(id(mgr, zoneIdOrName)).iterator();
}
return transform(iterator, GeoResourceRecordSetToString.INSTANCE);
}
Expand All @@ -153,7 +153,7 @@ public static class GeoResourceRecordSetGet extends GeoResourceRecordSetCommand
public String group;

public Iterator<String> doRun(DNSApiManager mgr) {
GeoResourceRecordSetApi api = mgr.api().geoRecordSetsInZone(idOrName(mgr, zoneIdOrName));
GeoResourceRecordSetApi api = mgr.api().geoRecordSetsInZone(id(mgr, zoneIdOrName));
ResourceRecordSet<?> rrs = api.getByNameTypeAndQualifier(name, type, group);
return rrs != null ? singletonIterator(GeoResourceRecordSetToString.INSTANCE.apply(rrs))
: singletonIterator("");
Expand Down Expand Up @@ -193,7 +193,7 @@ public boolean hasNext() {

@Override
public String next() {
GeoResourceRecordSetApi api = mgr.api().geoRecordSetsInZone(idOrName(mgr, zoneIdOrName));
GeoResourceRecordSetApi api = mgr.api().geoRecordSetsInZone(id(mgr, zoneIdOrName));
ResourceRecordSet<?> rrs = api.getByNameTypeAndQualifier(name, type, group);
if (rrs != null &&
(rrs.ttl() == null || (rrs.ttl() != null && rrs.ttl().intValue() != ttl))) {
Expand Down Expand Up @@ -248,7 +248,7 @@ public static class GeoResourceRecordAddRegions extends GeoResourceRecordSetComm
public Iterator<String> doRun(final DNSApiManager mgr) {
checkArgument(!regions.isEmpty(), "specify regions to apply");
// resolve into a concrete zone id or name.
zoneIdOrName = idOrName(mgr, zoneIdOrName);
zoneIdOrName = id(mgr, zoneIdOrName);
final GeoResourceRecordSetApi api = mgr.api().geoRecordSetsInZone(zoneIdOrName);
checkArgument(api != null, "geo api not available on provider %s, zone %s", mgr.provider(),
zoneIdOrName);
Expand Down
16 changes: 8 additions & 8 deletions cli/src/main/java/denominator/cli/ResourceRecordSetCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import static com.google.common.collect.Iterators.forArray;
import static com.google.common.collect.Iterators.singletonIterator;
import static com.google.common.collect.Iterators.transform;
import static denominator.cli.Denominator.idOrName;
import static denominator.cli.Denominator.id;
import static denominator.common.Util.join;
import static java.lang.String.format;

Expand Down Expand Up @@ -74,7 +74,7 @@ public static class ResourceRecordSetList extends ResourceRecordSetCommand {

public Iterator<String> doRun(DNSApiManager mgr) {
Iterator<ResourceRecordSet<?>> iterator;
String zoneId = idOrName(mgr, zoneIdOrName);
String zoneId = id(mgr, zoneIdOrName);
if (type != null) {
iterator = mgr.api().recordSetsInZone(zoneId).iterateByNameAndType(name, type);
} else if (name != null) {
Expand Down Expand Up @@ -103,7 +103,7 @@ public static class ResourceRecordSetGet extends ResourceRecordSetCommand {

public Iterator<String> doRun(DNSApiManager mgr) {
ResourceRecordSet<?> rrs;
String zoneId = idOrName(mgr, zoneIdOrName);
String zoneId = id(mgr, zoneIdOrName);
if (qualifier != null) {
rrs = mgr.api().recordSetsInZone(zoneId).getByNameTypeAndQualifier(name, type, qualifier);
} else {
Expand Down Expand Up @@ -142,7 +142,7 @@ public boolean hasNext() {

@Override
public String next() {
ResourceRecordSetApi api = mgr.api().basicRecordSetsInZone(idOrName(mgr, zoneIdOrName));
ResourceRecordSetApi api = mgr.api().basicRecordSetsInZone(id(mgr, zoneIdOrName));
ResourceRecordSet<?> rrs = api.getByNameAndType(name, type);
if (rrs != null && rrs.ttl() != null && rrs.ttl().intValue() != ttl) {
api.put(ResourceRecordSet.builder()
Expand Down Expand Up @@ -295,7 +295,7 @@ public boolean hasNext() {

@Override
public String next() {
ResourceRecordSetApi api = mgr.api().basicRecordSetsInZone(idOrName(mgr, zoneIdOrName));
ResourceRecordSetApi api = mgr.api().basicRecordSetsInZone(id(mgr, zoneIdOrName));
ResourceRecordSet<?> rrset = api.getByNameAndType(name, type);
if (rrset != null) {
Set<Map<String, Object>> data = new LinkedHashSet<Map<String, Object>>(rrset.records());
Expand Down Expand Up @@ -350,7 +350,7 @@ public boolean hasNext() {

@Override
public String next() {
mgr.api().basicRecordSetsInZone(idOrName(mgr, zoneIdOrName)).put(toAdd);
mgr.api().basicRecordSetsInZone(id(mgr, zoneIdOrName)).put(toAdd);
done = true;
return ";; ok";
}
Expand Down Expand Up @@ -382,7 +382,7 @@ public boolean hasNext() {

@Override
public String next() {
ResourceRecordSetApi api = mgr.api().basicRecordSetsInZone(idOrName(mgr, zoneIdOrName));
ResourceRecordSetApi api = mgr.api().basicRecordSetsInZone(id(mgr, zoneIdOrName));
ResourceRecordSet<?> rrset = api.getByNameAndType(name, type);
if (rrset != null) {
Set<Map<String, Object>> data = new LinkedHashSet<Map<String, Object>>(rrset.records());
Expand Down Expand Up @@ -433,7 +433,7 @@ public boolean hasNext() {

@Override
public String next() {
mgr.api().basicRecordSetsInZone(idOrName(mgr, zoneIdOrName))
mgr.api().basicRecordSetsInZone(id(mgr, zoneIdOrName))
.deleteByNameAndType(name, type);
done = true;
return ";; ok";
Expand Down
Loading

0 comments on commit 6ad96a4

Please sign in to comment.