diff --git a/README.md b/README.md index 68c624c37489..6061d9dd4c8f 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This client supports the following Google Cloud Platform services: - [Google Cloud BigQuery] (#google-cloud-bigquery-alpha) (Alpha) - [Google Cloud Datastore] (#google-cloud-datastore) +- [Google Cloud DNS] (#google-cloud-dns-alpha) (Alpha) - [Google Cloud Resource Manager] (#google-cloud-resource-manager-alpha) (Alpha) - [Google Cloud Storage] (#google-cloud-storage) @@ -48,8 +49,10 @@ Example Applications - Read more about using this application on the [`BigQueryExample` docs page](http://googlecloudplatform.github.io/gcloud-java/apidocs/?com/google/gcloud/examples/bigquery/BigQueryExample.html). - [`Bookshelf`](https://github.com/GoogleCloudPlatform/getting-started-java/tree/master/bookshelf) - An App Engine app that manages a virtual bookshelf. - This app uses `gcloud-java` to interface with Cloud Datastore and Cloud Storage. It also uses Cloud SQL, another Google Cloud Platform service. -- [`DatastoreExample`](./gcloud-java-examples/src/main/java/com/google/gcloud/examples/datastore/DatastoreExample.java) - A simple command line interface for the Cloud Datastore +- [`DatastoreExample`](./gcloud-java-examples/src/main/java/com/google/gcloud/examples/datastore/DatastoreExample.java) - A simple command line interface for Cloud Datastore - Read more about using this application on the [`DatastoreExample` docs page](http://googlecloudplatform.github.io/gcloud-java/apidocs/?com/google/gcloud/examples/datastore/DatastoreExample.html). +- [`DnsExample`](./gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/DnsExample.java) - A simple command line interface for Cloud DNS + - Read more about using this application on the [`DnsExample` docs page](http://googlecloudplatform.github.io/gcloud-java/apidocs/?com/google/gcloud/examples/dns/DnsExample.html). - [`ResourceManagerExample`](./gcloud-java-examples/src/main/java/com/google/gcloud/examples/resourcemanager/ResourceManagerExample.java) - A simple command line interface providing some of Cloud Resource Manager's functionality - Read more about using this application on the [`ResourceManagerExample` docs page](http://googlecloudplatform.github.io/gcloud-java/apidocs/?com/google/gcloud/examples/resourcemanager/ResourceManagerExample.html). - [`SparkDemo`](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/master/managed_vms/sparkjava) - An example of using gcloud-java-datastore from within the SparkJava and App Engine Managed VM frameworks. @@ -218,6 +221,71 @@ if (entity != null) { } ``` +Google Cloud DNS (Alpha) +---------------------- +- [API Documentation][dns-api] +- [Official Documentation][cloud-dns-docs] + +*Follow the [activation instructions][cloud-dns-activation] to use the Google Cloud DNS API with your project.* + +#### Preview + +Here are two code snippets showing simple usage examples from within Compute/App Engine. Note that you must [supply credentials](#authentication) and a project ID if running this snippet elsewhere. + +The first snippet shows how to create a zone resource. Complete source code can be found on +[CreateZone.java](./gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateZone.java). + +```java +import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsOptions; +import com.google.gcloud.dns.Zone; +import com.google.gcloud.dns.ZoneInfo; + +Dns dns = DnsOptions.defaultInstance().service(); +String zoneName = "my-unique-zone"; +String domainName = "someexampledomain.com."; +String description = "This is a gcloud-java-dns sample zone."; +ZoneInfo zoneInfo = ZoneInfo.of(zoneName, domainName, description); +Zone zone = dns.create(zoneInfo); +``` + +The second snippet shows how to create records inside a zone. The complete code can be found on [CreateOrUpdateRecordSets.java](./gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateOrUpdateRecordSets.java). + +```java +import com.google.gcloud.dns.ChangeRequestInfo; +import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsOptions; +import com.google.gcloud.dns.RecordSet; +import com.google.gcloud.dns.Zone; + +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +Dns dns = DnsOptions.defaultInstance().service(); +String zoneName = "my-unique-zone"; +Zone zone = dns.getZone(zoneName); +String ip = "12.13.14.15"; +RecordSet toCreate = RecordSet.builder("www.someexampledomain.com.", RecordSet.Type.A) + .ttl(24, TimeUnit.HOURS) + .addRecord(ip) + .build(); +ChangeRequestInfo.Builder changeBuilder = ChangeRequestInfo.builder().add(toCreate); + +// Verify that the record does not exist yet. +// If it does exist, we will overwrite it with our prepared record. +Iterator recordSetIterator = zone.listRecordSets().iterateAll(); +while (recordSetIterator.hasNext()) { + RecordSet current = recordSetIterator.next(); + if (toCreate.name().equals(current.name()) && + toCreate.type().equals(current.type())) { + changeBuilder.delete(current); + } +} + +ChangeRequestInfo changeRequest = changeBuilder.build(); +zone.applyChangeRequest(changeRequest); +``` + Google Cloud Resource Manager (Alpha) ---------------------- @@ -359,6 +427,10 @@ Apache 2.0 - See [LICENSE] for more information. [cloud-datastore-activation]: https://cloud.google.com/datastore/docs/activate [datastore-api]: http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/datastore/package-summary.html +[dns-api]: http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/dns/package-summary.html +[cloud-dns-docs]: https://cloud.google.com/dns/docs +[cloud-dns-activation]: https://console.cloud.google.com/start/api?id=dns + [cloud-pubsub]: https://cloud.google.com/pubsub/ [cloud-pubsub-docs]: https://cloud.google.com/pubsub/docs diff --git a/gcloud-java-dns/README.md b/gcloud-java-dns/README.md new file mode 100644 index 000000000000..d2e4c85b3b76 --- /dev/null +++ b/gcloud-java-dns/README.md @@ -0,0 +1,393 @@ +Google Cloud Java Client for DNS +================================ + +Java idiomatic client for [Google Cloud DNS] (https://cloud.google.com/dns/). + +[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-java.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-java) +[![Coverage Status](https://coveralls.io/repos/GoogleCloudPlatform/gcloud-java/badge.svg?branch=master)](https://coveralls.io/r/GoogleCloudPlatform/gcloud-java?branch=master) +[![Maven](https://img.shields.io/maven-central/v/com.google.gcloud/gcloud-java-dns.svg)]( https://img.shields.io/maven-central/v/com.google.gcloud/gcloud-java-dns.svg) +[![Codacy Badge](https://api.codacy.com/project/badge/grade/9da006ad7c3a4fe1abd142e77c003917)](https://www.codacy.com/app/mziccard/gcloud-java) +[![Dependency Status](https://www.versioneye.com/user/projects/56bd8ee72a29ed002d2b0969/badge.svg?style=flat)](https://www.versioneye.com/user/projects/56bd8ee72a29ed002d2b0969) + +- [Homepage] (https://googlecloudplatform.github.io/gcloud-java/) +- [API Documentation] (http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/dns/package-summary.html) + +> Note: This client is a work-in-progress, and may occasionally +> make backwards-incompatible changes. + +Quickstart +---------- +If you are using Maven, add this to your pom.xml file +```xml + + com.google.gcloud + gcloud-java-dns + 0.1.5 + +``` +If you are using Gradle, add this to your dependencies +```Groovy +compile 'com.google.gcloud:gcloud-java-dns:0.1.5' +``` +If you are using SBT, add this to your dependencies +```Scala +libraryDependencies += "com.google.gcloud" % "gcloud-java-dns" % "0.1.5" +``` + +Example Application +------------------- + +[`DnsExample`](../gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/DnsExample.java) +is a simple command line interface that provides some of Google Cloud DNS's functionality. Read +more about using the application on the +[`DnsExample` docs page](http://googlecloudplatform.github.io/gcloud-java/apidocs/?com/google/gcloud/examples/dns/DnsExample.html). + +Authentication +-------------- + +See the [Authentication](https://github.com/GoogleCloudPlatform/gcloud-java#authentication) section +in the base directory's README. + +About Google Cloud DNS +-------------------------- + +[Google Cloud DNS][cloud-dns] is a scalable, reliable and managed authoritative Domain Name System +(DNS) service running on the same infrastructure as Google. It has low latency, high availability +and is a cost-effective way to make your applications and services available to your users. + +See the [Google Cloud DNS docs][dns-activate] for more details on how to activate +Cloud DNS for your project. + +See the [``gcloud-java-dns`` API documentation][dns-api] to learn how to interact +with the Cloud DNS using this client Library. + +Getting Started +--------------- +#### Prerequisites +For this tutorial, you will need a [Google Developers Console](https://console.developers.google.com/) +project with the DNS API enabled. You will need to [enable billing](https://support.google.com/cloud/answer/6158867?hl=en) +to use Google Cloud DNS. [Follow these instructions](https://cloud.google.com/docs/authentication#preparation) +to get your project set up. You will also need to set up the local development environment by +[installing the Google Cloud SDK](https://cloud.google.com/sdk/) and running the following commands +in command line: `gcloud auth login` and `gcloud config set project [YOUR PROJECT ID]`. + +#### Installation and setup +You'll need to obtain the `gcloud-java-dns` library. See the [Quickstart](#quickstart) section to +add `gcloud-java-dns` as a dependency in your code. + +#### Creating an authorized service object +To make authenticated requests to Google Cloud DNS, you must create a service object with +credentials. You can then make API calls by calling methods on the DNS service object. The simplest +way to authenticate is to use [Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials). +These credentials are automatically inferred from your environment, so you only need the following +code to create your service object: + +```java +import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsOptions; + +Dns dns = DnsOptions.defaultInstance().service(); +``` + +For other authentication options, see the [Authentication](https://github.com/GoogleCloudPlatform/gcloud-java#authentication) page. + +#### Managing Zones +Record sets in `gcloud-java-dns` are managed inside containers called "zones". `ZoneInfo` is a class +which encapsulates metadata that describe a zone in Google Cloud DNS. `Zone`, a subclass of `ZoneInfo`, adds service-related +functionality over `ZoneInfo`. + +*Important: Zone names must be unique to the project. If you choose a zone name that already +exists within your project, you'll get a helpful error message telling you to choose another name. In the code below, +replace "my-unique-zone" with a unique zone name. See more about naming rules [here](https://cloud.google.com/dns/api/v1/managedZones#name).* + +In this code snippet, we create a new zone to manage record sets for domain `someexampledomain.com.` + +*Important: The service may require that you verify ownership of the domain for which you are creating a zone. +Hence, we recommend that you do so beforehand. You can verify ownership of +a domain name [here](https://www.google.com/webmasters/verification/home). Note that Cloud DNS +requires fully qualified domain names which must end with a period.* + +Add the following imports at the top of your file: + +```java +import com.google.gcloud.dns.Zone; +import com.google.gcloud.dns.ZoneInfo; +``` + +Then add the following code to create a zone. + +```java +// Create a zone metadata object +String zoneName = "my-unique-zone"; // Change this zone name which is unique within your project +String domainName = "someexampledomain.com."; // Change this to a domain which you own +String description = "This is a gcloud-java-dns sample zone."; +ZoneInfo zoneInfo = ZoneInfo.of(zoneName, domainName, description); + +// Create zone in Google Cloud DNS +Zone zone = dns.create(zoneInfo); +System.out.printf("Zone was created and assigned ID %s.%n", zone.id()); +``` + +You now have an empty zone hosted in Google Cloud DNS which is ready to be populated with +record sets for domain name `someexampledomain.com.` Upon creating the zone, the cloud service +assigned a set of DNS servers to host records for this zone and +created the required SOA and NS records for the domain. The following snippet prints the list of servers +assigned to the zone created above. First, import + +```java +import java.util.List; +``` + +and then add + +```java +// Print assigned name servers +List nameServers = zone.nameServers(); +for(String nameServer : nameServers) { + System.out.println(nameServer); +} +``` + +You can now instruct your domain registrar to [update your domain name servers] (https://cloud.google.com/dns/update-name-servers). +As soon as this happens and the change propagates through cached values in DNS resolvers, +all the DNS queries will be directed to and answered by the Google Cloud DNS service. + +#### Creating Record Sets +Now that we have a zone, we can add some record sets. The record sets held within zones are +modified by "change requests". In this example, we create and apply a change request to +our zone that creates a record set of type A and points URL www.someexampledomain.com to +IP address 12.13.14.15. Start by adding + +```java +import com.google.gcloud.dns.ChangeRequestInfo; +import com.google.gcloud.dns.RecordSet; + +import java.util.concurrent.TimeUnit; +``` + +and proceed with: + +```java +// Prepare a www.someexampledomain.com. type A record set with ttl of 24 hours +String ip = "12.13.14.15"; +RecordSet toCreate = RecordSet.builder("www." + zone.dnsName(), RecordSet.Type.A) + .ttl(24, TimeUnit.HOURS) + .addRecord(ip) + .build(); + +// Make a change +ChangeRequestInfo changeRequest = ChangeRequestInfo.builder().add(toCreate).build(); + +// Build and apply the change request to our zone +changeRequest = zone.applyChangeRequest(changeRequest); +``` + +The `addRecord` method of `RecordSet.Builder` accepts records in the form of +strings. The format of the strings depends on the type of the record sets to be added. +More information on the supported record set types and record formats can be found [here](https://cloud.google.com/dns/what-is-cloud-dns#supported_record_types). + +If you already have a record set, Cloud DNS will return an error upon an attempt to create a duplicate of it. +You can modify the code above to create a record set or update it if it already exists by making the +following adjustment in your imports + +```java +import java.util.Iterator; +``` + +and in the code + +```java +// Make a change +ChangeRequestInfo.Builder changeBuilder = ChangeRequestInfo.builder().add(toCreate); + +// Verify the type A record does not exist yet. +// If it does exist, we will overwrite it with our prepared record. +Iterator recordSetIterator = zone.listRecordSets().iterateAll(); +while (recordSetIterator.hasNext()) { + RecordSet current = recordSetIterator.next(); + if (toCreate.name().equals(current.name()) && toCreate.type().equals(current.type())) { + changeBuilder.delete(current); + } +} + +// Build and apply the change request to our zone +ChangeRequestInfo changeRequest = changeBuilder.build(); +zone.applyChangeRequest(changeRequest); +``` +You can find more information about changes in the [Cloud DNS documentation] (https://cloud.google.com/dns/what-is-cloud-dns#cloud_dns_api_concepts). + +When the change request is applied, it is registered with the Cloud DNS service for processing. We +can wait for its completion as follows: + +```java +while (ChangeRequestInfo.Status.PENDING.equals(changeRequest.status())) { + try { + Thread.sleep(500L); + } catch (InterruptedException e) { + System.err.println("The thread was interrupted while waiting..."); + } + changeRequest = dns.getChangeRequest(zone.name(), changeRequest.id()); +} +System.out.println("The change request has been applied."); +``` + +Change requests are applied atomically to all the assigned DNS servers at once. Note that when this +happens, it may still take a while for the change to be registered by the DNS cache resolvers. +See more on this topic [here](https://cloud.google.com/dns/monitoring). + +#### Listing Zones and Record Sets +Suppose that you have added more zones and record sets, and now you want to list them. +First, import the following (unless you have done so in the previous section): + +```java +import java.util.Iterator; +``` + +Then add the following code to list all your zones and record sets. + +```java +// List all your zones +Iterator zoneIterator = dns.listZones().iterateAll(); +int counter = 1; +while (zoneIterator.hasNext()) { + System.out.printf("#%d.: %s%n%n", counter, zoneIterator.next()); + counter++; +} + +// List the record sets in a particular zone +recordSetIterator = zone.listRecordSets().iterateAll(); +System.out.println(String.format("Record sets inside %s:", zone.name())); +while (recordSetIterator.hasNext()) { + System.out.println(recordSetIterator.next()); +} +``` + +You can also list the history of change requests that were applied to a zone. +First add: + +```java +import java.util.ChangeRequest; +``` + +and then: + +```java + +// List the change requests applied to a particular zone +Iterator changeIterator = zone.listChangeRequests().iterateAll(); +System.out.println(String.format("The history of changes in %s:", zone.name())); +while (changeIterator.hasNext()) { + System.out.println(changeIterator.next()); +} +``` + +#### Deleting Zones + +If you no longer want to host a zone in Cloud DNS, you can delete it. +First, you need to empty the zone by deleting all its records except for the default SOA and NS record sets. + +```java +// Make a change for deleting the record sets +changeBuilder = ChangeRequestInfo.builder(); +while (recordIterator.hasNext()) { + RecordSet current = recordIterator.next(); + // SOA and NS records cannot be deleted + if (!RecordSet.Type.SOA.equals(current.type()) && !RecordSet.Type.NS.equals(current.type())) { + changeBuilder.delete(current); + } +} + +// Build and apply the change request to our zone if it contains records to delete +ChangeRequestInfo changeRequest = changeBuilder.build(); +if (!changeRequest.deletions().isEmpty()) { + changeRequest = dns.applyChangeRequest(zoneName, changeRequest); + + // Wait for change to finish, but save data traffic by transferring only ID and status + Dns.ChangeRequestOption option = + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.STATUS); + while (ChangeRequestInfo.Status.PENDING.equals(changeRequest.status())) { + System.out.println("Waiting for change to complete. Going to sleep for 500ms..."); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + System.err.println("The thread was interrupted while waiting for change request to be " + + "processed."); + } + + // Update the change, but fetch only change ID and status + changeRequest = dns.getChangeRequest(zoneName, changeRequest.id(), option); + } +} + +// Delete the zone +boolean result = dns.delete(zoneName); +if (result) { + System.out.println("Zone was deleted."); +} else { + System.out.println("Zone was not deleted because it does not exist."); +} +``` + +#### Complete Source Code + +We composed some of the aforementioned snippets into complete executable code samples. In +[CreateZones.java](../gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateZone.java) +we create a zone. In [CreateOrUpdateRecordSets.java](../gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateOrUpdateRecordSets.java) +we create a type A record set for a zone, or update an existing type A record set to a new IP address. We +demonstrate how to delete a zone in [DeleteZone.java](../gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/DeleteZone.java). +Finally, in [ManipulateZonesAndRecordSets.java](../gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/ManipulateZonesAndRecordSets.java) +we assemble all the code snippets together and create zone, create or update a record set, list zones, list record sets, list changes, and +delete a zone. The applications assume that they are running on Compute Engine or from your own desktop. To run any of these examples on App +Engine, simply move the code from the main method to your application's servlet class and change the +print statements to display on your webpage. + +Troubleshooting +--------------- + +To get help, follow the `gcloud-java` links in the `gcloud-*` [shared Troubleshooting document](https://github.com/GoogleCloudPlatform/gcloud-common/blob/master/troubleshooting/readme.md#troubleshooting). + +Java Versions +------------- + +Java 7 or above is required for using this client. + +Testing +------- + +This library has tools to help make tests for code using Cloud DNS. + +See [TESTING] to read more about testing. + +Versioning +---------- + +This library follows [Semantic Versioning] (http://semver.org/). + +It is currently in major version zero (``0.y.z``), which means that anything +may change at any time and the public API should not be considered +stable. + +Contributing +------------ + +Contributions to this library are always welcome and highly encouraged. + +See `gcloud-java`'s [CONTRIBUTING] documentation and the `gcloud-*` [shared documentation](https://github.com/GoogleCloudPlatform/gcloud-common/blob/master/contributing/readme.md#how-to-contribute-to-gcloud) for more information on how to get started. + +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. See [Code of Conduct][code-of-conduct] for more information. + +License +------- + +Apache 2.0 - See [LICENSE] for more information. + + +[CONTRIBUTING]:https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/CONTRIBUTING.md +[code-of-conduct]:https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/CODE_OF_CONDUCT.md#contributor-code-of-conduct +[LICENSE]: https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/LICENSE +[TESTING]: https://github.com/GoogleCloudPlatform/gcloud-java/blob/master/TESTING.md#testing-code-that-uses-storage +[cloud-platform]: https://cloud.google.com/ + +[cloud-dns]: https://cloud.google.com/dns/ +[dns-api]: http://googlecloudplatform.github.io/gcloud-java/apidocs/index.html?com/google/gcloud/dns/package-summary.html +[dns-activate]:https://cloud.google.com/dns/getting-started#prerequisites diff --git a/gcloud-java-dns/pom.xml b/gcloud-java-dns/pom.xml new file mode 100644 index 000000000000..b55200b8fc7d --- /dev/null +++ b/gcloud-java-dns/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + com.google.gcloud + gcloud-java-dns + jar + GCloud Java DNS + + Java idiomatic client for Google Cloud DNS. + + + com.google.gcloud + gcloud-java-pom + 0.1.6-SNAPSHOT + + + gcloud-java-dns + + + + ${project.groupId} + gcloud-java-core + ${project.version} + + + com.google.apis + google-api-services-dns + v1-rev7-1.21.0 + compile + + + com.google.guava + guava-jdk5 + + + com.google.api-client + google-api-client + + + + + ${project.groupId} + gcloud-java-core + ${project.version} + test-jar + test + + + junit + junit + 4.12 + test + + + org.easymock + easymock + 3.3 + test + + + diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/AbstractOption.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/AbstractOption.java new file mode 100644 index 000000000000..e12f7412e687 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/AbstractOption.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.MoreObjects; +import com.google.gcloud.dns.spi.DnsRpc; + +import java.io.Serializable; +import java.util.Objects; + +/** + * A base class for options. + */ +abstract class AbstractOption implements Serializable { + + private static final long serialVersionUID = -5912727967831484228L; + private final Object value; + private final DnsRpc.Option rpcOption; + + AbstractOption(DnsRpc.Option rpcOption, Object value) { + this.rpcOption = checkNotNull(rpcOption); + this.value = value; + } + + Object value() { + return value; + } + + DnsRpc.Option rpcOption() { + return rpcOption; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AbstractOption)) { + return false; + } + AbstractOption other = (AbstractOption) obj; + return Objects.equals(value, other.value) && Objects.equals(rpcOption, other.rpcOption); + } + + @Override + public int hashCode() { + return Objects.hash(value, rpcOption); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("value", value) + .add("rpcOption", rpcOption) + .toString(); + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ChangeRequest.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ChangeRequest.java new file mode 100644 index 000000000000..4b6369976ca6 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ChangeRequest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.services.dns.model.Change; +import com.google.common.base.Function; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.List; +import java.util.Objects; + +/** + * An immutable class representing an atomic update to a collection of {@link RecordSet}s within a + * {@code Zone}. + * + * @see Google Cloud DNS documentation + */ +public class ChangeRequest extends ChangeRequestInfo { + + private static final long serialVersionUID = 5335667200595081449L; + private final DnsOptions options; + private final String zone; + private transient Dns dns; + + /** + * A builder for {@code ChangeRequest}s. + */ + public static class Builder extends ChangeRequestInfo.Builder { + + private final Dns dns; + private final String zone; + private final ChangeRequestInfo.BuilderImpl infoBuilder; + + private Builder(ChangeRequest cr) { + this.dns = cr.dns; + this.zone = cr.zone; + this.infoBuilder = new ChangeRequestInfo.BuilderImpl(cr); + } + + @Override + public Builder additions(List additions) { + infoBuilder.additions(additions); + return this; + } + + @Override + public Builder deletions(List deletions) { + infoBuilder.deletions(deletions); + return this; + } + + @Override + public Builder add(RecordSet recordSet) { + infoBuilder.add(recordSet); + return this; + } + + @Override + public Builder delete(RecordSet recordSet) { + infoBuilder.delete(recordSet); + return this; + } + + @Override + public Builder clearAdditions() { + infoBuilder.clearAdditions(); + return this; + } + + @Override + public Builder clearDeletions() { + infoBuilder.clearDeletions(); + return this; + } + + @Override + public Builder removeAddition(RecordSet recordSet) { + infoBuilder.removeAddition(recordSet); + return this; + } + + @Override + public Builder removeDeletion(RecordSet recordSet) { + infoBuilder.removeDeletion(recordSet); + return this; + } + + @Override + Builder id(String id) { + infoBuilder.id(id); + return this; + } + + @Override + Builder startTimeMillis(long startTimeMillis) { + infoBuilder.startTimeMillis(startTimeMillis); + return this; + } + + @Override + Builder status(Status status) { + infoBuilder.status(status); + return this; + } + + @Override + public ChangeRequest build() { + return new ChangeRequest(dns, zone, infoBuilder); + } + } + + ChangeRequest(Dns dns, String zone, ChangeRequest.BuilderImpl infoBuilder) { + super(infoBuilder); + this.zone = checkNotNull(zone); + this.dns = checkNotNull(dns); + this.options = dns.options(); + } + + /** + * Returns the name of the {@link Zone} associated with this change request. + */ + public String zone() { + return this.zone; + } + + /** + * Returns the change request's {@code Dns} object used to issue requests. + */ + public Dns dns() { + return dns; + } + + /** + * Applies this change request to the associated zone. + */ + public ChangeRequest applyTo(Dns.ChangeRequestOption... options) { + return dns.applyChangeRequest(zone, this, options); + } + + @Override + public Builder toBuilder() { + return new Builder(this); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !obj.getClass().equals(ChangeRequest.class)) { + return false; + } else { + ChangeRequest other = (ChangeRequest) obj; + return Objects.equals(options, other.options) + && Objects.equals(zone, other.zone) + && Objects.equals(toPb(), other.toPb()); + } + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), options, zone); + } + + private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { + input.defaultReadObject(); + this.dns = options.service(); + } + + static ChangeRequest fromPb(Dns dns, String zoneName, Change pb) { + ChangeRequestInfo info = ChangeRequestInfo.fromPb(pb); + return new ChangeRequest(dns, zoneName, new ChangeRequestInfo.BuilderImpl(info)); + } + + static Function fromPbFunction(final Dns dns, final String zoneName) { + return new Function() { + @Override + public ChangeRequest apply(com.google.api.services.dns.model.Change pb) { + return ChangeRequest.fromPb(dns, zoneName, pb); + } + }; + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ChangeRequestInfo.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ChangeRequestInfo.java new file mode 100644 index 000000000000..b63b4f4a0788 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ChangeRequestInfo.java @@ -0,0 +1,358 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.services.dns.model.Change; +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +/** + * A class representing an atomic update to a collection of {@link RecordSet}s within a {@code + * Zone}. + * + * @see Google Cloud DNS documentation + */ +public class ChangeRequestInfo implements Serializable { + + static final Function FROM_PB_FUNCTION = + new Function() { + @Override + public ChangeRequestInfo apply(Change pb) { + return ChangeRequestInfo.fromPb(pb); + } + }; + private static final long serialVersionUID = -9027378042756366333L; + private final List additions; + private final List deletions; + private final String id; + private final Long startTimeMillis; + private final ChangeRequestInfo.Status status; + + /** + * This enumerates the possible states of a change request. + * + * @see Google Cloud DNS + * documentation + */ + public enum Status { + PENDING, + DONE + } + + /** + * A builder for {@code ChangeRequestInfo}. + */ + public abstract static class Builder { + + /** + * Sets a collection of {@link RecordSet}s which are to be added to the zone upon executing this + * {@code ChangeRequestInfo}. + */ + public abstract Builder additions(List additions); + + /** + * Sets a collection of {@link RecordSet}s which are to be deleted from the zone upon executing + * this {@code ChangeRequestInfo}. + */ + public abstract Builder deletions(List deletions); + + /** + * Adds a {@link RecordSet} to be added to the zone upon executing this {@code + * ChangeRequestInfo}. + */ + public abstract Builder add(RecordSet recordSet); + + /** + * Adds a {@link RecordSet} to be deleted to the zone upon executing this + * {@code ChangeRequestInfo}. + */ + public abstract Builder delete(RecordSet recordSet); + + /** + * Clears the collection of {@link RecordSet}s which are to be added to the zone upon executing + * this {@code ChangeRequestInfo}. + */ + public abstract Builder clearAdditions(); + + /** + * Clears the collection of {@link RecordSet}s which are to be deleted from the zone upon + * executing this {@code ChangeRequestInfo}. + */ + public abstract Builder clearDeletions(); + + /** + * Removes a single {@link RecordSet} from the collection of records to be + * added to the zone upon executing this {@code ChangeRequestInfo}. + */ + public abstract Builder removeAddition(RecordSet recordSet); + + /** + * Removes a single {@link RecordSet} from the collection of records to be + * deleted from the zone upon executing this {@code ChangeRequestInfo}. + */ + public abstract Builder removeDeletion(RecordSet recordSet); + + /** + * Associates a server-assigned id to this {@code ChangeRequestInfo}. + */ + abstract Builder id(String id); + + /** + * Sets the time when this change request was started by a server. + */ + abstract Builder startTimeMillis(long startTimeMillis); + + /** + * Sets the current status of this {@code ChangeRequest}. + */ + abstract Builder status(ChangeRequest.Status status); + + /** + * Creates a {@code ChangeRequestInfo} instance populated by the values associated with this + * builder. + */ + public abstract ChangeRequestInfo build(); + } + + static class BuilderImpl extends Builder { + private List additions; + private List deletions; + private String id; + private Long startTimeMillis; + private ChangeRequestInfo.Status status; + + BuilderImpl() { + this.additions = new LinkedList<>(); + this.deletions = new LinkedList<>(); + } + + BuilderImpl(ChangeRequestInfo info) { + this.additions = Lists.newLinkedList(info.additions()); + this.deletions = Lists.newLinkedList(info.deletions()); + this.id = info.id(); + this.startTimeMillis = info.startTimeMillis; + this.status = info.status; + } + + @Override + public Builder additions(List additions) { + this.additions = Lists.newLinkedList(checkNotNull(additions)); + return this; + } + + @Override + public Builder deletions(List deletions) { + this.deletions = Lists.newLinkedList(checkNotNull(deletions)); + return this; + } + + @Override + public Builder add(RecordSet recordSet) { + this.additions.add(checkNotNull(recordSet)); + return this; + } + + @Override + public Builder delete(RecordSet recordSet) { + this.deletions.add(checkNotNull(recordSet)); + return this; + } + + @Override + public Builder clearAdditions() { + this.additions.clear(); + return this; + } + + @Override + public Builder clearDeletions() { + this.deletions.clear(); + return this; + } + + @Override + public Builder removeAddition(RecordSet recordSet) { + this.additions.remove(recordSet); + return this; + } + + @Override + public Builder removeDeletion(RecordSet recordSet) { + this.deletions.remove(recordSet); + return this; + } + + @Override + public ChangeRequestInfo build() { + return new ChangeRequestInfo(this); + } + + @Override + Builder id(String id) { + this.id = checkNotNull(id); + return this; + } + + @Override + Builder startTimeMillis(long startTimeMillis) { + this.startTimeMillis = startTimeMillis; + return this; + } + + @Override + Builder status(ChangeRequestInfo.Status status) { + this.status = checkNotNull(status); + return this; + } + } + + ChangeRequestInfo(BuilderImpl builder) { + this.additions = ImmutableList.copyOf(builder.additions); + this.deletions = ImmutableList.copyOf(builder.deletions); + this.id = builder.id; + this.startTimeMillis = builder.startTimeMillis; + this.status = builder.status; + } + + /** + * Returns an empty builder for the {@code ChangeRequestInfo} class. + */ + public static Builder builder() { + return new BuilderImpl(); + } + + /** + * Creates a builder populated with values of this {@code ChangeRequestInfo}. + */ + public Builder toBuilder() { + return new BuilderImpl(this); + } + + /** + * Returns the list of {@link RecordSet}s to be added to the zone upon submitting this change + * request. + */ + public List additions() { + return additions; + } + + /** + * Returns the list of {@link RecordSet}s to be deleted from the zone upon submitting this change + * request. + */ + public List deletions() { + return deletions; + } + + /** + * Returns the id assigned to this {@code ChangeRequest} by the server. + */ + public String id() { + return id; + } + + /** + * Returns the time when this {@code ChangeRequest} was started by the server. + */ + public Long startTimeMillis() { + return startTimeMillis; + } + + /** + * Returns the status of this {@code ChangeRequest}. If the change request has not been applied + * yet, the status is {@code PENDING}. + */ + public ChangeRequestInfo.Status status() { + return status; + } + + Change toPb() { + Change pb = new Change(); + // set id + if (id() != null) { + pb.setId(id()); + } + // set timestamp + if (startTimeMillis() != null) { + pb.setStartTime(ISODateTimeFormat.dateTime().withZoneUTC().print(startTimeMillis())); + } + // set status + if (status() != null) { + pb.setStatus(status().name().toLowerCase()); + } + // set a list of additions + pb.setAdditions(Lists.transform(additions(), RecordSet.TO_PB_FUNCTION)); + // set a list of deletions + pb.setDeletions(Lists.transform(deletions(), RecordSet.TO_PB_FUNCTION)); + return pb; + } + + static ChangeRequestInfo fromPb(Change pb) { + Builder builder = builder(); + if (pb.getId() != null) { + builder.id(pb.getId()); + } + if (pb.getStartTime() != null) { + builder.startTimeMillis(DateTime.parse(pb.getStartTime()).getMillis()); + } + if (pb.getStatus() != null) { + // we are assuming that status indicated in pb is a lower case version of the enum name + builder.status(ChangeRequest.Status.valueOf(pb.getStatus().toUpperCase())); + } + if (pb.getDeletions() != null) { + builder.deletions(Lists.transform(pb.getDeletions(), RecordSet.FROM_PB_FUNCTION)); + } + if (pb.getAdditions() != null) { + builder.additions(Lists.transform(pb.getAdditions(), RecordSet.FROM_PB_FUNCTION)); + } + return builder.build(); + } + + @Override + public boolean equals(Object other) { + return other != null && other.getClass().equals(ChangeRequestInfo.class) + && other instanceof ChangeRequestInfo && toPb().equals(((ChangeRequestInfo) other).toPb()); + } + + @Override + public int hashCode() { + return Objects.hash(additions, deletions, id, startTimeMillis, status); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("additions", additions) + .add("deletions", deletions) + .add("id", id) + .add("startTimeMillis", startTimeMillis) + .add("status", status) + .toString(); + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Dns.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Dns.java new file mode 100644 index 000000000000..f2b42f30a9f6 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Dns.java @@ -0,0 +1,537 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import com.google.common.base.Joiner; +import com.google.common.collect.Sets; +import com.google.gcloud.Page; +import com.google.gcloud.Service; +import com.google.gcloud.dns.spi.DnsRpc; + +import java.io.Serializable; +import java.util.Set; + +/** + * An interface for the Google Cloud DNS service. + * + * @see Google Cloud DNS + */ +public interface Dns extends Service { + + /** + * The fields of a project. + * + *

These values can be used to specify the fields to include in a partial response when calling + * {@link Dns#getProject(ProjectOption...)}. Project ID is always returned, even if not + * specified. + */ + enum ProjectField { + PROJECT_ID("id"), + PROJECT_NUMBER("number"), + QUOTA("quota"); + + private final String selector; + + ProjectField(String selector) { + this.selector = selector; + } + + String selector() { + return selector; + } + + static String selector(ProjectField... fields) { + Set fieldStrings = Sets.newHashSetWithExpectedSize(fields.length + 1); + fieldStrings.add(PROJECT_ID.selector()); + for (ProjectField field : fields) { + fieldStrings.add(field.selector()); + } + return Joiner.on(',').join(fieldStrings); + } + } + + /** + * The fields of a zone. + * + *

These values can be used to specify the fields to include in a partial response when calling + * {@link Dns#getZone(String, ZoneOption...)}. The name is always returned, even if not + * specified. + */ + enum ZoneField { + CREATION_TIME("creationTime"), + DESCRIPTION("description"), + DNS_NAME("dnsName"), + ZONE_ID("id"), + NAME("name"), + NAME_SERVER_SET("nameServerSet"), + NAME_SERVERS("nameServers"); + + private final String selector; + + ZoneField(String selector) { + this.selector = selector; + } + + String selector() { + return selector; + } + + static String selector(ZoneField... fields) { + Set fieldStrings = Sets.newHashSetWithExpectedSize(fields.length + 1); + fieldStrings.add(NAME.selector()); + for (ZoneField field : fields) { + fieldStrings.add(field.selector()); + } + return Joiner.on(',').join(fieldStrings); + } + } + + /** + * The fields of a record set. + * + *

These values can be used to specify the fields to include in a partial response when calling + * {@link Dns#listRecordSets(String, RecordSetListOption...)}. The name and type are always + * returned even if not selected. + */ + enum RecordSetField { + DNS_RECORDS("rrdatas"), + NAME("name"), + TTL("ttl"), + TYPE("type"); + + private final String selector; + + RecordSetField(String selector) { + this.selector = selector; + } + + String selector() { + return selector; + } + + static String selector(RecordSetField... fields) { + Set fieldStrings = Sets.newHashSetWithExpectedSize(fields.length + 1); + fieldStrings.add(NAME.selector()); + fieldStrings.add(TYPE.selector()); + for (RecordSetField field : fields) { + fieldStrings.add(field.selector()); + } + return Joiner.on(',').join(fieldStrings); + } + } + + /** + * The fields of a change request. + * + *

These values can be used to specify the fields to include in a partial response when calling + * {@link Dns#applyChangeRequest(String, ChangeRequestInfo, ChangeRequestOption...)} The ID is always + * returned even if not selected. + */ + enum ChangeRequestField { + ID("id"), + START_TIME("startTime"), + STATUS("status"), + ADDITIONS("additions"), + DELETIONS("deletions"); + + private final String selector; + + ChangeRequestField(String selector) { + this.selector = selector; + } + + String selector() { + return selector; + } + + static String selector(ChangeRequestField... fields) { + Set fieldStrings = Sets.newHashSetWithExpectedSize(fields.length + 1); + fieldStrings.add(ID.selector()); + for (ChangeRequestField field : fields) { + fieldStrings.add(field.selector()); + } + return Joiner.on(',').join(fieldStrings); + } + } + + /** + * The sorting order for listing. + */ + enum SortingOrder { + DESCENDING, ASCENDING; + + public String selector() { + return name().toLowerCase(); + } + } + + /** + * Class for specifying record set listing options. + */ + class RecordSetListOption extends AbstractOption implements Serializable { + + private static final long serialVersionUID = 1009627025381096098L; + + RecordSetListOption(DnsRpc.Option option, Object value) { + super(option, value); + } + + /** + * Returns an option to specify the record set's fields to be returned by the RPC call. + * + *

If this option is not provided all record fields are returned. {@code + * RecordSetField.fields} can be used to specify only the fields of interest. The name of the + * record set in always returned, even if not specified. {@link RecordSetField} provides a list + * of fields that can be used. + */ + public static RecordSetListOption fields(RecordSetField... fields) { + StringBuilder builder = new StringBuilder(); + builder.append("nextPageToken,rrsets(").append(RecordSetField.selector(fields)) + .append(')'); + return new RecordSetListOption(DnsRpc.Option.FIELDS, builder.toString()); + } + + /** + * Returns an option to specify a page token. + * + *

The page token (returned from a previous call to list) indicates from where listing should + * continue. + */ + public static RecordSetListOption pageToken(String pageToken) { + return new RecordSetListOption(DnsRpc.Option.PAGE_TOKEN, pageToken); + } + + /** + * The maximum number of record sets to return per RPC. + * + *

The server can return fewer record sets than requested. When there are more results than + * the page size, the server will return a page token that can be used to fetch other results. + */ + public static RecordSetListOption pageSize(int pageSize) { + return new RecordSetListOption(DnsRpc.Option.PAGE_SIZE, pageSize); + } + + /** + * Restricts the list to only record sets with this fully qualified domain name. + */ + public static RecordSetListOption dnsName(String dnsName) { + return new RecordSetListOption(DnsRpc.Option.NAME, dnsName); + } + + /** + * Restricts the list to return only record sets of this type. If present, {@link + * RecordSetListOption#dnsName(String)} must also be present. + */ + public static RecordSetListOption type(RecordSet.Type type) { + return new RecordSetListOption(DnsRpc.Option.DNS_TYPE, type.name()); + } + } + + /** + * Class for specifying zone field options. + */ + class ZoneOption extends AbstractOption implements Serializable { + + private static final long serialVersionUID = -8065564464895945037L; + + ZoneOption(DnsRpc.Option option, Object value) { + super(option, value); + } + + /** + * Returns an option to specify the zones's fields to be returned by the RPC call. + * + *

If this option is not provided all zone fields are returned. {@code ZoneOption.fields} can + * be used to specify only the fields of interest. Zone ID is always returned, even if not + * specified. {@link ZoneField} provides a list of fields that can be used. + */ + public static ZoneOption fields(ZoneField... fields) { + return new ZoneOption(DnsRpc.Option.FIELDS, ZoneField.selector(fields)); + } + } + + /** + * Class for specifying zone listing options. + */ + class ZoneListOption extends AbstractOption implements Serializable { + + private static final long serialVersionUID = -2830645032124504717L; + + ZoneListOption(DnsRpc.Option option, Object value) { + super(option, value); + } + + /** + * Returns an option to specify the zones's fields to be returned by the RPC call. + * + *

If this option is not provided all zone fields are returned. {@code ZoneOption.fields} can + * be used to specify only the fields of interest. Zone ID is always returned, even if not + * specified. {@link ZoneField} provides a list of fields that can be used. + */ + public static ZoneListOption fields(ZoneField... fields) { + StringBuilder builder = new StringBuilder(); + builder.append("nextPageToken,managedZones(").append(ZoneField.selector(fields)).append(')'); + return new ZoneListOption(DnsRpc.Option.FIELDS, builder.toString()); + } + + /** + * Returns an option to specify a page token. + * + *

The page token (returned from a previous call to list) indicates from where listing should + * continue. + */ + public static ZoneListOption pageToken(String pageToken) { + return new ZoneListOption(DnsRpc.Option.PAGE_TOKEN, pageToken); + } + + /** + * Restricts the list to only zone with this fully qualified domain name. + */ + public static ZoneListOption dnsName(String dnsName) { + StringBuilder builder = new StringBuilder(); + return new ZoneListOption(DnsRpc.Option.DNS_NAME, dnsName); + } + + /** + * The maximum number of zones to return per RPC. + * + *

The server can return fewer zones than requested. When there are more results than the + * page size, the server will return a page token that can be used to fetch other results. + */ + public static ZoneListOption pageSize(int pageSize) { + return new ZoneListOption(DnsRpc.Option.PAGE_SIZE, pageSize); + } + } + + /** + * Class for specifying project options. + */ + class ProjectOption extends AbstractOption implements Serializable { + + private static final long serialVersionUID = 6817937338218847748L; + + ProjectOption(DnsRpc.Option option, Object value) { + super(option, value); + } + + /** + * Returns an option to specify the project's fields to be returned by the RPC call. + * + *

If this option is not provided all project fields are returned. {@code + * ProjectOption.fields} can be used to specify only the fields of interest. Project ID is + * always returned, even if not specified. {@link ProjectField} provides a list of fields that + * can be used. + */ + public static ProjectOption fields(ProjectField... fields) { + return new ProjectOption(DnsRpc.Option.FIELDS, ProjectField.selector(fields)); + } + } + + /** + * Class for specifying change request field options. + */ + class ChangeRequestOption extends AbstractOption implements Serializable { + + private static final long serialVersionUID = 1067273695061077782L; + + ChangeRequestOption(DnsRpc.Option option, Object value) { + super(option, value); + } + + /** + * Returns an option to specify which fields of {@link ChangeRequest} should be returned by the + * service. + * + *

If this option is not provided all change request fields are returned. {@code + * ChangeRequestOption.fields} can be used to specify only the fields of interest. The ID of the + * change request is always returned, even if not specified. {@link ChangeRequestField} provides + * a list of fields that can be used. + */ + public static ChangeRequestOption fields(ChangeRequestField... fields) { + return new ChangeRequestOption( + DnsRpc.Option.FIELDS, + ChangeRequestField.selector(fields) + ); + } + } + + /** + * Class for specifying change request listing options. + */ + class ChangeRequestListOption extends AbstractOption implements Serializable { + + private static final long serialVersionUID = -900209143895376089L; + + ChangeRequestListOption(DnsRpc.Option option, Object value) { + super(option, value); + } + + /** + * Returns an option to specify which fields of{@link ChangeRequest} should be returned by the + * service. + * + *

If this option is not provided all change request fields are returned. {@code + * ChangeRequestOption.fields} can be used to specify only the fields of interest. The ID of the + * change request is always returned, even if not specified. {@link ChangeRequestField} provides + * a list of fields that can be used. + */ + public static ChangeRequestListOption fields(ChangeRequestField... fields) { + StringBuilder builder = new StringBuilder(); + builder.append("nextPageToken,changes(").append(ChangeRequestField.selector(fields)) + .append(')'); + return new ChangeRequestListOption(DnsRpc.Option.FIELDS, builder.toString()); + } + + /** + * Returns an option to specify a page token. + * + *

The page token (returned from a previous call to list) indicates from where listing should + * continue. + */ + public static ChangeRequestListOption pageToken(String pageToken) { + return new ChangeRequestListOption(DnsRpc.Option.PAGE_TOKEN, pageToken); + } + + /** + * The maximum number of change requests to return per RPC. + * + *

The server can return fewer change requests than requested. When there are more results + * than the page size, the server will return a page token that can be used to fetch other + * results. + */ + public static ChangeRequestListOption pageSize(int pageSize) { + return new ChangeRequestListOption(DnsRpc.Option.PAGE_SIZE, pageSize); + } + + /** + * Returns an option to specify whether the the change requests should be listed in ascending + * (most-recent last) or descending (most-recent first) order with respect to when the change + * request was accepted by the server. If this option is not provided, the listing order is + * undefined. + */ + public static ChangeRequestListOption sortOrder(SortingOrder order) { + return new ChangeRequestListOption(DnsRpc.Option.SORTING_ORDER, order.selector()); + } + } + + /** + * Creates a new zone. + * + *

Returns {@link Zone} object representing the new zone's information. In addition to the + * name, dns name and description (supplied by the user within the {@code zoneInfo} parameter), + * the returned object can include the following read-only fields supplied by the server: creation + * time, id, and list of name servers. The returned fields can be optionally restricted by + * specifying {@link ZoneOption}s. + * + * @throws DnsException upon failure + * @see Cloud DNS Managed Zones: + * create + */ + Zone create(ZoneInfo zoneInfo, ZoneOption... options); + + /** + * Returns the zone by the specified zone name. Returns {@code null} if the zone is not found. The + * returned fields can be optionally restricted by specifying {@link ZoneOption}s. + * + * @throws DnsException upon failure + * @see Cloud DNS Managed Zones: + * get + */ + Zone getZone(String zoneName, ZoneOption... options); + + /** + * Lists the zones inside the project. + * + *

This method returns zones in an unspecified order. New zones do not necessarily appear at + * the end of the list. Use {@link ZoneListOption} to restrict the listing to a domain name, set + * page size, and set page token. + * + * @return a page of zones + * @throws DnsException upon failure + * @see Cloud DNS Managed Zones: + * list + */ + Page listZones(ZoneListOption... options); + + /** + * Deletes an existing zone identified by name. Returns {@code true} if the zone was successfully + * deleted and {@code false} otherwise. + * + * @return {@code true} if zone was found and deleted and {@code false} otherwise + * @throws DnsException upon failure + * @see Cloud DNS Managed Zones: + * delete + */ + boolean delete(String zoneName); // delete does not admit any options + + /** + * Lists the record sets in the zone identified by name. + * + *

The fields to be returned, page size and page tokens can be specified using {@link + * RecordSetListOption}s. + * + * @throws DnsException upon failure or if the zone cannot be found + * @see Cloud DNS + * ResourceRecordSets: list + */ + Page listRecordSets(String zoneName, RecordSetListOption... options); + + /** + * Retrieves the information about the current project. The returned fields can be optionally + * restricted by specifying {@link ProjectOption}s. + * + * @throws DnsException upon failure + * @see Cloud DNS Projects: get + */ + ProjectInfo getProject(ProjectOption... fields); + + /** + * Submits a change request for the specified zone. The returned object contains the following + * read-only fields supplied by the server: id, start time and status. time, id, and list of name + * servers. The fields to be returned can be selected by {@link ChangeRequestOption}s. + * + * @return the new {@link ChangeRequest} + * @throws DnsException upon failure if zone is not found + * @see Cloud DNS Changes: create + */ + ChangeRequest applyChangeRequest(String zoneName, ChangeRequestInfo changeRequest, + ChangeRequestOption... options); + + /** + * Retrieves updated information about a change request previously submitted for a zone identified + * by ID. Returns {@code null} if the request cannot be found and throws an exception if the zone + * does not exist. The fields to be returned using can be specified using {@link + * ChangeRequestOption}s. + * + * @throws DnsException upon failure or if the zone cannot be found + * @see Cloud DNS Chages: get + */ + ChangeRequest getChangeRequest(String zoneName, String changeRequestId, + ChangeRequestOption... options); + + /** + * Lists the change requests for the zone identified by name that were submitted to the service. + * + *

The sorting order for changes (based on when they were received by the server), fields to be + * returned, page size and page token can be specified using {@link ChangeRequestListOption}s. + * + * @return A page of change requests + * @throws DnsException upon failure or if the zone cannot be found + * @see Cloud DNS Chages: list + */ + Page listChangeRequests(String zoneName, ChangeRequestListOption... options); +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsException.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsException.java new file mode 100644 index 000000000000..1ecb98a3fdc6 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsException.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import com.google.common.collect.ImmutableSet; +import com.google.gcloud.BaseServiceException; +import com.google.gcloud.RetryHelper.RetryHelperException; +import com.google.gcloud.RetryHelper.RetryInterruptedException; + +import java.io.IOException; +import java.util.Set; + +/** + * DNS service exception. + */ +public class DnsException extends BaseServiceException { + + // see: https://cloud.google.com/dns/troubleshooting + private static final Set RETRYABLE_ERRORS = ImmutableSet.of( + new Error(429, null), + new Error(500, null), + new Error(502, null), + new Error(503, null), + new Error(null, "userRateLimitExceeded"), + new Error(null, "rateLimitExceeded")); + private static final long serialVersionUID = 490302380416260252L; + + public DnsException(IOException exception) { + super(exception, true); + } + + private DnsException(int code, String message) { + super(code, message, null, true); + } + + @Override + protected Set retryableErrors() { + return RETRYABLE_ERRORS; + } + + /** + * Translate RetryHelperException to the DnsException that caused the error. This method will + * always throw an exception. + * + * @throws DnsException when {@code ex} was caused by a {@code DnsException} + * @throws RetryInterruptedException when {@code ex} is a {@code RetryInterruptedException} + */ + static DnsException translateAndThrow(RetryHelperException ex) { + BaseServiceException.translateAndPropagateIfPossible(ex); + throw new DnsException(UNKNOWN_CODE, ex.getMessage()); + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsFactory.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsFactory.java new file mode 100644 index 000000000000..734652afb24d --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsFactory.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import com.google.gcloud.ServiceFactory; + +/** + * An interface for Dns factories. + */ +public interface DnsFactory extends ServiceFactory { +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsImpl.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsImpl.java new file mode 100644 index 000000000000..51ab0bd92720 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsImpl.java @@ -0,0 +1,320 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.gcloud.RetryHelper.RetryHelperException; +import static com.google.gcloud.RetryHelper.runWithRetries; + +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.ResourceRecordSet; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.google.gcloud.BaseService; +import com.google.gcloud.Page; +import com.google.gcloud.PageImpl; +import com.google.gcloud.RetryHelper; +import com.google.gcloud.dns.spi.DnsRpc; + +import java.util.Map; +import java.util.concurrent.Callable; + +/** + * A default implementation of Dns. + */ +final class DnsImpl extends BaseService implements Dns { + + private final DnsRpc dnsRpc; + + private static class ZonePageFetcher implements PageImpl.NextPageFetcher { + + private static final long serialVersionUID = 2158209410430566961L; + private final Map requestOptions; + private final DnsOptions serviceOptions; + + ZonePageFetcher(DnsOptions serviceOptions, String cursor, + Map optionMap) { + this.requestOptions = + PageImpl.nextRequestOptions(DnsRpc.Option.PAGE_TOKEN, cursor, optionMap); + this.serviceOptions = serviceOptions; + } + + @Override + public Page nextPage() { + return listZones(serviceOptions, requestOptions); + } + } + + private static class ChangeRequestPageFetcher implements PageImpl.NextPageFetcher { + + private static final long serialVersionUID = 4473265130673029139L; + private final String zoneName; + private final Map requestOptions; + private final DnsOptions serviceOptions; + + ChangeRequestPageFetcher(String zoneName, DnsOptions serviceOptions, String cursor, + Map optionMap) { + this.zoneName = zoneName; + this.requestOptions = + PageImpl.nextRequestOptions(DnsRpc.Option.PAGE_TOKEN, cursor, optionMap); + this.serviceOptions = serviceOptions; + } + + @Override + public Page nextPage() { + return listChangeRequests(zoneName, serviceOptions, requestOptions); + } + } + + private static class DnsRecordPageFetcher implements PageImpl.NextPageFetcher { + + private static final long serialVersionUID = -6039369212511530846L; + private final Map requestOptions; + private final DnsOptions serviceOptions; + private final String zoneName; + + DnsRecordPageFetcher(String zoneName, DnsOptions serviceOptions, String cursor, + Map optionMap) { + this.zoneName = zoneName; + this.requestOptions = + PageImpl.nextRequestOptions(DnsRpc.Option.PAGE_TOKEN, cursor, optionMap); + this.serviceOptions = serviceOptions; + } + + @Override + public Page nextPage() { + return listRecordSets(zoneName, serviceOptions, requestOptions); + } + } + + DnsImpl(DnsOptions options) { + super(options); + dnsRpc = options.rpc(); + } + + @Override + public Page listZones(ZoneListOption... options) { + return listZones(options(), optionMap(options)); + } + + private static Page listZones(final DnsOptions serviceOptions, + final Map optionsMap) { + // define transformation function + // this differs from the other list operations since zone is functional and requires dns service + Function pbToZoneFunction = new Function() { + @Override + public Zone apply( + com.google.api.services.dns.model.ManagedZone zonePb) { + return Zone.fromPb(serviceOptions.service(), zonePb); + } + }; + try { + // get a list of managed zones + final DnsRpc rpc = serviceOptions.rpc(); + DnsRpc.ListResult result = + runWithRetries(new Callable>() { + @Override + public DnsRpc.ListResult call() { + return rpc.listZones(optionsMap); + } + }, serviceOptions.retryParams(), EXCEPTION_HANDLER); + String cursor = result.pageToken(); + // transform that list into zone objects + Iterable zones = result.results() == null + ? ImmutableList.of() : Iterables.transform(result.results(), pbToZoneFunction); + return new PageImpl<>(new ZonePageFetcher(serviceOptions, cursor, optionsMap), + cursor, zones); + } catch (RetryHelperException e) { + throw DnsException.translateAndThrow(e); + } + } + + @Override + public Page listChangeRequests(String zoneName, + ChangeRequestListOption... options) { + return listChangeRequests(zoneName, options(), optionMap(options)); + } + + private static Page listChangeRequests(final String zoneName, + final DnsOptions serviceOptions, final Map optionsMap) { + try { + // get a list of changes + final DnsRpc rpc = serviceOptions.rpc(); + DnsRpc.ListResult result = runWithRetries(new Callable>() { + @Override + public DnsRpc.ListResult call() { + return rpc.listChangeRequests(zoneName, optionsMap); + } + }, serviceOptions.retryParams(), EXCEPTION_HANDLER); + String cursor = result.pageToken(); + // transform that list into change request objects + Iterable changes = result.results() == null + ? ImmutableList.of() + : Iterables.transform(result.results(), + ChangeRequest.fromPbFunction(serviceOptions.service(), zoneName)); + return new PageImpl<>(new ChangeRequestPageFetcher(zoneName, serviceOptions, cursor, + optionsMap), cursor, changes); + } catch (RetryHelperException e) { + throw DnsException.translateAndThrow(e); + } + } + + @Override + public Page listRecordSets(String zoneName, RecordSetListOption... options) { + return listRecordSets(zoneName, options(), optionMap(options)); + } + + private static Page listRecordSets(final String zoneName, + final DnsOptions serviceOptions, final Map optionsMap) { + try { + // get a list of record sets + final DnsRpc rpc = serviceOptions.rpc(); + DnsRpc.ListResult result = runWithRetries( + new Callable>() { + @Override + public DnsRpc.ListResult call() { + return rpc.listRecordSets(zoneName, optionsMap); + } + }, serviceOptions.retryParams(), EXCEPTION_HANDLER); + String cursor = result.pageToken(); + // transform that list into record sets + Iterable recordSets = result.results() == null + ? ImmutableList.of() + : Iterables.transform(result.results(), RecordSet.FROM_PB_FUNCTION); + return new PageImpl<>(new DnsRecordPageFetcher(zoneName, serviceOptions, cursor, optionsMap), + cursor, recordSets); + } catch (RetryHelperException e) { + throw DnsException.translateAndThrow(e); + } + } + + @Override + public Zone create(final ZoneInfo zoneInfo, Dns.ZoneOption... options) { + final Map optionsMap = optionMap(options); + try { + com.google.api.services.dns.model.ManagedZone answer = runWithRetries( + new Callable() { + @Override + public com.google.api.services.dns.model.ManagedZone call() { + return dnsRpc.create(zoneInfo.toPb(), optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER); + return answer == null ? null : Zone.fromPb(this, answer); + } catch (RetryHelper.RetryHelperException ex) { + throw DnsException.translateAndThrow(ex); + } + } + + @Override + public Zone getZone(final String zoneName, Dns.ZoneOption... options) { + final Map optionsMap = optionMap(options); + try { + com.google.api.services.dns.model.ManagedZone answer = runWithRetries( + new Callable() { + @Override + public com.google.api.services.dns.model.ManagedZone call() { + return dnsRpc.getZone(zoneName, optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER); + return answer == null ? null : Zone.fromPb(this, answer); + } catch (RetryHelper.RetryHelperException ex) { + throw DnsException.translateAndThrow(ex); + } + } + + @Override + public boolean delete(final String zoneName) { + try { + return runWithRetries(new Callable() { + @Override + public Boolean call() { + return dnsRpc.deleteZone(zoneName); + } + }, options().retryParams(), EXCEPTION_HANDLER); + } catch (RetryHelper.RetryHelperException ex) { + throw DnsException.translateAndThrow(ex); + } + } + + @Override + public ProjectInfo getProject(Dns.ProjectOption... fields) { + final Map optionsMap = optionMap(fields); + try { + com.google.api.services.dns.model.Project answer = runWithRetries( + new Callable() { + @Override + public com.google.api.services.dns.model.Project call() { + return dnsRpc.getProject(optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER); + return answer == null ? null : ProjectInfo.fromPb(answer); // should never be null + } catch (RetryHelper.RetryHelperException ex) { + throw DnsException.translateAndThrow(ex); + } + } + + @Override + public ChangeRequest applyChangeRequest(final String zoneName, + final ChangeRequestInfo changeRequest, ChangeRequestOption... options) { + final Map optionsMap = optionMap(options); + try { + com.google.api.services.dns.model.Change answer = + runWithRetries( + new Callable() { + @Override + public com.google.api.services.dns.model.Change call() { + return dnsRpc.applyChangeRequest(zoneName, changeRequest.toPb(), optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER); + return answer == null ? null : ChangeRequest.fromPb(this, zoneName, answer); // not null + } catch (RetryHelper.RetryHelperException ex) { + throw DnsException.translateAndThrow(ex); + } + } + + @Override + public ChangeRequest getChangeRequest(final String zoneName, final String changeRequestId, + Dns.ChangeRequestOption... options) { + final Map optionsMap = optionMap(options); + try { + com.google.api.services.dns.model.Change answer = + runWithRetries( + new Callable() { + @Override + public com.google.api.services.dns.model.Change call() { + return dnsRpc.getChangeRequest(zoneName, changeRequestId, optionsMap); + } + }, options().retryParams(), EXCEPTION_HANDLER); + return answer == null ? null : ChangeRequest.fromPb(this, zoneName, answer); + } catch (RetryHelper.RetryHelperException ex) { + throw DnsException.translateAndThrow(ex); + } + } + + private Map optionMap(AbstractOption... options) { + Map temp = Maps.newEnumMap(DnsRpc.Option.class); + for (AbstractOption option : options) { + Object prev = temp.put(option.rpcOption(), option.value()); + checkArgument(prev == null, "Duplicate option %s", option); + } + return ImmutableMap.copyOf(temp); + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsOptions.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsOptions.java new file mode 100644 index 000000000000..541e7a6c6ea7 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/DnsOptions.java @@ -0,0 +1,116 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import com.google.common.collect.ImmutableSet; +import com.google.gcloud.ServiceOptions; +import com.google.gcloud.dns.spi.DefaultDnsRpc; +import com.google.gcloud.dns.spi.DnsRpc; +import com.google.gcloud.dns.spi.DnsRpcFactory; + +import java.util.Set; + +public class DnsOptions extends ServiceOptions { + + private static final long serialVersionUID = -519128051411747771L; + private static final String GC_DNS_RW = "https://www.googleapis.com/auth/ndev.clouddns.readwrite"; + private static final Set SCOPES = ImmutableSet.of(GC_DNS_RW); + + public static class DefaultDnsFactory implements DnsFactory { + private static final DnsFactory INSTANCE = new DefaultDnsFactory(); + + @Override + public Dns create(DnsOptions options) { + return new DnsImpl(options); + } + } + + public static class DefaultDnsRpcFactory implements DnsRpcFactory { + + private static final DnsRpcFactory INSTANCE = new DefaultDnsRpcFactory(); + + @Override + public DnsRpc create(DnsOptions options) { + return new DefaultDnsRpc(options); + } + } + + public static class Builder extends ServiceOptions.Builder { + + private Builder() { + } + + private Builder(DnsOptions options) { + super(options); + } + + @Override + public DnsOptions build() { + return new DnsOptions(this); + } + } + + private DnsOptions(Builder builder) { + super(DnsFactory.class, DnsRpcFactory.class, builder); + } + + @SuppressWarnings("unchecked") + @Override + protected DnsFactory defaultServiceFactory() { + return DefaultDnsFactory.INSTANCE; + } + + @SuppressWarnings("unchecked") + @Override + protected DnsRpcFactory defaultRpcFactory() { + return DefaultDnsRpcFactory.INSTANCE; + } + + @Override + protected Set scopes() { + return SCOPES; + } + + @SuppressWarnings("unchecked") + @Override + public Builder toBuilder() { + return new Builder(this); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a default instance of {@code DnsOptions} with the project ID and credentials inferred + * from the environment. + */ + public static DnsOptions defaultInstance() { + return builder().build(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof DnsOptions && baseEquals((DnsOptions) obj); + } + + @Override + public int hashCode() { + return baseHashCode(); + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ProjectInfo.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ProjectInfo.java new file mode 100644 index 000000000000..319f06ad2444 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ProjectInfo.java @@ -0,0 +1,288 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static com.google.api.client.repackaged.com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.MoreObjects; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Objects; + +/** + * The class provides the Google Cloud DNS information associated with this project. A project is a + * top level container for resources including {@code Zone}s. Projects can be created only in the + * APIs console. + * + * @see Google Cloud DNS documentation + */ +public class ProjectInfo implements Serializable { + + private static final long serialVersionUID = 8696578863323485036L; + private final String id; + private final BigInteger number; + private final Quota quota; + + /** + * This class represents quotas assigned to the {@code ProjectInfo}. + * + * @see Google Cloud DNS + * documentation + */ + public static class Quota implements Serializable { + + private static final long serialVersionUID = 6854685970605363639L; + private final int zones; + private final int resourceRecordsPerRrset; + private final int rrsetAdditionsPerChange; + private final int rrsetDeletionsPerChange; + private final int rrsetsPerZone; + private final int totalRrdataSizePerChange; + + /** + * Creates an instance of {@code Quota}. + * + *

This is the only way of creating an instance of {@code Quota}. As the service does not + * allow for specifying options, quota is an "all-or-nothing object" and we do not need a + * builder. + */ + Quota(int zones, + int resourceRecordsPerRrset, + int rrsetAdditionsPerChange, + int rrsetDeletionsPerChange, + int rrsetsPerZone, + int totalRrdataSizePerChange) { + this.zones = zones; + this.resourceRecordsPerRrset = resourceRecordsPerRrset; + this.rrsetAdditionsPerChange = rrsetAdditionsPerChange; + this.rrsetDeletionsPerChange = rrsetDeletionsPerChange; + this.rrsetsPerZone = rrsetsPerZone; + this.totalRrdataSizePerChange = totalRrdataSizePerChange; + } + + /** + * Returns the maximum allowed number of zones in the project. + */ + public int zones() { + return zones; + } + + /** + * Returns the maximum allowed number of records per {@link RecordSet}. + */ + public int resourceRecordsPerRrset() { + return resourceRecordsPerRrset; + } + + /** + * Returns the maximum allowed number of {@link RecordSet}s to add per {@link + * ChangeRequest}. + */ + public int rrsetAdditionsPerChange() { + return rrsetAdditionsPerChange; + } + + /** + * Returns the maximum allowed number of {@link RecordSet}s to delete per {@link + * ChangeRequest}. + */ + public int rrsetDeletionsPerChange() { + return rrsetDeletionsPerChange; + } + + /** + * Returns the maximum allowed number of {@link RecordSet}s per {@link ZoneInfo} in the + * project. + */ + public int rrsetsPerZone() { + return rrsetsPerZone; + } + + /** + * Returns the maximum allowed size for total records in one ChangesRequest in bytes. + */ + public int totalRrdataSizePerChange() { + return totalRrdataSizePerChange; + } + + @Override + public boolean equals(Object other) { + return (other instanceof Quota) && this.toPb().equals(((Quota) other).toPb()); + } + + @Override + public int hashCode() { + return Objects.hash(zones, resourceRecordsPerRrset, rrsetAdditionsPerChange, + rrsetDeletionsPerChange, rrsetsPerZone, totalRrdataSizePerChange); + } + + com.google.api.services.dns.model.Quota toPb() { + com.google.api.services.dns.model.Quota pb = new com.google.api.services.dns.model.Quota(); + pb.setManagedZones(zones); + pb.setResourceRecordsPerRrset(resourceRecordsPerRrset); + pb.setRrsetAdditionsPerChange(rrsetAdditionsPerChange); + pb.setRrsetDeletionsPerChange(rrsetDeletionsPerChange); + pb.setRrsetsPerManagedZone(rrsetsPerZone); + pb.setTotalRrdataSizePerChange(totalRrdataSizePerChange); + return pb; + } + + static Quota fromPb(com.google.api.services.dns.model.Quota pb) { + Quota quota = new Quota(pb.getManagedZones(), + pb.getResourceRecordsPerRrset(), + pb.getRrsetAdditionsPerChange(), + pb.getRrsetDeletionsPerChange(), + pb.getRrsetsPerManagedZone(), + pb.getTotalRrdataSizePerChange() + ); + return quota; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("zones", zones) + .add("resourceRecordsPerRrset", resourceRecordsPerRrset) + .add("rrsetAdditionsPerChange", rrsetAdditionsPerChange) + .add("rrsetDeletionsPerChange", rrsetDeletionsPerChange) + .add("rrsetsPerZone", rrsetsPerZone) + .add("totalRrdataSizePerChange", totalRrdataSizePerChange) + .toString(); + } + } + + /** + * A builder for {@code ProjectInfo}. + */ + static class Builder { + private String id; + private BigInteger number; + private Quota quota; + + private Builder() { + } + + /** + * Sets an id of the project. + */ + Builder id(String id) { + this.id = checkNotNull(id); + return this; + } + + /** + * Sets a number of the project. + */ + Builder number(BigInteger number) { + this.number = checkNotNull(number); + return this; + } + + /** + * Sets quotas assigned to the project. + */ + Builder quota(Quota quota) { + this.quota = checkNotNull(quota); + return this; + } + + /** + * Builds an instance of the {@code ProjectInfo}. + */ + ProjectInfo build() { + return new ProjectInfo(this); + } + } + + private ProjectInfo(Builder builder) { + this.id = builder.id; + this.number = builder.number; + this.quota = builder.quota; + } + + /** + * Returns a builder for {@code ProjectInfo}. + */ + static Builder builder() { + return new Builder(); + } + + /** + * Returns the {@code Quota} object which contains quotas assigned to this project. + */ + public Quota quota() { + return quota; + } + + /** + * Returns project number. For internal use only. + */ + BigInteger number() { + return number; + } + + /** + * Returns project id. For internal use only. + */ + String id() { + return id; + } + + com.google.api.services.dns.model.Project toPb() { + com.google.api.services.dns.model.Project pb = new com.google.api.services.dns.model.Project(); + pb.setId(id); + pb.setNumber(number); + if (this.quota != null) { + pb.setQuota(quota.toPb()); + } + return pb; + } + + static ProjectInfo fromPb(com.google.api.services.dns.model.Project pb) { + Builder builder = builder(); + if (pb.getId() != null) { + builder.id(pb.getId()); + } + if (pb.getNumber() != null) { + builder.number(pb.getNumber()); + } + if (pb.getQuota() != null) { + builder.quota(Quota.fromPb(pb.getQuota())); + } + return builder.build(); + } + + @Override + public boolean equals(Object other) { + return (other instanceof ProjectInfo) && toPb().equals(((ProjectInfo) other).toPb()); + } + + @Override + public int hashCode() { + return Objects.hash(id, number, quota); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("number", number) + .add("quota", quota) + .toString(); + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/RecordSet.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/RecordSet.java new file mode 100644 index 000000000000..dc6d956406c3 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/RecordSet.java @@ -0,0 +1,318 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.services.dns.model.ResourceRecordSet; +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.primitives.Ints; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * A class that represents a Google Cloud DNS record set. + * + *

A {@code RecordSet} is the unit of data that will be returned by the DNS servers upon a DNS + * request for a specific domain. The {@code RecordSet} holds the current state of the DNS records + * that make up a zone. You can read the records but you cannot modify them directly. Rather, you + * edit the records in a zone by creating a {@link ChangeRequest}. + * + * @see Google Cloud DNS + * documentation + */ +public class RecordSet implements Serializable { + + static final Function FROM_PB_FUNCTION = + new Function() { + @Override + public RecordSet apply(ResourceRecordSet pb) { + return RecordSet.fromPb(pb); + } + }; + static final Function TO_PB_FUNCTION = + new Function() { + @Override + public ResourceRecordSet apply(RecordSet recordSet) { + return recordSet.toPb(); + } + }; + private static final long serialVersionUID = 8148009870800115261L; + private final String name; + private final List rrdatas; + private final Integer ttl; // this is in seconds + private final Type type; + + /** + * Enum for the DNS record types supported by Cloud DNS. + * + *

Google Cloud DNS currently supports records of type A, AAAA, CNAME, MX NAPTR, NS, PTR, SOA, + * SPF, SRV, TXT. + * + * @see Cloud DNS + * supported record types + */ + public enum Type { + /** + * Address record, which is used to map host names to their IPv4 address. + */ + A, + /** + * IPv6 Address record, which is used to map host names to their IPv6 address. + */ + AAAA, + /** + * Canonical name record, which is used to alias names. + */ + CNAME, + /** + * Mail exchange record, which is used in routing requests to mail servers. + */ + MX, + /** + * Naming authority pointer record, defined by RFC3403. + */ + NAPTR, + /** + * Name server record, which delegates a DNS zone to an authoritative server. + */ + NS, + /** + * Pointer record, which is often used for reverse DNS lookups. + */ + PTR, + /** + * Start of authority record, which specifies authoritative information about a DNS zone. + */ + SOA, + /** + * Sender policy framework record, which is used in email validation systems. + */ + SPF, + /** + * Service locator record, which is used by some voice over IP, instant messaging protocols and + * other applications. + */ + SRV, + /** + * Text record, which can contain arbitrary text and can also be used to define machine readable + * data such as security or abuse prevention information. + */ + TXT + } + + /** + * A builder for {@link RecordSet}. + */ + public static class Builder { + + private List rrdatas = new LinkedList<>(); + private String name; + private Integer ttl; + private Type type; + + private Builder(String name, Type type) { + this.name = checkNotNull(name); + this.type = checkNotNull(type); + } + + /** + * Creates a builder and pre-populates attributes with the values from the provided {@code + * RecordSet} instance. + */ + private Builder(RecordSet record) { + this.name = record.name; + this.ttl = record.ttl; + this.type = record.type; + this.rrdatas.addAll(record.rrdatas); + } + + /** + * Adds a record to the record set. The records should be as defined in RFC 1035 (section 5) and + * RFC 1034 (section 3.6.1). Examples of records are available in Google DNS documentation. + * + * @see Google + * DNS documentation . + */ + public Builder addRecord(String record) { + this.rrdatas.add(checkNotNull(record)); + return this; + } + + /** + * Removes a record from the set. An exact match is required. + */ + public Builder removeRecord(String record) { + this.rrdatas.remove(checkNotNull(record)); + return this; + } + + /** + * Removes all the records. + */ + public Builder clearRecords() { + this.rrdatas.clear(); + return this; + } + + /** + * Replaces the current records with the provided list of records. + */ + public Builder records(List records) { + this.rrdatas = Lists.newLinkedList(checkNotNull(records)); + return this; + } + + /** + * Sets the name for this record set. For example, www.example.com. + */ + public Builder name(String name) { + this.name = checkNotNull(name); + return this; + } + + /** + * Sets the time that this record can be cached by resolvers. This number must be non-negative. + * The maximum duration must be equivalent to at most {@link Integer#MAX_VALUE} seconds. + * + * @param duration A non-negative number of time units + * @param unit The unit of the ttl parameter + */ + public Builder ttl(int duration, TimeUnit unit) { + checkArgument(duration >= 0, + "Duration cannot be negative. The supplied value was %s.", duration); + checkNotNull(unit); + // we cannot have long because pb does not support it + long converted = unit.toSeconds(duration); + ttl = Ints.checkedCast(converted); + return this; + } + + /** + * The identifier of a supported record type, for example, A, AAAA, MX, TXT, and so on. + */ + public Builder type(Type type) { + this.type = checkNotNull(type); + return this; + } + + /** + * Builds the record set. + */ + public RecordSet build() { + return new RecordSet(this); + } + } + + private RecordSet(Builder builder) { + this.name = builder.name; + this.rrdatas = ImmutableList.copyOf(builder.rrdatas); + this.ttl = builder.ttl; + this.type = builder.type; + } + + /** + * Creates a builder pre-populated with the attribute values of this instance. + */ + public Builder toBuilder() { + return new Builder(this); + } + + /** + * Creates a {@code RecordSet} builder for the given {@code name} and {@code type}. + */ + public static Builder builder(String name, Type type) { + return new Builder(name, type); + } + + /** + * Returns the user-assigned name of this record set. + */ + public String name() { + return name; + } + + /** + * Returns a list of records stored in this record set. + */ + public List records() { + return rrdatas; + } + + /** + * Returns the number of seconds that this record set can be cached by resolvers. + */ + public Integer ttl() { + return ttl; + } + + /** + * Returns the type of this record set. + */ + public Type type() { + return type; + } + + @Override + public int hashCode() { + return Objects.hash(name, rrdatas, ttl, type); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof RecordSet && Objects.equals(this.toPb(), ((RecordSet) obj).toPb()); + } + + com.google.api.services.dns.model.ResourceRecordSet toPb() { + com.google.api.services.dns.model.ResourceRecordSet pb = + new com.google.api.services.dns.model.ResourceRecordSet(); + pb.setName(this.name()); + pb.setRrdatas(this.records()); + pb.setTtl(this.ttl()); + pb.setType(this.type().name()); + return pb; + } + + static RecordSet fromPb(com.google.api.services.dns.model.ResourceRecordSet pb) { + Builder builder = builder(pb.getName(), Type.valueOf(pb.getType())); + if (pb.getRrdatas() != null) { + builder.records(pb.getRrdatas()); + } + if (pb.getTtl() != null) { + builder.ttl(pb.getTtl(), TimeUnit.SECONDS); + } + return builder.build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name()) + .add("rrdatas", records()) + .add("ttl", ttl()) + .add("type", type()) + .toString(); + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Zone.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Zone.java new file mode 100644 index 000000000000..9930bfdbad67 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/Zone.java @@ -0,0 +1,217 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.gcloud.Page; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.List; +import java.util.Objects; + +/** + * A Google Cloud DNS Zone object. + * + *

A zone is the container for all of your record sets that share the same DNS name prefix, for + * example, example.com. Zones are automatically assigned a set of name servers when they are + * created to handle responding to DNS queries for that zone. A zone has quotas for the number of + * record sets that it can include. + * + * @see Google Cloud DNS managed zone + * documentation + */ +public class Zone extends ZoneInfo { + + private static final long serialVersionUID = -5817771337847861598L; + private final DnsOptions options; + private transient Dns dns; + + /** + * Builder for {@code Zone}. + */ + public static class Builder extends ZoneInfo.Builder { + private final Dns dns; + private final ZoneInfo.BuilderImpl infoBuilder; + + private Builder(Zone zone) { + this.dns = zone.dns; + this.infoBuilder = new ZoneInfo.BuilderImpl(zone); + } + + @Override + public Builder name(String name) { + infoBuilder.name(name); + return this; + } + + @Override + Builder id(String id) { + infoBuilder.id(id); + return this; + } + + @Override + Builder creationTimeMillis(long creationTimeMillis) { + infoBuilder.creationTimeMillis(creationTimeMillis); + return this; + } + + @Override + public Builder dnsName(String dnsName) { + infoBuilder.dnsName(dnsName); + return this; + } + + @Override + public Builder description(String description) { + infoBuilder.description(description); + return this; + } + + @Override + Builder nameServerSet(String nameServerSet) { + infoBuilder.nameServerSet(nameServerSet); + return this; + } + + @Override + Builder nameServers(List nameServers) { + infoBuilder.nameServers(nameServers); // infoBuilder makes a copy + return this; + } + + @Override + public Zone build() { + return new Zone(dns, infoBuilder); + } + } + + Zone(Dns dns, ZoneInfo.BuilderImpl infoBuilder) { + super(infoBuilder); + this.dns = dns; + this.options = dns.options(); + } + + @Override + public Builder toBuilder() { + return new Builder(this); + } + + /** + * Retrieves the latest information about the zone. The method retrieves the zone by name. + * + * @param options optional restriction on what fields should be fetched + * @return zone object containing updated information or {@code null} if not not found + * @throws DnsException upon failure + */ + public Zone reload(Dns.ZoneOption... options) { + return dns.getZone(name(), options); + } + + /** + * Deletes the zone. The method deletes the zone by name. + * + * @return {@code true} is zone was found and deleted and {@code false} otherwise + * @throws DnsException upon failure + */ + public boolean delete() { + return dns.delete(name()); + } + + /** + * Lists all {@link RecordSet}s associated with this zone. The method searches for zone by name. + * + * @param options optional restriction on listing and fields of {@link RecordSet}s returned + * @return a page of record sets + * @throws DnsException upon failure or if the zone is not found + */ + public Page listRecordSets(Dns.RecordSetListOption... options) { + return dns.listRecordSets(name(), options); + } + + /** + * Submits {@link ChangeRequestInfo} to the service for it to applied to this zone. The method + * searches for zone by name. + * + * @param options optional restriction on what fields of {@link ChangeRequest} should be returned + * @return ChangeRequest with server-assigned ID + * @throws DnsException upon failure or if the zone is not found + */ + public ChangeRequest applyChangeRequest(ChangeRequestInfo changeRequest, + Dns.ChangeRequestOption... options) { + checkNotNull(changeRequest); + return dns.applyChangeRequest(name(), changeRequest, options); + } + + /** + * Retrieves an updated information about a change request previously submitted to be applied to + * this zone. Returns a {@link ChangeRequest} or {@code null} if the change request was not found. + * Throws {@link DnsException} if the zone is not found. + * + * @param options optional restriction on what fields of {@link ChangeRequest} should be returned + * @return updated ChangeRequest + * @throws DnsException upon failure or if the zone is not found + * @throws NullPointerException if {@code changeRequestId} is null + */ + public ChangeRequest getChangeRequest(String changeRequestId, + Dns.ChangeRequestOption... options) { + checkNotNull(changeRequestId); + return dns.getChangeRequest(name(), changeRequestId, options); + } + + /** + * Retrieves all change requests for this zone. The method searches for zone by name. Returns a + * page of {@link ChangeRequest}s. + * + * @param options optional restriction on listing and fields to be returned + * @return a page of change requests + * @throws DnsException upon failure or if the zone is not found + */ + public Page listChangeRequests(Dns.ChangeRequestListOption... options) { + return dns.listChangeRequests(name(), options); + } + + /** + * Returns the {@link Dns} service object associated with this zone. + */ + public Dns dns() { + return this.dns; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Zone && Objects.equals(toPb(), ((Zone) obj).toPb()) + && Objects.equals(options, ((Zone) obj).options); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), options); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + this.dns = options.service(); + } + + static Zone fromPb(Dns dns, com.google.api.services.dns.model.ManagedZone zone) { + ZoneInfo info = ZoneInfo.fromPb(zone); + return new Zone(dns, new ZoneInfo.BuilderImpl(info)); + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ZoneInfo.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ZoneInfo.java new file mode 100644 index 000000000000..38a88b67777e --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/ZoneInfo.java @@ -0,0 +1,317 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; + +/** + * A {@code Zone} represents a DNS zone hosted by the Google Cloud DNS service. A zone is a subtree + * of the DNS namespace under one administrative responsibility. See Google Cloud DNS documentation for + * more information. + */ +public class ZoneInfo implements Serializable { + + private static final long serialVersionUID = 201601191647L; + private final String name; + private final String id; + private final Long creationTimeMillis; + private final String dnsName; + private final String description; + private final String nameServerSet; + private final List nameServers; + + /** + * Builder for {@code ZoneInfo}. + */ + public abstract static class Builder { + /** + * Sets a mandatory user-provided name for the zone. It must be unique within the project. + */ + public abstract Builder name(String name); + + /** + * Sets an id for the zone which is assigned to the zone by the server. + */ + abstract Builder id(String id); + + /** + * Sets the time when this zone was created. + */ + abstract Builder creationTimeMillis(long creationTimeMillis); + + /** + * Sets a mandatory DNS name of this zone, for instance "example.com.". + */ + public abstract Builder dnsName(String dnsName); + + /** + * Sets a mandatory description for this zone. The value is a string of at most 1024 characters + * which has no effect on the zone's function. + */ + public abstract Builder description(String description); + + /** + * Optionally specifies the NameServerSet for this zone. A NameServerSet is a set of DNS name + * servers that all host the same zones. Most users will not need to specify this value. + */ + abstract Builder nameServerSet(String nameServerSet); + // this should not be included in tooling as per the service owners + + /** + * Sets a list of servers that hold the information about the zone. This information is provided + * by Google Cloud DNS and is read only. + */ + abstract Builder nameServers(List nameServers); + + /** + * Builds the instance of {@code ZoneInfo} based on the information set by this builder. + */ + public abstract ZoneInfo build(); + } + + static class BuilderImpl extends Builder { + private String name; + private String id; + private Long creationTimeMillis; + private String dnsName; + private String description; + private String nameServerSet; + private List nameServers; + + private BuilderImpl(String name) { + this.name = checkNotNull(name); + } + + /** + * Creates a builder from an existing ZoneInfo object. + */ + BuilderImpl(ZoneInfo info) { + this.name = info.name; + this.id = info.id; + this.creationTimeMillis = info.creationTimeMillis; + this.dnsName = info.dnsName; + this.description = info.description; + this.nameServerSet = info.nameServerSet; + if (info.nameServers != null) { + this.nameServers = ImmutableList.copyOf(info.nameServers); + } + } + + @Override + public Builder name(String name) { + this.name = checkNotNull(name); + return this; + } + + @Override + Builder id(String id) { + this.id = id; + return this; + } + + @Override + Builder creationTimeMillis(long creationTimeMillis) { + this.creationTimeMillis = creationTimeMillis; + return this; + } + + @Override + public Builder dnsName(String dnsName) { + this.dnsName = checkNotNull(dnsName); + return this; + } + + @Override + public Builder description(String description) { + this.description = checkNotNull(description); + return this; + } + + @Override + Builder nameServerSet(String nameServerSet) { + this.nameServerSet = checkNotNull(nameServerSet); + return this; + } + + @Override + Builder nameServers(List nameServers) { + checkNotNull(nameServers); + this.nameServers = Lists.newLinkedList(nameServers); + return this; + } + + @Override + public ZoneInfo build() { + return new ZoneInfo(this); + } + } + + ZoneInfo(BuilderImpl builder) { + this.name = builder.name; + this.id = builder.id; + this.creationTimeMillis = builder.creationTimeMillis; + this.dnsName = builder.dnsName; + this.description = builder.description; + this.nameServerSet = builder.nameServerSet; + this.nameServers = builder.nameServers == null + ? null : ImmutableList.copyOf(builder.nameServers); + } + + /** + * Returns a ZoneInfo object with assigned {@code name}, {@code dnsName} and {@code description}. + */ + public static ZoneInfo of(String name, String dnsName, String description) { + return new BuilderImpl(name).dnsName(dnsName).description(description).build(); + } + + /** + * Returns the user-defined name of the zone. + */ + public String name() { + return name; + } + + /** + * Returns the read-only zone id assigned by the server. + */ + public String id() { + return id; + } + + /** + * Returns the time when this zone was created on the server. + */ + public Long creationTimeMillis() { + return creationTimeMillis; + } + + /** + * Returns the DNS name of this zone, for instance "example.com.". + */ + public String dnsName() { + return dnsName; + } + + /** + * Returns the description of this zone. + */ + public String description() { + return description; + } + + /** + * Returns the optionally specified set of DNS name servers that all host this zone. This value is + * set only for specific use cases and is left empty for vast majority of users. + */ + public String nameServerSet() { + return nameServerSet; + } + + /** + * The nameservers that the zone should be delegated to. This is defined by the Google DNS cloud. + */ + public List nameServers() { + return nameServers == null ? ImmutableList.of() : nameServers; + } + + /** + * Returns a builder for {@code ZoneInfo} prepopulated with the metadata of this zone. + */ + public Builder toBuilder() { + return new BuilderImpl(this); + } + + com.google.api.services.dns.model.ManagedZone toPb() { + com.google.api.services.dns.model.ManagedZone pb = + new com.google.api.services.dns.model.ManagedZone(); + pb.setDescription(this.description()); + pb.setDnsName(this.dnsName()); + if (this.id() != null) { + pb.setId(new BigInteger(this.id())); + } + pb.setName(this.name()); + pb.setNameServers(this.nameServers); // do use real attribute value which may be null + pb.setNameServerSet(this.nameServerSet()); + if (this.creationTimeMillis() != null) { + pb.setCreationTime(ISODateTimeFormat.dateTime() + .withZoneUTC() + .print(this.creationTimeMillis())); + } + return pb; + } + + static ZoneInfo fromPb(com.google.api.services.dns.model.ManagedZone pb) { + Builder builder = new BuilderImpl(pb.getName()); + if (pb.getDescription() != null) { + builder.description(pb.getDescription()); + } + if (pb.getDnsName() != null) { + builder.dnsName(pb.getDnsName()); + } + if (pb.getId() != null) { + builder.id(pb.getId().toString()); + } + if (pb.getNameServers() != null) { + builder.nameServers(pb.getNameServers()); + } + if (pb.getNameServerSet() != null) { + builder.nameServerSet(pb.getNameServerSet()); + } + if (pb.getCreationTime() != null) { + builder.creationTimeMillis(DateTime.parse(pb.getCreationTime()).getMillis()); + } + return builder.build(); + } + + @Override + public boolean equals(Object obj) { + return obj != null && obj.getClass().equals(ZoneInfo.class) + && Objects.equals(toPb(), ((ZoneInfo) obj).toPb()); + } + + @Override + public int hashCode() { + return Objects.hash(name, id, creationTimeMillis, dnsName, + description, nameServerSet, nameServers); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name()) + .add("id", id()) + .add("description", description()) + .add("dnsName", dnsName()) + .add("nameServerSet", nameServerSet()) + .add("nameServers", nameServers()) + .add("creationTimeMillis", creationTimeMillis()) + .toString(); + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/package-info.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/package-info.java new file mode 100644 index 000000000000..36f41852400c --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/package-info.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A client to the Google Cloud DNS. + * + *

Here are two simple usage examples from within Compute/App Engine. + * + * The first snippet shows how to create a zone resource. The complete source code can be found on + * + * CreateAndListZones.java. Note that you need to replace the {@code domainName} with a domain + * name that you own and the ownership of which you verified with Google. + * + *

 {@code
+ * Dns dns = DnsOptions.defaultInstance().service();
+ * String zoneName = "my-unique-zone";
+ * String domainName = "someexampledomain.com.";
+ * String description = "This is a gcloud-java-dns sample zone.";
+ * ZoneInfo zoneInfo = ZoneInfo.of(zoneName, domainName, description);
+ * Zone createdZone = dns.create(zoneInfo);
+ * } 
+ * + *

The second example shows how to create records inside a zone. The complete code can be found + * on + * CreateAndListDnsRecords.java. + * + *

 {@code
+ * Dns dns = DnsOptions.defaultInstance().service();
+ * String zoneName = "my-unique-zone";
+ * Zone zone = dns.getZone(zoneName);
+ * String ip = "12.13.14.15";
+ * RecordSet toCreate = RecordSet.builder("www.someexampledomain.com.", RecordSet.Type.A)
+ *   .ttl(24, TimeUnit.HOURS)
+ *   .addRecord(ip)
+ *   .build();
+ * ChangeRequestInfo changeRequest = ChangeRequestInfo.builder().add(toCreate).build();
+ * zone.applyChangeRequest(changeRequest);
+ * } 
+ * + *

When using gcloud-java from outside of App/Compute Engine, you have to specify a + * project ID and provide + * credentials. + * + * @see Google Cloud DNS + */ +package com.google.gcloud.dns; diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DefaultDnsRpc.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DefaultDnsRpc.java new file mode 100644 index 000000000000..cbebd19d0d73 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DefaultDnsRpc.java @@ -0,0 +1,196 @@ +package com.google.gcloud.dns.spi; + +import static com.google.gcloud.dns.spi.DnsRpc.ListResult.of; +import static com.google.gcloud.dns.spi.DnsRpc.Option.DNS_NAME; +import static com.google.gcloud.dns.spi.DnsRpc.Option.DNS_TYPE; +import static com.google.gcloud.dns.spi.DnsRpc.Option.FIELDS; +import static com.google.gcloud.dns.spi.DnsRpc.Option.NAME; +import static com.google.gcloud.dns.spi.DnsRpc.Option.PAGE_SIZE; +import static com.google.gcloud.dns.spi.DnsRpc.Option.PAGE_TOKEN; +import static com.google.gcloud.dns.spi.DnsRpc.Option.SORTING_ORDER; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; + +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.jackson.JacksonFactory; +import com.google.api.services.dns.Dns; +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ChangesListResponse; +import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.ManagedZonesListResponse; +import com.google.api.services.dns.model.Project; +import com.google.api.services.dns.model.ResourceRecordSet; +import com.google.api.services.dns.model.ResourceRecordSetsListResponse; +import com.google.gcloud.dns.DnsException; +import com.google.gcloud.dns.DnsOptions; + +import java.io.IOException; +import java.util.Map; + +/** + * A default implementation of the DnsRpc interface. + */ +public class DefaultDnsRpc implements DnsRpc { + + private static final String SORT_BY = "changeSequence"; + private final Dns dns; + private final DnsOptions options; + + private static DnsException translate(IOException exception) { + return new DnsException(exception); + } + + /** + * Constructs an instance of this rpc client with provided {@link DnsOptions}. + */ + public DefaultDnsRpc(DnsOptions options) { + HttpTransport transport = options.httpTransportFactory().create(); + HttpRequestInitializer initializer = options.httpRequestInitializer(); + this.dns = new Dns.Builder(transport, new JacksonFactory(), initializer) + .setRootUrl(options.host()) + .setApplicationName(options.applicationName()) + .build(); + this.options = options; + } + + @Override + public ManagedZone create(ManagedZone zone, Map options) throws DnsException { + try { + return dns.managedZones() + .create(this.options.projectId(), zone) + .setFields(FIELDS.getString(options)) + .execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public ManagedZone getZone(String zoneName, Map options) throws DnsException { + // just fields option + try { + return dns.managedZones().get(this.options.projectId(), zoneName) + .setFields(FIELDS.getString(options)) + .execute(); + } catch (IOException ex) { + DnsException serviceException = translate(ex); + if (serviceException.code() == HTTP_NOT_FOUND) { + return null; + } + throw serviceException; + } + } + + @Override + public ListResult listZones(Map options) throws DnsException { + // fields, page token, page size + try { + ManagedZonesListResponse zoneList = dns.managedZones().list(this.options.projectId()) + .setFields(FIELDS.getString(options)) + .setMaxResults(PAGE_SIZE.getInt(options)) + .setDnsName(DNS_NAME.getString(options)) + .setPageToken(PAGE_TOKEN.getString(options)) + .execute(); + return of(zoneList.getNextPageToken(), zoneList.getManagedZones()); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public boolean deleteZone(String zoneName) throws DnsException { + try { + dns.managedZones().delete(this.options.projectId(), zoneName).execute(); + return true; + } catch (IOException ex) { + DnsException serviceException = translate(ex); + if (serviceException.code() == HTTP_NOT_FOUND) { + return false; + } + throw serviceException; + } + } + + @Override + public ListResult listRecordSets(String zoneName, Map options) + throws DnsException { + // options are fields, page token, dns name, type + try { + ResourceRecordSetsListResponse response = dns.resourceRecordSets() + .list(this.options.projectId(), zoneName) + .setFields(FIELDS.getString(options)) + .setPageToken(PAGE_TOKEN.getString(options)) + .setMaxResults(PAGE_SIZE.getInt(options)) + .setName(NAME.getString(options)) + .setType(DNS_TYPE.getString(options)) + .execute(); + return of(response.getNextPageToken(), response.getRrsets()); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public Project getProject(Map options) throws DnsException { + try { + return dns.projects().get(this.options.projectId()) + .setFields(FIELDS.getString(options)).execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public Change applyChangeRequest(String zoneName, Change changeRequest, Map options) + throws DnsException { + try { + return dns.changes().create(this.options.projectId(), zoneName, changeRequest) + .setFields(FIELDS.getString(options)) + .execute(); + } catch (IOException ex) { + throw translate(ex); + } + } + + @Override + public Change getChangeRequest(String zoneName, String changeRequestId, Map options) + throws DnsException { + try { + return dns.changes().get(this.options.projectId(), zoneName, changeRequestId) + .setFields(FIELDS.getString(options)) + .execute(); + } catch (IOException ex) { + DnsException serviceException = translate(ex); + if (serviceException.code() == HTTP_NOT_FOUND) { + if ("entity.parameters.changeId".equals(serviceException.location()) + || (serviceException.getMessage() != null + && serviceException.getMessage().contains("parameters.changeId"))) { + // the change id was not found, but the zone exists + return null; + } + // the zone does not exist, so throw an exception + } + throw serviceException; + } + } + + @Override + public ListResult listChangeRequests(String zoneName, Map options) + throws DnsException { + // options are fields, page token, page size, sort order + try { + Dns.Changes.List request = dns.changes().list(this.options.projectId(), zoneName) + .setFields(FIELDS.getString(options)) + .setMaxResults(PAGE_SIZE.getInt(options)) + .setPageToken(PAGE_TOKEN.getString(options)); + if (SORTING_ORDER.getString(options) != null) { + // todo check and change if more sorting options are implemented, issue #604 + request = request.setSortBy(SORT_BY).setSortOrder(SORTING_ORDER.getString(options)); + } + ChangesListResponse response = request.execute(); + return of(response.getNextPageToken(), response.getChanges()); + } catch (IOException ex) { + throw translate(ex); + } + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpc.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpc.java new file mode 100644 index 000000000000..c7478016db27 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpc.java @@ -0,0 +1,174 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns.spi; + +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.Project; +import com.google.api.services.dns.model.ResourceRecordSet; +import com.google.common.collect.ImmutableList; +import com.google.gcloud.dns.DnsException; + +import java.util.Map; + +public interface DnsRpc { + + enum Option { + FIELDS("fields"), + PAGE_SIZE("maxResults"), + PAGE_TOKEN("pageToken"), + DNS_NAME("dnsName"), + NAME("name"), + DNS_TYPE("type"), + SORTING_ORDER("sortOrder"); + + private final String value; + + Option(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @SuppressWarnings("unchecked") + T get(Map options) { + return (T) options.get(this); + } + + String getString(Map options) { + return get(options); + } + + Integer getInt(Map options) { + return get(options); + } + } + + class ListResult { + + private final Iterable results; + private final String pageToken; + + public ListResult(String pageToken, Iterable results) { + this.results = ImmutableList.copyOf(results); + this.pageToken = pageToken; + } + + public static ListResult of(String pageToken, Iterable list) { + return new ListResult<>(pageToken, list); + } + + public Iterable results() { + return results; + } + + public String pageToken() { + return pageToken; + } + } + + /** + * Creates a new zone. + * + * @param zone a zone to be created + * @param options a map of options for the service call + * @return Updated {@code ManagedZone} object + * @throws DnsException upon failure + */ + ManagedZone create(ManagedZone zone, Map options) throws DnsException; + + /** + * Retrieves and returns an existing zone. + * + * @param zoneName name of the zone to be returned + * @param options a map of options for the service call + * @return a zone or {@code null} if not found + * @throws DnsException upon failure + */ + ManagedZone getZone(String zoneName, Map options) throws DnsException; + + /** + * Lists the zones that exist within the project. + * + * @param options a map of options for the service call + * @throws DnsException upon failure + */ + ListResult listZones(Map options) throws DnsException; + + /** + * Deletes the zone identified by the name. + * + * @return {@code true} if the zone was deleted and {@code false} otherwise + * @throws DnsException upon failure + */ + boolean deleteZone(String zoneName) throws DnsException; + + /** + * Lists record sets for a given zone. + * + * @param zoneName name of the zone to be listed + * @param options a map of options for the service call + * @throws DnsException upon failure or if zone was not found + */ + ListResult listRecordSets(String zoneName, Map options) + throws DnsException; + + /** + * Returns information about the current project. + * + * @param options a map of options for the service call + * @return up-to-date project information + * @throws DnsException upon failure or if the project is not found + */ + Project getProject(Map options) throws DnsException; + + /** + * Applies change request to a zone. + * + * @param zoneName the name of a zone to which the {@code Change} should be applied + * @param changeRequest change to be applied + * @param options a map of options for the service call + * @return updated change object with server-assigned ID + * @throws DnsException upon failure or if zone was not found + */ + Change applyChangeRequest(String zoneName, Change changeRequest, Map options) + throws DnsException; + + /** + * Returns an existing change request. + * + * @param zoneName the name of a zone to which the {@code Change} was be applied + * @param changeRequestId the unique id assigned to the change by the server + * @param options a map of options for the service call + * @return up-to-date change object or {@code null} if change was not found + * @throws DnsException upon failure or if zone was not found + */ + Change getChangeRequest(String zoneName, String changeRequestId, Map options) + throws DnsException; + + /** + * List existing change requests for a zone. + * + * @param zoneName the name of a zone to which the {@code Change}s were be applied + * @param options a map of options for the service call + * @throws DnsException upon failure or if zone was not found + */ + ListResult listChangeRequests(String zoneName, Map options) + throws DnsException; +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpcFactory.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpcFactory.java new file mode 100644 index 000000000000..ca1b1a0dd018 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/spi/DnsRpcFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns.spi; + +import com.google.gcloud.dns.DnsOptions; +import com.google.gcloud.spi.ServiceRpcFactory; + +/** + * An interface for DnsRpc factory. Implementation will be loaded via {@link + * java.util.ServiceLoader}. + */ +public interface DnsRpcFactory extends ServiceRpcFactory { +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/LocalDnsHelper.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/LocalDnsHelper.java new file mode 100644 index 000000000000..0ae2c37b9b4d --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/LocalDnsHelper.java @@ -0,0 +1,1266 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns.testing; + +import static com.google.common.net.InetAddresses.isInetAddress; +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static java.net.HttpURLConnection.HTTP_OK; + +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson.JacksonFactory; +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.Project; +import com.google.api.services.dns.model.Quota; +import com.google.api.services.dns.model.ResourceRecordSet; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.io.ByteStreams; +import com.google.gcloud.dns.DnsOptions; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import org.joda.time.format.ISODateTimeFormat; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Random; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; + +/** + * A local Google Cloud DNS mock. + * + *

The mock runs in a separate thread, listening for HTTP requests on the local machine at an + * ephemeral port. + * + *

While the mock attempts to simulate the service, there are some differences in the behaviour. + * The mock will accept any project ID and never returns a notFound or another error because of + * project ID. It assumes that all project IDs exist and that the user has all the necessary + * privileges to manipulate any project. Similarly, the local simulation does not require + * verification of domain name ownership. Any request for creating a managed zone will be approved. + * The mock does not track quota and will allow the user to exceed it. The mock provides only basic + * validation of the DNS data for record sets of type A and AAAA. It does not validate any other + * record set types. + */ +public class LocalDnsHelper { + + private final ConcurrentSkipListMap projects + = new ConcurrentSkipListMap<>(); + private static final URI BASE_CONTEXT; + private static final Logger log = Logger.getLogger(LocalDnsHelper.class.getName()); + private static final JsonFactory jsonFactory = new JacksonFactory(); + private static final Random ID_GENERATOR = new Random(); + private static final String VERSION = "v1"; + private static final String CONTEXT = "/dns/" + VERSION + "/projects"; + private static final Set ENCODINGS = ImmutableSet.of("gzip", "x-gzip"); + private static final List TYPES = ImmutableList.of("A", "AAAA", "CNAME", "MX", "NAPTR", + "NS", "PTR", "SOA", "SPF", "SRV", "TXT"); + private static final TreeSet FORBIDDEN = Sets.newTreeSet( + ImmutableList.of("google.com.", "com.", "example.com.", "net.", "org.")); + private static final Pattern ZONE_NAME_RE = Pattern.compile("[a-z][a-z0-9-]*"); + private static final ScheduledExecutorService EXECUTORS = + Executors.newScheduledThreadPool(2, Executors.defaultThreadFactory()); + private static final String PROJECT_ID = "dummyprojectid"; + + static { + try { + BASE_CONTEXT = new URI(CONTEXT); + } catch (URISyntaxException e) { + throw new IllegalArgumentException( + "Could not initialize LocalDnsHelper due to URISyntaxException.", e); + } + } + + private long delayChange; + private final HttpServer server; + private final int port; + + /** + * For matching URLs to operations. + */ + private enum CallRegex { + CHANGE_CREATE("POST", CONTEXT + "/[^/]+/managedZones/[^/]+/changes"), + CHANGE_GET("GET", CONTEXT + "/[^/]+/managedZones/[^/]+/changes/[^/]+"), + CHANGE_LIST("GET", CONTEXT + "/[^/]+/managedZones/[^/]+/changes"), + ZONE_CREATE("POST", CONTEXT + "/[^/]+/managedZones"), + ZONE_DELETE("DELETE", CONTEXT + "/[^/]+/managedZones/[^/]+"), + ZONE_GET("GET", CONTEXT + "/[^/]+/managedZones/[^/]+"), + ZONE_LIST("GET", CONTEXT + "/[^/]+/managedZones"), + PROJECT_GET("GET", CONTEXT + "/[^/]+"), + RECORD_LIST("GET", CONTEXT + "/[^/]+/managedZones/[^/]+/rrsets"); + + private String method; + private String pathRegex; + + CallRegex(String method, String pathRegex) { + this.pathRegex = pathRegex; + this.method = method; + } + } + + /** + * Associates a project with a collection of ManagedZones. + */ + static class ProjectContainer { + private final Project project; + private final ConcurrentSkipListMap zones = + new ConcurrentSkipListMap<>(); + + ProjectContainer(Project project) { + this.project = project; + } + + Project project() { + return project; + } + + ConcurrentSkipListMap zones() { + return zones; + } + } + + /** + * Associates a zone with a collection of changes and dns records. + */ + static class ZoneContainer { + private final ManagedZone zone; + private final AtomicReference> + dnsRecords = new AtomicReference<>(ImmutableSortedMap.of()); + private final ConcurrentLinkedQueue changes = new ConcurrentLinkedQueue<>(); + + ZoneContainer(ManagedZone zone) { + this.zone = zone; + this.dnsRecords.set(ImmutableSortedMap.of()); + } + + ManagedZone zone() { + return zone; + } + + AtomicReference> dnsRecords() { + return dnsRecords; + } + + ConcurrentLinkedQueue changes() { + return changes; + } + + Change findChange(String changeId) { + for (Change current : changes) { + if (changeId.equals(current.getId())) { + return current; + } + } + return null; + } + } + + static class Response { + private final int code; + private final String body; + + Response(int code, String body) { + this.code = code; + this.body = body; + } + + int code() { + return code; + } + + String body() { + return body; + } + } + + private enum Error { + REQUIRED(400, "global", "required", "REQUIRED"), + INTERNAL_ERROR(500, "global", "internalError", "INTERNAL_ERROR"), + BAD_REQUEST(400, "global", "badRequest", "BAD_REQUEST"), + INVALID(400, "global", "invalid", "INVALID"), + CONTAINER_NOT_EMPTY(400, "global", "containerNotEmpty", "CONTAINER_NOT_EMPTY"), + NOT_AVAILABLE(400, "global", "managedZoneDnsNameNotAvailable", "NOT_AVAILABLE"), + NOT_FOUND(404, "global", "notFound", "NOT_FOUND"), + ALREADY_EXISTS(409, "global", "alreadyExists", "ALREADY_EXISTS"), + CONDITION_NOT_MET(412, "global", "conditionNotMet", "CONDITION_NOT_MET"), + INVALID_ZONE_APEX(400, "global", "invalidZoneApex", "INVALID_ZONE_APEX"); + + private final int code; + private final String domain; + private final String reason; + private final String status; + + Error(int code, String domain, String reason, String status) { + this.code = code; + this.domain = domain; + this.reason = reason; + this.status = status; + } + + Response response(String message) { + try { + return new Response(code, toJson(message)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response("Error when generating JSON error response."); + } + } + + private String toJson(String message) throws IOException { + Map errors = new HashMap<>(); + errors.put("domain", domain); + errors.put("message", message); + errors.put("reason", reason); + Map args = new HashMap<>(); + args.put("errors", ImmutableList.of(errors)); + args.put("code", code); + args.put("message", message); + args.put("status", status); + return jsonFactory.toString(ImmutableMap.of("error", args)); + } + } + + private class RequestHandler implements HttpHandler { + + private Response pickHandler(HttpExchange exchange, CallRegex regex) { + URI relative = BASE_CONTEXT.relativize(exchange.getRequestURI()); + String path = relative.getPath(); + String[] tokens = path.split("/"); + String projectId = tokens.length > 0 ? tokens[0] : null; + String zoneName = tokens.length > 2 ? tokens[2] : null; + String changeId = tokens.length > 4 ? tokens[4] : null; + String query = relative.getQuery(); + switch (regex) { + case CHANGE_GET: + return getChange(projectId, zoneName, changeId, query); + case CHANGE_LIST: + return listChanges(projectId, zoneName, query); + case ZONE_GET: + return getZone(projectId, zoneName, query); + case ZONE_DELETE: + return deleteZone(projectId, zoneName); + case ZONE_LIST: + return listZones(projectId, query); + case PROJECT_GET: + return getProject(projectId, query); + case RECORD_LIST: + return listDnsRecords(projectId, zoneName, query); + case ZONE_CREATE: + try { + return handleZoneCreate(exchange, projectId, query); + } catch (IOException ex) { + return Error.BAD_REQUEST.response(ex.getMessage()); + } + case CHANGE_CREATE: + try { + return handleChangeCreate(exchange, projectId, zoneName, query); + } catch (IOException ex) { + return Error.BAD_REQUEST.response(ex.getMessage()); + } + default: + return Error.INTERNAL_ERROR.response("Operation without a handler."); + } + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String requestMethod = exchange.getRequestMethod(); + String rawPath = exchange.getRequestURI().getRawPath(); + for (CallRegex regex : CallRegex.values()) { + if (requestMethod.equals(regex.method) && rawPath.matches(regex.pathRegex)) { + Response response = pickHandler(exchange, regex); + writeResponse(exchange, response); + return; + } + } + writeResponse(exchange, Error.NOT_FOUND.response(String.format( + "The url %s for %s method does not match any API call.", + requestMethod, exchange.getRequestURI()))); + } + + /** + * @throws IOException if the request cannot be parsed. + */ + private Response handleChangeCreate(HttpExchange exchange, String projectId, String zoneName, + String query) throws IOException { + String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); + Change change; + try { + change = jsonFactory.fromString(requestBody, Change.class); + } catch (IllegalArgumentException ex) { + return Error.REQUIRED.response( + "The 'entity.change' parameter is required but was missing."); + } + String[] fields = OptionParsers.parseGetOptions(query); + return createChange(projectId, zoneName, change, fields); + } + + /** + * @throws IOException if the request cannot be parsed. + */ + private Response handleZoneCreate(HttpExchange exchange, String projectId, String query) + throws IOException { + String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); + ManagedZone zone; + try { + zone = jsonFactory.fromString(requestBody, ManagedZone.class); + } catch (IllegalArgumentException ex) { + return Error.REQUIRED.response( + "The 'entity.managedZone' parameter is required but was missing."); + } + String[] options = OptionParsers.parseGetOptions(query); + return createZone(projectId, zone, options); + } + } + + private LocalDnsHelper(long delay) { + this.delayChange = delay; + try { + server = HttpServer.create(new InetSocketAddress(0), 0); + port = server.getAddress().getPort(); + server.createContext(CONTEXT, new RequestHandler()); + } catch (IOException e) { + throw new RuntimeException("Could not bind the mock DNS server.", e); + } + } + + /** + * Accessor for testing purposes. + */ + ConcurrentSkipListMap projects() { + return projects; + } + + /** + * Creates new {@link LocalDnsHelper} instance that listens to requests on the local machine. This + * instance processes changes in separate thread. The parameter determines how long a thread + * should wait before processing a change. If it is set to 0, the threading is turned off and the + * mock will behave synchronously. + * + * @param delay delay for processing changes in ms or 0 for synchronous processing + */ + public static LocalDnsHelper create(Long delay) { + return new LocalDnsHelper(delay); + } + + /** + * Returns a {@link DnsOptions} instance that sets the host to use the mock server. + */ + public DnsOptions options() { + return DnsOptions.builder().projectId(PROJECT_ID).host("http://localhost:" + port).build(); + } + + /** + * Starts the thread that runs the local DNS server. + */ + public void start() { + server.start(); + } + + /** + * Stops the thread that runs the mock DNS server. + */ + public void stop() { + server.stop(1); + } + + private static void writeResponse(HttpExchange exchange, Response response) { + exchange.getResponseHeaders().set("Content-type", "application/json; charset=UTF-8"); + OutputStream outputStream = exchange.getResponseBody(); + try { + exchange.getResponseHeaders().add("Connection", "close"); + exchange.sendResponseHeaders(response.code(), response.body().length()); + if (response.code() != 204) { + // the server automatically sends headers and closes output stream when 204 is returned + outputStream.write(response.body().getBytes(StandardCharsets.UTF_8)); + } + outputStream.close(); + } catch (IOException e) { + log.log(Level.WARNING, "IOException encountered when sending response.", e); + } + } + + private static String decodeContent(Headers headers, InputStream inputStream) throws IOException { + List contentEncoding = headers.get("Content-encoding"); + InputStream input = inputStream; + try { + if (contentEncoding != null && !contentEncoding.isEmpty()) { + String encoding = contentEncoding.get(0); + if (ENCODINGS.contains(encoding)) { + input = new GZIPInputStream(inputStream); + } else if (!"identity".equals(encoding)) { + throw new IOException( + "The request has the following unsupported HTTP content encoding: " + encoding); + } + } + return new String(ByteStreams.toByteArray(input), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IOException("Exception encountered when decoding request content.", e); + } + } + + /** + * Generates a JSON response. + * + * @param context managedZones | projects | rrsets | changes + */ + @VisibleForTesting + static Response toListResponse(List serializedObjects, String context, String pageToken, + boolean includePageToken) { + StringBuilder responseBody = new StringBuilder(); + responseBody.append("{\"").append(context).append("\": ["); + Joiner.on(",").appendTo(responseBody, serializedObjects); + responseBody.append(']'); + // add page token only if it exists and is asked for + if (pageToken != null && includePageToken) { + responseBody.append(",\"nextPageToken\": \"").append(pageToken).append('"'); + } + responseBody.append('}'); + return new Response(HTTP_OK, responseBody.toString()); + } + + /** + * Prepares record sets that are created by default for each zone. + */ + private static ImmutableSortedMap defaultRecords(ManagedZone zone) { + ResourceRecordSet soa = new ResourceRecordSet(); + soa.setTtl(21600); + soa.setName(zone.getDnsName()); + soa.setRrdatas(ImmutableList.of( + // taken from the service + "ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 0 21600 3600 1209600 312" + )); + soa.setType("SOA"); + ResourceRecordSet ns = new ResourceRecordSet(); + ns.setTtl(21600); + ns.setName(zone.getDnsName()); + ns.setRrdatas(zone.getNameServers()); + ns.setType("NS"); + String nsId = getUniqueId(ImmutableSet.of()); + String soaId = getUniqueId(ImmutableSet.of(nsId)); + return ImmutableSortedMap.of(nsId, ns, soaId, soa); + } + + /** + * Returns a list of four nameservers randomly chosen from the predefined set. + */ + @VisibleForTesting + static List randomNameservers() { + ArrayList nameservers = Lists.newArrayList( + "dns1.googlecloud.com", "dns2.googlecloud.com", "dns3.googlecloud.com", + "dns4.googlecloud.com", "dns5.googlecloud.com", "dns6.googlecloud.com" + ); + while (nameservers.size() != 4) { + int index = ID_GENERATOR.nextInt(nameservers.size()); + nameservers.remove(index); + } + return nameservers; + } + + /** + * Returns a hex string id (used for a record set) unique within the set of ids. + */ + @VisibleForTesting + static String getUniqueId(Set ids) { + String id; + do { + id = Long.toHexString(System.currentTimeMillis()) + + Long.toHexString(Math.abs(ID_GENERATOR.nextLong())); + } while (ids.contains(id)); + return id; + } + + /** + * Tests if a record set matches name and type (if provided). Used for filtering. + */ + @VisibleForTesting + static boolean matchesCriteria(ResourceRecordSet recordSet, String name, String type) { + if (type != null && !recordSet.getType().equals(type)) { + return false; + } + return name == null || recordSet.getName().equals(name); + } + + /** + * Returns a project container. Never returns {@code null} because we assume that all projects + * exists. + */ + private ProjectContainer findProject(String projectId) { + ProjectContainer defaultProject = createProject(projectId); + projects.putIfAbsent(projectId, defaultProject); + return projects.get(projectId); + } + + /** + * Returns a zone container. Returns {@code null} if zone does not exist within project. + */ + @VisibleForTesting + ZoneContainer findZone(String projectId, String zoneName) { + ProjectContainer projectContainer = findProject(projectId); // never null + return projectContainer.zones().get(zoneName); + } + + /** + * Returns a change found by its id. Returns {@code null} if such a change does not exist. + */ + @VisibleForTesting + Change findChange(String projectId, String zoneName, String changeId) { + ZoneContainer wrapper = findZone(projectId, zoneName); + return wrapper == null ? null : wrapper.findChange(changeId); + } + + /** + * Returns a response to getChange service call. + */ + @VisibleForTesting + Response getChange(String projectId, String zoneName, String changeId, String query) { + Change change = findChange(projectId, zoneName, changeId); + if (change == null) { + ZoneContainer zone = findZone(projectId, zoneName); + if (zone == null) { + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)); + } + return Error.NOT_FOUND.response(String.format( + "The 'parameters.changeId' resource named '%s' does not exist.", changeId)); + } + String[] fields = OptionParsers.parseGetOptions(query); + Change result = OptionParsers.extractFields(change, fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing change %s in managed zone %s in project %s.", + changeId, zoneName, projectId)); + } + } + + /** + * Returns a response to getZone service call. + */ + @VisibleForTesting + Response getZone(String projectId, String zoneName, String query) { + ZoneContainer container = findZone(projectId, zoneName); + if (container == null) { + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)); + } + String[] fields = OptionParsers.parseGetOptions(query); + ManagedZone result = OptionParsers.extractFields(container.zone(), fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing managed zone %s in project %s.", zoneName, projectId)); + } + } + + /** + * We assume that every project exists. If we do not have it in the collection yet, we just create + * a new default project instance with default quota. + */ + @VisibleForTesting + Response getProject(String projectId, String query) { + String[] fields = OptionParsers.parseGetOptions(query); + Project project = findProject(projectId).project(); // creates project if needed + Project result = OptionParsers.extractFields(project, fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + String.format("Error when serializing project %s.", projectId)); + } + } + + /** + * Creates a project. It generates a project number randomly. + */ + private ProjectContainer createProject(String projectId) { + Quota quota = new Quota(); + quota.setManagedZones(10000); + quota.setRrsetsPerManagedZone(10000); + quota.setRrsetAdditionsPerChange(100); + quota.setRrsetDeletionsPerChange(100); + quota.setTotalRrdataSizePerChange(10000); + quota.setResourceRecordsPerRrset(100); + Project project = new Project(); + project.setId(projectId); + project.setNumber(new BigInteger(String.valueOf( + Math.abs(ID_GENERATOR.nextLong() % Long.MAX_VALUE)))); + project.setQuota(quota); + return new ProjectContainer(project); + } + + @VisibleForTesting + Response deleteZone(String projectId, String zoneName) { + ZoneContainer zone = findZone(projectId, zoneName); + ImmutableSortedMap rrsets = zone == null + ? ImmutableSortedMap.of() : zone.dnsRecords().get(); + ImmutableList defaults = ImmutableList.of("NS", "SOA"); + for (ResourceRecordSet current : rrsets.values()) { + if (!defaults.contains(current.getType())) { + return Error.CONTAINER_NOT_EMPTY.response(String.format( + "The resource named '%s' cannot be deleted because it is not empty", zoneName)); + } + } + ProjectContainer projectContainer = projects.get(projectId); + ZoneContainer previous = projectContainer.zones.remove(zoneName); + return previous == null + ? Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)) + : new Response(HTTP_NO_CONTENT, "{}"); + } + + /** + * Creates new managed zone and stores it in the collection. Assumes that project exists. + */ + @VisibleForTesting + Response createZone(String projectId, ManagedZone zone, String... fields) { + Response errorResponse = checkZone(zone); + if (errorResponse != null) { + return errorResponse; + } + ManagedZone completeZone = new ManagedZone(); + completeZone.setName(zone.getName()); + completeZone.setDnsName(zone.getDnsName()); + completeZone.setDescription(zone.getDescription()); + completeZone.setNameServerSet(zone.getNameServerSet()); + completeZone.setCreationTime(ISODateTimeFormat.dateTime().withZoneUTC() + .print(System.currentTimeMillis())); + completeZone.setId(BigInteger.valueOf(Math.abs(ID_GENERATOR.nextLong() % Long.MAX_VALUE))); + completeZone.setNameServers(randomNameservers()); + ZoneContainer zoneContainer = new ZoneContainer(completeZone); + ImmutableSortedMap defaultsRecords = defaultRecords(completeZone); + zoneContainer.dnsRecords().set(defaultsRecords); + Change change = new Change(); + change.setAdditions(ImmutableList.copyOf(defaultsRecords.values())); + change.setStatus("done"); + change.setId("0"); + change.setStartTime(ISODateTimeFormat.dateTime().withZoneUTC() + .print(System.currentTimeMillis())); + zoneContainer.changes().add(change); + ProjectContainer projectContainer = findProject(projectId); + ZoneContainer oldValue = projectContainer.zones().putIfAbsent( + completeZone.getName(), zoneContainer); + if (oldValue != null) { + return Error.ALREADY_EXISTS.response(String.format( + "The resource 'entity.managedZone' named '%s' already exists", completeZone.getName())); + } + ManagedZone result = OptionParsers.extractFields(completeZone, fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + String.format("Error when serializing managed zone %s.", result.getName())); + } + } + + /** + * Creates a new change, stores it, and if delayChange > 0, invokes processing in a new thread. + */ + Response createChange(String projectId, String zoneName, Change change, String... fields) { + ZoneContainer zoneContainer = findZone(projectId, zoneName); + if (zoneContainer == null) { + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named %s does not exist.", zoneName)); + } + Response response = checkChange(change, zoneContainer); + if (response != null) { + return response; + } + Change completeChange = new Change(); + if (change.getAdditions() != null) { + completeChange.setAdditions(ImmutableList.copyOf(change.getAdditions())); + } + if (change.getDeletions() != null) { + completeChange.setDeletions(ImmutableList.copyOf(change.getDeletions())); + } + /* We need to set ID for the change. We are working in concurrent environment. We know that the + element fell on an index between 1 and maxId (index 0 is the default change which creates SOA + and NS), so we will reset all IDs between 0 and maxId (all of them are valid for the respective + objects). */ + ConcurrentLinkedQueue changeSequence = zoneContainer.changes(); + changeSequence.add(completeChange); + int maxId = changeSequence.size(); + int index = 0; + for (Change c : changeSequence) { + if (index == maxId) { + break; + } + c.setId(String.valueOf(index++)); + } + completeChange.setStatus("pending"); + completeChange.setStartTime(ISODateTimeFormat.dateTime().withZoneUTC() + .print(System.currentTimeMillis())); + invokeChange(projectId, zoneName, completeChange.getId()); + Change result = OptionParsers.extractFields(completeChange, fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + String.format("Error when serializing change %s in managed zone %s in project %s.", + result.getId(), zoneName, projectId)); + } + } + + /** + * Applies change. Uses a different pooled thread which applies the change only if {@code + * delayChange} is > 0. + */ + private void invokeChange(final String projectId, final String zoneName, + final String changeId) { + if (delayChange > 0) { + EXECUTORS.schedule(new Runnable() { + @Override + public void run() { + applyExistingChange(projectId, zoneName, changeId); + } + }, delayChange, TimeUnit.MILLISECONDS); + } else { + applyExistingChange(projectId, zoneName, changeId); + } + } + + /** + * Applies changes to a zone. Repeatedly tries until succeeds. Thread safe and deadlock safe. + */ + private void applyExistingChange(String projectId, String zoneName, String changeId) { + Change change = findChange(projectId, zoneName, changeId); + if (change == null) { + return; // no such change exists, nothing to do + } + ZoneContainer wrapper = findZone(projectId, zoneName); + if (wrapper == null) { + return; // no such zone exists; it might have been deleted by another thread + } + AtomicReference> dnsRecords = + wrapper.dnsRecords(); + while (true) { + // managed zone must have a set of records which is not null + ImmutableSortedMap original = dnsRecords.get(); + // the copy will be populated when handling deletions + SortedMap copy = new TreeMap<>(); + // apply deletions first + List deletions = change.getDeletions(); + if (deletions != null) { + for (Map.Entry entry : original.entrySet()) { + if (!deletions.contains(entry.getValue())) { + copy.put(entry.getKey(), entry.getValue()); + } + } + } else { + copy.putAll(original); + } + // apply additions + List additions = change.getAdditions(); + if (additions != null) { + for (ResourceRecordSet addition : additions) { + ResourceRecordSet rrset = new ResourceRecordSet(); + rrset.setName(addition.getName()); + rrset.setRrdatas(ImmutableList.copyOf(addition.getRrdatas())); + rrset.setTtl(addition.getTtl()); + rrset.setType(addition.getType()); + String id = getUniqueId(copy.keySet()); + copy.put(id, rrset); + } + } + boolean success = dnsRecords.compareAndSet(original, ImmutableSortedMap.copyOf(copy)); + if (success) { + break; // success if no other thread modified the value in the meantime + } + } + change.setStatus("done"); + } + + /** + * Lists zones. Next page token is the last listed zone name and is returned only of there is more + * to list and if the user does not exclude nextPageToken from field options. + */ + @VisibleForTesting + Response listZones(String projectId, String query) { + Map options = OptionParsers.parseListZonesOptions(query); + Response response = checkListOptions(options); + if (response != null) { + return response; + } + ConcurrentSkipListMap containers = findProject(projectId).zones(); + String[] fields = (String[]) options.get("fields"); + String dnsName = (String) options.get("dnsName"); + String pageToken = (String) options.get("pageToken"); + Integer maxResults = options.get("maxResults") == null + ? null : Integer.valueOf((String) options.get("maxResults")); + boolean sizeReached = false; + boolean hasMorePages = false; + LinkedList serializedZones = new LinkedList<>(); + String lastZoneName = null; + ConcurrentNavigableMap fragment = + pageToken != null ? containers.tailMap(pageToken, false) : containers; + for (ZoneContainer zoneContainer : fragment.values()) { + ManagedZone zone = zoneContainer.zone(); + if (dnsName == null || zone.getDnsName().equals(dnsName)) { + if (sizeReached) { + // we do not add this, just note that there would be more and there should be a token + hasMorePages = true; + break; + } else { + try { + lastZoneName = zone.getName(); + serializedZones.addLast(jsonFactory.toString( + OptionParsers.extractFields(zone, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing managed zone %s in project %s", lastZoneName, projectId)); + } + } + } + sizeReached = maxResults != null && maxResults.equals(serializedZones.size()); + } + boolean includePageToken = + hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken")); + return toListResponse(serializedZones, "managedZones", lastZoneName, includePageToken); + } + + /** + * Lists record sets for a zone. Next page token is the ID of the last record listed. + */ + @VisibleForTesting + Response listDnsRecords(String projectId, String zoneName, String query) { + Map options = OptionParsers.parseListDnsRecordsOptions(query); + Response response = checkListOptions(options); + if (response != null) { + return response; + } + ZoneContainer zoneContainer = findZone(projectId, zoneName); + if (zoneContainer == null) { + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)); + } + ImmutableSortedMap dnsRecords = zoneContainer.dnsRecords().get(); + String[] fields = (String[]) options.get("fields"); + String name = (String) options.get("name"); + String type = (String) options.get("type"); + String pageToken = (String) options.get("pageToken"); + ImmutableSortedMap fragment = + pageToken != null ? dnsRecords.tailMap(pageToken, false) : dnsRecords; + Integer maxResults = options.get("maxResults") == null + ? null : Integer.valueOf((String) options.get("maxResults")); + boolean sizeReached = false; + boolean hasMorePages = false; + LinkedList serializedRrsets = new LinkedList<>(); + String lastRecordId = null; + for (String recordSetId : fragment.keySet()) { + ResourceRecordSet recordSet = fragment.get(recordSetId); + if (matchesCriteria(recordSet, name, type)) { + if (sizeReached) { + // we do not add this, just note that there would be more and there should be a token + hasMorePages = true; + break; + } else { + lastRecordId = recordSetId; + try { + serializedRrsets.addLast(jsonFactory.toString( + OptionParsers.extractFields(recordSet, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing resource record set in managed zone %s in project %s", + zoneName, projectId)); + } + } + } + sizeReached = maxResults != null && maxResults.equals(serializedRrsets.size()); + } + boolean includePageToken = + hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken")); + return toListResponse(serializedRrsets, "rrsets", lastRecordId, includePageToken); + } + + /** + * Lists changes. Next page token is the ID of the last change listed. + */ + @VisibleForTesting + Response listChanges(String projectId, String zoneName, String query) { + Map options = OptionParsers.parseListChangesOptions(query); + Response response = checkListOptions(options); + if (response != null) { + return response; + } + ZoneContainer zoneContainer = findZone(projectId, zoneName); + if (zoneContainer == null) { + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist", zoneName)); + } + // take a sorted snapshot of the current change list + NavigableMap changes = new TreeMap<>(); + for (Change c : zoneContainer.changes()) { + if (c.getId() != null) { + changes.put(Integer.valueOf(c.getId()), c); + } + } + String[] fields = (String[]) options.get("fields"); + String sortOrder = (String) options.get("sortOrder"); + String pageToken = (String) options.get("pageToken"); + Integer maxResults = options.get("maxResults") == null + ? null : Integer.valueOf((String) options.get("maxResults")); + // as the only supported field is change sequence, we are not reading sortBy + NavigableSet keys; + if ("descending".equals(sortOrder)) { + keys = changes.descendingKeySet(); + } else { + keys = changes.navigableKeySet(); + } + Integer from = null; + try { + from = Integer.valueOf(pageToken); + } catch (NumberFormatException ex) { + // ignore page token + } + keys = from != null ? keys.tailSet(from, false) : keys; + NavigableMap fragment = + from != null && changes.containsKey(from) ? changes.tailMap(from, false) : changes; + boolean sizeReached = false; + boolean hasMorePages = false; + LinkedList serializedResults = new LinkedList<>(); + String lastChangeId = null; + for (Integer key : keys) { + Change change = fragment.get(key); + if (sizeReached) { + // we do not add this, just note that there would be more and there should be a token + hasMorePages = true; + break; + } else { + lastChangeId = change.getId(); + try { + serializedResults.addLast(jsonFactory.toString( + OptionParsers.extractFields(change, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing change %s in managed zone %s in project %s", + lastChangeId, zoneName, projectId)); + } + } + sizeReached = maxResults != null && maxResults.equals(serializedResults.size()); + } + boolean includePageToken = + hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken")); + return toListResponse(serializedResults, "changes", lastChangeId, includePageToken); + } + + /** + * Validates a zone to be created. + */ + private static Response checkZone(ManagedZone zone) { + if (zone.getName() == null) { + return Error.REQUIRED.response( + "The 'entity.managedZone.name' parameter is required but was missing."); + } + if (zone.getDnsName() == null) { + return Error.REQUIRED.response( + "The 'entity.managedZone.dnsName' parameter is required but was missing."); + } + if (zone.getDescription() == null) { + return Error.REQUIRED.response( + "The 'entity.managedZone.description' parameter is required but was missing."); + } + try { + int number = Integer.valueOf(zone.getName()); + return Error.INVALID.response( + String.format("Invalid value for 'entity.managedZone.name': '%s'", number)); + } catch (NumberFormatException ex) { + // expected + } + if (zone.getName().isEmpty() || zone.getName().length() > 32 + || !ZONE_NAME_RE.matcher(zone.getName()).matches()) { + return Error.INVALID.response( + String.format("Invalid value for 'entity.managedZone.name': '%s'", zone.getName())); + } + if (zone.getDnsName().isEmpty() || !zone.getDnsName().endsWith(".")) { + return Error.INVALID.response( + String.format("Invalid value for 'entity.managedZone.dnsName': '%s'", zone.getDnsName())); + } + if (FORBIDDEN.contains(zone.getDnsName())) { + return Error.NOT_AVAILABLE.response(String.format( + "The '%s' managed zone is not available to be created.", zone.getDnsName())); + } + return null; + } + + /** + * Validates a change to be created. + */ + @VisibleForTesting + static Response checkChange(Change change, ZoneContainer zone) { + if ((change.getDeletions() == null || change.getDeletions().size() <= 0) + && (change.getAdditions() == null || change.getAdditions().size() <= 0)) { + return Error.REQUIRED.response("The 'entity.change' parameter is required but was missing."); + } + if (change.getAdditions() != null) { + int counter = 0; + for (ResourceRecordSet addition : change.getAdditions()) { + Response response = checkRrset(addition, zone, counter, "additions"); + if (response != null) { + return response; + } + counter++; + } + } + if (change.getDeletions() != null) { + int counter = 0; + for (ResourceRecordSet deletion : change.getDeletions()) { + Response response = checkRrset(deletion, zone, counter, "deletions"); + if (response != null) { + return response; + } + counter++; + } + } + return checkAdditionsDeletions(change.getAdditions(), change.getDeletions(), zone); + // null if everything is ok + } + + /** + * Checks a rrset within a change. + * + * @param type [additions|deletions] + * @param index the index or addition or deletion in the list + * @param zone the zone that this change is applied to + */ + @VisibleForTesting + static Response checkRrset(ResourceRecordSet rrset, ZoneContainer zone, int index, String type) { + if (rrset.getName() == null || !rrset.getName().endsWith(zone.zone().getDnsName())) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].name': '%s'", type, index, rrset.getName())); + } + if (rrset.getType() == null || !TYPES.contains(rrset.getType())) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].type': '%s'", type, index, rrset.getType())); + } + if (rrset.getTtl() != null && rrset.getTtl() != 0 && rrset.getTtl() < 0) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].ttl': '%s'", type, index, rrset.getTtl())); + } + if (rrset.getRrdatas() == null || rrset.isEmpty()) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].rrdata': '%s'", type, index, "")); + } + int counter = 0; + for (String record : rrset.getRrdatas()) { + if (!checkRrData(record, rrset.getType())) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].rrdata[%s]': '%s'", + type, index, counter, record)); + } + counter++; + } + if ("deletions".equals(type)) { + // check that deletion has a match by name and type + boolean found = false; + for (ResourceRecordSet wrappedRrset : zone.dnsRecords().get().values()) { + if (rrset.getName().equals(wrappedRrset.getName()) + && rrset.getType().equals(wrappedRrset.getType())) { + found = true; + break; + } + } + if (!found) { + return Error.NOT_FOUND.response(String.format( + "The 'entity.change.deletions[%s]' resource named '%s (%s)' does not exist.", + index, rrset.getName(), rrset.getType())); + } + // if found, we still need an exact match + if ("deletions".equals(type) + && !zone.dnsRecords().get().containsValue(rrset)) { + // such a record does not exist + return Error.CONDITION_NOT_MET.response(String.format( + "Precondition not met for 'entity.change.deletions[%s]", index)); + } + } + return null; + } + + /** + * Checks against duplicate additions (for each record set to be added that already exists, we + * must have a matching deletion. Furthermore, check that mandatory SOA and NS records stay. + */ + static Response checkAdditionsDeletions(List additions, + List deletions, ZoneContainer zone) { + if (additions != null) { + int index = 0; + for (ResourceRecordSet rrset : additions) { + for (ResourceRecordSet wrappedRrset : zone.dnsRecords().get().values()) { + if (rrset.getName().equals(wrappedRrset.getName()) + && rrset.getType().equals(wrappedRrset.getType()) + // such a record set exists and we must have a deletion + && (deletions == null || !deletions.contains(wrappedRrset))) { + return Error.ALREADY_EXISTS.response(String.format( + "The 'entity.change.additions[%s]' resource named '%s (%s)' already exists.", + index, rrset.getName(), rrset.getType())); + } + } + if (rrset.getType().equals("SOA") && findByNameAndType(deletions, null, "SOA") == null) { + return Error.INVALID_ZONE_APEX.response(String.format("The resource record set 'entity" + + ".change.additions[%s]' is invalid because a zone must contain exactly one resource" + + " record set of type 'SOA' at the apex.", index)); + } + if (rrset.getType().equals("NS") && findByNameAndType(deletions, null, "NS") == null) { + return Error.INVALID_ZONE_APEX.response(String.format("The resource record set 'entity" + + ".change.additions[%s]' is invalid because a zone must contain exactly one resource" + + " record set of type 'NS' at the apex.", index)); + } + index++; + } + } + if (deletions != null) { + int index = 0; + for (ResourceRecordSet rrset : deletions) { + if (rrset.getType().equals("SOA") && findByNameAndType(additions, null, "SOA") == null) { + return Error.INVALID_ZONE_APEX.response(String.format("The resource record set 'entity" + + ".change.deletions[%s]' is invalid because a zone must contain exactly one resource" + + " record set of type 'SOA' at the apex.", index)); + } + if (rrset.getType().equals("NS") && findByNameAndType(additions, null, "NS") == null) { + return Error.INVALID_ZONE_APEX.response(String.format("The resource record set 'entity" + + ".change.deletions[%s]' is invalid because a zone must contain exactly one resource" + + " record set of type 'NS' at the apex.", index)); + } + index++; + } + } + return null; + } + + /** + * Helper for searching rrsets in a collection. + */ + private static ResourceRecordSet findByNameAndType(Iterable recordSets, + String name, String type) { + if (recordSets != null) { + for (ResourceRecordSet rrset : recordSets) { + if ((name == null || name.equals(rrset.getName())) + && (type == null || type.equals(rrset.getType()))) { + return rrset; + } + } + } + return null; + } + + /** + * We only provide the most basic validation for A and AAAA record sets. + */ + static boolean checkRrData(String data, String type) { + switch (type) { + case "A": + return !data.contains(":") && isInetAddress(data); + case "AAAA": + return data.contains(":") && isInetAddress(data); + default: + return true; + } + } + + /** + * Check supplied listing options. + */ + @VisibleForTesting + static Response checkListOptions(Map options) { + // for general listing + String maxResultsString = (String) options.get("maxResults"); + if (maxResultsString != null) { + Integer maxResults; + try { + maxResults = Integer.valueOf(maxResultsString); + } catch (NumberFormatException ex) { + return Error.INVALID.response(String.format( + "Invalid integer value': '%s'.", maxResultsString)); + } + if (maxResults <= 0) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.maxResults': '%s'", maxResults)); + } + } + String dnsName = (String) options.get("dnsName"); + if (dnsName != null && !dnsName.endsWith(".")) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.dnsName': '%s'", dnsName)); + } + // for listing record sets, name must be fully qualified + String name = (String) options.get("name"); + if (name != null && !name.endsWith(".")) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.name': '%s'", name)); + } + String type = (String) options.get("type"); // must be provided with name + if (type != null) { + if (name == null) { + return Error.INVALID.response("Invalid value for 'parameters.name': '' " + + "(name must be specified if type is specified)"); + } + if (!TYPES.contains(type)) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.type': '%s'", type)); + } + } + // for listing changes + String order = (String) options.get("sortOrder"); + if (order != null && !"ascending".equals(order) && !"descending".equals(order)) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.sortOrder': '%s'", order)); + } + String sortBy = (String) options.get("sortBy"); // case insensitive + if (sortBy != null && !"changesequence".equals(sortBy.toLowerCase())) { + return Error.INVALID.response(String.format( + "Invalid string value: '%s'. Allowed values: [changesequence]", sortBy)); + } + return null; + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/OptionParsers.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/OptionParsers.java new file mode 100644 index 000000000000..578a0b52db3d --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/OptionParsers.java @@ -0,0 +1,255 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns.testing; + +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.Project; +import com.google.api.services.dns.model.ResourceRecordSet; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility helpers for LocalDnsHelper. + */ +class OptionParsers { + + static Map parseListZonesOptions(String query) { + Map options = new HashMap<>(); + if (query != null) { + String[] args = query.split("&"); + for (String arg : args) { + String[] argEntry = arg.split("="); + switch (argEntry[0]) { + case "fields": + // List fields are in the form "managedZones(field1, field2, ...),nextPageToken" + String replaced = argEntry[1].replace("managedZones(", ","); + replaced = replaced.replace(")", ","); + // we will get empty strings, but it does not matter, they will be ignored + options.put("fields", replaced.split(",")); + break; + case "dnsName": + options.put("dnsName", argEntry[1]); + break; + case "pageToken": + options.put("pageToken", argEntry[1]); + break; + case "maxResults": + // parsing to int is done while handling + options.put("maxResults", argEntry[1]); + break; + default: + break; + } + } + } + return options; + } + + static String[] parseGetOptions(String query) { + if (query != null) { + String[] args = query.split("&"); + for (String arg : args) { + String[] argEntry = arg.split("="); + if (argEntry[0].equals("fields")) { + // List fields are in the form "fields=field1, field2,..." + return argEntry[1].split(","); + } + } + } + return new String[0]; + } + + static ManagedZone extractFields(ManagedZone fullZone, String... fields) { + if (fields == null || fields.length == 0) { + return fullZone; + } + ManagedZone managedZone = new ManagedZone(); + for (String field : fields) { + switch (field) { + case "creationTime": + managedZone.setCreationTime(fullZone.getCreationTime()); + break; + case "description": + managedZone.setDescription(fullZone.getDescription()); + break; + case "dnsName": + managedZone.setDnsName(fullZone.getDnsName()); + break; + case "id": + managedZone.setId(fullZone.getId()); + break; + case "name": + managedZone.setName(fullZone.getName()); + break; + case "nameServerSet": + managedZone.setNameServerSet(fullZone.getNameServerSet()); + break; + case "nameServers": + managedZone.setNameServers(fullZone.getNameServers()); + break; + default: + break; + } + } + return managedZone; + } + + static Change extractFields(Change fullChange, String... fields) { + if (fields == null || fields.length == 0) { + return fullChange; + } + Change change = new Change(); + for (String field : fields) { + switch (field) { + case "additions": + change.setAdditions(fullChange.getAdditions()); + break; + case "deletions": + change.setDeletions(fullChange.getDeletions()); + break; + case "id": + change.setId(fullChange.getId()); + break; + case "startTime": + change.setStartTime(fullChange.getStartTime()); + break; + case "status": + change.setStatus(fullChange.getStatus()); + break; + default: + break; + } + } + return change; + } + + static Project extractFields(Project fullProject, String... fields) { + if (fields == null || fields.length == 0) { + return fullProject; + } + Project project = new Project(); + for (String field : fields) { + switch (field) { + case "id": + project.setId(fullProject.getId()); + break; + case "number": + project.setNumber(fullProject.getNumber()); + break; + case "quota": + project.setQuota(fullProject.getQuota()); + break; + default: + break; + } + } + return project; + } + + static ResourceRecordSet extractFields(ResourceRecordSet fullRecord, String... fields) { + if (fields == null || fields.length == 0) { + return fullRecord; + } + ResourceRecordSet recordSet = new ResourceRecordSet(); + for (String field : fields) { + switch (field) { + case "name": + recordSet.setName(fullRecord.getName()); + break; + case "rrdatas": + recordSet.setRrdatas(fullRecord.getRrdatas()); + break; + case "type": + recordSet.setType(fullRecord.getType()); + break; + case "ttl": + recordSet.setTtl(fullRecord.getTtl()); + break; + default: + break; + } + } + return recordSet; + } + + static Map parseListChangesOptions(String query) { + Map options = new HashMap<>(); + if (query != null) { + String[] args = query.split("&"); + for (String arg : args) { + String[] argEntry = arg.split("="); + switch (argEntry[0]) { + case "fields": + String replaced = argEntry[1].replace("changes(", ",").replace(")", ","); + options.put("fields", replaced.split(",")); // empty strings will be ignored + break; + case "pageToken": + options.put("pageToken", argEntry[1]); + break; + case "sortBy": + options.put("sortBy", argEntry[1]); + break; + case "sortOrder": + options.put("sortOrder", argEntry[1]); + break; + case "maxResults": + // parsing to int is done while handling + options.put("maxResults", argEntry[1]); + break; + default: + break; + } + } + } + return options; + } + + static Map parseListDnsRecordsOptions(String query) { + Map options = new HashMap<>(); + if (query != null) { + String[] args = query.split("&"); + for (String arg : args) { + String[] argEntry = arg.split("="); + switch (argEntry[0]) { + case "fields": + String replace = argEntry[1].replace("rrsets(", ","); + replace = replace.replace(")", ","); + options.put("fields", replace.split(",")); // empty strings do not matter + break; + case "name": + options.put("name", argEntry[1]); + break; + case "type": + options.put("type", argEntry[1]); + break; + case "pageToken": + options.put("pageToken", argEntry[1]); + break; + case "maxResults": + // parsing to int is done while handling + options.put("maxResults", argEntry[1]); + break; + default: + break; + } + } + } + return options; + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/AbstractOptionTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/AbstractOptionTest.java new file mode 100644 index 000000000000..d88ea85c5846 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/AbstractOptionTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; + +import com.google.gcloud.dns.spi.DnsRpc; + +import org.junit.Test; + +public class AbstractOptionTest { + + private static final DnsRpc.Option RPC_OPTION = DnsRpc.Option.DNS_TYPE; + private static final DnsRpc.Option ANOTHER_RPC_OPTION = DnsRpc.Option.DNS_NAME; + private static final String VALUE = "some value"; + private static final String OTHER_VALUE = "another value"; + private static final AbstractOption OPTION = new AbstractOption(RPC_OPTION, VALUE) {}; + private static final AbstractOption OPTION_EQUALS = new AbstractOption(RPC_OPTION, VALUE) {}; + private static final AbstractOption OPTION_NOT_EQUALS1 = + new AbstractOption(RPC_OPTION, OTHER_VALUE) {}; + private static final AbstractOption OPTION_NOT_EQUALS2 = + new AbstractOption(ANOTHER_RPC_OPTION, VALUE) {}; + + @Test + public void testEquals() { + assertEquals(OPTION, OPTION_EQUALS); + assertNotEquals(OPTION, OPTION_NOT_EQUALS1); + assertNotEquals(OPTION, OPTION_NOT_EQUALS2); + } + + @Test + public void testHashCode() { + assertEquals(OPTION.hashCode(), OPTION_EQUALS.hashCode()); + } + + @Test + public void testConstructor() { + assertEquals(RPC_OPTION, OPTION.rpcOption()); + assertEquals(VALUE, OPTION.value()); + try { + new AbstractOption(null, VALUE) {}; + fail("Cannot build with empty option."); + } catch (NullPointerException e) { + // expected + } + new AbstractOption(RPC_OPTION, null) {}; // null value is ok + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ChangeRequestInfoTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ChangeRequestInfoTest.java new file mode 100644 index 000000000000..55f2af0824ec --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ChangeRequestInfoTest.java @@ -0,0 +1,218 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import java.util.List; + +public class ChangeRequestInfoTest { + + private static final String ID = "cr-id-1"; + private static final Long START_TIME_MILLIS = 12334567890L; + private static final ChangeRequest.Status STATUS = ChangeRequest.Status.PENDING; + private static final String NAME1 = "dns1"; + private static final RecordSet.Type TYPE1 = RecordSet.Type.A; + private static final String NAME2 = "dns2"; + private static final RecordSet.Type TYPE2 = RecordSet.Type.AAAA; + private static final String NAME3 = "dns3"; + private static final RecordSet.Type TYPE3 = RecordSet.Type.MX; + private static final RecordSet RECORD1 = RecordSet.builder(NAME1, TYPE1).build(); + private static final RecordSet RECORD2 = RecordSet.builder(NAME2, TYPE2).build(); + private static final RecordSet RECORD3 = RecordSet.builder(NAME3, TYPE3).build(); + private static final List ADDITIONS = ImmutableList.of(RECORD1, RECORD2); + private static final List DELETIONS = ImmutableList.of(RECORD3); + private static final ChangeRequestInfo CHANGE = ChangeRequest.builder() + .add(RECORD1) + .add(RECORD2) + .delete(RECORD3) + .startTimeMillis(START_TIME_MILLIS) + .status(STATUS) + .id(ID) + .build(); + + @Test + public void testEmptyBuilder() { + ChangeRequestInfo cr = ChangeRequest.builder().build(); + assertNotNull(cr.deletions()); + assertTrue(cr.deletions().isEmpty()); + assertNotNull(cr.additions()); + assertTrue(cr.additions().isEmpty()); + } + + @Test + public void testBuilder() { + assertEquals(ID, CHANGE.id()); + assertEquals(STATUS, CHANGE.status()); + assertEquals(START_TIME_MILLIS, CHANGE.startTimeMillis()); + assertEquals(ADDITIONS, CHANGE.additions()); + assertEquals(DELETIONS, CHANGE.deletions()); + List recordList = ImmutableList.of(RECORD1); + ChangeRequestInfo another = CHANGE.toBuilder().additions(recordList).build(); + assertEquals(recordList, another.additions()); + assertEquals(CHANGE.deletions(), another.deletions()); + another = CHANGE.toBuilder().deletions(recordList).build(); + assertEquals(recordList, another.deletions()); + assertEquals(CHANGE.additions(), another.additions()); + } + + @Test + public void testEqualsAndNotEquals() { + ChangeRequestInfo clone = CHANGE.toBuilder().build(); + assertEquals(CHANGE, clone); + clone = ChangeRequest.fromPb(CHANGE.toPb()); + assertEquals(CHANGE, clone); + clone = CHANGE.toBuilder().id("some-other-id").build(); + assertNotEquals(CHANGE, clone); + clone = CHANGE.toBuilder().startTimeMillis(CHANGE.startTimeMillis() + 1).build(); + assertNotEquals(CHANGE, clone); + clone = CHANGE.toBuilder().add(RECORD3).build(); + assertNotEquals(CHANGE, clone); + clone = CHANGE.toBuilder().delete(RECORD1).build(); + assertNotEquals(CHANGE, clone); + ChangeRequestInfo empty = ChangeRequest.builder().build(); + assertNotEquals(CHANGE, empty); + assertEquals(empty, ChangeRequest.builder().build()); + } + + @Test + public void testSameHashCodeOnEquals() { + ChangeRequestInfo clone = CHANGE.toBuilder().build(); + assertEquals(CHANGE, clone); + assertEquals(CHANGE.hashCode(), clone.hashCode()); + ChangeRequestInfo empty = ChangeRequest.builder().build(); + assertEquals(empty.hashCode(), ChangeRequest.builder().build().hashCode()); + } + + @Test + public void testToAndFromPb() { + assertEquals(CHANGE, ChangeRequest.fromPb(CHANGE.toPb())); + ChangeRequestInfo partial = ChangeRequest.builder().build(); + assertEquals(partial, ChangeRequest.fromPb(partial.toPb())); + partial = ChangeRequest.builder().id(ID).build(); + assertEquals(partial, ChangeRequest.fromPb(partial.toPb())); + partial = ChangeRequest.builder().add(RECORD1).build(); + assertEquals(partial, ChangeRequest.fromPb(partial.toPb())); + partial = ChangeRequest.builder().delete(RECORD1).build(); + assertEquals(partial, ChangeRequest.fromPb(partial.toPb())); + partial = ChangeRequest.builder().additions(ADDITIONS).build(); + assertEquals(partial, ChangeRequest.fromPb(partial.toPb())); + partial = ChangeRequest.builder().deletions(DELETIONS).build(); + assertEquals(partial, ChangeRequest.fromPb(partial.toPb())); + partial = ChangeRequest.builder().startTimeMillis(START_TIME_MILLIS).build(); + assertEquals(partial, ChangeRequest.fromPb(partial.toPb())); + partial = ChangeRequest.builder().status(STATUS).build(); + assertEquals(partial, ChangeRequest.fromPb(partial.toPb())); + } + + @Test + public void testToBuilder() { + assertEquals(CHANGE, CHANGE.toBuilder().build()); + ChangeRequestInfo partial = ChangeRequest.builder().build(); + assertEquals(partial, partial.toBuilder().build()); + partial = ChangeRequest.builder().id(ID).build(); + assertEquals(partial, partial.toBuilder().build()); + partial = ChangeRequest.builder().add(RECORD1).build(); + assertEquals(partial, partial.toBuilder().build()); + partial = ChangeRequest.builder().delete(RECORD1).build(); + assertEquals(partial, partial.toBuilder().build()); + partial = ChangeRequest.builder().additions(ADDITIONS).build(); + assertEquals(partial, partial.toBuilder().build()); + partial = ChangeRequest.builder().deletions(DELETIONS).build(); + assertEquals(partial, partial.toBuilder().build()); + partial = ChangeRequest.builder().startTimeMillis(START_TIME_MILLIS).build(); + assertEquals(partial, partial.toBuilder().build()); + partial = ChangeRequest.builder().status(STATUS).build(); + assertEquals(partial, partial.toBuilder().build()); + } + + @Test + public void testClearAdditions() { + ChangeRequestInfo clone = CHANGE.toBuilder().clearAdditions().build(); + assertTrue(clone.additions().isEmpty()); + assertFalse(clone.deletions().isEmpty()); + } + + @Test + public void testAddAddition() { + try { + CHANGE.toBuilder().add(null); + fail("Should not be able to add null RecordSet."); + } catch (NullPointerException e) { + // expected + } + ChangeRequestInfo clone = CHANGE.toBuilder().add(RECORD1).build(); + assertEquals(CHANGE.additions().size() + 1, clone.additions().size()); + } + + @Test + public void testAddDeletion() { + try { + CHANGE.toBuilder().delete(null); + fail("Should not be able to delete null RecordSet."); + } catch (NullPointerException e) { + // expected + } + ChangeRequestInfo clone = CHANGE.toBuilder().delete(RECORD1).build(); + assertEquals(CHANGE.deletions().size() + 1, clone.deletions().size()); + } + + @Test + public void testClearDeletions() { + ChangeRequestInfo clone = CHANGE.toBuilder().clearDeletions().build(); + assertTrue(clone.deletions().isEmpty()); + assertFalse(clone.additions().isEmpty()); + } + + @Test + public void testRemoveAddition() { + ChangeRequestInfo clone = CHANGE.toBuilder().removeAddition(RECORD1).build(); + assertTrue(clone.additions().contains(RECORD2)); + assertFalse(clone.additions().contains(RECORD1)); + assertTrue(clone.deletions().contains(RECORD3)); + clone = CHANGE.toBuilder().removeAddition(RECORD2).removeAddition(RECORD1).build(); + assertFalse(clone.additions().contains(RECORD2)); + assertFalse(clone.additions().contains(RECORD1)); + assertTrue(clone.additions().isEmpty()); + assertTrue(clone.deletions().contains(RECORD3)); + } + + @Test + public void testRemoveDeletion() { + ChangeRequestInfo clone = CHANGE.toBuilder().removeDeletion(RECORD3).build(); + assertTrue(clone.deletions().isEmpty()); + } + + @Test + public void testDateParsing() { + String startTime = "2016-01-26T18:33:43.512Z"; // obtained from service + com.google.api.services.dns.model.Change change = CHANGE.toPb().setStartTime(startTime); + ChangeRequestInfo converted = ChangeRequest.fromPb(change); + assertNotNull(converted.startTimeMillis()); + assertEquals(change, converted.toPb()); + assertEquals(change.getStartTime(), converted.toPb().getStartTime()); + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ChangeRequestTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ChangeRequestTest.java new file mode 100644 index 000000000000..bfd1d0f512f4 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ChangeRequestTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ChangeRequestTest { + + private static final String ZONE_NAME = "dns-zone-name"; + private static final ChangeRequestInfo CHANGE_REQUEST_INFO = ChangeRequest.builder() + .add(RecordSet.builder("name", RecordSet.Type.A).build()) + .delete(RecordSet.builder("othername", RecordSet.Type.AAAA).build()) + .build(); + private static final DnsOptions OPTIONS = createStrictMock(DnsOptions.class); + + private Dns dns; + private ChangeRequest changeRequest; + private ChangeRequest changeRequestPartial; + + @Before + public void setUp() throws Exception { + dns = createStrictMock(Dns.class); + expect(dns.options()).andReturn(OPTIONS).times(2); + replay(dns); + changeRequest = new ChangeRequest(dns, ZONE_NAME, new ChangeRequestInfo.BuilderImpl( + CHANGE_REQUEST_INFO.toBuilder() + .startTimeMillis(132L) + .id("12") + .status(ChangeRequest.Status.DONE) + .build())); + changeRequestPartial = new ChangeRequest(dns, ZONE_NAME, + new ChangeRequest.BuilderImpl(CHANGE_REQUEST_INFO)); + reset(dns); + } + + @After + public void tearDown() throws Exception { + verify(dns); + } + + @Test + public void testConstructor() { + expect(dns.options()).andReturn(OPTIONS); + replay(dns); + assertEquals(new ChangeRequest(dns, ZONE_NAME, + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_INFO)), changeRequestPartial); + assertNotNull(changeRequest.dns()); + assertEquals(ZONE_NAME, changeRequest.zone()); + assertSame(dns, changeRequestPartial.dns()); + assertEquals(ZONE_NAME, changeRequestPartial.zone()); + } + + @Test + public void testFromPb() { + expect(dns.options()).andReturn(OPTIONS).times(2); + replay(dns); + assertEquals(changeRequest, ChangeRequest.fromPb(dns, ZONE_NAME, changeRequest.toPb())); + assertEquals(changeRequestPartial, + ChangeRequest.fromPb(dns, ZONE_NAME, changeRequestPartial.toPb())); + } + + @Test + public void testEqualsAndToBuilder() { + expect(dns.options()).andReturn(OPTIONS).times(2); + replay(dns); + ChangeRequest compare = changeRequest.toBuilder().build(); + assertEquals(changeRequest, compare); + assertEquals(changeRequest.hashCode(), compare.hashCode()); + compare = changeRequestPartial.toBuilder().build(); + assertEquals(changeRequestPartial, compare); + assertEquals(changeRequestPartial.hashCode(), compare.hashCode()); + } + + @Test + public void testBuilder() { + // one for each build() call because it invokes a constructor + expect(dns.options()).andReturn(OPTIONS).times(9); + replay(dns); + String id = changeRequest.id() + "aaa"; + assertEquals(id, changeRequest.toBuilder().id(id).build().id()); + ChangeRequest modified = + changeRequest.toBuilder().status(ChangeRequest.Status.PENDING).build(); + assertEquals(ChangeRequest.Status.PENDING, modified.status()); + modified = changeRequest.toBuilder().clearDeletions().build(); + assertTrue(modified.deletions().isEmpty()); + modified = changeRequest.toBuilder().clearAdditions().build(); + assertTrue(modified.additions().isEmpty()); + modified = changeRequest.toBuilder().additions(ImmutableList.of()).build(); + assertTrue(modified.additions().isEmpty()); + modified = changeRequest.toBuilder().deletions(ImmutableList.of()).build(); + assertTrue(modified.deletions().isEmpty()); + RecordSet cname = RecordSet.builder("last", RecordSet.Type.CNAME).build(); + modified = changeRequest.toBuilder().add(cname).build(); + assertTrue(modified.additions().contains(cname)); + modified = changeRequest.toBuilder().delete(cname).build(); + assertTrue(modified.deletions().contains(cname)); + modified = changeRequest.toBuilder().startTimeMillis(0L).build(); + assertEquals(Long.valueOf(0), modified.startTimeMillis()); + } + + @Test + public void testApplyTo() { + expect(dns.applyChangeRequest(ZONE_NAME, changeRequest)).andReturn(changeRequest); + expect(dns.applyChangeRequest(ZONE_NAME, changeRequest, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.START_TIME))) + .andReturn(changeRequest); + replay(dns); + assertSame(changeRequest, changeRequest.applyTo()); + assertSame(changeRequest, + changeRequest.applyTo(Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.START_TIME))); + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/DnsImplTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/DnsImplTest.java new file mode 100644 index 000000000000..94ed4a3da3f7 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/DnsImplTest.java @@ -0,0 +1,388 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ManagedZone; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.gcloud.Page; +import com.google.gcloud.RetryParams; +import com.google.gcloud.ServiceOptions; +import com.google.gcloud.dns.spi.DnsRpc; +import com.google.gcloud.dns.spi.DnsRpcFactory; + +import org.easymock.Capture; +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Map; + +public class DnsImplTest { + + // Dns entities + private static final String ZONE_NAME = "some zone name"; + private static final String DNS_NAME = "example.com."; + private static final String DESCRIPTION = "desc"; + private static final String CHANGE_ID = "some change id"; + private static final RecordSet DNS_RECORD1 = + RecordSet.builder("Something", RecordSet.Type.AAAA).build(); + private static final RecordSet DNS_RECORD2 = + RecordSet.builder("Different", RecordSet.Type.AAAA).build(); + private static final Integer MAX_SIZE = 20; + private static final String PAGE_TOKEN = "some token"; + private static final ZoneInfo ZONE_INFO = ZoneInfo.of(ZONE_NAME, DNS_NAME, DESCRIPTION); + private static final ProjectInfo PROJECT_INFO = ProjectInfo.builder().build(); + private static final ChangeRequestInfo CHANGE_REQUEST_PARTIAL = ChangeRequestInfo.builder() + .add(DNS_RECORD1) + .build(); + private static final ChangeRequestInfo CHANGE_REQUEST_COMPLETE = ChangeRequestInfo.builder() + .add(DNS_RECORD1) + .startTimeMillis(123L) + .status(ChangeRequest.Status.PENDING) + .id(CHANGE_ID) + .build(); + + // Result lists + private static final DnsRpc.ListResult LIST_RESULT_OF_PB_CHANGES = + DnsRpc.ListResult.of("cursor", ImmutableList.of(CHANGE_REQUEST_COMPLETE.toPb(), + CHANGE_REQUEST_PARTIAL.toPb())); + private static final DnsRpc.ListResult LIST_RESULT_OF_PB_ZONES = + DnsRpc.ListResult.of("cursor", ImmutableList.of(ZONE_INFO.toPb())); + private static final DnsRpc.ListResult + LIST_OF_PB_DNS_RECORDS = + DnsRpc.ListResult.of("cursor", ImmutableList.of(DNS_RECORD1.toPb(), DNS_RECORD2.toPb())); + + // Field options + private static final Dns.ZoneOption ZONE_FIELDS = + Dns.ZoneOption.fields(Dns.ZoneField.CREATION_TIME); + private static final Dns.ProjectOption PROJECT_FIELDS = + Dns.ProjectOption.fields(Dns.ProjectField.QUOTA); + private static final Dns.ChangeRequestOption CHANGE_GET_FIELDS = + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.STATUS); + + // Listing options + private static final Dns.ZoneListOption[] ZONE_LIST_OPTIONS = { + Dns.ZoneListOption.pageSize(MAX_SIZE), Dns.ZoneListOption.pageToken(PAGE_TOKEN), + Dns.ZoneListOption.fields(Dns.ZoneField.DESCRIPTION), + Dns.ZoneListOption.dnsName(DNS_NAME)}; + private static final Dns.ChangeRequestListOption[] CHANGE_LIST_OPTIONS = { + Dns.ChangeRequestListOption.pageSize(MAX_SIZE), + Dns.ChangeRequestListOption.pageToken(PAGE_TOKEN), + Dns.ChangeRequestListOption.fields(Dns.ChangeRequestField.STATUS), + Dns.ChangeRequestListOption.sortOrder(Dns.SortingOrder.ASCENDING)}; + private static final Dns.RecordSetListOption[] DNS_RECORD_LIST_OPTIONS = { + Dns.RecordSetListOption.pageSize(MAX_SIZE), + Dns.RecordSetListOption.pageToken(PAGE_TOKEN), + Dns.RecordSetListOption.fields(Dns.RecordSetField.TTL), + Dns.RecordSetListOption.dnsName(DNS_NAME), + Dns.RecordSetListOption.type(RecordSet.Type.AAAA)}; + + // Other + private static final Map EMPTY_RPC_OPTIONS = ImmutableMap.of(); + private static final ServiceOptions.Clock TIME_SOURCE = new ServiceOptions.Clock() { + @Override + public long millis() { + return 42000L; + } + }; + + private DnsOptions options; + private DnsRpcFactory rpcFactoryMock; + private DnsRpc dnsRpcMock; + private Dns dns; + + @Before + public void setUp() { + rpcFactoryMock = EasyMock.createMock(DnsRpcFactory.class); + dnsRpcMock = EasyMock.createMock(DnsRpc.class); + EasyMock.expect(rpcFactoryMock.create(EasyMock.anyObject(DnsOptions.class))) + .andReturn(dnsRpcMock); + EasyMock.replay(rpcFactoryMock); + options = DnsOptions.builder() + .projectId("projectId") + .clock(TIME_SOURCE) + .serviceRpcFactory(rpcFactoryMock) + .retryParams(RetryParams.noRetries()) + .build(); + } + + @After + public void tearDown() throws Exception { + EasyMock.verify(rpcFactoryMock); + } + + @Test + public void testCreateZone() { + EasyMock.expect(dnsRpcMock.create(ZONE_INFO.toPb(), EMPTY_RPC_OPTIONS)) + .andReturn(ZONE_INFO.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Zone zone = dns.create(ZONE_INFO); + assertEquals(new Zone(dns, new ZoneInfo.BuilderImpl(ZONE_INFO)), zone); + } + + @Test + public void testCreateZoneWithOptions() { + Capture> capturedOptions = Capture.newInstance(); + EasyMock.expect(dnsRpcMock.create(EasyMock.eq(ZONE_INFO.toPb()), + EasyMock.capture(capturedOptions))).andReturn(ZONE_INFO.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Zone zone = dns.create(ZONE_INFO, ZONE_FIELDS); + String selector = (String) capturedOptions.getValue().get(ZONE_FIELDS.rpcOption()); + assertEquals(new Zone(dns, new ZoneInfo.BuilderImpl(ZONE_INFO)), zone); + assertTrue(selector.contains(Dns.ZoneField.CREATION_TIME.selector())); + assertTrue(selector.contains(Dns.ZoneField.NAME.selector())); + } + + @Test + public void testGetZone() { + EasyMock.expect(dnsRpcMock.getZone(ZONE_INFO.name(), EMPTY_RPC_OPTIONS)) + .andReturn(ZONE_INFO.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Zone zone = dns.getZone(ZONE_INFO.name()); + assertEquals(new Zone(dns, new ZoneInfo.BuilderImpl(ZONE_INFO)), zone); + } + + @Test + public void testGetZoneWithOptions() { + Capture> capturedOptions = Capture.newInstance(); + EasyMock.expect(dnsRpcMock.getZone(EasyMock.eq(ZONE_INFO.name()), + EasyMock.capture(capturedOptions))).andReturn(ZONE_INFO.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Zone zone = dns.getZone(ZONE_INFO.name(), ZONE_FIELDS); + String selector = (String) capturedOptions.getValue().get(ZONE_FIELDS.rpcOption()); + assertEquals(new Zone(dns, new ZoneInfo.BuilderImpl(ZONE_INFO)), zone); + assertTrue(selector.contains(Dns.ZoneField.CREATION_TIME.selector())); + assertTrue(selector.contains(Dns.ZoneField.NAME.selector())); + } + + @Test + public void testDeleteZone() { + EasyMock.expect(dnsRpcMock.deleteZone(ZONE_INFO.name())) + .andReturn(true); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + assertTrue(dns.delete(ZONE_INFO.name())); + } + + @Test + public void testGetProject() { + EasyMock.expect(dnsRpcMock.getProject(EMPTY_RPC_OPTIONS)) + .andReturn(PROJECT_INFO.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + ProjectInfo projectInfo = dns.getProject(); + assertEquals(PROJECT_INFO, projectInfo); + } + + @Test + public void testProjectGetWithOptions() { + Capture> capturedOptions = Capture.newInstance(); + EasyMock.expect(dnsRpcMock.getProject(EasyMock.capture(capturedOptions))) + .andReturn(PROJECT_INFO.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + ProjectInfo projectInfo = dns.getProject(PROJECT_FIELDS); + String selector = (String) capturedOptions.getValue().get(PROJECT_FIELDS.rpcOption()); + assertEquals(PROJECT_INFO, projectInfo); + assertTrue(selector.contains(Dns.ProjectField.QUOTA.selector())); + assertTrue(selector.contains(Dns.ProjectField.PROJECT_ID.selector())); + } + + @Test + public void testGetChangeRequest() { + EasyMock.expect(dnsRpcMock.getChangeRequest(ZONE_INFO.name(), CHANGE_REQUEST_COMPLETE.id(), + EMPTY_RPC_OPTIONS)).andReturn(CHANGE_REQUEST_COMPLETE.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + ChangeRequest changeRequest = dns.getChangeRequest(ZONE_INFO.name(), + CHANGE_REQUEST_COMPLETE.id()); + assertEquals(new ChangeRequest(dns, ZONE_INFO.name(), + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_COMPLETE)), changeRequest); + } + + @Test + public void testGetChangeRequestWithOptions() { + Capture> capturedOptions = Capture.newInstance(); + EasyMock.expect(dnsRpcMock.getChangeRequest(EasyMock.eq(ZONE_INFO.name()), + EasyMock.eq(CHANGE_REQUEST_COMPLETE.id()), EasyMock.capture(capturedOptions))) + .andReturn(CHANGE_REQUEST_COMPLETE.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + ChangeRequest changeRequest = dns.getChangeRequest(ZONE_INFO.name(), + CHANGE_REQUEST_COMPLETE.id(), CHANGE_GET_FIELDS); + String selector = (String) capturedOptions.getValue().get(CHANGE_GET_FIELDS.rpcOption()); + assertEquals(new ChangeRequest(dns, ZONE_INFO.name(), + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_COMPLETE)), changeRequest); + assertTrue(selector.contains(Dns.ChangeRequestField.STATUS.selector())); + assertTrue(selector.contains(Dns.ChangeRequestField.ID.selector())); + } + + @Test + public void testApplyChangeRequest() { + EasyMock.expect(dnsRpcMock.applyChangeRequest(ZONE_INFO.name(), CHANGE_REQUEST_PARTIAL.toPb(), + EMPTY_RPC_OPTIONS)).andReturn(CHANGE_REQUEST_COMPLETE.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + ChangeRequest changeRequest = dns.applyChangeRequest(ZONE_INFO.name(), + CHANGE_REQUEST_PARTIAL); + assertEquals(new ChangeRequest(dns, ZONE_INFO.name(), + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_COMPLETE)), changeRequest); + } + + @Test + public void testApplyChangeRequestWithOptions() { + Capture> capturedOptions = Capture.newInstance(); + EasyMock.expect(dnsRpcMock.applyChangeRequest(EasyMock.eq(ZONE_INFO.name()), + EasyMock.eq(CHANGE_REQUEST_PARTIAL.toPb()), EasyMock.capture(capturedOptions))) + .andReturn(CHANGE_REQUEST_COMPLETE.toPb()); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + ChangeRequest changeRequest = dns.applyChangeRequest(ZONE_INFO.name(), + CHANGE_REQUEST_PARTIAL, CHANGE_GET_FIELDS); + String selector = (String) capturedOptions.getValue().get(CHANGE_GET_FIELDS.rpcOption()); + assertEquals(new ChangeRequest(dns, ZONE_INFO.name(), + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_COMPLETE)), changeRequest); + assertTrue(selector.contains(Dns.ChangeRequestField.STATUS.selector())); + assertTrue(selector.contains(Dns.ChangeRequestField.ID.selector())); + } + + // lists + @Test + public void testListChangeRequests() { + EasyMock.expect(dnsRpcMock.listChangeRequests(ZONE_INFO.name(), EMPTY_RPC_OPTIONS)) + .andReturn(LIST_RESULT_OF_PB_CHANGES); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Page changeRequestPage = dns.listChangeRequests(ZONE_INFO.name()); + assertTrue(Lists.newArrayList(changeRequestPage.values()).contains( + new ChangeRequest(dns, ZONE_INFO.name(), + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_COMPLETE)))); + assertTrue(Lists.newArrayList(changeRequestPage.values()).contains( + new ChangeRequest(dns, ZONE_INFO.name(), + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_PARTIAL)))); + assertEquals(2, Lists.newArrayList(changeRequestPage.values()).size()); + } + + @Test + public void testListChangeRequestsWithOptions() { + Capture> capturedOptions = Capture.newInstance(); + EasyMock.expect(dnsRpcMock.listChangeRequests(EasyMock.eq(ZONE_NAME), + EasyMock.capture(capturedOptions))).andReturn(LIST_RESULT_OF_PB_CHANGES); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Page changeRequestPage = dns.listChangeRequests(ZONE_NAME, CHANGE_LIST_OPTIONS); + assertTrue(Lists.newArrayList(changeRequestPage.values()).contains( + new ChangeRequest(dns, ZONE_INFO.name(), + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_COMPLETE)))); + assertTrue(Lists.newArrayList(changeRequestPage.values()).contains( + new ChangeRequest(dns, ZONE_INFO.name(), + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_PARTIAL)))); + assertEquals(2, Lists.newArrayList(changeRequestPage.values()).size()); + Integer size = (Integer) capturedOptions.getValue().get(CHANGE_LIST_OPTIONS[0].rpcOption()); + assertEquals(MAX_SIZE, size); + String selector = (String) capturedOptions.getValue().get(CHANGE_LIST_OPTIONS[1].rpcOption()); + assertEquals(PAGE_TOKEN, selector); + selector = (String) capturedOptions.getValue().get(CHANGE_LIST_OPTIONS[2].rpcOption()); + assertTrue(selector.contains(Dns.ChangeRequestField.STATUS.selector())); + assertTrue(selector.contains(Dns.ChangeRequestField.ID.selector())); + selector = (String) capturedOptions.getValue().get(CHANGE_LIST_OPTIONS[3].rpcOption()); + assertTrue(selector.contains(Dns.SortingOrder.ASCENDING.selector())); + } + + @Test + public void testListZones() { + EasyMock.expect(dnsRpcMock.listZones(EMPTY_RPC_OPTIONS)) + .andReturn(LIST_RESULT_OF_PB_ZONES); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Page zonePage = dns.listZones(); + assertEquals(1, Lists.newArrayList(zonePage.values()).size()); + assertEquals(new Zone(dns, new ZoneInfo.BuilderImpl(ZONE_INFO)), + Lists.newArrayList(zonePage.values()).get(0)); + } + + @Test + public void testListZonesWithOptions() { + Capture> capturedOptions = Capture.newInstance(); + EasyMock.expect(dnsRpcMock.listZones(EasyMock.capture(capturedOptions))) + .andReturn(LIST_RESULT_OF_PB_ZONES); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Page zonePage = dns.listZones(ZONE_LIST_OPTIONS); + assertEquals(1, Lists.newArrayList(zonePage.values()).size()); + assertEquals(new Zone(dns, new ZoneInfo.BuilderImpl(ZONE_INFO)), + Lists.newArrayList(zonePage.values()).get(0)); + Integer size = (Integer) capturedOptions.getValue().get(ZONE_LIST_OPTIONS[0].rpcOption()); + assertEquals(MAX_SIZE, size); + String selector = (String) capturedOptions.getValue().get(ZONE_LIST_OPTIONS[1].rpcOption()); + assertEquals(PAGE_TOKEN, selector); + selector = (String) capturedOptions.getValue().get(ZONE_LIST_OPTIONS[2].rpcOption()); + assertTrue(selector.contains(Dns.ZoneField.DESCRIPTION.selector())); + assertTrue(selector.contains(Dns.ZoneField.NAME.selector())); + selector = (String) capturedOptions.getValue().get(ZONE_LIST_OPTIONS[3].rpcOption()); + assertEquals(DNS_NAME, selector); + } + + @Test + public void testListDnsRecords() { + EasyMock.expect(dnsRpcMock.listRecordSets(ZONE_INFO.name(), EMPTY_RPC_OPTIONS)) + .andReturn(LIST_OF_PB_DNS_RECORDS); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Page dnsPage = dns.listRecordSets(ZONE_INFO.name()); + assertEquals(2, Lists.newArrayList(dnsPage.values()).size()); + assertTrue(Lists.newArrayList(dnsPage.values()).contains(DNS_RECORD1)); + assertTrue(Lists.newArrayList(dnsPage.values()).contains(DNS_RECORD2)); + } + + @Test + public void testListDnsRecordsWithOptions() { + Capture> capturedOptions = Capture.newInstance(); + EasyMock.expect(dnsRpcMock.listRecordSets(EasyMock.eq(ZONE_NAME), + EasyMock.capture(capturedOptions))).andReturn(LIST_OF_PB_DNS_RECORDS); + EasyMock.replay(dnsRpcMock); + dns = options.service(); // creates DnsImpl + Page dnsPage = dns.listRecordSets(ZONE_NAME, DNS_RECORD_LIST_OPTIONS); + assertEquals(2, Lists.newArrayList(dnsPage.values()).size()); + assertTrue(Lists.newArrayList(dnsPage.values()).contains(DNS_RECORD1)); + assertTrue(Lists.newArrayList(dnsPage.values()).contains(DNS_RECORD2)); + Integer size = (Integer) capturedOptions.getValue().get(DNS_RECORD_LIST_OPTIONS[0].rpcOption()); + assertEquals(MAX_SIZE, size); + String selector = (String) capturedOptions.getValue() + .get(DNS_RECORD_LIST_OPTIONS[1].rpcOption()); + assertEquals(PAGE_TOKEN, selector); + selector = (String) capturedOptions.getValue().get(DNS_RECORD_LIST_OPTIONS[2].rpcOption()); + assertTrue(selector.contains(Dns.RecordSetField.NAME.selector())); + assertTrue(selector.contains(Dns.RecordSetField.TTL.selector())); + selector = (String) capturedOptions.getValue().get(DNS_RECORD_LIST_OPTIONS[3].rpcOption()); + assertEquals(DNS_RECORD_LIST_OPTIONS[3].value(), selector); + String type = (String) capturedOptions.getValue().get(DNS_RECORD_LIST_OPTIONS[4] + .rpcOption()); + assertEquals(DNS_RECORD_LIST_OPTIONS[4].value(), type); + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/DnsTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/DnsTest.java new file mode 100644 index 000000000000..df86d6ebd495 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/DnsTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.gcloud.dns.spi.DnsRpc; + +import org.junit.Test; + +public class DnsTest { + + private static final Integer PAGE_SIZE = 20; + private static final String PAGE_TOKEN = "page token"; + private static final String DNS_NAME = "www.example.com."; + + @Test + public void testRecordSetListOption() { + // dns name + String dnsName = "some name"; + Dns.RecordSetListOption recordSetListOption = Dns.RecordSetListOption.dnsName(dnsName); + assertEquals(dnsName, recordSetListOption.value()); + assertEquals(DnsRpc.Option.NAME, recordSetListOption.rpcOption()); + // page token + recordSetListOption = Dns.RecordSetListOption.pageToken(PAGE_TOKEN); + assertEquals(PAGE_TOKEN, recordSetListOption.value()); + assertEquals(DnsRpc.Option.PAGE_TOKEN, recordSetListOption.rpcOption()); + // page size + recordSetListOption = Dns.RecordSetListOption.pageSize(PAGE_SIZE); + assertEquals(PAGE_SIZE, recordSetListOption.value()); + assertEquals(DnsRpc.Option.PAGE_SIZE, recordSetListOption.rpcOption()); + // record type + RecordSet.Type recordType = RecordSet.Type.AAAA; + recordSetListOption = Dns.RecordSetListOption.type(recordType); + assertEquals(recordType.name(), recordSetListOption.value()); + assertEquals(DnsRpc.Option.DNS_TYPE, recordSetListOption.rpcOption()); + // fields + recordSetListOption = Dns.RecordSetListOption.fields(Dns.RecordSetField.NAME, + Dns.RecordSetField.TTL); + assertEquals(DnsRpc.Option.FIELDS, recordSetListOption.rpcOption()); + assertTrue(recordSetListOption.value() instanceof String); + assertTrue(((String) recordSetListOption.value()).contains( + Dns.RecordSetField.NAME.selector())); + assertTrue(((String) recordSetListOption.value()).contains( + Dns.RecordSetField.TTL.selector())); + assertTrue(((String) recordSetListOption.value()).contains( + Dns.RecordSetField.NAME.selector())); + } + + @Test + public void testZoneOption() { + Dns.ZoneOption fields = Dns.ZoneOption.fields(Dns.ZoneField.CREATION_TIME, + Dns.ZoneField.DESCRIPTION); + assertEquals(DnsRpc.Option.FIELDS, fields.rpcOption()); + assertTrue(fields.value() instanceof String); + assertTrue(((String) fields.value()).contains(Dns.ZoneField.CREATION_TIME.selector())); + assertTrue(((String) fields.value()).contains(Dns.ZoneField.DESCRIPTION.selector())); + } + + @Test + public void testZoneList() { + // fields + Dns.ZoneListOption fields = Dns.ZoneListOption.fields(Dns.ZoneField.CREATION_TIME, + Dns.ZoneField.DESCRIPTION); + assertEquals(DnsRpc.Option.FIELDS, fields.rpcOption()); + assertTrue(fields.value() instanceof String); + assertTrue(((String) fields.value()).contains(Dns.ZoneField.CREATION_TIME.selector())); + assertTrue(((String) fields.value()).contains(Dns.ZoneField.DESCRIPTION.selector())); + assertTrue(((String) fields.value()).contains(Dns.ZoneField.NAME.selector())); + // page token + Dns.ZoneListOption option = Dns.ZoneListOption.pageToken(PAGE_TOKEN); + assertEquals(PAGE_TOKEN, option.value()); + assertEquals(DnsRpc.Option.PAGE_TOKEN, option.rpcOption()); + // page size + option = Dns.ZoneListOption.pageSize(PAGE_SIZE); + assertEquals(PAGE_SIZE, option.value()); + assertEquals(DnsRpc.Option.PAGE_SIZE, option.rpcOption()); + // dnsName filter + option = Dns.ZoneListOption.dnsName(DNS_NAME); + assertEquals(DNS_NAME, option.value()); + assertEquals(DnsRpc.Option.DNS_NAME, option.rpcOption()); + } + + @Test + public void testProjectGetOption() { + // fields + Dns.ProjectOption fields = Dns.ProjectOption.fields(Dns.ProjectField.QUOTA); + assertEquals(DnsRpc.Option.FIELDS, fields.rpcOption()); + assertTrue(fields.value() instanceof String); + assertTrue(((String) fields.value()).contains(Dns.ProjectField.QUOTA.selector())); + assertTrue(((String) fields.value()).contains(Dns.ProjectField.PROJECT_ID.selector())); + } + + @Test + public void testChangeRequestOption() { + // fields + Dns.ChangeRequestOption fields = Dns.ChangeRequestOption.fields( + Dns.ChangeRequestField.START_TIME, Dns.ChangeRequestField.STATUS); + assertEquals(DnsRpc.Option.FIELDS, fields.rpcOption()); + assertTrue(fields.value() instanceof String); + assertTrue(((String) fields.value()).contains( + Dns.ChangeRequestField.START_TIME.selector())); + assertTrue(((String) fields.value()).contains(Dns.ChangeRequestField.STATUS.selector())); + assertTrue(((String) fields.value()).contains(Dns.ChangeRequestField.ID.selector())); + } + + @Test + public void testChangeRequestListOption() { + // fields + Dns.ChangeRequestListOption fields = Dns.ChangeRequestListOption.fields( + Dns.ChangeRequestField.START_TIME, Dns.ChangeRequestField.STATUS); + assertEquals(DnsRpc.Option.FIELDS, fields.rpcOption()); + assertTrue(fields.value() instanceof String); + assertTrue(((String) fields.value()).contains( + Dns.ChangeRequestField.START_TIME.selector())); + assertTrue(((String) fields.value()).contains(Dns.ChangeRequestField.STATUS.selector())); + assertTrue(((String) fields.value()).contains(Dns.ChangeRequestField.ID.selector())); + // page token + Dns.ChangeRequestListOption option = Dns.ChangeRequestListOption.pageToken(PAGE_TOKEN); + assertEquals(PAGE_TOKEN, option.value()); + assertEquals(DnsRpc.Option.PAGE_TOKEN, option.rpcOption()); + // page size + option = Dns.ChangeRequestListOption.pageSize(PAGE_SIZE); + assertEquals(PAGE_SIZE, option.value()); + assertEquals(DnsRpc.Option.PAGE_SIZE, option.rpcOption()); + // sort order + option = Dns.ChangeRequestListOption.sortOrder(Dns.SortingOrder.ASCENDING); + assertEquals(DnsRpc.Option.SORTING_ORDER, option.rpcOption()); + assertEquals(Dns.SortingOrder.ASCENDING.selector(), option.value()); + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ProjectInfoTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ProjectInfoTest.java new file mode 100644 index 000000000000..d959d44d4351 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ProjectInfoTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +import java.math.BigInteger; + +public class ProjectInfoTest { + + private static final String ID = "project-id-123"; + private static final BigInteger NUMBER = new BigInteger("123"); + private static final ProjectInfo.Quota QUOTA = new ProjectInfo.Quota(1, 2, 3, 4, 5, 6); + private static final ProjectInfo PROJECT_INFO = ProjectInfo.builder() + .id(ID).number(NUMBER).quota(QUOTA).build(); + + @Test + public void testBuilder() { + ProjectInfo withId = ProjectInfo.builder().id(ID).build(); + assertEquals(ID, withId.id()); + assertNull(withId.number()); + assertNull(withId.quota()); + ProjectInfo withNumber = ProjectInfo.builder().number(NUMBER).build(); + assertEquals(NUMBER, withNumber.number()); + assertNull(withNumber.quota()); + assertNull(withNumber.id()); + ProjectInfo withQuota = ProjectInfo.builder().quota(QUOTA).build(); + assertEquals(QUOTA, withQuota.quota()); + assertNull(withQuota.id()); + assertNull(withQuota.number()); + assertEquals(QUOTA, PROJECT_INFO.quota()); + assertEquals(NUMBER, PROJECT_INFO.number()); + assertEquals(ID, PROJECT_INFO.id()); + } + + @Test + public void testQuotaConstructor() { + assertEquals(1, QUOTA.zones()); + assertEquals(2, QUOTA.resourceRecordsPerRrset()); + assertEquals(3, QUOTA.rrsetAdditionsPerChange()); + assertEquals(4, QUOTA.rrsetDeletionsPerChange()); + assertEquals(5, QUOTA.rrsetsPerZone()); + assertEquals(6, QUOTA.totalRrdataSizePerChange()); + } + + @Test + public void testEqualsAndNotEqualsQuota() { + ProjectInfo.Quota clone = new ProjectInfo.Quota(6, 5, 4, 3, 2, 1); + assertNotEquals(QUOTA, clone); + clone = ProjectInfo.Quota.fromPb(QUOTA.toPb()); + assertEquals(QUOTA, clone); + } + + @Test + public void testSameHashCodeOnEqualsQuota() { + ProjectInfo.Quota clone = ProjectInfo.Quota.fromPb(QUOTA.toPb()); + assertEquals(QUOTA, clone); + assertEquals(QUOTA.hashCode(), clone.hashCode()); + } + + @Test + public void testEqualsAndNotEquals() { + ProjectInfo clone = ProjectInfo.builder().build(); + assertNotEquals(PROJECT_INFO, clone); + clone = ProjectInfo.builder().id(PROJECT_INFO.id()).number(PROJECT_INFO.number()).build(); + assertNotEquals(PROJECT_INFO, clone); + clone = ProjectInfo.builder().id(PROJECT_INFO.id()).quota(PROJECT_INFO.quota()).build(); + assertNotEquals(PROJECT_INFO, clone); + clone = ProjectInfo.builder().number(PROJECT_INFO.number()).quota(PROJECT_INFO.quota()).build(); + assertNotEquals(PROJECT_INFO, clone); + clone = ProjectInfo.fromPb(PROJECT_INFO.toPb()); + assertEquals(PROJECT_INFO, clone); + } + + @Test + public void testSameHashCodeOnEquals() { + ProjectInfo clone = ProjectInfo.fromPb(PROJECT_INFO.toPb()); + assertEquals(PROJECT_INFO, clone); + assertEquals(PROJECT_INFO.hashCode(), clone.hashCode()); + } + + @Test + public void testToAndFromPb() { + assertEquals(PROJECT_INFO, ProjectInfo.fromPb(PROJECT_INFO.toPb())); + ProjectInfo partial = ProjectInfo.builder().id(ID).build(); + assertEquals(partial, ProjectInfo.fromPb(partial.toPb())); + partial = ProjectInfo.builder().number(NUMBER).build(); + assertEquals(partial, ProjectInfo.fromPb(partial.toPb())); + partial = ProjectInfo.builder().quota(QUOTA).build(); + assertEquals(partial, ProjectInfo.fromPb(partial.toPb())); + assertNotEquals(PROJECT_INFO, partial); + } + + @Test + public void testToAndFromPbQuota() { + assertEquals(QUOTA, ProjectInfo.Quota.fromPb(QUOTA.toPb())); + ProjectInfo.Quota wrong = new ProjectInfo.Quota(5, 6, 3, 6, 2, 1); + assertNotEquals(QUOTA, ProjectInfo.Quota.fromPb(wrong.toPb())); + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/RecordSetTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/RecordSetTest.java new file mode 100644 index 000000000000..369e078a48c7 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/RecordSetTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static com.google.gcloud.dns.RecordSet.builder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +public class RecordSetTest { + + private static final String NAME = "example.com."; + private static final Integer TTL = 3600; + private static final TimeUnit UNIT = TimeUnit.HOURS; + private static final Integer UNIT_TTL = 1; + private static final RecordSet.Type TYPE = RecordSet.Type.AAAA; + private static final RecordSet recordSet = builder(NAME, TYPE) + .ttl(UNIT_TTL, UNIT) + .build(); + + @Test + public void testDefaultDnsRecord() { + RecordSet recordSet = builder(NAME, TYPE).build(); + assertEquals(0, recordSet.records().size()); + assertEquals(TYPE, recordSet.type()); + assertEquals(NAME, recordSet.name()); + } + + @Test + public void testBuilder() { + assertEquals(NAME, recordSet.name()); + assertEquals(TTL, recordSet.ttl()); + assertEquals(TYPE, recordSet.type()); + assertEquals(0, recordSet.records().size()); + // verify that one can add records to the record set + String testingRecord = "Testing recordSet"; + String anotherTestingRecord = "Another recordSet 123"; + RecordSet anotherRecord = recordSet.toBuilder() + .addRecord(testingRecord) + .addRecord(anotherTestingRecord) + .build(); + assertEquals(2, anotherRecord.records().size()); + assertTrue(anotherRecord.records().contains(testingRecord)); + assertTrue(anotherRecord.records().contains(anotherTestingRecord)); + } + + @Test + public void testValidTtl() { + try { + builder(NAME, TYPE).ttl(-1, TimeUnit.SECONDS); + fail("A negative value is not acceptable for ttl."); + } catch (IllegalArgumentException e) { + // expected + } + builder(NAME, TYPE).ttl(0, TimeUnit.SECONDS); + builder(NAME, TYPE).ttl(Integer.MAX_VALUE, TimeUnit.SECONDS); + try { + builder(NAME, TYPE).ttl(Integer.MAX_VALUE, TimeUnit.HOURS); + fail("This value is too large for int."); + } catch (IllegalArgumentException e) { + // expected + } + RecordSet record = RecordSet.builder(NAME, TYPE).ttl(UNIT_TTL, UNIT).build(); + assertEquals(TTL, record.ttl()); + } + + @Test + public void testEqualsAndNotEquals() { + RecordSet clone = recordSet.toBuilder().build(); + assertEquals(recordSet, clone); + clone = recordSet.toBuilder().addRecord("another recordSet").build(); + assertNotEquals(recordSet, clone); + String differentName = "totally different name"; + clone = recordSet.toBuilder().name(differentName).build(); + assertNotEquals(recordSet, clone); + clone = recordSet.toBuilder().ttl(recordSet.ttl() + 1, TimeUnit.SECONDS).build(); + assertNotEquals(recordSet, clone); + clone = recordSet.toBuilder().type(RecordSet.Type.TXT).build(); + assertNotEquals(recordSet, clone); + } + + @Test + public void testSameHashCodeOnEquals() { + int hash = recordSet.hashCode(); + RecordSet clone = recordSet.toBuilder().build(); + assertEquals(clone.hashCode(), hash); + } + + @Test + public void testToAndFromPb() { + assertEquals(recordSet, RecordSet.fromPb(recordSet.toPb())); + RecordSet partial = builder(NAME, TYPE).build(); + assertEquals(partial, RecordSet.fromPb(partial.toPb())); + partial = builder(NAME, TYPE).addRecord("test").build(); + assertEquals(partial, RecordSet.fromPb(partial.toPb())); + partial = builder(NAME, TYPE).ttl(15, TimeUnit.SECONDS).build(); + assertEquals(partial, RecordSet.fromPb(partial.toPb())); + } + + @Test + public void testToBuilder() { + assertEquals(recordSet, recordSet.toBuilder().build()); + RecordSet partial = builder(NAME, TYPE).build(); + assertEquals(partial, partial.toBuilder().build()); + partial = builder(NAME, TYPE).addRecord("test").build(); + assertEquals(partial, partial.toBuilder().build()); + partial = builder(NAME, TYPE).ttl(15, TimeUnit.SECONDS).build(); + assertEquals(partial, partial.toBuilder().build()); + } + + @Test + public void clearRecordSet() { + // make sure that we are starting not empty + RecordSet clone = + recordSet.toBuilder().addRecord("record").addRecord("another").build(); + assertNotEquals(0, clone.records().size()); + clone = clone.toBuilder().clearRecords().build(); + assertEquals(0, clone.records().size()); + clone.toPb(); // verify that pb allows it + } + + @Test + public void removeFromRecordSet() { + String recordString = "record"; + // make sure that we are starting not empty + RecordSet clone = recordSet.toBuilder().addRecord(recordString).build(); + assertNotEquals(0, clone.records().size()); + clone = clone.toBuilder().removeRecord(recordString).build(); + assertEquals(0, clone.records().size()); + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/SerializationTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/SerializationTest.java new file mode 100644 index 000000000000..ad25b31068dd --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/SerializationTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import com.google.common.collect.ImmutableList; +import com.google.gcloud.AuthCredentials; +import com.google.gcloud.BaseSerializationTest; +import com.google.gcloud.Restorable; +import com.google.gcloud.RetryParams; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.concurrent.TimeUnit; + +public class SerializationTest extends BaseSerializationTest { + + private static final ZoneInfo FULL_ZONE_INFO = Zone.of("some zone name", "www.example.com", + "some descriptions").toBuilder() + .creationTimeMillis(132L) + .id("123333") + .nameServers(ImmutableList.of("server 1", "server 2")) + .nameServerSet("specificationstring") + .build(); + private static final ZoneInfo PARTIAL_ZONE_INFO = Zone.of("some zone name", "www.example.com", + "some descriptions").toBuilder().build(); + private static final ProjectInfo PARTIAL_PROJECT_INFO = ProjectInfo.builder().id("13").build(); + private static final ProjectInfo FULL_PROJECT_INFO = ProjectInfo.builder() + .id("342") + .number(new BigInteger("2343245")) + .quota(new ProjectInfo.Quota(12, 13, 14, 15, 16, 17)) + .build(); + private static final Dns.ZoneListOption ZONE_LIST_OPTION = + Dns.ZoneListOption.dnsName("www.example.com."); + private static final Dns.RecordSetListOption RECORD_SET_LIST_OPTION = + Dns.RecordSetListOption.fields(Dns.RecordSetField.TTL); + private static final Dns.ChangeRequestListOption CHANGE_REQUEST_LIST_OPTION = + Dns.ChangeRequestListOption.fields(Dns.ChangeRequestField.STATUS); + private static final Dns.ZoneOption ZONE_OPTION = + Dns.ZoneOption.fields(Dns.ZoneField.CREATION_TIME); + private static final Dns.ChangeRequestOption CHANGE_REQUEST_OPTION = + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.STATUS); + private static final Dns.ProjectOption PROJECT_OPTION = + Dns.ProjectOption.fields(Dns.ProjectField.QUOTA); + private static final DnsOptions OPTIONS = DnsOptions.builder() + .projectId("some-unnecessary-project-ID") + .retryParams(RetryParams.defaultInstance()) + .build(); + private static final Dns DNS = OPTIONS.service(); + private static final Zone FULL_ZONE = new Zone(DNS, new ZoneInfo.BuilderImpl(FULL_ZONE_INFO)); + private static final Zone PARTIAL_ZONE = + new Zone(DNS, new ZoneInfo.BuilderImpl(PARTIAL_ZONE_INFO)); + private static final ChangeRequestInfo CHANGE_REQUEST_INFO_PARTIAL = + ChangeRequest.builder().build(); + private static final ChangeRequest CHANGE_REQUEST_PARTIAL = new ChangeRequest(DNS, "name", + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_INFO_PARTIAL)); + private static final RecordSet RECORD_SET_PARTIAL = + RecordSet.builder("www.www.com", RecordSet.Type.AAAA).build(); + private static final RecordSet RECORD_SET_COMPLETE = + RecordSet.builder("www.sadfa.com", RecordSet.Type.A) + .ttl(12, TimeUnit.HOURS) + .addRecord("record") + .build(); + private static final ChangeRequestInfo CHANGE_REQUEST_INFO_COMPLETE = ChangeRequestInfo.builder() + .add(RECORD_SET_COMPLETE) + .delete(RECORD_SET_PARTIAL) + .status(ChangeRequest.Status.PENDING) + .id("some id") + .startTimeMillis(132L) + .build(); + private static final ChangeRequest CHANGE_REQUEST_COMPLETE = new ChangeRequest(DNS, "name", + new ChangeRequestInfo.BuilderImpl(CHANGE_REQUEST_INFO_COMPLETE)); + + @Override + protected Serializable[] serializableObjects() { + DnsOptions options = DnsOptions.builder() + .authCredentials(AuthCredentials.createForAppEngine()) + .projectId("id1") + .build(); + DnsOptions otherOptions = options.toBuilder() + .authCredentials(null) + .build(); + return new Serializable[]{FULL_ZONE_INFO, PARTIAL_ZONE_INFO, ZONE_LIST_OPTION, + RECORD_SET_LIST_OPTION, CHANGE_REQUEST_LIST_OPTION, ZONE_OPTION, CHANGE_REQUEST_OPTION, + PROJECT_OPTION, PARTIAL_PROJECT_INFO, FULL_PROJECT_INFO, OPTIONS, FULL_ZONE, PARTIAL_ZONE, + OPTIONS, CHANGE_REQUEST_INFO_PARTIAL, CHANGE_REQUEST_PARTIAL, RECORD_SET_PARTIAL, + RECORD_SET_COMPLETE, CHANGE_REQUEST_INFO_COMPLETE, CHANGE_REQUEST_COMPLETE, options, + otherOptions}; + } + + @Override + protected Restorable[] restorableObjects() { + return new Restorable[0]; + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ZoneInfoTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ZoneInfoTest.java new file mode 100644 index 000000000000..923672bb85a7 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ZoneInfoTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import org.junit.Test; + +import java.util.LinkedList; +import java.util.List; + +public class ZoneInfoTest { + + private static final String NAME = "mz-example.com"; + private static final String ID = "123456"; + private static final Long CREATION_TIME_MILLIS = 1123468321321L; + private static final String DNS_NAME = "example.com."; + private static final String DESCRIPTION = "description for the zone"; + private static final String NAME_SERVER_SET = "some set"; + private static final String NS1 = "name server 1"; + private static final String NS2 = "name server 2"; + private static final String NS3 = "name server 3"; + private static final List NAME_SERVERS = ImmutableList.of(NS1, NS2, NS3); + private static final ZoneInfo INFO = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION).toBuilder() + .creationTimeMillis(CREATION_TIME_MILLIS) + .id(ID) + .nameServerSet(NAME_SERVER_SET) + .nameServers(NAME_SERVERS) + .build(); + + @Test + public void testOf() { + ZoneInfo partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION); + assertTrue(partial.nameServers().isEmpty()); + assertEquals(NAME, partial.name()); + assertNull(partial.id()); + assertNull(partial.creationTimeMillis()); + assertNull(partial.nameServerSet()); + assertEquals(DESCRIPTION, partial.description()); + assertEquals(DNS_NAME, partial.dnsName()); + } + + @Test + public void testBuilder() { + assertEquals(3, INFO.nameServers().size()); + assertEquals(NS1, INFO.nameServers().get(0)); + assertEquals(NS2, INFO.nameServers().get(1)); + assertEquals(NS3, INFO.nameServers().get(2)); + assertEquals(NAME, INFO.name()); + assertEquals(ID, INFO.id()); + assertEquals(CREATION_TIME_MILLIS, INFO.creationTimeMillis()); + assertEquals(NAME_SERVER_SET, INFO.nameServerSet()); + assertEquals(DESCRIPTION, INFO.description()); + assertEquals(DNS_NAME, INFO.dnsName()); + } + + @Test + public void testEqualsAndNotEquals() { + ZoneInfo clone = INFO.toBuilder().build(); + assertEquals(INFO, clone); + List moreServers = Lists.newLinkedList(NAME_SERVERS); + moreServers.add(NS1); + clone = INFO.toBuilder().nameServers(moreServers).build(); + assertNotEquals(INFO, clone); + String differentName = "totally different name"; + clone = INFO.toBuilder().name(differentName).build(); + assertNotEquals(INFO, clone); + clone = INFO.toBuilder().creationTimeMillis(INFO.creationTimeMillis() + 1).build(); + assertNotEquals(INFO, clone); + clone = INFO.toBuilder().description(INFO.description() + "aaaa").build(); + assertNotEquals(INFO, clone); + clone = INFO.toBuilder().dnsName(differentName).build(); + assertNotEquals(INFO, clone); + clone = INFO.toBuilder().id(INFO.id() + "1111").build(); + assertNotEquals(INFO, clone); + clone = INFO.toBuilder().nameServerSet(INFO.nameServerSet() + "salt").build(); + assertNotEquals(INFO, clone); + } + + @Test + public void testSameHashCodeOnEquals() { + int hash = INFO.hashCode(); + ZoneInfo clone = INFO.toBuilder().build(); + assertEquals(clone.hashCode(), hash); + } + + @Test + public void testToBuilder() { + assertEquals(INFO, INFO.toBuilder().build()); + ZoneInfo partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION); + assertEquals(partial, partial.toBuilder().build()); + partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION).toBuilder().id(ID).build(); + assertEquals(partial, partial.toBuilder().build()); + partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION).toBuilder() + .creationTimeMillis(CREATION_TIME_MILLIS).build(); + assertEquals(partial, partial.toBuilder().build()); + List nameServers = new LinkedList<>(); + nameServers.add(NS1); + partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION).toBuilder().nameServers(nameServers).build(); + assertEquals(partial, partial.toBuilder().build()); + partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION).toBuilder().nameServerSet(NAME_SERVER_SET) + .build(); + assertEquals(partial, partial.toBuilder().build()); + } + + @Test + public void testToAndFromPb() { + assertEquals(INFO, ZoneInfo.fromPb(INFO.toPb())); + ZoneInfo partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION); + assertEquals(partial, ZoneInfo.fromPb(partial.toPb())); + partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION).toBuilder().id(ID).build(); + assertEquals(partial, ZoneInfo.fromPb(partial.toPb())); + partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION).toBuilder() + .creationTimeMillis(CREATION_TIME_MILLIS).build(); + assertEquals(partial, ZoneInfo.fromPb(partial.toPb())); + List nameServers = new LinkedList<>(); + nameServers.add(NS1); + partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION).toBuilder().nameServers(nameServers).build(); + assertEquals(partial, ZoneInfo.fromPb(partial.toPb())); + partial = ZoneInfo.of(NAME, DNS_NAME, DESCRIPTION).toBuilder().nameServerSet(NAME_SERVER_SET) + .build(); + assertEquals(partial, ZoneInfo.fromPb(partial.toPb())); + } + + @Test + public void testEmptyNameServers() { + ZoneInfo clone = INFO.toBuilder().nameServers(new LinkedList()).build(); + assertTrue(clone.nameServers().isEmpty()); + clone.toPb(); // test that this is allowed + } + + @Test + public void testDateParsing() { + com.google.api.services.dns.model.ManagedZone pb = INFO.toPb(); + pb.setCreationTime("2016-01-19T18:00:12.854Z"); // a real value obtained from Google Cloud DNS + ZoneInfo mz = ZoneInfo.fromPb(pb); // parses the string timestamp to millis + com.google.api.services.dns.model.ManagedZone pbClone = mz.toPb(); // converts it back to string + assertEquals(pb, pbClone); + assertEquals(pb.getCreationTime(), pbClone.getCreationTime()); + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ZoneTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ZoneTest.java new file mode 100644 index 000000000000..ba4493abfca8 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/ZoneTest.java @@ -0,0 +1,500 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns; + +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.gcloud.Page; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; + +public class ZoneTest { + + private static final String ZONE_NAME = "dns-zone-name"; + private static final String ZONE_ID = "123"; + private static final ZoneInfo ZONE_INFO = Zone.of(ZONE_NAME, "example.com", "description") + .toBuilder() + .id(ZONE_ID) + .creationTimeMillis(123478946464L) + .build(); + private static final ZoneInfo NO_ID_INFO = + ZoneInfo.of(ZONE_NAME, "another-example.com", "description").toBuilder() + .creationTimeMillis(893123464L) + .build(); + private static final Dns.ZoneOption ZONE_FIELD_OPTIONS = + Dns.ZoneOption.fields(Dns.ZoneField.CREATION_TIME); + private static final Dns.RecordSetListOption DNS_RECORD_OPTIONS = + Dns.RecordSetListOption.dnsName("some-dns"); + private static final Dns.ChangeRequestOption CHANGE_REQUEST_FIELD_OPTIONS = + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.START_TIME); + private static final Dns.ChangeRequestListOption CHANGE_REQUEST_LIST_OPTIONS = + Dns.ChangeRequestListOption.fields(Dns.ChangeRequestField.START_TIME); + private static final ChangeRequestInfo CHANGE_REQUEST = + ChangeRequestInfo.builder().id("someid").build(); + private static final ChangeRequestInfo CHANGE_REQUEST_NO_ID = + ChangeRequestInfo.builder().build(); + private static final DnsException EXCEPTION = createStrictMock(DnsException.class); + private static final DnsOptions OPTIONS = createStrictMock(DnsOptions.class); + + private Dns dns; + private Zone zone; + private Zone zoneNoId; + private ChangeRequest changeRequestAfter; + + @Before + public void setUp() throws Exception { + dns = createStrictMock(Dns.class); + expect(dns.options()).andReturn(OPTIONS).times(3); + replay(dns); + zone = new Zone(dns, new ZoneInfo.BuilderImpl(ZONE_INFO)); + zoneNoId = new Zone(dns, new ZoneInfo.BuilderImpl(NO_ID_INFO)); + changeRequestAfter = new ChangeRequest(dns, ZONE_NAME, new ChangeRequestInfo.BuilderImpl( + CHANGE_REQUEST.toBuilder().startTimeMillis(123465L).build())); + reset(dns); + } + + @After + public void tearDown() throws Exception { + verify(dns); + } + + @Test + public void testConstructor() { + replay(dns); + assertEquals(ZONE_INFO.toPb(), zone.toPb()); + assertNotNull(zone.dns()); + assertEquals(dns, zone.dns()); + } + + @Test + public void deleteByNameAndFound() { + expect(dns.delete(ZONE_NAME)).andReturn(true).times(2); + replay(dns); + boolean result = zone.delete(); + assertTrue(result); + result = zoneNoId.delete(); + assertTrue(result); + } + + @Test + public void deleteByNameAndNotFound() { + expect(dns.delete(ZONE_NAME)).andReturn(false).times(2); + replay(dns); + boolean result = zoneNoId.delete(); + assertFalse(result); + result = zone.delete(); + assertFalse(result); + } + + @Test + public void listDnsRecordsByNameAndFound() { + @SuppressWarnings("unchecked") + Page pageMock = createStrictMock(Page.class); + replay(pageMock); + expect(dns.listRecordSets(ZONE_NAME)).andReturn(pageMock).times(2); + // again for options + expect(dns.listRecordSets(ZONE_NAME, DNS_RECORD_OPTIONS)).andReturn(pageMock).times(2); + replay(dns); + Page result = zone.listRecordSets(); + assertSame(pageMock, result); + result = zoneNoId.listRecordSets(); + assertSame(pageMock, result); + verify(pageMock); + zone.listRecordSets(DNS_RECORD_OPTIONS); // check options + zoneNoId.listRecordSets(DNS_RECORD_OPTIONS); // check options + } + + @Test + public void listDnsRecordsByNameAndNotFound() { + expect(dns.listRecordSets(ZONE_NAME)).andThrow(EXCEPTION).times(2); + // again for options + expect(dns.listRecordSets(ZONE_NAME, DNS_RECORD_OPTIONS)).andThrow(EXCEPTION).times(2); + replay(dns); + try { + zoneNoId.listRecordSets(); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zone.listRecordSets(); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zoneNoId.listRecordSets(DNS_RECORD_OPTIONS); // check options + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zone.listRecordSets(DNS_RECORD_OPTIONS); // check options + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + } + + @Test + public void reloadByNameAndFound() { + expect(dns.getZone(ZONE_NAME)).andReturn(zone).times(2); + // again for options + expect(dns.getZone(ZONE_NAME, ZONE_FIELD_OPTIONS)).andReturn(zoneNoId); + expect(dns.getZone(ZONE_NAME, ZONE_FIELD_OPTIONS)).andReturn(zone); + replay(dns); + Zone result = zoneNoId.reload(); + assertSame(zone.dns(), result.dns()); + assertEquals(zone, result); + result = zone.reload(); + assertSame(zone.dns(), result.dns()); + assertEquals(zone, result); + zoneNoId.reload(ZONE_FIELD_OPTIONS); // check options + zone.reload(ZONE_FIELD_OPTIONS); // check options + } + + @Test + public void reloadByNameAndNotFound() { + expect(dns.getZone(ZONE_NAME)).andReturn(null).times(2); + // again for options + expect(dns.getZone(ZONE_NAME, ZONE_FIELD_OPTIONS)).andReturn(null).times(2); + replay(dns); + Zone result = zoneNoId.reload(); + assertNull(result); + result = zone.reload(); + assertNull(result); + zoneNoId.reload(ZONE_FIELD_OPTIONS); // for options + zone.reload(ZONE_FIELD_OPTIONS); // for options + } + + @Test + public void applyChangeByNameAndFound() { + expect(dns.applyChangeRequest(ZONE_NAME, CHANGE_REQUEST)) + .andReturn(changeRequestAfter); + expect(dns.applyChangeRequest(ZONE_NAME, CHANGE_REQUEST)) + .andReturn(changeRequestAfter); + // again for options + expect(dns.applyChangeRequest(ZONE_NAME, CHANGE_REQUEST, CHANGE_REQUEST_FIELD_OPTIONS)) + .andReturn(changeRequestAfter); + expect(dns.applyChangeRequest(ZONE_NAME, CHANGE_REQUEST, CHANGE_REQUEST_FIELD_OPTIONS)) + .andReturn(changeRequestAfter); + replay(dns); + ChangeRequest result = zoneNoId.applyChangeRequest(CHANGE_REQUEST); + assertEquals(changeRequestAfter, result); + result = zone.applyChangeRequest(CHANGE_REQUEST); + assertEquals(changeRequestAfter, result); + // check options + result = zoneNoId.applyChangeRequest(CHANGE_REQUEST, CHANGE_REQUEST_FIELD_OPTIONS); + assertEquals(changeRequestAfter, result); + result = zone.applyChangeRequest(CHANGE_REQUEST, CHANGE_REQUEST_FIELD_OPTIONS); + assertEquals(changeRequestAfter, result); + } + + @Test + public void applyChangeByNameAndNotFound() { + // ID is not set + expect(dns.applyChangeRequest(ZONE_NAME, CHANGE_REQUEST)).andThrow(EXCEPTION).times(2); + // again for options + expect(dns.applyChangeRequest(ZONE_NAME, CHANGE_REQUEST, CHANGE_REQUEST_FIELD_OPTIONS)) + .andThrow(EXCEPTION).times(2); + replay(dns); + try { + zoneNoId.applyChangeRequest(CHANGE_REQUEST); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zone.applyChangeRequest(CHANGE_REQUEST); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + // check options + try { + zoneNoId.applyChangeRequest(CHANGE_REQUEST, CHANGE_REQUEST_FIELD_OPTIONS); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zone.applyChangeRequest(CHANGE_REQUEST, CHANGE_REQUEST_FIELD_OPTIONS); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + } + + @Test + public void applyNullChangeRequest() { + replay(dns); // no calls expected + try { + zone.applyChangeRequest(null); + fail("Cannot apply null ChangeRequest."); + } catch (NullPointerException e) { + // expected + } + try { + zone.applyChangeRequest(null, CHANGE_REQUEST_FIELD_OPTIONS); + fail("Cannot apply null ChangeRequest."); + } catch (NullPointerException e) { + // expected + } + try { + zoneNoId.applyChangeRequest(null); + fail("Cannot apply null ChangeRequest."); + } catch (NullPointerException e) { + // expected + } + try { + zoneNoId.applyChangeRequest(null, CHANGE_REQUEST_FIELD_OPTIONS); + fail("Cannot apply null ChangeRequest."); + } catch (NullPointerException e) { + // expected + } + } + + @Test + public void getChangeAndZoneFoundByName() { + expect(dns.getChangeRequest(ZONE_NAME, CHANGE_REQUEST.id())) + .andReturn(changeRequestAfter).times(2); + // again for options + expect(dns.getChangeRequest(ZONE_NAME, CHANGE_REQUEST.id(), CHANGE_REQUEST_FIELD_OPTIONS)) + .andReturn(changeRequestAfter).times(2); + replay(dns); + ChangeRequest result = zoneNoId.getChangeRequest(CHANGE_REQUEST.id()); + assertEquals(changeRequestAfter, result); + result = zone.getChangeRequest(CHANGE_REQUEST.id()); + assertEquals(changeRequestAfter, result); + // check options + result = zoneNoId.getChangeRequest(CHANGE_REQUEST.id(), CHANGE_REQUEST_FIELD_OPTIONS); + assertEquals(changeRequestAfter, result); + result = zone.getChangeRequest(CHANGE_REQUEST.id(), CHANGE_REQUEST_FIELD_OPTIONS); + assertEquals(changeRequestAfter, result); + } + + @Test + public void getChangeAndZoneNotFoundByName() { + expect(dns.getChangeRequest(ZONE_NAME, CHANGE_REQUEST.id())).andThrow(EXCEPTION).times(2); + // again for options + expect(dns.getChangeRequest(ZONE_NAME, CHANGE_REQUEST.id(), CHANGE_REQUEST_FIELD_OPTIONS)) + .andThrow(EXCEPTION).times(2); + replay(dns); + try { + zoneNoId.getChangeRequest(CHANGE_REQUEST.id()); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zone.getChangeRequest(CHANGE_REQUEST.id()); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + // check options + try { + zoneNoId.getChangeRequest(CHANGE_REQUEST.id(), CHANGE_REQUEST_FIELD_OPTIONS); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zone.getChangeRequest(CHANGE_REQUEST.id(), CHANGE_REQUEST_FIELD_OPTIONS); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + } + + @Test + public void getChangedWhichDoesNotExistZoneFound() { + expect(dns.getChangeRequest(ZONE_NAME, CHANGE_REQUEST.id())).andReturn(null).times(2); + // again for options + expect(dns.getChangeRequest(ZONE_NAME, CHANGE_REQUEST.id(), CHANGE_REQUEST_FIELD_OPTIONS)) + .andReturn(null).times(2); + replay(dns); + assertNull(zoneNoId.getChangeRequest(CHANGE_REQUEST.id())); + assertNull(zone.getChangeRequest(CHANGE_REQUEST.id())); + assertNull(zoneNoId.getChangeRequest(CHANGE_REQUEST.id(), CHANGE_REQUEST_FIELD_OPTIONS)); + assertNull(zone.getChangeRequest(CHANGE_REQUEST.id(), CHANGE_REQUEST_FIELD_OPTIONS)); + } + + @Test + public void getNullChangeRequest() { + replay(dns); // no calls expected + try { + zone.getChangeRequest(null); + fail("Cannot get null ChangeRequest."); + } catch (NullPointerException e) { + // expected + } + try { + zone.getChangeRequest(null, CHANGE_REQUEST_FIELD_OPTIONS); + fail("Cannot get null ChangeRequest."); + } catch (NullPointerException e) { + // expected + } + try { + zoneNoId.getChangeRequest(null); + fail("Cannot get null ChangeRequest."); + } catch (NullPointerException e) { + // expected + } + try { + zoneNoId.getChangeRequest(null, CHANGE_REQUEST_FIELD_OPTIONS); + fail("Cannot get null ChangeRequest."); + } catch (NullPointerException e) { + // expected + } + } + + @Test + public void getChangeRequestWithNoId() { + replay(dns); // no calls expected + try { + zone.getChangeRequest(CHANGE_REQUEST_NO_ID.id()); + fail("Cannot get ChangeRequest by null id."); + } catch (NullPointerException e) { + // expected + } + try { + zone.getChangeRequest(CHANGE_REQUEST_NO_ID.id(), CHANGE_REQUEST_FIELD_OPTIONS); + fail("Cannot get ChangeRequest by null id."); + } catch (NullPointerException e) { + // expected + } + try { + zoneNoId.getChangeRequest(CHANGE_REQUEST_NO_ID.id()); + fail("Cannot get ChangeRequest by null id."); + } catch (NullPointerException e) { + // expected + } + try { + zoneNoId.getChangeRequest(CHANGE_REQUEST_NO_ID.id(), CHANGE_REQUEST_FIELD_OPTIONS); + fail("Cannot get ChangeRequest by null id."); + } catch (NullPointerException e) { + // expected + } + } + + @Test + public void listChangeRequestsAndZoneFound() { + @SuppressWarnings("unchecked") + Page pageMock = createStrictMock(Page.class); + replay(pageMock); + expect(dns.listChangeRequests(ZONE_NAME)).andReturn(pageMock).times(2); + // again for options + expect(dns.listChangeRequests(ZONE_NAME, CHANGE_REQUEST_LIST_OPTIONS)) + .andReturn(pageMock).times(2); + replay(dns); + Page result = zoneNoId.listChangeRequests(); + assertSame(pageMock, result); + result = zone.listChangeRequests(); + assertSame(pageMock, result); + verify(pageMock); + zoneNoId.listChangeRequests(CHANGE_REQUEST_LIST_OPTIONS); // check options + zone.listChangeRequests(CHANGE_REQUEST_LIST_OPTIONS); // check options + } + + @Test + public void listChangeRequestsAndZoneNotFound() { + expect(dns.listChangeRequests(ZONE_NAME)).andThrow(EXCEPTION).times(2); + // again for options + expect(dns.listChangeRequests(ZONE_NAME, CHANGE_REQUEST_LIST_OPTIONS)).andThrow(EXCEPTION) + .times(2); + replay(dns); + try { + zoneNoId.listChangeRequests(); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zone.listChangeRequests(); + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zoneNoId.listChangeRequests(CHANGE_REQUEST_LIST_OPTIONS); // check options + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + try { + zone.listChangeRequests(CHANGE_REQUEST_LIST_OPTIONS); // check options + fail("Parent container not found, should throw an exception."); + } catch (DnsException e) { + // expected + } + } + + @Test + public void testFromPb() { + expect(dns.options()).andReturn(OPTIONS); + replay(dns); + assertEquals(Zone.fromPb(dns, zone.toPb()), zone); + } + + @Test + public void testEqualsAndToBuilder() { + expect(dns.options()).andReturn(OPTIONS).times(2); + replay(dns); + assertEquals(zone, zone.toBuilder().build()); + assertEquals(zone.hashCode(), zone.toBuilder().build().hashCode()); + } + + @Test + public void testBuilder() { + // one for each build() call because it invokes a constructor + expect(dns.options()).andReturn(OPTIONS).times(8); + replay(dns); + assertNotEquals(zone, zone.toBuilder() + .id((new BigInteger(zone.id())).add(BigInteger.ONE).toString()) + .build()); + assertNotEquals(zone, zone.toBuilder().dnsName(zone.name() + "aaaa").build()); + assertNotEquals(zone, zone.toBuilder().nameServerSet(zone.nameServerSet() + "aaaa").build()); + assertNotEquals(zone, zone.toBuilder().nameServers(ImmutableList.of("nameserverpppp")).build()); + assertNotEquals(zone, zone.toBuilder().dnsName(zone.dnsName() + "aaaa").build()); + assertNotEquals(zone, zone.toBuilder().creationTimeMillis(zone.creationTimeMillis() + 1) + .build()); + Zone.Builder builder = zone.toBuilder(); + builder.id(ZONE_ID) + .dnsName("example.com") + .creationTimeMillis(123478946464L) + .build(); + assertEquals(zone, builder.build()); + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/it/ITDnsTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/it/ITDnsTest.java new file mode 100644 index 000000000000..dd8e21043181 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/it/ITDnsTest.java @@ -0,0 +1,960 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.gcloud.Page; +import com.google.gcloud.dns.ChangeRequest; +import com.google.gcloud.dns.ChangeRequestInfo; +import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsException; +import com.google.gcloud.dns.DnsOptions; +import com.google.gcloud.dns.ProjectInfo; +import com.google.gcloud.dns.RecordSet; +import com.google.gcloud.dns.Zone; +import com.google.gcloud.dns.ZoneInfo; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class ITDnsTest { + + private static final String PREFIX = "gcldjvit-"; + private static final Dns DNS = DnsOptions.defaultInstance().service(); + private static final String ZONE_NAME1 = (PREFIX + UUID.randomUUID()).substring(0, 32); + private static final String ZONE_NAME_EMPTY_DESCRIPTION = + (PREFIX + UUID.randomUUID()).substring(0, 32); + private static final String ZONE_NAME_TOO_LONG = PREFIX + UUID.randomUUID(); + private static final String ZONE_DESCRIPTION1 = "first zone"; + private static final String ZONE_DNS_NAME1 = ZONE_NAME1 + ".com."; + private static final String ZONE_DNS_EMPTY_DESCRIPTION = ZONE_NAME_EMPTY_DESCRIPTION + ".com."; + private static final String ZONE_DNS_NAME_NO_PERIOD = ZONE_NAME1 + ".com"; + private static final ZoneInfo ZONE1 = + ZoneInfo.of(ZONE_NAME1, ZONE_DNS_EMPTY_DESCRIPTION, ZONE_DESCRIPTION1); + private static final ZoneInfo ZONE_EMPTY_DESCRIPTION = + ZoneInfo.of(ZONE_NAME_EMPTY_DESCRIPTION, ZONE_DNS_NAME1, ZONE_DESCRIPTION1); + private static final ZoneInfo ZONE_NAME_ERROR = + ZoneInfo.of(ZONE_NAME_TOO_LONG, ZONE_DNS_NAME1, ZONE_DESCRIPTION1); + private static final ZoneInfo ZONE_DNS_NO_PERIOD = + ZoneInfo.of(ZONE_NAME1, ZONE_DNS_NAME_NO_PERIOD, ZONE_DESCRIPTION1); + private static final RecordSet A_RECORD_ZONE1 = + RecordSet.builder("www." + ZONE1.dnsName(), RecordSet.Type.A) + .records(ImmutableList.of("123.123.55.1")) + .ttl(25, TimeUnit.SECONDS) + .build(); + private static final RecordSet AAAA_RECORD_ZONE1 = + RecordSet.builder("www." + ZONE1.dnsName(), RecordSet.Type.AAAA) + .records(ImmutableList.of("ed:ed:12:aa:36:3:3:105")) + .ttl(25, TimeUnit.SECONDS) + .build(); + private static final ChangeRequestInfo CHANGE_ADD_ZONE1 = ChangeRequest.builder() + .add(A_RECORD_ZONE1) + .add(AAAA_RECORD_ZONE1) + .build(); + private static final ChangeRequestInfo CHANGE_DELETE_ZONE1 = ChangeRequest.builder() + .delete(A_RECORD_ZONE1) + .delete(AAAA_RECORD_ZONE1) + .build(); + private static final List ZONE_NAMES = ImmutableList.of(ZONE_NAME1, + ZONE_NAME_EMPTY_DESCRIPTION); + + private static void clear() { + for (String zoneName : ZONE_NAMES) { + Zone zone = DNS.getZone(zoneName); + if (zone != null) { + /* We wait for all changes to complete before retrieving a list of DNS records to be + deleted. Waiting is necessary as changes potentially might create more records between + when the list has been retrieved and executing the subsequent delete operation. */ + Iterator iterator = zone.listChangeRequests().iterateAll(); + while (iterator.hasNext()) { + waitForChangeToComplete(zoneName, iterator.next().id()); + } + Iterator recordSetIterator = zone.listRecordSets().iterateAll(); + List toDelete = new LinkedList<>(); + while (recordSetIterator.hasNext()) { + RecordSet recordSet = recordSetIterator.next(); + if (!ImmutableList.of(RecordSet.Type.NS, RecordSet.Type.SOA) + .contains(recordSet.type())) { + toDelete.add(recordSet); + } + } + if (!toDelete.isEmpty()) { + ChangeRequest deletion = + zone.applyChangeRequest(ChangeRequest.builder().deletions(toDelete).build()); + waitForChangeToComplete(zone.name(), deletion.id()); + } + zone.delete(); + } + } + } + + private static List filter(Iterator iterator) { + List result = new LinkedList<>(); + while (iterator.hasNext()) { + Zone zone = iterator.next(); + if (ZONE_NAMES.contains(zone.name())) { + result.add(zone); + } + } + return result; + } + + @BeforeClass + public static void before() { + clear(); + } + + @AfterClass + public static void after() { + clear(); + } + + private static void assertEqChangesIgnoreStatus(ChangeRequest expected, ChangeRequest actual) { + assertEquals(expected.additions(), actual.additions()); + assertEquals(expected.deletions(), actual.deletions()); + assertEquals(expected.id(), actual.id()); + assertEquals(expected.startTimeMillis(), actual.startTimeMillis()); + } + + private static void waitForChangeToComplete(String zoneName, String changeId) { + while (true) { + ChangeRequest changeRequest = DNS.getChangeRequest(zoneName, changeId, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.STATUS)); + if (ChangeRequest.Status.DONE.equals(changeRequest.status())) { + return; + } + try { + Thread.sleep(500); + } catch (InterruptedException e) { + fail("Thread was interrupted while waiting for change processing."); + } + } + } + + @Rule + public Timeout globalTimeout = Timeout.seconds(300); + + @Test + public void testCreateValidZone() { + try { + Zone created = DNS.create(ZONE1); + assertEquals(ZONE1.description(), created.description()); + assertEquals(ZONE1.dnsName(), created.dnsName()); + assertEquals(ZONE1.name(), created.name()); + assertNotNull(created.creationTimeMillis()); + assertNotNull(created.nameServers()); + assertNull(created.nameServerSet()); + assertNotNull(created.id()); + Zone retrieved = DNS.getZone(ZONE1.name()); + assertEquals(created, retrieved); + created = DNS.create(ZONE_EMPTY_DESCRIPTION); + assertEquals(ZONE_EMPTY_DESCRIPTION.description(), created.description()); + assertEquals(ZONE_EMPTY_DESCRIPTION.dnsName(), created.dnsName()); + assertEquals(ZONE_EMPTY_DESCRIPTION.name(), created.name()); + assertNotNull(created.creationTimeMillis()); + assertNotNull(created.nameServers()); + assertNull(created.nameServerSet()); + assertNotNull(created.id()); + retrieved = DNS.getZone(ZONE_EMPTY_DESCRIPTION.name()); + assertEquals(created, retrieved); + } finally { + DNS.delete(ZONE1.name()); + DNS.delete(ZONE_EMPTY_DESCRIPTION.name()); + } + } + + @Test + public void testCreateZoneWithErrors() { + try { + try { + DNS.create(ZONE_NAME_ERROR); + fail("Zone name is missing a period. The service returns an error."); + } catch (DnsException ex) { + // expected + assertFalse(ex.retryable()); + } + try { + DNS.create(ZONE_DNS_NO_PERIOD); + fail("Zone name is missing a period. The service returns an error."); + } catch (DnsException ex) { + // expected + assertFalse(ex.retryable()); + } + } finally { + DNS.delete(ZONE_NAME_ERROR.name()); + DNS.delete(ZONE_DNS_NO_PERIOD.name()); + } + } + + @Test + public void testCreateZoneWithOptions() { + try { + Zone created = DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.CREATION_TIME)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNotNull(created.creationTimeMillis()); + assertNull(created.description()); + assertNull(created.dnsName()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); + assertNull(created.id()); + created.delete(); + created = DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.DESCRIPTION)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertEquals(ZONE1.description(), created.description()); + assertNull(created.dnsName()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); + assertNull(created.id()); + created.delete(); + created = DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.DNS_NAME)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertEquals(ZONE1.dnsName(), created.dnsName()); + assertNull(created.description()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); + assertNull(created.id()); + created.delete(); + created = DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.NAME)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertNull(created.description()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); + assertNull(created.id()); + created.delete(); + created = DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.NAME_SERVER_SET)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertNull(created.description()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); // we did not set it + assertNull(created.id()); + created.delete(); + created = DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.NAME_SERVERS)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertNull(created.description()); + assertFalse(created.nameServers().isEmpty()); + assertNull(created.nameServerSet()); + assertNull(created.id()); + created.delete(); + created = DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.ZONE_ID)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertNull(created.description()); + assertNotNull(created.nameServers()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNotNull(created.id()); + created.delete(); + // combination of multiple things + created = DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.ZONE_ID, + Dns.ZoneField.NAME_SERVERS, Dns.ZoneField.NAME_SERVER_SET, Dns.ZoneField.DESCRIPTION)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertEquals(ZONE1.description(), created.description()); + assertFalse(created.nameServers().isEmpty()); + assertNull(created.nameServerSet()); // we did not set it + assertNotNull(created.id()); + } finally { + DNS.delete(ZONE1.name()); + } + } + + @Test + public void testGetZone() { + try { + DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.NAME)); + Zone created = DNS.getZone(ZONE1.name(), Dns.ZoneOption.fields(Dns.ZoneField.CREATION_TIME)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNotNull(created.creationTimeMillis()); + assertNull(created.description()); + assertNull(created.dnsName()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); + assertNull(created.id()); + created = DNS.getZone(ZONE1.name(), Dns.ZoneOption.fields(Dns.ZoneField.DESCRIPTION)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertEquals(ZONE1.description(), created.description()); + assertNull(created.dnsName()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); + assertNull(created.id()); + created = DNS.getZone(ZONE1.name(), Dns.ZoneOption.fields(Dns.ZoneField.DNS_NAME)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertEquals(ZONE1.dnsName(), created.dnsName()); + assertNull(created.description()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); + assertNull(created.id()); + created = DNS.getZone(ZONE1.name(), Dns.ZoneOption.fields(Dns.ZoneField.NAME)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertNull(created.description()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); + assertNull(created.id()); + created = DNS.getZone(ZONE1.name(), Dns.ZoneOption.fields(Dns.ZoneField.NAME_SERVER_SET)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertNull(created.description()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNull(created.nameServerSet()); // we did not set it + assertNull(created.id()); + created = DNS.getZone(ZONE1.name(), Dns.ZoneOption.fields(Dns.ZoneField.NAME_SERVERS)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertNull(created.description()); + assertFalse(created.nameServers().isEmpty()); + assertNull(created.nameServerSet()); + assertNull(created.id()); + created = DNS.getZone(ZONE1.name(), Dns.ZoneOption.fields(Dns.ZoneField.ZONE_ID)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertNull(created.description()); + assertNotNull(created.nameServers()); + assertTrue(created.nameServers().isEmpty()); // never returns null + assertNotNull(created.id()); + // combination of multiple things + created = DNS.getZone(ZONE1.name(), Dns.ZoneOption.fields(Dns.ZoneField.ZONE_ID, + Dns.ZoneField.NAME_SERVERS, Dns.ZoneField.NAME_SERVER_SET, Dns.ZoneField.DESCRIPTION)); + assertEquals(ZONE1.name(), created.name()); // always returned + assertNull(created.creationTimeMillis()); + assertNull(created.dnsName()); + assertEquals(ZONE1.description(), created.description()); + assertFalse(created.nameServers().isEmpty()); + assertNull(created.nameServerSet()); // we did not set it + assertNotNull(created.id()); + } finally { + DNS.delete(ZONE1.name()); + } + } + + @Test + public void testListZones() { + try { + List zones = filter(DNS.listZones().iterateAll()); + assertEquals(0, zones.size()); + // some zones exists + Zone created = DNS.create(ZONE1); + zones = filter(DNS.listZones().iterateAll()); + assertEquals(created, zones.get(0)); + assertEquals(1, zones.size()); + created = DNS.create(ZONE_EMPTY_DESCRIPTION); + zones = filter(DNS.listZones().iterateAll()); + assertEquals(2, zones.size()); + assertTrue(zones.contains(created)); + // error in options + try { + DNS.listZones(Dns.ZoneListOption.pageSize(0)); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertFalse(ex.retryable()); + } + try { + DNS.listZones(Dns.ZoneListOption.pageSize(-1)); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertFalse(ex.retryable()); + } + // ok size + zones = filter(DNS.listZones(Dns.ZoneListOption.pageSize(1000)).iterateAll()); + assertEquals(2, zones.size()); // we still have only 2 zones + // dns name problems + try { + DNS.listZones(Dns.ZoneListOption.dnsName("aaaaa")); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertFalse(ex.retryable()); + } + // ok name + zones = filter(DNS.listZones(Dns.ZoneListOption.dnsName(ZONE1.dnsName())).iterateAll()); + assertEquals(1, zones.size()); + // field options + Iterator zoneIterator = DNS.listZones(Dns.ZoneListOption.dnsName(ZONE1.dnsName()), + Dns.ZoneListOption.fields(Dns.ZoneField.ZONE_ID)).iterateAll(); + Zone zone = zoneIterator.next(); + assertNull(zone.creationTimeMillis()); + assertNotNull(zone.name()); + assertNull(zone.dnsName()); + assertNull(zone.description()); + assertNull(zone.nameServerSet()); + assertTrue(zone.nameServers().isEmpty()); + assertNotNull(zone.id()); + assertFalse(zoneIterator.hasNext()); + zoneIterator = DNS.listZones(Dns.ZoneListOption.dnsName(ZONE1.dnsName()), + Dns.ZoneListOption.fields(Dns.ZoneField.CREATION_TIME)).iterateAll(); + zone = zoneIterator.next(); + assertNotNull(zone.creationTimeMillis()); + assertNotNull(zone.name()); + assertNull(zone.dnsName()); + assertNull(zone.description()); + assertNull(zone.nameServerSet()); + assertTrue(zone.nameServers().isEmpty()); + assertNull(zone.id()); + assertFalse(zoneIterator.hasNext()); + zoneIterator = DNS.listZones(Dns.ZoneListOption.dnsName(ZONE1.dnsName()), + Dns.ZoneListOption.fields(Dns.ZoneField.DNS_NAME)).iterateAll(); + zone = zoneIterator.next(); + assertNull(zone.creationTimeMillis()); + assertNotNull(zone.name()); + assertNotNull(zone.dnsName()); + assertNull(zone.description()); + assertNull(zone.nameServerSet()); + assertTrue(zone.nameServers().isEmpty()); + assertNull(zone.id()); + assertFalse(zoneIterator.hasNext()); + zoneIterator = DNS.listZones(Dns.ZoneListOption.dnsName(ZONE1.dnsName()), + Dns.ZoneListOption.fields(Dns.ZoneField.DESCRIPTION)).iterateAll(); + zone = zoneIterator.next(); + assertNull(zone.creationTimeMillis()); + assertNotNull(zone.name()); + assertNull(zone.dnsName()); + assertNotNull(zone.description()); + assertNull(zone.nameServerSet()); + assertTrue(zone.nameServers().isEmpty()); + assertNull(zone.id()); + assertFalse(zoneIterator.hasNext()); + zoneIterator = DNS.listZones(Dns.ZoneListOption.dnsName(ZONE1.dnsName()), + Dns.ZoneListOption.fields(Dns.ZoneField.NAME_SERVERS)).iterateAll(); + zone = zoneIterator.next(); + assertNull(zone.creationTimeMillis()); + assertNotNull(zone.name()); + assertNull(zone.dnsName()); + assertNull(zone.description()); + assertNull(zone.nameServerSet()); + assertTrue(!zone.nameServers().isEmpty()); + assertNull(zone.id()); + assertFalse(zoneIterator.hasNext()); + zoneIterator = DNS.listZones(Dns.ZoneListOption.dnsName(ZONE1.dnsName()), + Dns.ZoneListOption.fields(Dns.ZoneField.NAME_SERVER_SET)).iterateAll(); + zone = zoneIterator.next(); + assertNull(zone.creationTimeMillis()); + assertNotNull(zone.name()); + assertNull(zone.dnsName()); + assertNull(zone.description()); + assertNull(zone.nameServerSet()); // we cannot set it using gcloud java + assertTrue(zone.nameServers().isEmpty()); + assertNull(zone.id()); + assertFalse(zoneIterator.hasNext()); + // several combined + zones = filter(DNS.listZones(Dns.ZoneListOption.fields(Dns.ZoneField.ZONE_ID, + Dns.ZoneField.DESCRIPTION), + Dns.ZoneListOption.pageSize(1)).iterateAll()); + assertEquals(2, zones.size()); + for (Zone current : zones) { + assertNull(current.creationTimeMillis()); + assertNotNull(current.name()); + assertNull(current.dnsName()); + assertNotNull(current.description()); + assertNull(current.nameServerSet()); + assertTrue(zone.nameServers().isEmpty()); + assertNotNull(current.id()); + } + } finally { + DNS.delete(ZONE1.name()); + DNS.delete(ZONE_EMPTY_DESCRIPTION.name()); + } + } + + @Test + public void testDeleteZone() { + try { + Zone created = DNS.create(ZONE1); + assertEquals(created, DNS.getZone(ZONE1.name())); + DNS.delete(ZONE1.name()); + assertNull(DNS.getZone(ZONE1.name())); + } finally { + DNS.delete(ZONE1.name()); + } + } + + @Test + public void testCreateChange() { + try { + DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.NAME)); + ChangeRequest created = DNS.applyChangeRequest(ZONE1.name(), CHANGE_ADD_ZONE1); + assertEquals(CHANGE_ADD_ZONE1.additions(), created.additions()); + assertNotNull(created.startTimeMillis()); + assertTrue(created.deletions().isEmpty()); + assertEquals("1", created.id()); + assertTrue(ImmutableList.of(ChangeRequest.Status.PENDING, ChangeRequest.Status.DONE) + .contains(created.status())); + assertEqChangesIgnoreStatus(created, DNS.getChangeRequest(ZONE1.name(), "1")); + waitForChangeToComplete(ZONE1.name(), "1"); + DNS.applyChangeRequest(ZONE1.name(), CHANGE_DELETE_ZONE1); + waitForChangeToComplete(ZONE1.name(), "2"); + // with options + created = DNS.applyChangeRequest(ZONE1.name(), CHANGE_ADD_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.ID)); + assertTrue(created.additions().isEmpty()); + assertNull(created.startTimeMillis()); + assertTrue(created.deletions().isEmpty()); + assertEquals("3", created.id()); + assertNull(created.status()); + waitForChangeToComplete(ZONE1.name(), "3"); + DNS.applyChangeRequest(ZONE1.name(), CHANGE_DELETE_ZONE1); + waitForChangeToComplete(ZONE1.name(), "4"); + created = DNS.applyChangeRequest(ZONE1.name(), CHANGE_ADD_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.STATUS)); + assertTrue(created.additions().isEmpty()); + assertNull(created.startTimeMillis()); + assertTrue(created.deletions().isEmpty()); + assertEquals("5", created.id()); + assertNotNull(created.status()); + waitForChangeToComplete(ZONE1.name(), "5"); + DNS.applyChangeRequest(ZONE1.name(), CHANGE_DELETE_ZONE1); + waitForChangeToComplete(ZONE1.name(), "6"); + created = DNS.applyChangeRequest(ZONE1.name(), CHANGE_ADD_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.START_TIME)); + assertTrue(created.additions().isEmpty()); + assertNotNull(created.startTimeMillis()); + assertTrue(created.deletions().isEmpty()); + assertEquals("7", created.id()); + assertNull(created.status()); + waitForChangeToComplete(ZONE1.name(), "7"); + DNS.applyChangeRequest(ZONE1.name(), CHANGE_DELETE_ZONE1); + waitForChangeToComplete(ZONE1.name(), "8"); + created = DNS.applyChangeRequest(ZONE1.name(), CHANGE_ADD_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.ADDITIONS)); + assertEquals(CHANGE_ADD_ZONE1.additions(), created.additions()); + assertNull(created.startTimeMillis()); + assertTrue(created.deletions().isEmpty()); + assertEquals("9", created.id()); + assertNull(created.status()); + // finishes with delete otherwise we cannot delete the zone + waitForChangeToComplete(ZONE1.name(), "9"); + created = DNS.applyChangeRequest(ZONE1.name(), CHANGE_DELETE_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.DELETIONS)); + waitForChangeToComplete(ZONE1.name(), "10"); + assertEquals(CHANGE_DELETE_ZONE1.deletions(), created.deletions()); + assertNull(created.startTimeMillis()); + assertTrue(created.additions().isEmpty()); + assertEquals("10", created.id()); + assertNull(created.status()); + waitForChangeToComplete(ZONE1.name(), "10"); + } finally { + clear(); + } + } + + @Test + public void testInvalidChangeRequest() { + Zone zone = DNS.create(ZONE1); + RecordSet validA = + RecordSet.builder("subdomain." + zone.dnsName(), RecordSet.Type.A) + .records(ImmutableList.of("0.255.1.5")) + .build(); + try { + ChangeRequestInfo validChange = ChangeRequest.builder().add(validA).build(); + zone.applyChangeRequest(validChange); + try { + zone.applyChangeRequest(validChange); + fail("Created a record set which already exists."); + } catch (DnsException ex) { + // expected + assertFalse(ex.retryable()); + assertEquals(409, ex.code()); + } + // delete with field mismatch + RecordSet mismatch = validA.toBuilder().ttl(20, TimeUnit.SECONDS).build(); + ChangeRequestInfo deletion = ChangeRequest.builder().delete(mismatch).build(); + try { + zone.applyChangeRequest(deletion); + fail("Deleted a record set without a complete match."); + } catch (DnsException ex) { + // expected + assertEquals(412, ex.code()); + assertFalse(ex.retryable()); + } + // delete and add SOA + Iterator recordSetIterator = zone.listRecordSets().iterateAll(); + LinkedList deletions = new LinkedList<>(); + LinkedList additions = new LinkedList<>(); + while (recordSetIterator.hasNext()) { + RecordSet recordSet = recordSetIterator.next(); + if (recordSet.type() == RecordSet.Type.SOA) { + deletions.add(recordSet); + // the subdomain is necessary to get 400 instead of 412 + RecordSet copy = recordSet.toBuilder().name("x." + recordSet.name()).build(); + additions.add(copy); + break; + } + } + deletion = deletion.toBuilder().deletions(deletions).build(); + ChangeRequestInfo addition = ChangeRequest.builder().additions(additions).build(); + try { + zone.applyChangeRequest(deletion); + fail("Deleted SOA."); + } catch (DnsException ex) { + // expected + assertFalse(ex.retryable()); + assertEquals(400, ex.code()); + } + try { + zone.applyChangeRequest(addition); + fail("Added second SOA."); + } catch (DnsException ex) { + // expected + assertFalse(ex.retryable()); + assertEquals(400, ex.code()); + } + } finally { + ChangeRequestInfo deletion = ChangeRequest.builder().delete(validA).build(); + ChangeRequest request = zone.applyChangeRequest(deletion); + waitForChangeToComplete(zone.name(), request.id()); + zone.delete(); + } + } + + @Test + public void testListChanges() { + try { + // no such zone exists + try { + DNS.listChangeRequests(ZONE1.name()); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(404, ex.code()); + assertFalse(ex.retryable()); + } + // zone exists but has no changes + DNS.create(ZONE1); + ImmutableList changes = ImmutableList.copyOf( + DNS.listChangeRequests(ZONE1.name()).iterateAll()); + assertEquals(1, changes.size()); // default change creating SOA and NS + // zone has changes + ChangeRequest change = DNS.applyChangeRequest(ZONE1.name(), CHANGE_ADD_ZONE1); + waitForChangeToComplete(ZONE1.name(), change.id()); + change = DNS.applyChangeRequest(ZONE1.name(), CHANGE_DELETE_ZONE1); + waitForChangeToComplete(ZONE1.name(), change.id()); + change = DNS.applyChangeRequest(ZONE1.name(), CHANGE_ADD_ZONE1); + waitForChangeToComplete(ZONE1.name(), change.id()); + change = DNS.applyChangeRequest(ZONE1.name(), CHANGE_DELETE_ZONE1); + waitForChangeToComplete(ZONE1.name(), change.id()); + changes = ImmutableList.copyOf(DNS.listChangeRequests(ZONE1.name()).iterateAll()); + assertEquals(5, changes.size()); + // error in options + try { + DNS.listChangeRequests(ZONE1.name(), Dns.ChangeRequestListOption.pageSize(0)); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertFalse(ex.retryable()); + } + try { + DNS.listChangeRequests(ZONE1.name(), Dns.ChangeRequestListOption.pageSize(-1)); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertFalse(ex.retryable()); + } + // sorting order + ImmutableList ascending = ImmutableList.copyOf(DNS.listChangeRequests( + ZONE1.name(), + Dns.ChangeRequestListOption.sortOrder(Dns.SortingOrder.ASCENDING)).iterateAll()); + ImmutableList descending = ImmutableList.copyOf(DNS.listChangeRequests( + ZONE1.name(), + Dns.ChangeRequestListOption.sortOrder(Dns.SortingOrder.DESCENDING)).iterateAll()); + int size = 5; + assertEquals(size, descending.size()); + assertEquals(size, ascending.size()); + for (int i = 0; i < size; i++) { + assertEquals(descending.get(i), ascending.get(size - i - 1)); + } + // field options + changes = ImmutableList.copyOf(DNS.listChangeRequests(ZONE1.name(), + Dns.ChangeRequestListOption.sortOrder(Dns.SortingOrder.ASCENDING), + Dns.ChangeRequestListOption.fields(Dns.ChangeRequestField.ADDITIONS)).iterateAll()); + change = changes.get(1); + assertEquals(CHANGE_ADD_ZONE1.additions(), change.additions()); + assertTrue(change.deletions().isEmpty()); + assertEquals("1", change.id()); + assertNull(change.startTimeMillis()); + assertNull(change.status()); + changes = ImmutableList.copyOf(DNS.listChangeRequests(ZONE1.name(), + Dns.ChangeRequestListOption.sortOrder(Dns.SortingOrder.ASCENDING), + Dns.ChangeRequestListOption.fields(Dns.ChangeRequestField.DELETIONS)).iterateAll()); + change = changes.get(2); + assertTrue(change.additions().isEmpty()); + assertNotNull(change.deletions()); + assertEquals("2", change.id()); + assertNull(change.startTimeMillis()); + assertNull(change.status()); + changes = ImmutableList.copyOf(DNS.listChangeRequests(ZONE1.name(), + Dns.ChangeRequestListOption.sortOrder(Dns.SortingOrder.ASCENDING), + Dns.ChangeRequestListOption.fields(Dns.ChangeRequestField.ID)).iterateAll()); + change = changes.get(1); + assertTrue(change.additions().isEmpty()); + assertTrue(change.deletions().isEmpty()); + assertEquals("1", change.id()); + assertNull(change.startTimeMillis()); + assertNull(change.status()); + changes = ImmutableList.copyOf(DNS.listChangeRequests(ZONE1.name(), + Dns.ChangeRequestListOption.sortOrder(Dns.SortingOrder.ASCENDING), + Dns.ChangeRequestListOption.fields(Dns.ChangeRequestField.START_TIME)).iterateAll()); + change = changes.get(1); + assertTrue(change.additions().isEmpty()); + assertTrue(change.deletions().isEmpty()); + assertEquals("1", change.id()); + assertNotNull(change.startTimeMillis()); + assertNull(change.status()); + changes = ImmutableList.copyOf(DNS.listChangeRequests(ZONE1.name(), + Dns.ChangeRequestListOption.sortOrder(Dns.SortingOrder.ASCENDING), + Dns.ChangeRequestListOption.fields(Dns.ChangeRequestField.STATUS)).iterateAll()); + change = changes.get(1); + assertTrue(change.additions().isEmpty()); + assertTrue(change.deletions().isEmpty()); + assertEquals("1", change.id()); + assertNull(change.startTimeMillis()); + assertEquals(ChangeRequest.Status.DONE, change.status()); + } finally { + clear(); + } + } + + @Test + public void testGetChange() { + try { + Zone zone = DNS.create(ZONE1, Dns.ZoneOption.fields(Dns.ZoneField.NAME)); + ChangeRequest created = zone.applyChangeRequest(CHANGE_ADD_ZONE1); + ChangeRequest retrieved = DNS.getChangeRequest(zone.name(), created.id()); + assertEqChangesIgnoreStatus(created, retrieved); + waitForChangeToComplete(zone.name(), created.id()); + zone.applyChangeRequest(CHANGE_DELETE_ZONE1); + // with options + created = zone.applyChangeRequest(CHANGE_ADD_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.ID)); + retrieved = DNS.getChangeRequest(zone.name(), created.id(), + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.ID)); + assertEqChangesIgnoreStatus(created, retrieved); + waitForChangeToComplete(zone.name(), created.id()); + zone.applyChangeRequest(CHANGE_DELETE_ZONE1); + created = zone.applyChangeRequest(CHANGE_ADD_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.STATUS)); + retrieved = DNS.getChangeRequest(zone.name(), created.id(), + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.STATUS)); + assertEqChangesIgnoreStatus(created, retrieved); + waitForChangeToComplete(zone.name(), created.id()); + zone.applyChangeRequest(CHANGE_DELETE_ZONE1); + created = zone.applyChangeRequest(CHANGE_ADD_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.START_TIME)); + retrieved = DNS.getChangeRequest(zone.name(), created.id(), + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.START_TIME)); + assertEqChangesIgnoreStatus(created, retrieved); + waitForChangeToComplete(zone.name(), created.id()); + zone.applyChangeRequest(CHANGE_DELETE_ZONE1); + created = zone.applyChangeRequest(CHANGE_ADD_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.ADDITIONS)); + retrieved = DNS.getChangeRequest(zone.name(), created.id(), + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.ADDITIONS)); + assertEqChangesIgnoreStatus(created, retrieved); + waitForChangeToComplete(zone.name(), created.id()); + // finishes with delete otherwise we cannot delete the zone + created = zone.applyChangeRequest(CHANGE_DELETE_ZONE1, + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.DELETIONS)); + retrieved = DNS.getChangeRequest(zone.name(), created.id(), + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.DELETIONS)); + assertEqChangesIgnoreStatus(created, retrieved); + waitForChangeToComplete(zone.name(), created.id()); + } finally { + clear(); + } + } + + @Test + public void testGetProject() { + // fetches all fields + ProjectInfo project = DNS.getProject(); + assertNotNull(project.quota()); + // options + project = DNS.getProject(Dns.ProjectOption.fields(Dns.ProjectField.QUOTA)); + assertNotNull(project.quota()); + project = DNS.getProject(Dns.ProjectOption.fields(Dns.ProjectField.PROJECT_ID)); + assertNull(project.quota()); + project = DNS.getProject(Dns.ProjectOption.fields(Dns.ProjectField.PROJECT_NUMBER)); + assertNull(project.quota()); + project = DNS.getProject(Dns.ProjectOption.fields(Dns.ProjectField.PROJECT_NUMBER, + Dns.ProjectField.QUOTA, Dns.ProjectField.PROJECT_ID)); + assertNotNull(project.quota()); + } + + @Test + public void testListDnsRecords() { + try { + Zone zone = DNS.create(ZONE1); + ImmutableList recordSets = ImmutableList.copyOf( + DNS.listRecordSets(zone.name()).iterateAll()); + assertEquals(2, recordSets.size()); + ImmutableList defaultRecords = + ImmutableList.of(RecordSet.Type.NS, RecordSet.Type.SOA); + for (RecordSet recordSet : recordSets) { + assertTrue(defaultRecords.contains(recordSet.type())); + } + // field options + Iterator recordSetIterator = DNS.listRecordSets(zone.name(), + Dns.RecordSetListOption.fields(Dns.RecordSetField.TTL)).iterateAll(); + int counter = 0; + while (recordSetIterator.hasNext()) { + RecordSet recordSet = recordSetIterator.next(); + assertEquals(recordSets.get(counter).ttl(), recordSet.ttl()); + assertEquals(recordSets.get(counter).name(), recordSet.name()); + assertEquals(recordSets.get(counter).type(), recordSet.type()); + assertTrue(recordSet.records().isEmpty()); + counter++; + } + assertEquals(2, counter); + recordSetIterator = DNS.listRecordSets(zone.name(), + Dns.RecordSetListOption.fields(Dns.RecordSetField.NAME)).iterateAll(); + counter = 0; + while (recordSetIterator.hasNext()) { + RecordSet recordSet = recordSetIterator.next(); + assertEquals(recordSets.get(counter).name(), recordSet.name()); + assertEquals(recordSets.get(counter).type(), recordSet.type()); + assertTrue(recordSet.records().isEmpty()); + assertNull(recordSet.ttl()); + counter++; + } + assertEquals(2, counter); + recordSetIterator = DNS.listRecordSets(zone.name(), + Dns.RecordSetListOption.fields(Dns.RecordSetField.DNS_RECORDS)) + .iterateAll(); + counter = 0; + while (recordSetIterator.hasNext()) { + RecordSet recordSet = recordSetIterator.next(); + assertEquals(recordSets.get(counter).records(), recordSet.records()); + assertEquals(recordSets.get(counter).name(), recordSet.name()); + assertEquals(recordSets.get(counter).type(), recordSet.type()); + assertNull(recordSet.ttl()); + counter++; + } + assertEquals(2, counter); + recordSetIterator = DNS.listRecordSets(zone.name(), + Dns.RecordSetListOption.fields(Dns.RecordSetField.TYPE), + Dns.RecordSetListOption.pageSize(1)).iterateAll(); // also test paging + counter = 0; + while (recordSetIterator.hasNext()) { + RecordSet recordSet = recordSetIterator.next(); + assertEquals(recordSets.get(counter).type(), recordSet.type()); + assertEquals(recordSets.get(counter).name(), recordSet.name()); + assertTrue(recordSet.records().isEmpty()); + assertNull(recordSet.ttl()); + counter++; + } + assertEquals(2, counter); + // test page size + Page recordSetPage = DNS.listRecordSets(zone.name(), + Dns.RecordSetListOption.fields(Dns.RecordSetField.TYPE), + Dns.RecordSetListOption.pageSize(1)); + assertEquals(1, ImmutableList.copyOf(recordSetPage.values().iterator()).size()); + // test name filter + ChangeRequest change = DNS.applyChangeRequest(ZONE1.name(), CHANGE_ADD_ZONE1); + waitForChangeToComplete(ZONE1.name(), change.id()); + recordSetIterator = DNS.listRecordSets(ZONE1.name(), + Dns.RecordSetListOption.dnsName(A_RECORD_ZONE1.name())).iterateAll(); + counter = 0; + while (recordSetIterator.hasNext()) { + RecordSet recordSet = recordSetIterator.next(); + assertTrue(ImmutableList.of(A_RECORD_ZONE1.type(), AAAA_RECORD_ZONE1.type()) + .contains(recordSet.type())); + counter++; + } + assertEquals(2, counter); + // test type filter + waitForChangeToComplete(ZONE1.name(), change.id()); + recordSetIterator = DNS.listRecordSets(ZONE1.name(), + Dns.RecordSetListOption.dnsName(A_RECORD_ZONE1.name()), + Dns.RecordSetListOption.type(A_RECORD_ZONE1.type())) + .iterateAll(); + counter = 0; + while (recordSetIterator.hasNext()) { + RecordSet recordSet = recordSetIterator.next(); + assertEquals(A_RECORD_ZONE1, recordSet); + counter++; + } + assertEquals(1, counter); + change = zone.applyChangeRequest(CHANGE_DELETE_ZONE1); + // check wrong arguments + try { + // name is not set + DNS.listRecordSets(ZONE1.name(), + Dns.RecordSetListOption.type(A_RECORD_ZONE1.type())); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertFalse(ex.retryable()); + } + try { + DNS.listRecordSets(ZONE1.name(), Dns.RecordSetListOption.pageSize(0)); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertFalse(ex.retryable()); + } + try { + DNS.listRecordSets(ZONE1.name(), Dns.RecordSetListOption.pageSize(-1)); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertFalse(ex.retryable()); + } + waitForChangeToComplete(ZONE1.name(), change.id()); + } finally { + clear(); + } + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/testing/LocalDnsHelperTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/testing/LocalDnsHelperTest.java new file mode 100644 index 000000000000..44516f47c657 --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/testing/LocalDnsHelperTest.java @@ -0,0 +1,1455 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.dns.testing; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.Project; +import com.google.api.services.dns.model.ResourceRecordSet; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.gcloud.dns.DnsException; +import com.google.gcloud.dns.spi.DefaultDnsRpc; +import com.google.gcloud.dns.spi.DnsRpc; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class LocalDnsHelperTest { + + private static final String RRSET_TYPE = "A"; + private static final ResourceRecordSet RRSET1 = new ResourceRecordSet(); + private static final ResourceRecordSet RRSET2 = new ResourceRecordSet(); + private static final ResourceRecordSet RRSET_KEEP = new ResourceRecordSet(); + private static final String PROJECT_ID1 = "2135436541254"; + private static final String PROJECT_ID2 = "882248761325"; + private static final String ZONE_NAME1 = "my-little-zone"; + private static final String ZONE_NAME2 = "another-zone-name"; + private static final ManagedZone ZONE1 = new ManagedZone(); + private static final ManagedZone ZONE2 = new ManagedZone(); + private static final String DNS_NAME = "www.example.com."; + private static final Change CHANGE1 = new Change(); + private static final Change CHANGE2 = new Change(); + private static final Change CHANGE_KEEP = new Change(); + private static final Change CHANGE_COMPLEX = new Change(); + private static final LocalDnsHelper LOCAL_DNS_HELPER = LocalDnsHelper.create(0L); + private static final Map EMPTY_RPC_OPTIONS = ImmutableMap.of(); + private static final DnsRpc RPC = new DefaultDnsRpc(LOCAL_DNS_HELPER.options()); + private static final String REAL_PROJECT_ID = LOCAL_DNS_HELPER.options().projectId(); + private Map optionsMap; + + @BeforeClass + public static void before() { + ZONE1.setName(ZONE_NAME1); + ZONE1.setDescription(""); + ZONE1.setDnsName(DNS_NAME); + ZONE1.setNameServerSet("somenameserverset"); + ZONE2.setName(ZONE_NAME2); + ZONE2.setDescription(""); + ZONE2.setDnsName(DNS_NAME); + ZONE2.setNameServerSet("somenameserverset"); + RRSET1.setName(DNS_NAME); + RRSET1.setType(RRSET_TYPE); + RRSET1.setRrdatas(ImmutableList.of("1.1.1.1")); + RRSET2.setName(DNS_NAME); + RRSET2.setType(RRSET_TYPE); + RRSET2.setRrdatas(ImmutableList.of("123.132.153.156")); + RRSET_KEEP.setName(DNS_NAME); + RRSET_KEEP.setType("MX"); + RRSET_KEEP.setRrdatas(ImmutableList.of("255.255.255.254")); + CHANGE1.setAdditions(ImmutableList.of(RRSET1, RRSET2)); + CHANGE2.setDeletions(ImmutableList.of(RRSET2)); + CHANGE_KEEP.setAdditions(ImmutableList.of(RRSET_KEEP)); + CHANGE_COMPLEX.setAdditions(ImmutableList.of(RRSET_KEEP)); + CHANGE_COMPLEX.setDeletions(ImmutableList.of(RRSET_KEEP)); + LOCAL_DNS_HELPER.start(); + } + + @Rule + public Timeout globalTimeout = Timeout.seconds(60); + + @Before + public void setUp() { + resetProjects(); + optionsMap = new HashMap<>(); + } + + @AfterClass + public static void after() { + LOCAL_DNS_HELPER.stop(); + } + + private static void resetProjects() { + for (String project : LOCAL_DNS_HELPER.projects().keySet()) { + LOCAL_DNS_HELPER.projects().remove(project); + } + } + + private static void assertEqChangesIgnoreStatus(Change expected, Change actual) { + assertEquals(expected.getAdditions(), actual.getAdditions()); + assertEquals(expected.getDeletions(), actual.getDeletions()); + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getStartTime(), actual.getStartTime()); + } + + @Test + public void testCreateZone() { + ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + // check that default records were created + DnsRpc.ListResult listResult + = RPC.listRecordSets(ZONE1.getName(), EMPTY_RPC_OPTIONS); + ImmutableList defaultTypes = ImmutableList.of("SOA", "NS"); + Iterator iterator = listResult.results().iterator(); + assertTrue(defaultTypes.contains(iterator.next().getType())); + assertTrue(defaultTypes.contains(iterator.next().getType())); + assertFalse(iterator.hasNext()); + assertEquals(created, LOCAL_DNS_HELPER.findZone(REAL_PROJECT_ID, ZONE1.getName()).zone()); + ManagedZone zone = RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS); + assertEquals(created, zone); + try { + RPC.create(null, EMPTY_RPC_OPTIONS); + fail("Zone cannot be null"); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("entity.managedZone")); + } + // create zone twice + try { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + fail("Zone already exists."); + } catch (DnsException ex) { + // expected + assertEquals(409, ex.code()); + assertTrue(ex.getMessage().contains("already exists")); + } + // field options + resetProjects(); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "id"); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + resetProjects(); + options.put(DnsRpc.Option.FIELDS, "creationTime"); + zone = RPC.create(ZONE1, options); + assertNotNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "dnsName"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNotNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "description"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "nameServers"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNotNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "nameServerSet"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNull(zone.getId()); + // several combined + options.put(DnsRpc.Option.FIELDS, "nameServerSet,description,id,name"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNotNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + } + + @Test + public void testGetZone() { + // non-existent + assertNull(RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS)); + // existent + ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + ManagedZone zone = RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS); + assertEquals(created, zone); + assertEquals(ZONE1.getName(), zone.getName()); + // field options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "id"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "creationTime"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNotNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "dnsName"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNotNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "description"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "nameServers"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNotNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "nameServerSet"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNull(zone.getId()); + // several combined + options.put(DnsRpc.Option.FIELDS, "nameServerSet,description,id,name"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNotNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + } + + @Test + public void testDeleteZone() { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + assertTrue(RPC.deleteZone(ZONE1.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + // deleting non-existent zone + assertFalse(RPC.deleteZone(ZONE1.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + RPC.create(ZONE2, EMPTY_RPC_OPTIONS); + assertNotNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + assertNotNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); + // delete in reverse order + assertTrue(RPC.deleteZone(ZONE1.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + assertNotNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); + assertTrue(RPC.deleteZone(ZONE2.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + assertNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + try { + RPC.deleteZone(ZONE1.getName()); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("not empty")); + } + } + + @Test + public void testCreateAndApplyChange() { + executeCreateAndApplyChangeTest(RPC); + } + + @Test + public void testCreateAndApplyChangeWithThreads() { + LocalDnsHelper localDnsThreaded = LocalDnsHelper.create(50L); + localDnsThreaded.start(); + DnsRpc rpc = new DefaultDnsRpc(localDnsThreaded.options()); + executeCreateAndApplyChangeTest(rpc); + localDnsThreaded.stop(); + } + + private static void waitForChangeToComplete(DnsRpc rpc, String zoneName, String changeId) { + while (true) { + Change change = rpc.getChangeRequest(zoneName, changeId, EMPTY_RPC_OPTIONS); + if ("done".equals(change.getStatus())) { + return; + } + try { + Thread.sleep(50L); + } catch (InterruptedException e) { + fail("Thread was interrupted while waiting for change processing."); + } + } + } + + private static void executeCreateAndApplyChangeTest(DnsRpc rpc) { + rpc.create(ZONE1, EMPTY_RPC_OPTIONS); + assertNull(rpc.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); + // add + Change createdChange = rpc.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + assertEquals(CHANGE1.getAdditions(), createdChange.getAdditions()); + assertEquals(CHANGE1.getDeletions(), createdChange.getDeletions()); + assertNotNull(createdChange.getStartTime()); + assertEquals("1", createdChange.getId()); + waitForChangeToComplete(rpc, ZONE1.getName(), "1"); // necessary for the following to return 409 + try { + rpc.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + fail(); + } catch (DnsException ex) { + assertEquals(409, ex.code()); + assertTrue(ex.getMessage().contains("already exists")); + } + assertNotNull(rpc.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); + assertNull(rpc.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + // delete + rpc.applyChangeRequest(ZONE1.getName(), CHANGE2, EMPTY_RPC_OPTIONS); + assertNotNull(rpc.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); + assertNotNull(rpc.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + waitForChangeToComplete(rpc, ZONE1.getName(), "2"); + rpc.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + waitForChangeToComplete(rpc, ZONE1.getName(), "3"); + Iterable results = + rpc.listRecordSets(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); + List defaults = ImmutableList.of("SOA", "NS"); + boolean rrsetKeep = false; + boolean rrset1 = false; + for (ResourceRecordSet dnsRecord : results) { + if (dnsRecord.getName().equals(RRSET_KEEP.getName()) + && dnsRecord.getType().equals(RRSET_KEEP.getType())) { + rrsetKeep = true; + } else if (dnsRecord.getName().equals(RRSET1.getName()) + && dnsRecord.getType().equals(RRSET1.getType())) { + rrset1 = true; + } else if (!defaults.contains(dnsRecord.getType())) { + fail(String.format("Record with type %s should not exist", dnsRecord.getType())); + } + } + assertTrue(rrset1); + assertTrue(rrsetKeep); + } + + @Test + public void testGetProject() { + // the projects are automatically created when getProject is called + assertNotNull(LOCAL_DNS_HELPER.getProject(PROJECT_ID1, null)); + assertNotNull(LOCAL_DNS_HELPER.getProject(PROJECT_ID2, null)); + Project project = RPC.getProject(EMPTY_RPC_OPTIONS); + assertNotNull(project.getQuota()); + assertEquals(REAL_PROJECT_ID, project.getId()); + // fields options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "number"); + project = RPC.getProject(options); + assertNull(project.getId()); + assertNotNull(project.getNumber()); + assertNull(project.getQuota()); + options.put(DnsRpc.Option.FIELDS, "id"); + project = RPC.getProject(options); + assertNotNull(project.getId()); + assertNull(project.getNumber()); + assertNull(project.getQuota()); + options.put(DnsRpc.Option.FIELDS, "quota"); + project = RPC.getProject(options); + assertNull(project.getId()); + assertNull(project.getNumber()); + assertNotNull(project.getQuota()); + } + + @Test + public void testCreateChange() { + // non-existent zone + try { + RPC.applyChangeRequest(ZONE_NAME1, CHANGE1, EMPTY_RPC_OPTIONS); + fail("Zone was not created yet."); + } catch (DnsException ex) { + assertEquals(404, ex.code()); + } + // existent zone + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + assertNull(RPC.getChangeRequest(ZONE_NAME1, "1", EMPTY_RPC_OPTIONS)); + Change created = RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + assertEquals(created, RPC.getChangeRequest(ZONE_NAME1, "1", EMPTY_RPC_OPTIONS)); + // field options + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "additions"); + Change complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNotNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "deletions"); + complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNull(complex.getAdditions()); + assertNotNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "id"); + complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNotNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "startTime"); + complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNotNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "status"); + complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNotNull(complex.getStatus()); + } + + @Test + public void testGetChange() { + // existent + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + Change created = RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + Change retrieved = RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS); + assertEquals(created, retrieved); + // non-existent + assertNull(RPC.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + // non-existent zone + try { + RPC.getChangeRequest(ZONE_NAME2, "1", EMPTY_RPC_OPTIONS); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(404, ex.code()); + assertTrue(ex.getMessage().contains("managedZone")); + } + // field options + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + Change change = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, EMPTY_RPC_OPTIONS); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "additions"); + Change complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNotNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "deletions"); + complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNull(complex.getAdditions()); + assertNotNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "id"); + complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNotNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "startTime"); + complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNotNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "status"); + complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNotNull(complex.getStatus()); + } + + @Test + public void testListZones() { + Iterable results = RPC.listZones(EMPTY_RPC_OPTIONS).results(); + ImmutableList zones = ImmutableList.copyOf(results); + assertEquals(0, zones.size()); + // some zones exists + ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + results = RPC.listZones(EMPTY_RPC_OPTIONS).results(); + zones = ImmutableList.copyOf(results); + assertEquals(created, zones.get(0)); + assertEquals(1, zones.size()); + created = RPC.create(ZONE2, EMPTY_RPC_OPTIONS); + results = RPC.listZones(EMPTY_RPC_OPTIONS).results(); + zones = ImmutableList.copyOf(results); + assertEquals(2, zones.size()); + assertTrue(zones.contains(created)); + // error in options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 0); + try { + RPC.listZones(options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); + } + options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, -1); + try { + RPC.listZones(options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); + } + // ok size + options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 335); + results = RPC.listZones(options).results(); + zones = ImmutableList.copyOf(results); + assertEquals(2, zones.size()); + // dns name problems + options = new HashMap<>(); + options.put(DnsRpc.Option.DNS_NAME, "aaa"); + try { + RPC.listZones(options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.dnsName")); + } + // ok name + options = new HashMap<>(); + options.put(DnsRpc.Option.DNS_NAME, "aaaa."); + results = RPC.listZones(options).results(); + zones = ImmutableList.copyOf(results); + assertEquals(0, zones.size()); + // field options + options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "managedZones(id)"); + ManagedZone zone = RPC.listZones(options).results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(creationTime)"); + zone = RPC.listZones(options).results().iterator().next(); + assertNotNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(dnsName)"); + zone = RPC.listZones(options).results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNotNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(description)"); + zone = RPC.listZones(options).results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(nameServers)"); + zone = RPC.listZones(options).results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNotNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(nameServerSet)"); + DnsRpc.ListResult listResult = RPC.listZones(options); + zone = listResult.results().iterator().next(); + assertNull(listResult.pageToken()); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNull(zone.getId()); + // several combined + options.put(DnsRpc.Option.FIELDS, + "managedZones(nameServerSet,description,id,name),nextPageToken"); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + listResult = RPC.listZones(options); + zone = listResult.results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNotNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + assertEquals(zone.getName(), listResult.pageToken()); + } + + @Test + public void testListDnsRecords() { + // no zone exists + try { + RPC.listRecordSets(ZONE_NAME1, EMPTY_RPC_OPTIONS); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(404, ex.code()); + assertTrue(ex.getMessage().contains("managedZone")); + } + // zone exists but has no records + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + Iterable results = + RPC.listRecordSets(ZONE_NAME1, EMPTY_RPC_OPTIONS).results(); + ImmutableList records = ImmutableList.copyOf(results); + assertEquals(2, records.size()); // contains default NS and SOA + // zone has records + RPC.applyChangeRequest(ZONE_NAME1, CHANGE_KEEP, EMPTY_RPC_OPTIONS); + results = RPC.listRecordSets(ZONE_NAME1, EMPTY_RPC_OPTIONS).results(); + records = ImmutableList.copyOf(results); + assertEquals(3, records.size()); + // error in options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 0); + try { + RPC.listRecordSets(ZONE1.getName(), options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); + } + options.put(DnsRpc.Option.PAGE_SIZE, -1); + try { + RPC.listRecordSets(ZONE1.getName(), options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); + } + options.put(DnsRpc.Option.PAGE_SIZE, 15); + results = RPC.listRecordSets(ZONE1.getName(), options).results(); + records = ImmutableList.copyOf(results); + assertEquals(3, records.size()); + // dnsName filter + options = new HashMap<>(); + options.put(DnsRpc.Option.NAME, "aaa"); + try { + RPC.listRecordSets(ZONE1.getName(), options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.name")); + } + options.put(DnsRpc.Option.NAME, "aaa."); + results = RPC.listRecordSets(ZONE1.getName(), options).results(); + records = ImmutableList.copyOf(results); + assertEquals(0, records.size()); + options.put(DnsRpc.Option.NAME, null); + options.put(DnsRpc.Option.DNS_TYPE, "A"); + try { + RPC.listRecordSets(ZONE1.getName(), options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.name")); + } + options.put(DnsRpc.Option.NAME, "aaa."); + options.put(DnsRpc.Option.DNS_TYPE, "a"); + try { + RPC.listRecordSets(ZONE1.getName(), options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.type")); + } + options.put(DnsRpc.Option.NAME, DNS_NAME); + options.put(DnsRpc.Option.DNS_TYPE, "SOA"); + results = RPC.listRecordSets(ZONE1.getName(), options).results(); + records = ImmutableList.copyOf(results); + assertEquals(1, records.size()); + // field options + options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "rrsets(name)"); + DnsRpc.ListResult listResult = + RPC.listRecordSets(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); + ResourceRecordSet record = records.get(0); + assertNotNull(record.getName()); + assertNull(record.getRrdatas()); + assertNull(record.getType()); + assertNull(record.getTtl()); + assertNull(listResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "rrsets(rrdatas)"); + listResult = RPC.listRecordSets(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); + record = records.get(0); + assertNull(record.getName()); + assertNotNull(record.getRrdatas()); + assertNull(record.getType()); + assertNull(record.getTtl()); + assertNull(listResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "rrsets(ttl)"); + listResult = RPC.listRecordSets(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); + record = records.get(0); + assertNull(record.getName()); + assertNull(record.getRrdatas()); + assertNull(record.getType()); + assertNotNull(record.getTtl()); + assertNull(listResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "rrsets(type)"); + listResult = RPC.listRecordSets(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); + record = records.get(0); + assertNull(record.getName()); + assertNull(record.getRrdatas()); + assertNotNull(record.getType()); + assertNull(record.getTtl()); + assertNull(listResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "nextPageToken"); + listResult = RPC.listRecordSets(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); + record = records.get(0); + assertNull(record.getName()); + assertNull(record.getRrdatas()); + assertNull(record.getType()); + assertNull(record.getTtl()); + assertNull(listResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "nextPageToken,rrsets(name,rrdatas)"); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + listResult = RPC.listRecordSets(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); + assertEquals(1, records.size()); + record = records.get(0); + assertNotNull(record.getName()); + assertNotNull(record.getRrdatas()); + assertNull(record.getType()); + assertNull(record.getTtl()); + assertNotNull(listResult.pageToken()); + } + + @Test + public void testListChanges() { + // no such zone exists + try { + RPC.listChangeRequests(ZONE_NAME1, EMPTY_RPC_OPTIONS); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(404, ex.code()); + assertTrue(ex.getMessage().contains("managedZone")); + } + // zone exists but has no changes + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + Iterable results = RPC.listChangeRequests(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); + ImmutableList changes = ImmutableList.copyOf(results); + assertEquals(1, changes.size()); + // zone has changes + RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE2, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + results = RPC.listChangeRequests(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); + changes = ImmutableList.copyOf(results); + assertEquals(4, changes.size()); + // error in options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 0); + try { + RPC.listChangeRequests(ZONE1.getName(), options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); + } + options.put(DnsRpc.Option.PAGE_SIZE, -1); + try { + RPC.listChangeRequests(ZONE1.getName(), options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); + } + options.put(DnsRpc.Option.PAGE_SIZE, 15); + results = RPC.listChangeRequests(ZONE1.getName(), options).results(); + changes = ImmutableList.copyOf(results); + assertEquals(4, changes.size()); + options = new HashMap<>(); + options.put(DnsRpc.Option.SORTING_ORDER, "descending"); + results = RPC.listChangeRequests(ZONE1.getName(), options).results(); + ImmutableList descending = ImmutableList.copyOf(results); + results = RPC.listChangeRequests(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); + ImmutableList ascending = ImmutableList.copyOf(results); + int size = 4; + assertEquals(size, descending.size()); + for (int i = 0; i < size; i++) { + assertEquals(descending.get(i), ascending.get(size - i - 1)); + } + options.put(DnsRpc.Option.SORTING_ORDER, "something else"); + try { + RPC.listChangeRequests(ZONE1.getName(), options); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.sortOrder")); + } + // field options + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, EMPTY_RPC_OPTIONS); + options = new HashMap<>(); + options.put(DnsRpc.Option.SORTING_ORDER, "descending"); + options.put(DnsRpc.Option.FIELDS, "changes(additions)"); + DnsRpc.ListResult changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + Change complex = changes.get(0); + assertNotNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "changes(deletions)"); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNotNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "changes(id)"); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNotNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "changes(startTime)"); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNotNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "changes(status)"); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNotNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "nextPageToken"); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNotNull(changeListResult.pageToken()); + } + + @Test + public void testDnsRecordPaging() { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + List complete = ImmutableList.copyOf( + RPC.listRecordSets(ZONE1.getName(), EMPTY_RPC_OPTIONS).results()); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + DnsRpc.ListResult listResult = RPC.listRecordSets(ZONE1.getName(), options); + ImmutableList records = ImmutableList.copyOf(listResult.results()); + assertEquals(1, records.size()); + assertEquals(complete.get(0), records.get(0)); + options.put(DnsRpc.Option.PAGE_TOKEN, listResult.pageToken()); + listResult = RPC.listRecordSets(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); + assertEquals(1, records.size()); + assertEquals(complete.get(1), records.get(0)); + } + + @Test + public void testZonePaging() { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + RPC.create(ZONE2, EMPTY_RPC_OPTIONS); + ImmutableList complete = ImmutableList.copyOf( + RPC.listZones(EMPTY_RPC_OPTIONS).results()); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + DnsRpc.ListResult listResult = RPC.listZones(options); + ImmutableList page1 = ImmutableList.copyOf(listResult.results()); + assertEquals(1, page1.size()); + assertEquals(complete.get(0), page1.get(0)); + assertEquals(page1.get(0).getName(), listResult.pageToken()); + options.put(DnsRpc.Option.PAGE_TOKEN, listResult.pageToken()); + listResult = RPC.listZones(options); + ImmutableList page2 = ImmutableList.copyOf(listResult.results()); + assertEquals(1, page2.size()); + assertEquals(complete.get(1), page2.get(0)); + assertNull(listResult.pageToken()); + } + + @Test + public void testChangePaging() { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE2, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + ImmutableList complete = + ImmutableList.copyOf(RPC.listChangeRequests(ZONE1.getName(), EMPTY_RPC_OPTIONS).results()); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + DnsRpc.ListResult changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + List changes = ImmutableList.copyOf(changeListResult.results()); + assertEquals(1, changes.size()); + assertEquals(complete.get(0), changes.get(0)); + assertEquals(complete.get(0).getId(), changeListResult.pageToken()); + options.put(DnsRpc.Option.PAGE_TOKEN, changeListResult.pageToken()); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + assertEquals(1, changes.size()); + assertEquals(complete.get(1), changes.get(0)); + assertEquals(complete.get(1).getId(), changeListResult.pageToken()); + } + + @Test + public void testToListResponse() { + LocalDnsHelper.Response response = LocalDnsHelper.toListResponse( + Lists.newArrayList("some", "multiple", "words"), "contextA", "IncludeThisPageToken", true); + assertTrue(response.body().contains("IncludeThisPageToken")); + assertTrue(response.body().contains("contextA")); + response = LocalDnsHelper.toListResponse( + Lists.newArrayList("some", "multiple", "words"), "contextB", "IncludeThisPageToken", false); + assertFalse(response.body().contains("IncludeThisPageToken")); + assertTrue(response.body().contains("contextB")); + response = LocalDnsHelper.toListResponse( + Lists.newArrayList("some", "multiple", "words"), "contextC", null, true); + assertFalse(response.body().contains("pageToken")); + assertTrue(response.body().contains("contextC")); + } + + @Test + public void testCreateZoneValidation() { + ManagedZone minimalZone = copyZone(ZONE1); + // no name + ManagedZone copy = copyZone(minimalZone); + copy.setName(null); + LocalDnsHelper.Response response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.name")); + // no description + copy = copyZone(minimalZone); + copy.setDescription(null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.description")); + // no dns name + copy = copyZone(minimalZone); + copy.setDnsName(null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.dnsName")); + // zone name does not start with a letter + copy = copyZone(minimalZone); + copy.setName("1aaaaaa"); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.name")); + assertTrue(response.body().contains("Invalid")); + // zone name is too long + copy = copyZone(minimalZone); + copy.setName("123456aaaa123456aaaa123456aaaa123456aaaa123456aaaa123456aaaa123456aaaa123456aa"); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.name")); + assertTrue(response.body().contains("Invalid")); + // zone name contains invalid characters + copy = copyZone(minimalZone); + copy.setName("x1234AA6aa"); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.name")); + assertTrue(response.body().contains("Invalid")); + // zone name contains invalid characters + copy = copyZone(minimalZone); + copy.setName("x a"); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.name")); + assertTrue(response.body().contains("Invalid")); + // dns name does not end with period + copy = copyZone(minimalZone); + copy.setDnsName("aaaaaa.com"); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.dnsName")); + assertTrue(response.body().contains("Invalid")); + // dns name is reserved + copy = copyZone(minimalZone); + copy.setDnsName("com."); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("not available to be created.")); + // empty description should pass + copy = copyZone(minimalZone); + copy.setDescription(""); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); + assertEquals(200, response.code()); + } + + @Test + public void testCheckListOptions() { + // listing zones + optionsMap.put("maxResults", "-1"); + LocalDnsHelper.Response response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.maxResults")); + optionsMap.put("maxResults", "0"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.maxResults")); + optionsMap.put("maxResults", "aaaa"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("integer")); + optionsMap.put("maxResults", "15"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("dnsName", "aaa"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.dnsName")); + optionsMap.put("dnsName", "aaa."); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + // listing dns records + optionsMap.put("name", "aaa"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.name")); + optionsMap.put("name", "aaa."); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("name", null); + optionsMap.put("type", "A"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.name")); + optionsMap.put("name", "aaa."); + optionsMap.put("type", "a"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.type")); + optionsMap.put("name", "aaaa."); + optionsMap.put("type", "A"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + // listing changes + optionsMap.put("sortBy", "changeSequence"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("sortBy", "something else"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("Allowed values: [changesequence]")); + optionsMap.put("sortBy", "ChAnGeSeQuEnCe"); // is not case sensitive + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("sortOrder", "ascending"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("sortOrder", "descending"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("sortOrder", "somethingelse"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.sortOrder")); + } + + @Test + public void testCheckRrset() { + ResourceRecordSet valid = new ResourceRecordSet(); + valid.setName(ZONE1.getDnsName()); + valid.setType("A"); + valid.setRrdatas(ImmutableList.of("0.255.1.5")); + valid.setTtl(500); + Change validChange = new Change(); + validChange.setAdditions(ImmutableList.of(valid)); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange); + // delete with field mismatch + LocalDnsHelper.ZoneContainer zone = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1); + valid.setTtl(valid.getTtl() + 20); + LocalDnsHelper.Response response = LocalDnsHelper.checkRrset(valid, zone, 0, "deletions"); + assertEquals(412, response.code()); + assertTrue(response.body().contains("entity.change.deletions[0]")); + } + + @Test + public void testCheckRrdata() { + // A + assertTrue(LocalDnsHelper.checkRrData("255.255.255.255", "A")); + assertTrue(LocalDnsHelper.checkRrData("13.15.145.165", "A")); + assertTrue(LocalDnsHelper.checkRrData("0.0.0.0", "A")); + assertFalse(LocalDnsHelper.checkRrData("255.255.255.256", "A")); + assertFalse(LocalDnsHelper.checkRrData("-1.255.255.255", "A")); + assertFalse(LocalDnsHelper.checkRrData(".255.255.254", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255.255.", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255..22", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255.aa.22", "A")); + assertFalse(LocalDnsHelper.checkRrData("", "A")); + assertFalse(LocalDnsHelper.checkRrData("...", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255.12", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255.12.11.11", "A")); + // AAAA + assertTrue(LocalDnsHelper.checkRrData("1F:fa:09fd::343:aaaa:AAAA:0", "AAAA")); + assertTrue(LocalDnsHelper.checkRrData("0000:FFFF:09fd::343:aaaa:AAAA:0", "AAAA")); + assertFalse(LocalDnsHelper.checkRrData("-2:::::::", "AAAA")); + assertTrue(LocalDnsHelper.checkRrData("0::0", "AAAA")); + assertFalse(LocalDnsHelper.checkRrData("::1FFFF:::::", "AAAA")); + assertFalse(LocalDnsHelper.checkRrData("::aqaa:::::", "AAAA")); + assertFalse(LocalDnsHelper.checkRrData("::::::::", "AAAA")); // too long + assertFalse(LocalDnsHelper.checkRrData("::::::", "AAAA")); // too short + } + + @Test + public void testCheckChange() { + ResourceRecordSet validA = new ResourceRecordSet(); + validA.setName(ZONE1.getDnsName()); + validA.setType("A"); + validA.setRrdatas(ImmutableList.of("0.255.1.5")); + ResourceRecordSet invalidA = new ResourceRecordSet(); + invalidA.setName(ZONE1.getDnsName()); + invalidA.setType("A"); + invalidA.setRrdatas(ImmutableList.of("0.-255.1.5")); + Change validChange = new Change(); + validChange.setAdditions(ImmutableList.of(validA)); + Change invalidChange = new Change(); + invalidChange.setAdditions(ImmutableList.of(invalidA)); + LocalDnsHelper.ZoneContainer zoneContainer = new LocalDnsHelper.ZoneContainer(ZONE1); + LocalDnsHelper.Response response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertNull(response); + response = LocalDnsHelper.checkChange(invalidChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].rrdata[0]")); + // only empty additions/deletions + Change empty = new Change(); + empty.setAdditions(ImmutableList.of()); + empty.setDeletions(ImmutableList.of()); + response = LocalDnsHelper.checkChange(empty, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "The 'entity.change' parameter is required but was missing.")); + // null additions/deletions + empty = new Change(); + response = LocalDnsHelper.checkChange(empty, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "The 'entity.change' parameter is required but was missing.")); + // non-matching name + validA.setName(ZONE1.getDnsName() + ".aaa."); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].name")); + // wrong type + validA.setName(ZONE1.getDnsName()); // revert + validA.setType("ABCD"); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].type")); + // wrong ttl + validA.setType("A"); // revert + validA.setTtl(-1); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].ttl")); + validA.setTtl(null); + // null name + validA.setName(null); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].name")); + validA.setName(ZONE1.getDnsName()); + // null type + validA.setType(null); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].type")); + validA.setType("A"); + // null rrdata + final List temp = validA.getRrdatas(); // preserve + validA.setRrdatas(null); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].rrdata")); + validA.setRrdatas(temp); + // delete non-existent + ResourceRecordSet nonExistent = new ResourceRecordSet(); + nonExistent.setName(ZONE1.getDnsName()); + nonExistent.setType("AAAA"); + nonExistent.setRrdatas(ImmutableList.of("0:0:0:0:5::6")); + Change delete = new Change(); + delete.setDeletions(ImmutableList.of(nonExistent)); + response = LocalDnsHelper.checkChange(delete, zoneContainer); + assertEquals(404, response.code()); + assertTrue(response.body().contains("deletions[0]")); + } + + @Test + public void testCheckAdditionsDeletions() { + ResourceRecordSet validA = new ResourceRecordSet(); + validA.setName(ZONE1.getDnsName()); + validA.setType("A"); + validA.setRrdatas(ImmutableList.of("0.255.1.5")); + Change validChange = new Change(); + validChange.setAdditions(ImmutableList.of(validA)); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange); + LocalDnsHelper.ZoneContainer container = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1); + LocalDnsHelper.Response response = + LocalDnsHelper.checkAdditionsDeletions(ImmutableList.of(validA), null, container); + assertEquals(409, response.code()); + assertTrue(response.body().contains("already exists")); + } + + @Test + public void testCreateChangeContentValidation() { + ResourceRecordSet validA = new ResourceRecordSet(); + validA.setName(ZONE1.getDnsName()); + validA.setType("A"); + validA.setRrdatas(ImmutableList.of("0.255.1.5")); + Change validChange = new Change(); + validChange.setAdditions(ImmutableList.of(validA)); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange); + LocalDnsHelper.Response response = + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange); + assertEquals(409, response.code()); + assertTrue(response.body().contains("already exists")); + // delete with field mismatch + Change delete = new Change(); + validA.setTtl(20); // mismatch + delete.setDeletions(ImmutableList.of(validA)); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete); + assertEquals(412, response.code()); + assertTrue(response.body().contains("entity.change.deletions[0]")); + // delete and add SOA + Change addition = new Change(); + ImmutableCollection dnsRecords = + LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get().values(); + LinkedList deletions = new LinkedList<>(); + LinkedList additions = new LinkedList<>(); + for (ResourceRecordSet rrset : dnsRecords) { + if (rrset.getType().equals("SOA")) { + deletions.add(rrset); + ResourceRecordSet copy = copyRrset(rrset); + copy.setName("x." + copy.getName()); + additions.add(copy); + break; + } + } + delete.setDeletions(deletions); + addition.setAdditions(additions); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "zone must contain exactly one resource record set of type 'SOA' at the apex")); + assertTrue(response.body().contains("deletions[0]")); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "zone must contain exactly one resource record set of type 'SOA' at the apex")); + assertTrue(response.body().contains("additions[0]")); + // delete NS + deletions = new LinkedList<>(); + additions = new LinkedList<>(); + for (ResourceRecordSet rrset : dnsRecords) { + if (rrset.getType().equals("NS")) { + deletions.add(rrset); + ResourceRecordSet copy = copyRrset(rrset); + copy.setName("x." + copy.getName()); + additions.add(copy); + break; + } + } + delete.setDeletions(deletions); + addition.setAdditions(additions); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "zone must contain exactly one resource record set of type 'NS' at the apex")); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "zone must contain exactly one resource record set of type 'NS' at the apex")); + assertTrue(response.body().contains("additions[0]")); + // change (delete + add) + addition.setDeletions(deletions); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition); + assertEquals(200, response.code()); + } + + @Test + public void testMatchesCriteria() { + assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), RRSET1.getType())); + assertFalse(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), "anothertype")); + assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, null, RRSET1.getType())); + assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), null)); + assertFalse(LocalDnsHelper.matchesCriteria(RRSET1, "anothername", RRSET1.getType())); + } + + @Test + public void testGetUniqueId() { + assertNotNull(LocalDnsHelper.getUniqueId(new HashSet())); + } + + @Test + public void testRandomNameServers() { + assertEquals(4, LocalDnsHelper.randomNameservers().size()); + } + + private static ManagedZone copyZone(ManagedZone original) { + ManagedZone copy = new ManagedZone(); + copy.setDescription(original.getDescription()); + copy.setName(original.getName()); + copy.setCreationTime(original.getCreationTime()); + copy.setId(original.getId()); + copy.setNameServerSet(original.getNameServerSet()); + copy.setDnsName(original.getDnsName()); + if (original.getNameServers() != null) { + copy.setNameServers(ImmutableList.copyOf(original.getNameServers())); + } + return copy; + } + + private static ResourceRecordSet copyRrset(ResourceRecordSet set) { + ResourceRecordSet copy = new ResourceRecordSet(); + if (set.getRrdatas() != null) { + copy.setRrdatas(ImmutableList.copyOf(set.getRrdatas())); + } + copy.setTtl(set.getTtl()); + copy.setName(set.getName()); + copy.setType(set.getType()); + return copy; + } +} diff --git a/gcloud-java-examples/README.md b/gcloud-java-examples/README.md index 7d54b8f3e1a9..8084cee562f0 100644 --- a/gcloud-java-examples/README.md +++ b/gcloud-java-examples/README.md @@ -63,7 +63,7 @@ To run examples from your command line: ``` * Here's an example run of `DatastoreExample`. - + Be sure to change the placeholder project ID "your-project-id" with your own project ID. Also note that you have to enable the Google Cloud Datastore API on the [Google Developers Console][developers-console] before running the following commands. ``` mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.datastore.DatastoreExample" -Dexec.args="your-project-id my_name add my\ comment" @@ -71,6 +71,22 @@ To run examples from your command line: mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.datastore.DatastoreExample" -Dexec.args="your-project-id my_name delete" ``` + * Here's an example run of `DnsExample`. + + Note that you have to enable the Google Cloud DNS API on the [Google Developers Console][developers-console] before running the following commands. + You will need to replace the domain name `elaborateexample.com` with your own domain name with [verified ownership] (https://www.google.com/webmasters/verification/home). + Also, note that the example creates and deletes record sets of type A only. Operations with other record types are not implemented in the example. + ``` + mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.dns.DnsExample" -Dexec.args="create some-sample-zone elaborateexample.com. description" + mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.dns.DnsExample" -Dexec.args="list" + mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.dns.DnsExample" -Dexec.args="list some-sample-zone records" + mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.dns.DnsExample" -Dexec.args="add-record some-sample-zone www.elaborateexample.com. 12.13.14.15 69" + mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.dns.DnsExample" -Dexec.args="get some-sample-zone" + mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.dns.DnsExample" -Dexec.args="delete-record some-sample-zone www.elaborateexample.com. 12.13.14.15 69" + mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.dns.DnsExample" -Dexec.args="list some-sample-zone changes ascending" + mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.dns.DnsExample" -Dexec.args="delete some-sample-zone" + ``` + * Here's an example run of `ResourceManagerExample`. Be sure to change the placeholder project ID "your-project-id" with your own globally unique project ID. diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/DnsExample.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/DnsExample.java new file mode 100644 index 000000000000..a9e5c5d25377 --- /dev/null +++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/DnsExample.java @@ -0,0 +1,525 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud.examples.dns; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.gcloud.dns.ChangeRequest; +import com.google.gcloud.dns.ChangeRequestInfo; +import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsOptions; +import com.google.gcloud.dns.ProjectInfo; +import com.google.gcloud.dns.RecordSet; +import com.google.gcloud.dns.Zone; +import com.google.gcloud.dns.ZoneInfo; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * An example of using Google Cloud DNS. + * + *

This example creates, deletes, gets, and lists zones. It also creates and deletes + * record sets of type A, and lists record sets. + * + *

Steps needed for running the example: + *

    + *
  1. login using gcloud SDK - {@code gcloud auth login}.
  2. + *
  3. compile using maven - {@code mvn compile}
  4. + *
  5. run using maven - {@code mvn exec:java + * -Dexec.mainClass="com.google.gcloud.examples.dns.DnsExample" + * -Dexec.args="[] + * create | + * get | + * delete | + * list [ [changes [descending | ascending] | records]] | + * add-record | + * delete-record [] | + * quota}
  6. + *
+ * + *

The first parameter is an optional {@code project_id}. The project specified in the Google + * Cloud SDK configuration (see {@code gcloud config list}) will be used if the project ID is not + * supplied. The second parameter is a DNS operation (list, delete, create, ...). The remaining + * arguments are specific to the operation. See each action's {@code run} method for the specific + * arguments. + */ +public class DnsExample { + + private static final Map ACTIONS = new HashMap<>(); + private static final DateFormat FORMATTER = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"); + + private interface DnsAction { + void run(Dns dns, String... args); + + String params(); + + boolean check(String... args); + } + + private static class CreateZoneAction implements DnsAction { + + /** + * Creates a zone with the provided name, DNS name and description (in this order). + */ + @Override + public void run(Dns dns, String... args) { + String zoneName = args[0]; + String dnsName = args[1]; + String description = args[2]; + ZoneInfo zoneInfo = ZoneInfo.of(zoneName, dnsName, description); + Zone zone = dns.create(zoneInfo); + System.out.printf("Successfully created zone with name %s which was assigned ID %s.%n", + zone.name(), zone.id()); + } + + @Override + public String params() { + return " "; + } + + @Override + public boolean check(String... args) { + return args.length == 3; + } + } + + private static class ListZonesAction implements DnsAction { + + /** + * Lists all zones within the project. + */ + @Override + public void run(Dns dns, String... args) { + Iterator zoneIterator = dns.listZones().iterateAll(); + if (zoneIterator.hasNext()) { + System.out.println("The project contains the following zones:"); + while (zoneIterator.hasNext()) { + printZone(zoneIterator.next()); + } + } else { + System.out.println("Project contains no zones."); + } + } + + @Override + public String params() { + return ""; + } + + @Override + public boolean check(String... args) { + return args.length == 0; + } + } + + private static class GetZoneAction implements DnsAction { + + /** + * Gets details about a zone with the given name. + */ + @Override + public void run(Dns dns, String... args) { + String zoneName = args[0]; + Zone zone = dns.getZone(zoneName); + if (zone == null) { + System.out.printf("No zone with name '%s' exists.%n", zoneName); + } else { + printZone(zone); + } + } + + @Override + public String params() { + return ""; + } + + @Override + public boolean check(String... args) { + return args.length == 1; + } + } + + private static class DeleteZoneAction implements DnsAction { + + /** + * Deletes a zone with the given name. + */ + @Override + public void run(Dns dns, String... args) { + String zoneName = args[0]; + boolean deleted = dns.delete(zoneName); + if (deleted) { + System.out.printf("Zone %s was deleted.%n", zoneName); + } else { + System.out.printf("Zone %s was NOT deleted. It does not exist.%n", zoneName); + } + } + + @Override + public String params() { + return ""; + } + + @Override + public boolean check(String... args) { + return args.length == 1; + } + + } + + private static class DeleteDnsRecordAction implements DnsAction { + + /** + * Deletes a DNS record of type A from the given zone. The last parameter is ttl and it is not + * required. If ttl is not provided, a default value of 0 is used. The service requires a + * precise match (including ttl) for deleting a record. + */ + @Override + public void run(Dns dns, String... args) { + String zoneName = args[0]; + String recordName = args[1]; + String ip = args[2]; + int ttl = 0; + if (args.length > 3) { + ttl = Integer.parseInt(args[3]); + } + RecordSet recordSet = RecordSet.builder(recordName, RecordSet.Type.A) + .records(ImmutableList.of(ip)) + .ttl(ttl, TimeUnit.SECONDS) + .build(); + ChangeRequestInfo changeRequest = ChangeRequest.builder() + .delete(recordSet) + .build(); + changeRequest = dns.applyChangeRequest(zoneName, changeRequest); + System.out.printf("The request for deleting A record %s for zone %s was successfully " + + "submitted and assigned ID %s.%n", recordName, zoneName, changeRequest.id()); + System.out.print("Waiting for deletion to happen..."); + waitForChangeToFinish(dns, zoneName, changeRequest); + System.out.printf("%nThe deletion has been completed.%n"); + } + + @Override + public String params() { + return " []"; + } + + @Override + public boolean check(String... args) { + if (args.length == 4) { + // to check that it can be parsed + Integer.parseInt(args[3]); + return true; + } else { + return args.length == 3; + } + } + } + + private static class AddDnsRecordAction implements DnsAction { + + /** + * Adds a record set of type A. The last parameter is ttl and is not required. If ttl is not + * provided, a default value of 0 will be used. + */ + @Override + public void run(Dns dns, String... args) { + String zoneName = args[0]; + String recordName = args[1]; + String ip = args[2]; + int ttl = 0; + if (args.length > 3) { + ttl = Integer.parseInt(args[3]); + } + RecordSet recordSet = RecordSet.builder(recordName, RecordSet.Type.A) + .records(ImmutableList.of(ip)) + .ttl(ttl, TimeUnit.SECONDS) + .build(); + ChangeRequestInfo changeRequest = ChangeRequest.builder().add(recordSet).build(); + changeRequest = dns.applyChangeRequest(zoneName, changeRequest); + System.out.printf("The request for adding A record %s for zone %s was successfully " + + "submitted and assigned ID %s.%n", recordName, zoneName, changeRequest.id()); + System.out.print("Waiting for addition to happen..."); + waitForChangeToFinish(dns, zoneName, changeRequest); + System.out.printf("The addition has been completed.%n"); + } + + @Override + public String params() { + return " []"; + } + + @Override + public boolean check(String... args) { + if (args.length == 4) { + // to check that it can be parsed + Integer.parseInt(args[3]); + return true; + } else { + return args.length == 3; + } + } + } + + private static class ListDnsRecordsAction implements DnsAction { + + /** + * Lists all the record sets in the given zone. + */ + @Override + public void run(Dns dns, String... args) { + String zoneName = args[0]; + Iterator iterator = dns.listRecordSets(zoneName).iterateAll(); + if (iterator.hasNext()) { + System.out.printf("Record sets for zone %s:%n", zoneName); + while (iterator.hasNext()) { + RecordSet recordSet = iterator.next(); + System.out.printf("%nRecord name: %s%nTTL: %s%nRecords: %s%n", recordSet.name(), + recordSet.ttl(), Joiner.on(", ").join(recordSet.records())); + } + } else { + System.out.printf("Zone %s has no record sets records.%n", zoneName); + } + } + + @Override + public String params() { + return " records"; + } + + @Override + public boolean check(String... args) { + return args.length == 2; + } + } + + private static class ListChangesAction implements DnsAction { + + /** + * Lists all the changes for a given zone. Optionally, an order ("descending" or "ascending") + * can be specified using the last parameter. + */ + @Override + public void run(Dns dns, String... args) { + String zoneName = args[0]; + Iterator iterator; + if (args.length > 2) { + Dns.SortingOrder sortOrder = Dns.SortingOrder.valueOf(args[2].toUpperCase()); + iterator = dns.listChangeRequests(zoneName, + Dns.ChangeRequestListOption.sortOrder(sortOrder)).iterateAll(); + } else { + iterator = dns.listChangeRequests(zoneName).iterateAll(); + } + if (iterator.hasNext()) { + System.out.printf("Change requests for zone %s:%n", zoneName); + while (iterator.hasNext()) { + ChangeRequest change = iterator.next(); + System.out.printf("%nID: %s%n", change.id()); + System.out.printf("Status: %s%n", change.status()); + System.out.printf("Started: %s%n", FORMATTER.format(change.startTimeMillis())); + System.out.printf("Deletions: %s%n", Joiner.on(", ").join(change.deletions())); + System.out.printf("Additions: %s%n", Joiner.on(", ").join(change.additions())); + } + } else { + System.out.printf("Zone %s has no change requests.%n", zoneName); + } + } + + @Override + public String params() { + return " changes [descending | ascending]"; + } + + @Override + public boolean check(String... args) { + return args.length == 2 + || (args.length == 3 + && ImmutableList.of("descending", "ascending").contains(args[2].toLowerCase())); + } + } + + private static class ListAction implements DnsAction { + + /** + * Invokes a list action. If no parameter is provided, lists all zones. If zone name is the only + * parameter provided, lists both record sets and changes. Otherwise, invokes listing + * changes or zones based on the parameter provided. + */ + @Override + public void run(Dns dns, String... args) { + if (args.length == 0) { + new ListZonesAction().run(dns); + } else { + if (args.length == 1 || "records".equals(args[1])) { + new ListDnsRecordsAction().run(dns, args); + } + if (args.length == 1 || "changes".equals(args[1])) { + new ListChangesAction().run(dns, args); + } + } + } + + @Override + public boolean check(String... args) { + if (args.length == 0 || args.length == 1) { + return true; + } + if ("records".equals(args[1])) { + return new ListDnsRecordsAction().check(args); + } + if ("changes".equals(args[1])) { + return new ListChangesAction().check(args); + } + return false; + } + + @Override + public String params() { + return "[ [changes [descending | ascending] | records]]"; + } + } + + private static class GetProjectAction implements DnsAction { + + @Override + public void run(Dns dns, String... args) { + ProjectInfo project = dns.getProject(); + ProjectInfo.Quota quota = project.quota(); + System.out.printf("Project id: %s%nQuota:%n", dns.options().projectId()); + System.out.printf("\tZones: %d%n", quota.zones()); + System.out.printf("\tRecord sets per zone: %d%n", quota.rrsetsPerZone()); + System.out.printf("\tRecord sets per DNS record: %d%n", + quota.resourceRecordsPerRrset()); + System.out.printf("\tAdditions per change: %d%n", quota.rrsetAdditionsPerChange()); + System.out.printf("\tDeletions per change: %d%n", quota.rrsetDeletionsPerChange()); + System.out.printf("\tTotal data size per change: %d%n", + quota.totalRrdataSizePerChange()); + } + + @Override + public String params() { + return ""; + } + + @Override + public boolean check(String... args) { + return args.length == 0; + } + } + + static { + ACTIONS.put("create", new CreateZoneAction()); + ACTIONS.put("delete", new DeleteZoneAction()); + ACTIONS.put("get", new GetZoneAction()); + ACTIONS.put("list", new ListAction()); + ACTIONS.put("add-record", new AddDnsRecordAction()); + ACTIONS.put("delete-record", new DeleteDnsRecordAction()); + ACTIONS.put("quota", new GetProjectAction()); + } + + private static void printZone(Zone zone) { + System.out.printf("%nName: %s%n", zone.name()); + System.out.printf("ID: %s%n", zone.id()); + System.out.printf("Description: %s%n", zone.description()); + System.out.printf("Created: %s%n", FORMATTER.format(new Date(zone.creationTimeMillis()))); + System.out.printf("Name servers: %s%n", Joiner.on(", ").join(zone.nameServers())); + } + + private static ChangeRequestInfo waitForChangeToFinish(Dns dns, String zoneName, + ChangeRequestInfo request) { + ChangeRequestInfo current = request; + while (current.status().equals(ChangeRequest.Status.PENDING)) { + System.out.print("."); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + System.err.println("Thread was interrupted while waiting."); + } + current = dns.getChangeRequest(zoneName, current.id()); + } + return current; + } + + private static void printUsage() { + StringBuilder actionAndParams = new StringBuilder(); + for (Map.Entry entry : ACTIONS.entrySet()) { + actionAndParams.append(System.lineSeparator()).append('\t').append(entry.getKey()); + String param = entry.getValue().params(); + if (param != null && !param.isEmpty()) { + actionAndParams.append(' ').append(param); + } + } + System.out.printf("Usage: %s [] operation *%s%n", + DnsExample.class.getSimpleName(), actionAndParams); + } + + public static void main(String... args) throws Exception { + if (args.length < 1) { + System.out.println("Missing required action"); + printUsage(); + return; + } + String projectId = null; + DnsAction action; + String actionName; + if (args.length >= 2 && !ACTIONS.containsKey(args[0])) { + actionName = args[1]; + projectId = args[0]; + args = Arrays.copyOfRange(args, 2, args.length); + } else { + actionName = args[0]; + args = Arrays.copyOfRange(args, 1, args.length); + } + action = ACTIONS.get(actionName); + if (action == null) { + System.out.printf("Unrecognized action %s.%n", actionName); + printUsage(); + return; + } + boolean valid = false; + try { + valid = action.check(args); + } catch (NumberFormatException ex) { + System.out.println("Invalid input for action '" + actionName + "'."); + System.out.println("Ttl must be an integer."); + System.out.println("Expected: " + action.params()); + return; + } catch (Exception ex) { + System.out.println("Failed to parse request."); + System.out.println("Expected: " + action.params()); + ex.printStackTrace(); + return; + } + if (valid) { + DnsOptions.Builder optionsBuilder = DnsOptions.builder(); + if (projectId != null) { + optionsBuilder.projectId(projectId); + } + Dns dns = optionsBuilder.build().service(); + action.run(dns, args); + } else { + System.out.println("Invalid input for action '" + actionName + "'"); + System.out.println("Expected: " + action.params()); + } + } +} diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateOrUpdateRecordSets.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateOrUpdateRecordSets.java new file mode 100644 index 000000000000..e3ddbb10fc0f --- /dev/null +++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateOrUpdateRecordSets.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * EDITING INSTRUCTIONS + * This file is referenced in README's and javadoc. Any change to this file should be reflected in + * the project's README's and package-info.java. + */ + +package com.google.gcloud.examples.dns.snippets; + +import com.google.gcloud.dns.ChangeRequestInfo; +import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsOptions; +import com.google.gcloud.dns.RecordSet; +import com.google.gcloud.dns.Zone; + +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +/** + * A snippet for Google Cloud DNS showing how to create and update a resource record set. + */ +public class CreateOrUpdateRecordSets { + + public static void main(String... args) { + // Create a service object. + // The project ID and credentials will be inferred from the environment. + Dns dns = DnsOptions.defaultInstance().service(); + + // Change this to a zone name that exists within your project + String zoneName = "my-unique-zone"; + + // Get zone from the service + Zone zone = dns.getZone(zoneName); + + // Prepare a www.. type A record set with ttl of 24 hours + String ip = "12.13.14.15"; + RecordSet toCreate = RecordSet.builder("www." + zone.dnsName(), RecordSet.Type.A) + .ttl(24, TimeUnit.HOURS) + .addRecord(ip) + .build(); + + // Make a change + ChangeRequestInfo.Builder changeBuilder = ChangeRequestInfo.builder().add(toCreate); + + // Verify a www.. type A record does not exist yet. + // If it does exist, we will overwrite it with our prepared record. + Iterator recordSetIterator = zone.listRecordSets().iterateAll(); + while (recordSetIterator.hasNext()) { + RecordSet current = recordSetIterator.next(); + if (toCreate.name().equals(current.name()) && toCreate.type().equals(current.type())) { + changeBuilder.delete(current); + } + } + + // Build and apply the change request to our zone + ChangeRequestInfo changeRequest = changeBuilder.build(); + zone.applyChangeRequest(changeRequest); + } +} diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateZone.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateZone.java new file mode 100644 index 000000000000..2c2ba211bd86 --- /dev/null +++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/CreateZone.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * EDITING INSTRUCTIONS + * This file is referenced in README's and javadoc. Any change to this file should be reflected in + * the project's README's and package-info.java. + */ + +package com.google.gcloud.examples.dns.snippets; + +import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsOptions; +import com.google.gcloud.dns.Zone; +import com.google.gcloud.dns.ZoneInfo; + +/** + * A snippet for Google Cloud DNS showing how to create a zone. You will need to change the {@code + * domainName} to a domain name, the ownership of which you should verify with Google. + */ +public class CreateZone { + + public static void main(String... args) { + // Create a service object + // The project ID and credentials will be inferred from the environment. + Dns dns = DnsOptions.defaultInstance().service(); + + // Create a zone metadata object + String zoneName = "my-unique-zone"; // Change this zone name which is unique within your project + String domainName = "someexampledomain.com."; // Change this to a domain which you own + String description = "This is a gcloud-java-dns sample zone."; + ZoneInfo zoneInfo = ZoneInfo.of(zoneName, domainName, description); + + // Create zone in Google Cloud DNS + Zone zone = dns.create(zoneInfo); + System.out.printf("Zone was created and assigned ID %s.%n", zone.id()); + } +} diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/DeleteZone.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/DeleteZone.java new file mode 100644 index 000000000000..63f26eeebb2a --- /dev/null +++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/DeleteZone.java @@ -0,0 +1,88 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * EDITING INSTRUCTIONS + * This file is referenced in README's and javadoc. Any change to this file should be reflected in + * the project's README's and package-info.java. + */ + +package com.google.gcloud.examples.dns.snippets; + +import com.google.gcloud.dns.ChangeRequestInfo; +import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsOptions; +import com.google.gcloud.dns.RecordSet; + +import java.util.Iterator; + +/** + * A snippet for Google Cloud DNS showing how to delete a zone. It also shows how to list and delete + * DNS records. + */ +public class DeleteZone { + + public static void main(String... args) { + // Create a service object. + // The project ID and credentials will be inferred from the environment. + Dns dns = DnsOptions.defaultInstance().service(); + + // Change this to a zone name that exists within your project and that you want to delete. + String zoneName = "my-unique-zone"; + + // Get iterator for the existing record sets which have to be deleted before deleting the zone + Iterator recordIterator = dns.listRecordSets(zoneName).iterateAll(); + + // Make a change for deleting the records + ChangeRequestInfo.Builder changeBuilder = ChangeRequestInfo.builder(); + while (recordIterator.hasNext()) { + RecordSet current = recordIterator.next(); + // SOA and NS records cannot be deleted + if (!RecordSet.Type.SOA.equals(current.type()) && !RecordSet.Type.NS.equals(current.type())) { + changeBuilder.delete(current); + } + } + + // Build and apply the change request to our zone if it contains records to delete + ChangeRequestInfo changeRequest = changeBuilder.build(); + if (!changeRequest.deletions().isEmpty()) { + changeRequest = dns.applyChangeRequest(zoneName, changeRequest); + + // Wait for change to finish, but save data traffic by transferring only ID and status + Dns.ChangeRequestOption option = + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.STATUS); + while (ChangeRequestInfo.Status.PENDING.equals(changeRequest.status())) { + System.out.println("Waiting for change to complete. Going to sleep for 500ms..."); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + System.err.println("The thread was interrupted while waiting for change request to be " + + "processed."); + } + // Update the change, but fetch only change ID and status + changeRequest = dns.getChangeRequest(zoneName, changeRequest.id(), option); + } + } + + // Delete the zone + boolean result = dns.delete(zoneName); + if (result) { + System.out.println("Zone was deleted."); + } else { + System.out.println("Zone was not deleted because it does not exist."); + } + } +} diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/ManipulateZonesAndRecordSets.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/ManipulateZonesAndRecordSets.java new file mode 100644 index 000000000000..9c9a9e77289c --- /dev/null +++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/dns/snippets/ManipulateZonesAndRecordSets.java @@ -0,0 +1,158 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * EDITING INSTRUCTIONS + * This file is referenced in README's and javadoc. Any change to this file should be reflected in + * the project's README's and package-info.java. + */ + +package com.google.gcloud.examples.dns.snippets; + +import com.google.gcloud.dns.ChangeRequest; +import com.google.gcloud.dns.ChangeRequestInfo; +import com.google.gcloud.dns.Dns; +import com.google.gcloud.dns.DnsOptions; +import com.google.gcloud.dns.RecordSet; +import com.google.gcloud.dns.Zone; +import com.google.gcloud.dns.ZoneInfo; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * A complete snippet for Google Cloud DNS showing how to create and delete a zone. It also shows + * how to create, list and delete record sets, and how to list changes. + */ +public class ManipulateZonesAndRecordSets { + + public static void main(String... args) { + Dns dns = DnsOptions.defaultInstance().service(); + + // Create a zone metadata object + String zoneName = "my-unique-zone"; // Change this zone name which is unique within your project + String domainName = "someexampledomain.com."; // Change this to a domain which you own + String description = "This is a gcloud-java-dns sample zone."; + ZoneInfo zoneInfo = ZoneInfo.of(zoneName, domainName, description); + + // Create zone in Google Cloud DNS + Zone zone = dns.create(zoneInfo); + System.out.printf("Zone was created and assigned ID %s.%n", zone.id()); + + // Print assigned name servers + List nameServers = zone.nameServers(); + for (String nameServer : nameServers) { + System.out.println(nameServer); + } + + // Prepare a www.someexampledomain.com. type A record with ttl of 24 hours + String ip = "12.13.14.15"; + RecordSet toCreate = RecordSet.builder("www.someexampledomain.com.", RecordSet.Type.A) + .ttl(24, TimeUnit.HOURS) + .addRecord(ip) + .build(); + + // Make a change + ChangeRequestInfo.Builder changeBuilder = ChangeRequestInfo.builder().add(toCreate); + + // Verify the type A record does not exist yet. + // If it does exist, we will overwrite it with our prepared record. + Iterator recordSetIterator = zone.listRecordSets().iterateAll(); + while (recordSetIterator.hasNext()) { + RecordSet current = recordSetIterator.next(); + if (toCreate.name().equals(current.name()) && toCreate.type().equals(current.type())) { + changeBuilder.delete(current); + } + } + + // Build and apply the change request to our zone + ChangeRequestInfo changeRequest = changeBuilder.build(); + zone.applyChangeRequest(changeRequest); + + while (ChangeRequestInfo.Status.PENDING.equals(changeRequest.status())) { + try { + Thread.sleep(500L); + } catch (InterruptedException e) { + System.err.println("The thread was interrupted while waiting..."); + } + changeRequest = dns.getChangeRequest(zone.name(), changeRequest.id()); + } + System.out.println("The change request has been applied."); + + // List all your zones + Iterator zoneIterator = dns.listZones().iterateAll(); + int counter = 1; + while (zoneIterator.hasNext()) { + System.out.printf("#%d.: %s%n%n", counter, zoneIterator.next()); + counter++; + } + + // List the record sets in a particular zone + recordSetIterator = zone.listRecordSets().iterateAll(); + System.out.println(String.format("Record sets inside %s:", zone.name())); + while (recordSetIterator.hasNext()) { + System.out.println(recordSetIterator.next()); + } + + // List the change requests applied to a particular zone + Iterator changeIterator = zone.listChangeRequests().iterateAll(); + System.out.println(String.format("The history of changes in %s:", zone.name())); + while (changeIterator.hasNext()) { + System.out.println(changeIterator.next()); + } + + // Make a change for deleting the record sets + changeBuilder = ChangeRequestInfo.builder(); + while (recordSetIterator.hasNext()) { + RecordSet current = recordSetIterator.next(); + // SOA and NS records cannot be deleted + if (!RecordSet.Type.SOA.equals(current.type()) && !RecordSet.Type.NS.equals(current.type())) { + changeBuilder.delete(current); + } + } + + // Build and apply the change request to our zone if it contains records to delete + changeRequest = changeBuilder.build(); + if (!changeRequest.deletions().isEmpty()) { + changeRequest = dns.applyChangeRequest(zoneName, changeRequest); + + // Wait for change to finish, but save data traffic by transferring only ID and status + Dns.ChangeRequestOption option = + Dns.ChangeRequestOption.fields(Dns.ChangeRequestField.STATUS); + while (ChangeRequest.Status.PENDING.equals(changeRequest.status())) { + System.out.println("Waiting for change to complete. Going to sleep for 500ms..."); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + System.err.println("The thread was interrupted while waiting for change request to be " + + "processed."); + } + + // Update the change, but fetch only change ID and status + changeRequest = dns.getChangeRequest(zoneName, changeRequest.id(), option); + } + } + + // Delete the zone + boolean result = dns.delete(zoneName); + if (result) { + System.out.println("Zone was deleted."); + } else { + System.out.println("Zone was not deleted because it does not exist."); + } + } +} diff --git a/gcloud-java/pom.xml b/gcloud-java/pom.xml index 19536faa8c8d..654b34f92056 100644 --- a/gcloud-java/pom.xml +++ b/gcloud-java/pom.xml @@ -28,6 +28,11 @@ gcloud-java-datastore ${project.version} + + ${project.groupId} + gcloud-java-dns + ${project.version} + ${project.groupId} gcloud-java-resourcemanager diff --git a/pom.xml b/pom.xml index 1abfb094ffb8..d73956f506ee 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,7 @@ gcloud-java-contrib gcloud-java-core gcloud-java-datastore + gcloud-java-dns gcloud-java-examples gcloud-java-resourcemanager gcloud-java-storage