1 /* 2 * Copyright 2016 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.cloud.dns; 18 19 import static java.net.HttpURLConnection.HTTP_NOT_FOUND; 20 21 import com.google.api.client.googleapis.json.GoogleJsonError; 22 import com.google.api.gax.paging.Page; 23 import com.google.api.services.dns.model.Change; 24 import com.google.api.services.dns.model.ChangesListResponse; 25 import com.google.api.services.dns.model.ManagedZone; 26 import com.google.api.services.dns.model.ManagedZonesListResponse; 27 import com.google.api.services.dns.model.Project; 28 import com.google.api.services.dns.model.ResourceRecordSet; 29 import com.google.api.services.dns.model.ResourceRecordSetsListResponse; 30 import com.google.cloud.PageImpl; 31 import com.google.cloud.dns.spi.v1.DnsRpc; 32 import com.google.cloud.dns.spi.v1.RpcBatch; 33 import com.google.common.annotations.VisibleForTesting; 34 import com.google.common.collect.ImmutableList; 35 import com.google.common.collect.Iterables; 36 import java.util.List; 37 import java.util.Map; 38 39 /** A batch of operations to be submitted to Google Cloud DNS using a single RPC request. */ 40 public class DnsBatch { 41 42 private final RpcBatch batch; 43 private final DnsRpc dnsRpc; 44 private final DnsOptions options; 45 DnsBatch(DnsOptions options)46 DnsBatch(DnsOptions options) { 47 this.options = options; 48 this.dnsRpc = options.getDnsRpcV1(); 49 this.batch = dnsRpc.createBatch(); 50 } 51 52 @VisibleForTesting getBatch()53 Object getBatch() { 54 return batch; 55 } 56 57 @VisibleForTesting getDnsRpc()58 DnsRpc getDnsRpc() { 59 return dnsRpc; 60 } 61 62 @VisibleForTesting getOptions()63 DnsOptions getOptions() { 64 return options; 65 } 66 67 /** 68 * Adds a request representing the "list zones" operation to this batch. The {@code options} can 69 * be used to restrict the fields returned or provide page size limits in the same way as for 70 * {@link Dns#listZones(Dns.ZoneListOption...)}. Calling {@link DnsBatchResult#get()} on the 71 * return value yields a page of zones if successful and throws a {@link DnsException} otherwise. 72 */ listZones(Dns.ZoneListOption... options)73 public DnsBatchResult<Page<Zone>> listZones(Dns.ZoneListOption... options) { 74 DnsBatchResult<Page<Zone>> result = new DnsBatchResult<>(); 75 Map<DnsRpc.Option, ?> optionMap = DnsImpl.optionMap(options); 76 RpcBatch.Callback<ManagedZonesListResponse> callback = 77 createListZonesCallback(result, optionMap); 78 batch.addListZones(callback, optionMap); 79 return result; 80 } 81 82 /** 83 * Adds a request representing the "create zone" operation to this batch. The {@code options} can 84 * be used to restrict the fields returned in the same way as for {@link Dns#create(ZoneInfo, 85 * Dns.ZoneOption...)}. Calling {@link DnsBatchResult#get()} on the return value yields the 86 * created {@link Zone} if successful and throws a {@link DnsException} otherwise. 87 */ createZone(ZoneInfo zone, Dns.ZoneOption... options)88 public DnsBatchResult<Zone> createZone(ZoneInfo zone, Dns.ZoneOption... options) { 89 DnsBatchResult<Zone> result = new DnsBatchResult<>(); 90 // todo this can cause misleading report of a failure, intended to be fixed within #924 91 RpcBatch.Callback<ManagedZone> callback = createZoneCallback(this.options, result, false, true); 92 Map<DnsRpc.Option, ?> optionMap = DnsImpl.optionMap(options); 93 batch.addCreateZone(zone.toPb(), callback, optionMap); 94 return result; 95 } 96 97 /** 98 * Adds a request representing the "delete zone" operation to this batch. Calling {@link 99 * DnsBatchResult#get()} on the return value yields {@code true} upon successful deletion, {@code 100 * false} if the zone was not found, or throws a {@link DnsException} if the operation failed. 101 */ deleteZone(String zoneName)102 public DnsBatchResult<Boolean> deleteZone(String zoneName) { 103 DnsBatchResult<Boolean> result = new DnsBatchResult<>(); 104 RpcBatch.Callback<Void> callback = createDeleteZoneCallback(result); 105 batch.addDeleteZone(zoneName, callback); 106 return result; 107 } 108 109 /** 110 * Adds a request representing the "get zone" operation to this batch. The {@code options} can be 111 * used to restrict the fields returned in the same way as for {@link Dns#getZone(String, 112 * Dns.ZoneOption...)}. Calling {@link DnsBatchResult#get()} on the return value yields the 113 * requested {@link Zone} if successful, {@code null} if no such zone exists, or throws a {@link 114 * DnsException} if the operation failed. 115 */ getZone(String zoneName, Dns.ZoneOption... options)116 public DnsBatchResult<Zone> getZone(String zoneName, Dns.ZoneOption... options) { 117 DnsBatchResult<Zone> result = new DnsBatchResult<>(); 118 RpcBatch.Callback<ManagedZone> callback = createZoneCallback(this.options, result, true, true); 119 Map<DnsRpc.Option, ?> optionMap = DnsImpl.optionMap(options); 120 batch.addGetZone(zoneName, callback, optionMap); 121 return result; 122 } 123 124 /** 125 * Adds a request representing the "get project" operation to this batch. The {@code options} can 126 * be used to restrict the fields returned in the same way as for {@link 127 * Dns#getProject(Dns.ProjectOption...)}. Calling {@link DnsBatchResult#get()} on the return value 128 * yields the created {@link ProjectInfo} if successful and throws a {@link DnsException} if the 129 * operation failed. 130 */ getProject(Dns.ProjectOption... options)131 public DnsBatchResult<ProjectInfo> getProject(Dns.ProjectOption... options) { 132 DnsBatchResult<ProjectInfo> result = new DnsBatchResult<>(); 133 RpcBatch.Callback<Project> callback = createProjectCallback(result); 134 Map<DnsRpc.Option, ?> optionMap = DnsImpl.optionMap(options); 135 batch.addGetProject(callback, optionMap); 136 return result; 137 } 138 139 /** 140 * Adds a request representing the "list record sets" operation in the zone specified by {@code 141 * zoneName} to this batch. The {@code options} can be used to restrict the fields returned or 142 * provide page size limits in the same way as for {@link Dns#listRecordSets(String, 143 * Dns.RecordSetListOption...)}. Calling {@link DnsBatchResult#get()} on the return value yields a 144 * page of record sets if successful and throws a {@link DnsException} if the operation failed or 145 * the zone does not exist. 146 */ listRecordSets( String zoneName, Dns.RecordSetListOption... options)147 public DnsBatchResult<Page<RecordSet>> listRecordSets( 148 String zoneName, Dns.RecordSetListOption... options) { 149 DnsBatchResult<Page<RecordSet>> result = new DnsBatchResult<>(); 150 Map<DnsRpc.Option, ?> optionMap = DnsImpl.optionMap(options); 151 RpcBatch.Callback<ResourceRecordSetsListResponse> callback = 152 createListRecordSetsCallback(zoneName, result, optionMap); 153 batch.addListRecordSets(zoneName, callback, optionMap); 154 return result; 155 } 156 157 /** 158 * Adds a request representing the "list change requests" operation in the zone specified by 159 * {@code zoneName} to this batch. The {@code options} can be used to restrict the fields returned 160 * or provide page size limits in the same way as for {@link Dns#listChangeRequests(String, 161 * Dns.ChangeRequestListOption...)}. Calling {@link DnsBatchResult#get()} on the return value 162 * yields a page of change requests if successful and throws a {@link DnsException} if the 163 * operation failed or the zone does not exist. 164 */ listChangeRequests( String zoneName, Dns.ChangeRequestListOption... options)165 public DnsBatchResult<Page<ChangeRequest>> listChangeRequests( 166 String zoneName, Dns.ChangeRequestListOption... options) { 167 DnsBatchResult<Page<ChangeRequest>> result = new DnsBatchResult<>(); 168 Map<DnsRpc.Option, ?> optionMap = DnsImpl.optionMap(options); 169 RpcBatch.Callback<ChangesListResponse> callback = 170 createListChangeRequestsCallback(zoneName, result, optionMap); 171 batch.addListChangeRequests(zoneName, callback, optionMap); 172 return result; 173 } 174 175 /** 176 * Adds a request representing the "get change request" operation for the zone specified by {@code 177 * zoneName} to this batch. The {@code options} can be used to restrict the fields returned in the 178 * same way as for {@link Dns#getChangeRequest(String, String, Dns.ChangeRequestOption...)}. 179 * Calling {@link DnsBatchResult#get()} on the return value yields the requested {@link 180 * ChangeRequest} if successful, {@code null} if the change request does not exist, or throws a 181 * {@link DnsException} if the operation failed or the zone does not exist. 182 */ getChangeRequest( String zoneName, String changeRequestId, Dns.ChangeRequestOption... options)183 public DnsBatchResult<ChangeRequest> getChangeRequest( 184 String zoneName, String changeRequestId, Dns.ChangeRequestOption... options) { 185 DnsBatchResult<ChangeRequest> result = new DnsBatchResult<>(); 186 RpcBatch.Callback<Change> callback = createChangeRequestCallback(zoneName, result, true, true); 187 Map<DnsRpc.Option, ?> optionMap = DnsImpl.optionMap(options); 188 batch.addGetChangeRequest(zoneName, changeRequestId, callback, optionMap); 189 return result; 190 } 191 192 /** 193 * Adds a request representing the "apply change request" operation to the zone specified by 194 * {@code zoneName} to this batch. The {@code options} can be used to restrict the fields returned 195 * in the same way as for {@link Dns#applyChangeRequest(String, ChangeRequestInfo, 196 * Dns.ChangeRequestOption...)}. Calling {@link DnsBatchResult#get()} on the return value yields 197 * the created {@link ChangeRequest} if successful or throws a {@link DnsException} if the 198 * operation failed or the zone does not exist. 199 */ applyChangeRequest( String zoneName, ChangeRequestInfo changeRequest, Dns.ChangeRequestOption... options)200 public DnsBatchResult<ChangeRequest> applyChangeRequest( 201 String zoneName, ChangeRequestInfo changeRequest, Dns.ChangeRequestOption... options) { 202 DnsBatchResult<ChangeRequest> result = new DnsBatchResult<>(); 203 RpcBatch.Callback<Change> callback = 204 createChangeRequestCallback(zoneName, result, false, false); 205 Map<DnsRpc.Option, ?> optionMap = DnsImpl.optionMap(options); 206 batch.addApplyChangeRequest(zoneName, changeRequest.toPb(), callback, optionMap); 207 return result; 208 } 209 210 /** Submits this batch for processing using a single RPC request. */ submit()211 public void submit() { 212 batch.submit(); 213 } 214 createListZonesCallback( final DnsBatchResult<Page<Zone>> result, final Map<DnsRpc.Option, ?> optionMap)215 private RpcBatch.Callback<ManagedZonesListResponse> createListZonesCallback( 216 final DnsBatchResult<Page<Zone>> result, final Map<DnsRpc.Option, ?> optionMap) { 217 return new RpcBatch.Callback<ManagedZonesListResponse>() { 218 @Override 219 public void onSuccess(ManagedZonesListResponse response) { 220 List<ManagedZone> zones = response.getManagedZones(); 221 Page<Zone> zonePage = 222 new PageImpl<>( 223 new DnsImpl.ZonePageFetcher(options, response.getNextPageToken(), optionMap), 224 response.getNextPageToken(), 225 zones == null 226 ? ImmutableList.<Zone>of() 227 : Iterables.transform(zones, DnsImpl.zoneFromPb(options))); 228 result.success(zonePage); 229 } 230 231 @Override 232 public void onFailure(GoogleJsonError googleJsonError) { 233 result.error(new DnsException(googleJsonError, true)); 234 } 235 }; 236 } 237 238 private RpcBatch.Callback<Void> createDeleteZoneCallback(final DnsBatchResult<Boolean> result) { 239 return new RpcBatch.Callback<Void>() { 240 @Override 241 public void onSuccess(Void response) { 242 result.success(true); 243 } 244 245 @Override 246 public void onFailure(GoogleJsonError googleJsonError) { 247 DnsException serviceException = new DnsException(googleJsonError, false); 248 if (serviceException.getCode() == HTTP_NOT_FOUND) { 249 result.success(false); 250 } else { 251 result.error(serviceException); 252 } 253 } 254 }; 255 } 256 257 /** A joint callback for both "get zone" and "create zone" operations. */ 258 private RpcBatch.Callback<ManagedZone> createZoneCallback( 259 final DnsOptions serviceOptions, 260 final DnsBatchResult<Zone> result, 261 final boolean nullForNotFound, 262 final boolean idempotent) { 263 return new RpcBatch.Callback<ManagedZone>() { 264 @Override 265 public void onSuccess(ManagedZone response) { 266 result.success( 267 response == null ? null : Zone.fromPb(serviceOptions.getService(), response)); 268 } 269 270 @Override 271 public void onFailure(GoogleJsonError googleJsonError) { 272 DnsException serviceException = new DnsException(googleJsonError, idempotent); 273 if (nullForNotFound && serviceException.getCode() == HTTP_NOT_FOUND) { 274 result.success(null); 275 } else { 276 result.error(serviceException); 277 } 278 } 279 }; 280 } 281 282 private RpcBatch.Callback<Project> createProjectCallback( 283 final DnsBatchResult<ProjectInfo> result) { 284 return new RpcBatch.Callback<Project>() { 285 @Override 286 public void onSuccess(Project response) { 287 result.success(response == null ? null : ProjectInfo.fromPb(response)); 288 } 289 290 @Override 291 public void onFailure(GoogleJsonError googleJsonError) { 292 result.error(new DnsException(googleJsonError, true)); 293 } 294 }; 295 } 296 297 private RpcBatch.Callback<ResourceRecordSetsListResponse> createListRecordSetsCallback( 298 final String zoneName, 299 final DnsBatchResult<Page<RecordSet>> result, 300 final Map<DnsRpc.Option, ?> optionMap) { 301 return new RpcBatch.Callback<ResourceRecordSetsListResponse>() { 302 @Override 303 public void onSuccess(ResourceRecordSetsListResponse response) { 304 List<ResourceRecordSet> recordSets = response.getRrsets(); 305 Page<RecordSet> page = 306 new PageImpl<>( 307 new DnsImpl.RecordSetPageFetcher( 308 zoneName, options, response.getNextPageToken(), optionMap), 309 response.getNextPageToken(), 310 recordSets == null 311 ? ImmutableList.<RecordSet>of() 312 : Iterables.transform(recordSets, RecordSet.FROM_PB_FUNCTION)); 313 result.success(page); 314 } 315 316 @Override 317 public void onFailure(GoogleJsonError googleJsonError) { 318 result.error(new DnsException(googleJsonError, true)); 319 } 320 }; 321 } 322 323 private RpcBatch.Callback<ChangesListResponse> createListChangeRequestsCallback( 324 final String zoneName, 325 final DnsBatchResult<Page<ChangeRequest>> result, 326 final Map<DnsRpc.Option, ?> optionMap) { 327 return new RpcBatch.Callback<ChangesListResponse>() { 328 @Override 329 public void onSuccess(ChangesListResponse response) { 330 List<Change> changes = response.getChanges(); 331 Page<ChangeRequest> page = 332 new PageImpl<>( 333 new DnsImpl.ChangeRequestPageFetcher( 334 zoneName, options, response.getNextPageToken(), optionMap), 335 response.getNextPageToken(), 336 changes == null 337 ? ImmutableList.<ChangeRequest>of() 338 : Iterables.transform( 339 changes, ChangeRequest.fromPbFunction(options.getService(), zoneName))); 340 result.success(page); 341 } 342 343 @Override 344 public void onFailure(GoogleJsonError googleJsonError) { 345 result.error(new DnsException(googleJsonError, true)); 346 } 347 }; 348 } 349 350 /** A joint callback for both "get change request" and "create change request" operations. */ 351 private RpcBatch.Callback<Change> createChangeRequestCallback( 352 final String zoneName, 353 final DnsBatchResult<ChangeRequest> result, 354 final boolean nullForNotFound, 355 final boolean idempotent) { 356 return new RpcBatch.Callback<Change>() { 357 @Override 358 public void onSuccess(Change response) { 359 result.success( 360 response == null 361 ? null 362 : ChangeRequest.fromPb(options.getService(), zoneName, response)); 363 } 364 365 @Override 366 public void onFailure(GoogleJsonError googleJsonError) { 367 DnsException serviceException = new DnsException(googleJsonError, idempotent); 368 if (serviceException.getCode() == HTTP_NOT_FOUND) { 369 if ("entity.parameters.changeId".equals(serviceException.getLocation()) 370 || (serviceException.getMessage() != null 371 && serviceException.getMessage().contains("parameters.changeId"))) { 372 // the change id was not found, but the zone exists 373 result.success(null); 374 return; 375 } 376 // the zone does not exist, so throw an exception 377 } 378 result.error(serviceException); 379 } 380 }; 381 } 382 } 383